添付ファイル選択中にリストフォーマットを適用するとクラッシュする問題を修正
Lexxyエディタで添付ファイルが選択された状態でリスト形式(箇条書き・番号付き)を適用しようとするとクラッシュしていた問題を修正しました。$isRangeSelection によるガード処理を追加することで、テキスト範囲選択以外の選択状態を早期にバイアウトします。
背景
dispatchInsertUnorderedList と dispatchInsertOrderedList は、選択状態が null でないことだけをチェックしていたため、NodeSelection(ノード単体の選択)が渡された場合にクラッシュしていました。Lexxyでは、添付ファイルを画像として埋め込んだ際にユーザーがそのフィギュアをクリックすると、NodeSelection という選択状態が生成されます。NodeSelection には anchor プロパティが存在しないため、直後の selection.anchor.getNode() の呼び出しが "Cannot read properties of undefined (reading 'getNode')" エラーを引き起こしていました。
問題の根本は、ガード条件が「選択が存在するか(!selection)」のみを確認していた点にあります。Lexxyが利用するLexicalエディタフレームワークには、RangeSelection、NodeSelection、GridSelection など複数の選択型が存在し、anchor プロパティを持つのは RangeSelection のみです。リストフォーマットコマンドはテキスト範囲に対して意味を持つ操作であるため、RangeSelection 以外のケースでは処理を継続する必要がありません。
技術的な変更
src/editor/command_dispatcher.js の dispatchInsertUnorderedList と dispatchInsertOrderedList の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) は selection が null の場合も 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を使う他のコマンド実装においても参照すべき判断といえます。