テーブルをまたぐ選択範囲での削除操作クラッシュを修正

basecamp/lexxy

テーブルの anchor とテーブル外の focus にまたがる選択範囲に対して Backspace を押すと、Lexical error 148 でエディタがクラッシュするバグが修正されました。

背景

Lexical error 148 は「Expected to find a parent TableCellNode」という文脈で発生するエラーで、テーブルセル内を起点とした選択範囲の処理においてテーブルセルの祖先を前提としたコードパスに入ることが原因です。具体的には、テーブルが先頭要素にある状態でCtrl+A(全選択)を行い Backspace を押すと、選択の anchor がテーブル内セルに、focus がテーブル外(後続の段落など)に置かれた非対称な範囲が生成されます。この状態で isTableCellSelected ゲッターが呼ばれると、選択範囲がテーブルをまたいでいるにもかかわらずテーブルセル内の処理ロジックへ進んでしまい、クラッシュが引き起こされていました。

技術的な変更

isTableCellSelected ゲッターに、処理を継続する前に選択範囲の妥当性を検証するガード節が追加されました。

変更前:

get isTableCellSelected() {
  return this.nearestNodeOfType(TableCellNode) !== null
}

変更後:

get isTableCellSelected() {
  const selection = $getSelection()
  const { anchor, focus } = selection
  if (!$isRangeSelection(selection) || anchor.key !== focus.key) return false

  return this.nearestNodeOfType(TableCellNode) !== null
}

追加されたガード節は2つの条件を || で連結しています。まず $isRangeSelection(selection) で現在の選択が範囲選択であることを確認し、次に anchor.key !== focus.keyanchorfocus が同一ノードにあるかどうかを検証します。どちらかの条件が満たされない場合は即座に false を返し、nearestNodeOfType(TableCellNode) の呼び出しを行いません。これにより、テーブルセルと非テーブルノードにまたがる選択範囲では isTableCellSelectedfalse を返すようになり、テーブルセル専用の削除ロジックへ誤って流れ込むことを防ぎます。

テスト追加

リグレッション防止のためのブラウザテストが test/browser/tests/tables.test.js に追加されました。テストはテーブルが先頭要素として存在するエディタ状態を作成し、テーブル後の段落にカーソルを置いてから全選択・Backspace という操作手順を再現します。

// Listen for page errors (the bug threw: "Expected to find a parent TableCellNode")
const errors = []
page.on("pageerror", (error) => errors.push(error.message))

await editor.selectAll()
await editor.send("Backspace")
await editor.flush()

// Table should be removed without errors
await expect(editor.content.locator("table")).toHaveCount(0)
expect(errors.filter((e) => e.includes("#148"))).toHaveLength(0)

page.on("pageerror", ...) でランタイムエラーを収集し、操作後にエラーリストへの #148 の混入がないことをアサートする構造により、テーブル削除の成功とクラッシュ不在の両方を検証しています。

設計判断

ガード節を isTableCellSelected ゲッター自体に配置するアプローチが採用されました。選択範囲の非対称性チェックを呼び出し側に委ねるのではなく、プロパティが常に安全な値を返す責務を持つ設計です。anchor.key !== focus.key という条件は、選択の両端が同じノードに収まっている場合のみテーブルセル判定を続行させる意図を明示しており、将来の呼び出し元の増加に対しても防御的に機能します。

まとめ

isTableCellSelected に2行のガード節を追加するだけで、テーブルをまたぐ選択範囲という境界条件でのクラッシュを解消した修正です。変更箇所を最小限に絞りつつ、ブラウザテストでリグレッションを明示的に検証する形でその意図を記録に残している点も、保守性の観点から参考になります。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「リード文(総論)→背景・技術詳細(各論)→まとめ(結論)」という3部構成が明確に守られています。特に、任意項目である「設計判断」セクションが含まれており、変更の意図を深く解説できている点が優れています。

カスタムMarkdown構文 ⚠ WARNING

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

ファイル名付きシンタックスハイライトは正しく使用されています。しかし、PRへのリンク記法がガイドラインの例 `[#123](URL)` とは異なり `[PR #831](URL)` となっています。意味は通じますが、形式が不統一です。

対象読者への適合性 ✓ PASS

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

Lexicalのエラー、DOMノードのキー(anchor, focus)、選択範囲の妥当性検証といったトピックは、専門知識を持つエンジニアを対象としており、過度な説明がなく適切です。

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

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

各セクション、各パラグラフが「総論→各論」の構成で書かれています。全ての段落がトピックセンテンスで始まっており、1段落1トピックの原則が守られているため、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

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

記事内で引用されている`src/editor/selection.js`と`test/browser/tests/tables.test.js`のコードスニペットは、提供されたDiff情報と完全に一致しています。ファイルパスの指定も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`Lexical error 148`、`anchor`、`focus`、`isTableCellSelected`などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

テーブルをまたぐ選択範囲がクラッシュを引き起こす原因と、ガード節によってそれを防ぐという修正内容の説明は、技術的に正確で論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescription、Diff内のコード、およびコメントから裏付けられています。「設計判断」セクションはPRに明記されていませんが、コードの変更箇所から論理的に導出できる妥当な分析であり、ハルシネーションには該当しません。

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

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

PR番号「#831」、エラーコード「#148」など、記事に含まれる数値や固有名詞はすべて正確です。

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

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

記事のタイトル「テーブルをまたぐ選択範囲での削除操作クラッシュを修正」は、PRのタイトル「Table selection Lexical error fix」の内容をより具体的に、かつ正確に表現しています。

外部知識の正確性 ✓ PASS

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

バージョン情報やリリース予定など、PR情報に基づかない外部知識は一切含まれておらず、事実に忠実です。

時間表現の正確性 ✓ PASS

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

行われた修正について過去形を用いるなど、時間表現はPRの内容と一致しており、正確です。