コードブロックからの脱出操作を修正
Lexicalのデフォルト実装では、コードブロックを抜けるために3回のEnterキー入力が必要でしたが、本PRはその操作を「空行でのEnter1回」または「ArrowDownキー」に改善します。
背景
Lexicalの組み込み CodeNode.insertNewAfter() はコードブロックを抜けるための条件が過剰に厳格で、ユーザーの直感と一致しませんでした。正確には「2つのトレーリング LineBreakNode」と「カーソルが要素オフセットの末尾にある」の両方を要求しており、実質3回のEnter入力が必要な状態でした。これはユーザー体験として受け入れがたく、Issue #4978 として報告されていました。
この問題に対して、Lexicalのデフォルト挙動をオーバーライドする形で FormatEscaper クラスに専用ハンドラーを実装することで解決しています。
技術的な変更
変更の中心は src/editor/contents/format_escaper.js で、既存の FormatEscaper クラスに2つのコマンドハンドラーが追加されました。
KEY_ENTER_COMMAND ハンドラー(#handleCodeBlocks) は、カーソルがコードブロックの末尾の空行にある場合を検出し、その空行を削除してコードブロックの直後に新しい段落を挿入します。このハンドラーは COMMAND_PRIORITY_HIGH で登録されている既存の #handleEnterKey メソッド内から呼び出され、他の処理より前に評価されます。
#handleCodeBlocks(event, selection) {
if (!selection.isCollapsed()) return false
const codeNode = this.#getCodeNodeFromSelection(selection)
if (!codeNode) return false
if (this.#isCursorOnEmptyLastLineOfCodeBlock(selection, codeNode)) {
event?.preventDefault()
this.#exitCodeBlock(codeNode)
return true
}
// ...
}
KEY_ARROW_DOWN_COMMAND ハンドラー(#handleArrowDownInCodeBlock) は COMMAND_PRIORITY_NORMAL で別途登録され、コードブロックが最後の要素であるときに ↓ キーが押された場合、新しい段落を作成してカーソルを移動させます。
実装上の注目点として、ブラウザ間の差異への対応も含まれています。ChromiumはコードブロックのカーソルオフセットをCodeNodeのelement-levelで表現するのに対し、Firefoxはゼロ長の CodeHighlightNode を使用します。#isCursorOnEmptyLastLineOfCodeBlock はこの両方を正しく判定できるよう実装されています。
合わせて test/browser/tests/escape_format.test.js に2つのリグレッションテストが追加されました。
- Enter押下でのコードブロック脱出:
<pre><code>line one</code></pre>の末尾でEnter×2を押し、後続のテキストが<p>に入力されることを確認 - ArrowDown押下でのコードブロック脱出: 最後の要素であるコードブロックの末尾で ↓ を押し、後続テキストが
<p>に入力されることを確認
設計判断
Lexicalのコマンドシステムを活用し、CodeNode の組み込み動作を上書きする方式が採用されました。
KEY_ENTER_COMMAND は既存の COMMAND_PRIORITY_HIGH ハンドラー内に統合し、KEY_ARROW_DOWN_COMMAND は COMMAND_PRIORITY_NORMAL で独立登録するという優先度の使い分けが行われています。Enterキーはブロッククォートを含む他のフォーマット脱出処理と共存させるために既存ハンドラー内に組み込まれており、ArrowDownはコードブロック固有の処理なので独立したハンドラーとして切り出されています。
また、@lexical/code の $isCodeNode と @lexical/utils の $getNearestNodeOfType を活用することで、選択位置からコードブロックの親ノードを確実に特定しており、ネストされた構造にも対応できる実装になっています。
まとめ
Lexicalのデフォルト動作の厳格すぎる仕様を、コマンドの優先度システムとブラウザ差異の吸収によりクリーンに上書きした変更です。操作の直感性が向上するとともに、リグレッションテストにより今後の変更でも同様の動作が保証されます。