ブロック引用内リスト編集のバックスペースと挿入バグを修正

basecamp/lexxy

Lexical エディタで、ブロック引用内部の箇条書きリストに対する Backspace とリストトグルの動作が期待と異なる2件の欠陥を、選択判定とリスト挿入ロジックの拡張で根本的に解消しました。

背景

引用内部のリスト項目で Backspace を押すと、空行が <p><br></p> として残り、ユーザーがギャップを閉じられないという報告がありました。また、引用内の段落にリストを適用すると、引用全体が単一のリスト項目に吸収され、引用が壊れるという別の不具合も同時に報告されていました。これらは QuoteNodeListNode の階層判定が不十分だったことが根本原因です。

技術的な変更

選択が引用内部かを判定するプロパティ追加

src/editor/selection.jsisInsideBlockQuote プロパティを導入し、現在の選択が QuoteNode 配下にあるかを nearestNodeOfType(QuoteNode) で判定できるようにしました。

+  get isInsideBlockQuote() {
+    return this.nearestNodeOfType(QuoteNode)
+  }

このプロパティはリスト操作ロジックで引用内部かどうかを分岐させる基盤となります。

リスト適用ロジックの統合と引用対応

src/editor/contents.jsapplyUnorderedListFormatapplyOrderedListFormat は共通メソッド #applyListFormat に集約され、引用内部かどうかを判定した上で処理を分岐します。引用内部の場合は新メソッド #insertListInsideQuote が呼び出され、選択されたブロックをグループ化して個別にリスト化します。

+  #applyListFormat(listType, command) {
+    if (this.selection.isInsideBlockQuote) {
+      this.#insertListInsideQuote(listType)
+    } else {
+      this.#splitParagraphsAtLineBreaksUnlessInsideList()
+      this.editor.dispatchCommand(command)
+    }
+  }

連続ブロックをグループ化するヘルパー追加

src/helpers/lexical_helper.js$consecutiveSiblingGroups を導入し、選択範囲内で隣接したブロック要素を同一リストにまとめるロジックを提供します。これにより、引用内部の複数段落を一括でリスト化しつつ、引用構造を保持できます。

+export function $consecutiveSiblingGroups(blocks) {
+  const ordered = [...blocks].sort((a, b) => a.getIndexWithinParent() - b.getIndexWithinParent())
+  const groups = []
+  for (const block of ordered) {
+    const lastGroup = groups.at(-1)
+    const previous = lastGroup?.at(-1)
+    if (previous && previous.getParent().is(block.getParent()) && previous.getNextSibling()?.is(block)) {
+      lastGroup.push(block)
+    } else {
+      groups.push([block])
+    }
+  }
+  return groups
+}

空リスト項目のBackspace処理修正

src/editor/selection.js の空リスト項目削除ロジックに、対象が QuoteNode 配下にある場合の特別分岐を追加しました。引用内部では空項目を削除しカーソルを次の項目へ移動させ、段落を挿入してギャップを残す既存の振る舞いは保持します。

if (previousSibling) {
  previousSibling.selectEnd()
  listItem.remove()
} else if ($isQuoteNode(listNode.getParent())) {
  nextSibling.selectStart()
  listItem.remove()
} else {
  const paragraph = $createParagraphNode()
  listNode.insertBefore(paragraph)
  listItem.remove()
  paragraph.selectStart()
}

テスト追加

Playwright テスト test/browser/tests/formatting/list_in_quote.test.js が新規追加され、空リスト項目での Backspace、引用内段落へのリスト挿入、リストトグルのオンオフすべてが期待通りに動作することを自動検証しています。5 件の失敗テストが main ブランチで失敗し、今回の変更で全て成功します。

設計判断

引用内部かどうかの判定と処理分岐Selection オブジェクトにプロパティとして持たせ、Contents はそのフラグだけでリスト挿入ロジックを切り替える設計にしました。これにより、既存のリスト処理コードを大幅に改変せずに引用対応を追加でき、後方互換性と拡張性のバランスが取れています。

ヘルパー関数 $consecutiveSiblingGroups の導入は、引用内部でのブロックグルーピングという特定ニーズに応える汎用化です。現時点では contents.js のみで使用されていますが、将来的に類似パターンが増える場合に再利用が期待できます。

空リスト項目のBackspace挙動 の分岐は、ユーザー体験(ギャップが閉じない)に直結する UI の微調整です。引用外での既存振る舞いは保持しつつ、引用内部だけ特化したパスを追加した点が、UX 改善と後方互換性のトレードオフとして評価できます。

まとめ

今回の PR は、ブロック引用内部でのリスト編集に関わる 2 つの欠陥を、選択判定の拡張リストロジックの分離 で根本的に解消しました。isInsideBlockQuote による判定と $consecutiveSiblingGroups を用いたリスト化で、既存コードへの侵入度を最小限に抑えつつ UI の一貫性を回復しています。追加された自動テストにより回帰防止も確保され、エディタ全体の信頼性が向上しました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
e5077580

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景、技術的な変更、設計判断、まとめの5セクションが明確に配置され、総論→各論→結論の流れが保たれています。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい `言語:ファイルパス` 形式で記述されていますが、PRリンクが `[PR #1122](URL)` となっており、仕様の `[ #123](URL)` とは若干フォーマットが異なります。

対象読者への適合性 ✓ PASS

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

Lexicalエディタの内部構造に詳しいエンジニア向けの内容で、初心者向けの過度な説明はありません。

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

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

各セクションは総論パラグラフ→各論パラグラフ→結論パラグラフの構成で、段落はトピックセンテンスで始まり、1段落1トピック、長さも適切です。

Diff内容との照合 ✓ PASS

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

記事中のコードブロックは提供されたDiffと一致しており、抜粋や省略がなく、ファイル名もDiffのものと合致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

QuoteNode、ListNode、$isQuoteNode などの用語はPRで使用されているものと一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

技術的な説明はPRの記述と合致しており、因果関係も論理的です。

事実の突合 ✓ PASS

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

記事の主張はすべてPRタイトル、説明、Diff、テスト結果で裏付けられており、推測や捏造はありません。

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

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

「2 つの欠陥」や「5 件の失敗テスト」などの数値はPRと一致しています。

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

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

記事タイトルはPRの内容(ブロック引用内リスト編集の修正)を正確に日本語化して表現しています。

外部知識の正確性 ✓ PASS

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

記事はPR以外のバージョン情報や LTS など外部知識を追加していません。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PRの記述と一致しています。