添付ファイル選択中にリストフォーマットを適用するとクラッシュする問題を修正

basecamp/lexxy

Lexxyエディタで添付ファイルが選択された状態でリスト形式(箇条書き・番号付き)を適用しようとするとクラッシュしていた問題を修正しました。$isRangeSelection によるガード処理を追加することで、テキスト範囲選択以外の選択状態を早期にバイアウトします。

背景

dispatchInsertUnorderedListdispatchInsertOrderedList は、選択状態が null でないことだけをチェックしていたため、NodeSelection(ノード単体の選択)が渡された場合にクラッシュしていました。Lexxyでは、添付ファイルを画像として埋め込んだ際にユーザーがそのフィギュアをクリックすると、NodeSelection という選択状態が生成されます。NodeSelection には anchor プロパティが存在しないため、直後の selection.anchor.getNode() の呼び出しが "Cannot read properties of undefined (reading 'getNode')" エラーを引き起こしていました。

問題の根本は、ガード条件が「選択が存在するか(!selection)」のみを確認していた点にあります。Lexxyが利用するLexicalエディタフレームワークには、RangeSelectionNodeSelectionGridSelection など複数の選択型が存在し、anchor プロパティを持つのは RangeSelection のみです。リストフォーマットコマンドはテキスト範囲に対して意味を持つ操作であるため、RangeSelection 以外のケースでは処理を継続する必要がありません。

技術的な変更

src/editor/command_dispatcher.jsdispatchInsertUnorderedListdispatchInsertOrderedList の2箇所で、ガード条件が $isRangeSelection を使ったチェックに置き換えられました。

変更前:

dispatchInsertUnorderedList() {
  const selection = $getSelection()
  if (!selection) return

  const anchorNode = selection.anchor.getNode()
  // ...
}

dispatchInsertOrderedList() {
  const selection = $getSelection()
  if (!selection) return

  const anchorNode = selection.anchor.getNode()
  // ...
}

変更後:

dispatchInsertUnorderedList() {
  const selection = $getSelection()
  if (!$isRangeSelection(selection)) return

  const anchorNode = selection.anchor.getNode()
  // ...
}

dispatchInsertOrderedList() {
  const selection = $getSelection()
  if (!$isRangeSelection(selection)) return

  const anchorNode = selection.anchor.getNode()
  // ...
}

$isRangeSelection(selection)selectionnull の場合も false を返すため、従来の null チェックを包含しており後方互換性に問題はありません。また、PRの記述によれば command_dispatcher.js 内の他のフォーマットコマンドについても同様のパターンを監査した結果、それらはすでに適切なガードを持っていることが確認されています。

テストファイル test/browser/tests/formatting/list_format_with_attachment_selected.test.js が新たに追加されました。Playwrightを使ったブラウザテストで、添付ファイルを含むエディタコンテンツを設定し、フィギュアをクリックして NodeSelection を生成した後に箇条書き・番号付きリストボタンをクリックしてもJSエラーが発生しないことを検証しています。また、コマンドがno-opとして扱われることで既存のコンテンツ(段落と添付ファイル)が保持されることも確認しています。

設計判断

「早期リターン(bail out early)」パターン が一貫して採用されています。$isRangeSelection の否定条件で即時 return することで、それ以降のコードは RangeSelection であることが保証された状態で実行されます。これにより、selection.anchor へのアクセスに対してTypeScript/JSエンジンの型安全性の恩恵を最大限に活用できます。

コマンドがno-opになる設計も重要な判断です。添付ファイルが選択されている状態でリストフォーマットを適用しようとした場合、エラーを表示したりフォールバック処理を行うのではなく、単純に何もしないことを選んでいます。これはユーザー操作として意味を成さない状態に対して余計な副作用を生じさせない、防御的なアプローチです。

まとめ

2行の変更で、null チェックから $isRangeSelection チェックへの置き換えという最小限の修正がクラッシュを解消しています。選択型の違いを適切に区別するという原則は、同様のLexical APIを使う他のコマンド実装においても参照すべき判断といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
99824503

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術詳細(各論)→まとめ(結論)」の構成が明確で、理想的な構成です。特に、任意項目である「設計判断」セクションが含まれており、変更の意図を深く伝えています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:src/editor/command_dispatcher.js)とPR番号のリンク記法([PR #967](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「NodeSelection」「RangeSelection」「$isRangeSelection」といったLexicalフレームワーク固有の用語を適切に使用しており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、かつ各段落がトピックセンテンスで始まるため、非常に読みやすい構造です。1段落1トピックの原則も守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは、提供されたDiff情報と完全に一致しています。変更点(`!selection` から `$isRangeSelection(selection)`へ)が正確に引用されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「NodeSelection」「RangeSelection」「anchorプロパティ」など、PRの文脈における技術用語を正確に使用しており、問題の原因を明確に説明できています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「NodeSelectionにはanchorプロパティが存在しないためクラッシュする」という原因の説明が技術的に正確であり、PRのDescriptionと一致しています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(クラッシュの原因、修正内容、他のコマンドの監査結果、テストの追加)は、PRのDescriptionおよびDiff情報によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#967)が正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトルと完全に一致しており、変更内容を的確に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれない外部知識(バージョンのサポート状況、リリース日程など)の記載はなく、提供された情報に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「クラッシュしていた問題」のように、過去に存在した問題が修正されたという時間関係が正確に表現されています。