下矢印キーが添付ファイルに飛んでしまうバグの修正

basecamp/lexxy

ソフト改行(<br>)を含む段落の途中でカーソルが下矢印キーを押したとき、次のテキスト行ではなく添付ファイルが選択されてしまう問題を修正しました。オフセットガードを追加することで、テキストの境界にいるときだけ添付ファイルへのカーソル移動をインターセプトするように改善されています。

背景

Lexxy エディタでは、添付ファイル(ActionTextAttachment)の前後でカーソルが移動するとき、Lexicalのデフォルト動作ではなく独自の Selection クラスが次・前のノードを決定します。この仕組みは、添付ファイルノードへのフォーカス移動を正確に制御するために設計されています。

しかし、段落内に LineBreakNode<br>)が存在する場合、カーソルがテキストの途中にあっても topLevelNodeAfterCursor(下矢印)や topLevelNodeBeforeCursor(上矢印)がインターセプトしてしまい、まだナビゲートすべきテキスト行が残っているにもかかわらず添付ファイルを選択するという誤動作が発生していました。

技術的な変更

src/editor/selection.js にオフセットガードを2箇所追加することで、テキストノードの実際の境界でのみ添付ファイルへのナビゲーションをインターセプトするように変更されました。

変更前(topLevelNodeAfterCursor):

if ($isTextNode(anchorNode)) {
  return this.#getNextNodeFromTextEnd(anchorNode)
}

変更後(topLevelNodeAfterCursor):

if ($isTextNode(anchorNode)) {
  if (offset < anchorNode.getTextContentSize()) return null
  return this.#getNextNodeFromTextEnd(anchorNode)
}

変更前(topLevelNodeBeforeCursor):

if ($isTextNode(anchorNode)) {
  return this.#getPreviousNodeFromTextStart(anchorNode)
}

変更後(topLevelNodeBeforeCursor):

if ($isTextNode(anchorNode)) {
  if (offset > 0) return null
  return this.#getPreviousNodeFromTextStart(anchorNode)
}

下矢印の場合、offset < anchorNode.getTextContentSize() が真であればカーソルはテキストノードの末尾に到達していないため null を返してLexicalのデフォルトのカーソル移動に委ねます。上矢印の場合、offset > 0 が真であればカーソルはテキストノードの先頭にないため同様に null を返します。これにより #getNextNodeFromTextEnd#getPreviousNodeFromTextStart は、LineBreakNode のような非デコレータの兄弟ノードをスキップせず、テキストの真の境界でのみ呼ばれるようになります。

設計判断

Lexicalのデフォルト動作への委譲という方針が採用されました。

独自の添付ファイルナビゲーションロジックの適用範囲を最小化し、テキストノードの途中では Lexical の組み込みカーソル移動をそのまま使う設計です。オフセットチェックを Selection クラスの入口に集約することで、#getNextNodeFromTextEnd#getPreviousNodeFromTextStart の内部実装を変えずに問題を解消しています。

テストも刷新されており、変更前は「フォーマット済みテキスト(イタリック体)を右矢印で通過しても添付ファイルに飛ばない」ことを確認していたのに対し、変更後は「ソフト改行を含む段落で下矢印を押すと次のテキスト行に移動し、添付ファイルが選択されない」という、実際のバグシナリオに直接対応するテストケースに置き換えられています。

まとめ

本修正は、テキスト境界の判定をオフセットによって厳密化することで、添付ファイルへのカーソルジャンプを真に必要な場面のみに限定した変更です。変更は2行の追加のみと最小限でありながら、段落内に LineBreakNode が存在するあらゆるケースでの誤動作を防ぐ効果をもたらします。

記事メタデータ

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

この記事は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 #846](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

内容はLexicalエディタの内部実装に関するもので、専門知識を持つエンジニアという対象読者に適した技術レベルと表現になっています。

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

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

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

Diff内容との照合 ✓ PASS

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

記事で引用されているコード変更(オフセットガードの追加)は、提供されたDiffの内容と完全に一致しており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「オフセットガード」「LineBreakNode」「$isTextNode」など、PRの文脈に沿った正確な技術用語が使用されています。

説明の技術的正確性 ✓ PASS

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

オフセットをチェックしてテキストの境界でのみ処理を実行するという技術的な説明は、Diffの内容と論理的に整合しており、正確です。

事実の突合 ✓ PASS

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

記事の主張はすべてPRのTitle, Description, Diffの内容によって裏付けられており、ハルシネーション(捏造や根拠のない推測)は見られません。

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

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

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

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

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

記事のタイトルは、PRのタイトル「Fix down arrow skipping text lines to select attachment」の内容を的確に反映しています。

外部知識の正確性 ✓ PASS

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

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

時間表現の正確性 ✓ PASS

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

PR情報を歪曲するような不正確な時間表現は使用されていません。