インラインコードからのキャレット脱出時にフォーマットが引き継がれるバグを修正

basecamp/lexxy

<code> スパンの末尾で ArrowRight キーを押してキャレットを外に移動させた後、次に入力した文字がコードフォーマットのまま挿入される問題を修正しました。KEY_ARROW_RIGHT_COMMAND ハンドラを追加し、キャレットが <code> ノードを抜けるタイミングで明示的にフォーマットを解除することで解決しています。

背景

Lexical エディタでは、インラインフォーマット(code など)はキャレットが持つアクティブフォーマット情報として管理されています。通常、<code> 要素の外に移動すればフォーマットは自動的にクリアされることが期待されますが、ArrowRight によるキャレット移動ではこのクリアが行われないケースがありました。

具体的には、<code> ノードの末尾にキャレットが置かれた状態で ArrowRight を押すと、キャレットは視覚的に <code> の外へ移動します。しかし、アクティブなインラインフォーマットの状態は code のままとなり、その直後に文字を入力すると意図せずコードフォーマットで挿入されていました。

技術的な変更

CommandDispatcher クラスに KEY_ARROW_RIGHT_COMMAND のハンドラを新規追加することで、ArrowRight キー操作時のフォーマット状態を明示的に制御するようにしました。

変更前:

#registerKeyboardCommands() {
  this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleTabKey.bind(this), COMMAND_PRIORITY_NORMAL)
}

変更後:

#registerKeyboardCommands() {
  this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#handleArrowRightKey.bind(this), COMMAND_PRIORITY_NORMAL)
  this.editor.registerCommand(KEY_TAB_COMMAND, this.#handleTabKey.bind(this), COMMAND_PRIORITY_NORMAL)
}

#handleArrowRightKey(event) {
  const selection = $getSelection()
  if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
  if (this.selection.isInsideCodeBlock || !selection.hasFormat("code")) return false

  const anchorNode = selection.anchor.getNode()
  if (!$isTextNode(anchorNode) || selection.anchor.offset !== anchorNode.getTextContentSize()) return false
  if (this.selection.nodeAfterCursor !== null) return false

  event.preventDefault()
  selection.toggleFormat("code")
  return true
}

ハンドラが実際に介入する条件は複数のガード節で厳密に絞られています。具体的には以下のすべてを満たす場合にのみフォーマット解除を実行します:

  • 選択がコラプス済み(キャレット状態)であること
  • コードブロック内ではなくインラインコードであること
  • 現在のフォーマットに code が含まれていること
  • アンカーノードがテキストノードであり、オフセットがノードの末尾と一致すること
  • キャレット直後に続く同一フォーマットのノードが存在しないこと(nodeAfterCursor === null

これらの条件が揃ったとき、event.preventDefault() でデフォルトのキャレット移動を抑制した上で selection.toggleFormat("code") を呼び出してフォーマットをオフにし、return true でコマンドの処理済みを宣言します。

設計判断

フォーマット解除をキャレット移動に割り込む形で実装する方式が選ばれました。

Lexical のコマンドシステムでは、ハンドラが true を返すとそのコマンドの処理が止まり、以降の低優先度ハンドラやデフォルト動作が呼ばれなくなります。このPRでは COMMAND_PRIORITY_NORMAL でハンドラを登録し、event.preventDefault()return true の組み合わせによってキャレット移動そのものをカスタムロジックに置き換えています。これにより、フォーマット状態の変更とキャレット移動を一つのトランザクションとして一貫して処理できます。

また、isInsideCodeBlock による早期リターンも注目に値します。コードブロック(<pre><code>)内では同名の code フォーマットが使われますが、その文脈での ArrowRight は通常のテキスト移動として扱うべきです。このガード節によって、コードブロック内の動作には一切影響を与えないよう配慮されています。

付随する削除:dropFiles メソッドの廃止

このPRには、インラインコードの修正とは独立した dropFiles メソッドの削除も含まれています。Contents クラスから dropFiles メソッドが削除され、ドラッグ&ドロップ時のファイルアップロードは直接 uploadFiles を呼ぶように変更されました。

削除されたのは、OS のファイルマネージャーからのドラッグ中にブラウザが DOM のセレクションを更新しないため、caretRangeFromPoint / caretPositionFromPoint を用いてドロップ座標にキャレットを移動させる #moveSelectionToPoint メソッドと、それを呼び出していた dropFiles メソッドです。これらが削除されたことで、ドロップ位置へのキャレット移動ではなく、selectLast: true オプションによる最後にアップロードされたファイルの選択という動作に統一されています。

まとめ

本PRは、Lexical のコマンドシステムに ArrowRight ハンドラを挿入することで、インラインコードからのキャレット脱出時のフォーマット残留というエッジケースを精確に修正しています。複数のガード条件を組み合わせてコードブロックや範囲選択などの正常ケースを除外した上でのみ介入する設計は、既存の動作への影響を最小限に抑えながら問題を解消する手堅いアプローチです。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
32fccda1

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

「総論→各論→結論」の構成が明確です。リード文、背景、技術的変更、設計判断、まとめの各要素が適切に配置されています。特に、PRに含まれる関連性の低い変更を「付随する削除」として別セクションに分離しており、構成が非常に分かりやすいです。

カスタムMarkdown構文 ✓ PASS

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

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

対象読者への適合性 ✓ PASS

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

Lexicalエディタの内部実装に関する専門用語(CommandDispatcher, toggleFormatなど)が適切に使用されており、対象読者であるエンジニアに適した技術レベルで書かれています。

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

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

各セクション、各パラグラフが「総論→各論」の構造で書かれています。また、各段落がトピックセンテンスで始まり、1段落1トピックの原則が守られているため、非常に読みやすいです。段落の長さも適切です。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードブロックは、`src/editor/command_dispatcher.js` のDiff内容と完全に一致しています。コードの引用は正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「キャレット」「インラインフォーマット」「コマンドハンドラ」など、使用されている技術用語はすべて正確で、文脈に適しています。

説明の技術的正確性 ✓ PASS

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

技術的な説明は正確です。特に、`#handleArrowRightKey`メソッド内のガード節の役割や、「付随する削除」セクションで`dropFiles`が廃止された理由(DOMセレクションが更新されない問題)についての説明は、Diff内のコメント内容とも一致しており、的確です。

事実の突合 ✓ PASS

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

記事内のすべての主張は、PRのタイトル、説明、およびDiff内のコードやコメントによって裏付けられています。根拠のない推測や情報の創作(ハルシネーション)は見られません。

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

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

PR番号「#826」が正確に記載されています。

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

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

記事のタイトル「インラインコードからのキャレット脱出時にフォーマットが引き継がれるバグを修正」は、PRの主題(Inline code formatting can get stuck)を正確に反映しています。

外部知識の正確性 ✓ PASS

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

記事の内容はPR情報に限定されており、バージョンサポート状況やリリース日程など、PRの範囲を超えた外部知識の追加はありません。

時間表現の正確性 ✓ PASS

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

「修正しました」「削除も含まれています」など、過去形・完了形の表現が使われており、PRがマージ済みであるという事実と時間表現が一致しています。