コードブロックへの貼り付け時に空行で内容が外へ飛び出すバグを修正
コードブロックへのペースト操作中に空行(連続する改行)が含まれていると、コンテンツがコードブロック外のパラグラフに分割されてしまう問題が修正されました。$hasUpdateTag(PASTE_TAG) によるガード処理を追加し、ペースト操作中は基底クラスの安全な動作に委譲するようにしたことで、通常のEnterキーによる脱出動作には影響を与えずにバグを解消しています。
背景
Lexicalエディタは、ペースト操作の各改行ごとに insertNewAfter() を呼び出す仕組みを持っています。EarlyEscapeCodeNode はコードブロックからカーソルを抜け出させる独自のエスケープロジックをこのメソッドに実装しており、空行(連続する2つの改行)を含むテキストをペーストした際に問題が発生していました。
ペースト中に空行によって最終行が空になると、EarlyEscapeCodeNode のエスケープロジックが意図せず発火し、コンテンツをコードブロック外のパラグラフに分割してしまっていました。これはペースト操作の途中でエスケープ判定が行われることに起因しており、ユーザーがコードブロックを意図的に抜け出そうとした操作ではないにもかかわらず脱出が発生するという誤動作です。
この問題は、Lexicalがペースト操作を複数回の insertNewAfter() 呼び出しに分解して処理するという実装特性と、EarlyEscapeCodeNode 独自の脱出ロジックの組み合わせによって引き起こされていました。
技術的な変更
修正は src/nodes/early_escape_code_node.js への最小限の変更で実現されています。$hasUpdateTag および PASTE_TAG を lexical からインポートし、insertNewAfter() の冒頭にペースト操作の検出ガードを追加しました。
変更前:
insertNewAfter(selection, restoreSelection) {
if (!selection.isCollapsed()) return super.insertNewAfter(selection, restoreSelection)
if (this.#isCursorAtStart(selection)) {
return this.#insertParagraphBefore()
} else if (this.#isCursorOnWhitespaceOnlyLastLine(selection)) {
return this.#insertBlankLineBelow(selection, restoreSelection)
}
変更後:
insertNewAfter(selection, restoreSelection) {
if ($hasUpdateTag(PASTE_TAG) || !selection.isCollapsed()) {
return super.insertNewAfter(selection, restoreSelection)
} else if (this.#isCursorAtStart(selection)) {
return this.#insertParagraphBefore()
} else if (this.#isCursorOnWhitespaceOnlyLastLine(selection)) {
return this.#insertBlankLineBelow(selection, restoreSelection)
}
$hasUpdateTag(PASTE_TAG) が真の場合、独自のエスケープロジックをすべてスキップし、基底クラスである CodeNode の insertNewAfter() に処理を委譲します。CodeNode の基底実装はコードブロック内への改行挿入を維持するため、ペースト中の不正なコードブロック脱出が防止されます。既存の !selection.isCollapsed() 条件は同じ分岐にまとめられ、どちらの場合も super への委譲という一貫した挙動になっています。
合わせて test/browser/tests/paste/paste_into_code_block.test.js が新規追加され、以下の2つのシナリオをPlaywrightで検証しています:
- 既存コードがあるコードブロックへ空行を含むテキストを貼り付けた場合
- 空のコードブロックへ空行を含むコードを貼り付けた場合
どちらのテストでも、貼り付け後に <code> 要素が1つだけ存在し、コードブロック外に不正なパラグラフが生成されていないことを assertEditorContent で検証しています。
設計判断
Lexicalの $hasUpdateTag / PASTE_TAG API を使って更新コンテキストを判別するアプローチが採用されました。Lexicalはペースト操作を特定のタグ付きトランザクションとして実行するため、このAPIによってペーストかどうかを正確に判定できます。
別の修正アプローチとして、エスケープ条件の判定ロジック自体を修正する方法も考えられますが、ペーストコンテキストの検出という明確な責務分離の観点から、今回のガード方式は直感的です。ペースト中は EarlyEscapeCodeNode 固有のロジックを一切実行しないという明確な境界が引かれており、エスケープ機能の通常動作(Enter2回でコードブロックを抜ける)には何ら影響しません。
まとめ
ペーストコンテキストを $hasUpdateTag(PASTE_TAG) で検出し、基底クラスに委譲するというわずか4行の変更で、ペースト操作とインタラクティブ操作の責務を明確に分離することができました。Lexicalのトランザクションタグという既存の仕組みを活用したことで、エスケープ機能の既存動作を維持しながら、最小限の変更でバグを根本解決しています。