リスト内の@メンション後のBackspaceでアウトデントが発生するバグを修正

basecamp/lexxy

リストアイテム内のインラインメンションの直後でBackspaceを押すと、メンションが選択される代わりにリストがアウトデントされるバグが修正されました。#collapseListItemToParagraphメソッドにデコレータノードの位置チェックを追加することで、インラインメンションとブロック添付ファイルの2つのケースを正しく区別できるようになります。

背景

リスト内でBackspaceを押したとき、カーソル直前のノードがデコレータノードかどうかによって処理が分岐する設計になっていました。しかし、#collapseListItemToParagraphメソッドはデコレータノードがリストアイテムの「外側にあるブロック添付ファイル」のケースだけを想定して実装されており、「リストアイテムの内側にあるインラインメンション」のケースを考慮していませんでした。

コメントにも明記されていた通り、このメソッドの元々の目的は「添付ファイルの直後にあるリストをBackspaceで削除するとき、添付ファイルが誤って選択されないようにリストアイテムをパラグラフに畳み込む」ことです。ところがこのロジックが、リストアイテムの中にインラインメンションが含まれるケースでも無条件に発動し、意図しないアウトデントを引き起こしていました。

技術的な変更

#collapseListItemToParagraphメソッドにデコレータノードへの参照を渡し、そのノードがリストアイテムの子孫であるかを確認してから処理を続行するよう変更されました。

変更前:

if (this.#collapseListItemToParagraph()) return true
#collapseListItemToParagraph() {
  const anchorNode = $getSelection()?.anchor?.getNode()
  const listItem = anchorNode && $getNearestNodeOfType(anchorNode, ListItemNode)
  if (!listItem) return false

  const listNode = $getNearestNodeOfType(listItem, ListNode)
  if (!listNode) return false
  // ...
}

変更後:

if (this.#collapseListItemToParagraph(node)) return true
#collapseListItemToParagraph(decoratorNode) {
  const anchorNode = $getSelection()?.anchor?.getNode()
  const listItem = anchorNode && $getNearestNodeOfType(anchorNode, ListItemNode)
  if (!listItem) return false

  if (listItem.isParentOf(decoratorNode)) return false

  const listNode = $getNearestNodeOfType(listItem, ListNode)
  if (!listNode) return false
  // ...
}

変更の核心は listItem.isParentOf(decoratorNode) の1行です。デコレータノードがリストアイテムの子孫であれば false を即座に返してメソッドを抜け、呼び出し元での通常のデコレータ選択処理へ移行します。インラインメンションはリストアイテムの内側に存在するため、このガード節に引っかかり、誤ったアウトデントが抑制されます。一方、ブロック添付ファイルはリストの外側にあるため、isParentOffalseを返して既存のアウトデント抑止ロジックが従来通り動作します。

テストケースもtest/browser/tests/mention_deletion.test.jsに追加されました。リストアイテム内にメンションのみを含むHTMLをセットアップし、JavaScriptで精確なカーソル位置を制御した上で、2回のBackspace後にリスト構造(<ul>)が維持されていること、かつメンションに.node--selectedクラスが付与されることをアサートしています。

設計判断

デコレータノードへの参照をメソッドに引数として渡す方式が採用されました。

メソッド内部でデコレータノードを再取得する方法もありえましたが、呼び出し元ではすでに node として参照が確立されているため、それを引数として渡すほうが処理の重複を避けられます。また、「デコレータノードとリストアイテムの親子関係」という判定を#collapseListItemToParagraphの責務として内包させることで、呼び出し元のコードをシンプルに保っています。

メソッドのコメントも更新されており、「ブロック添付ファイルがリストの前にある場合にのみ適用し、リストアイテム内のインラインメンションには適用しない」という意図が明示されました。これはコードの正確性だけでなく、次の開発者がこのロジックを読み解く際の理解コストも下げる変更です。

まとめ

今回の修正は、#collapseListItemToParagraphが暗黙的に仮定していた「デコレータノードはリストの外側にある」という前提を明示的なガード節として表現したものです。1行のチェックを追加するだけで、ブロック添付ファイルに対する既存の動作を保ちながら、インラインメンションに対する誤ったアウトデントを解消しています。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術的な変更・設計判断(各論)→まとめ(結論)の3部構成が明確に適用されており、模範的な記事構成です。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

「デコレータノード」や「リストアイテム」などの専門用語を前提としており、専門知識を持つエンジニアという対象読者に適切です。

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

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

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

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは、提供されたDiffの内容(引数の追加、`isParentOf`チェックの追加)を正確に反映しています。ファイルパスも一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`#collapseListItemToParagraph`, `DecoratorNode`, `isParentOf` といった技術用語が、PRやコードの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「デコレータノードがリストアイテムの子孫であるかを確認する」という修正の核心を、`listItem.isParentOf(decoratorNode)` という具体的なコードと結びつけて論理的に説明できています。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのDescriptionやDiff内のコードで裏付けられています。「設計判断」セクションの解説も、コードの構造から導かれる妥当なものであり、ハルシネーションは検出されませんでした。

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

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

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

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

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

記事のタイトルはPRのタイトル(Fix backspace outdenting list with @mention)の内容を正確に反映しており、記事全体の内容とも一致しています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPR情報に完全に閉じており、バージョン情報やリリース予定など、PRに記載のない外部知識の持ち込みはありません。

時間表現の正確性 ✓ PASS

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

時間表現は「修正されました」といった完了形で統一されており、PRの内容を解説する記事として適切です。時間的な歪曲はありません。