コードブロックの末尾行が空白文字のみのときにEscapeできないバグを修正
コードブロックの末尾行にスペースやタブのみが入力されている場合、Enterキーで抜け出せなくなるバグが修正されました。2回のEnterで脱出するという既存の動作と一貫した方式で、空白のみの行を段階的に処理します。
背景
EarlyEscapeCodeNode には「最後の行が空行ならEnterでコードブロックを脱出する」仕組みが元々備わっていました。しかし、最後の行がスペースやタブのみで構成されている場合、この脱出機能が動作しないという問題がありました。
原因は #isCursorOnEmptyLastLine の実装にあります。このメソッドは textContent.endsWith("\n") で末尾が改行かどうかを判定していましたが、最後の行に空白文字が含まれる場合、textContent の末尾は \n ではなくスペースになります。このため、「末尾が空行」という判定が成立せず、脱出パスに到達できませんでした。
技術的な変更
insertNewAfter のディスパッチロジックと、空白のみの末尾行を検出・処理するメソッド群が追加されました。
変更前:
if (this.#isCursorAtStart(selection)) {
this.insertBefore($createParagraphNode())
return null
}
if (this.#isCursorOnEmptyLastLine(selection)) {
$trimTrailingBlankNodes(this)
const paragraph = $createParagraphNode()
this.insertAfter(paragraph)
return paragraph
}
return super.insertNewAfter(selection, restoreSelection)
変更後:
if (this.#isCursorAtStart(selection)) {
return this.#insertParagraphBefore()
} else if (this.#isCursorOnWhitespaceOnlyLastLine(selection)) {
return this.#insertBlankLineBelow(selection, restoreSelection)
} else if (this.#isCursorOnEmptyLastLine(selection)) {
return this.#escapeToNewParagraphAfter()
} else {
return super.insertNewAfter(selection, restoreSelection)
}
新たに追加された #isCursorOnWhitespaceOnlyLastLine は、textContent から最後の \n 以降の文字列を取り出し、長さが0より大きく trim() 後に空文字になるかどうかで判定します。
#isCursorOnWhitespaceOnlyLastLine(selection) {
if (!$isCursorOnLastLine(selection)) return false
const textContent = this.getTextContent()
const lastNewlineIndex = textContent.lastIndexOf("\n")
const lastLine = lastNewlineIndex === -1 ? textContent : textContent.slice(lastNewlineIndex + 1)
return lastLine.length > 0 && lastLine.trim() === ""
}
空白のみの末尾行でEnterが押された場合、#insertBlankLineBelow が呼び出されます。これは super.insertNewAfter で新しい行を追加したあと、直後に getLastChild().remove() でその行を削除します。結果としてカーソルは空の末尾行に移動し、次のEnterで通常の #isCursorOnEmptyLastLine による脱出が発動します。
#insertBlankLineBelow(selection, restoreSelection) {
super.insertNewAfter(selection, restoreSelection)
this.getLastChild().remove()
return null
}
この2段階処理により、空白のみの行を1回目のEnterでクリアし、2回目のEnterでコードブロックを脱出するという動作になります。これは内容のある行から脱出する際の既存の2回Enter方式と一貫しています。
設計判断
「空白行をクリアしてから既存の脱出フローに乗せる」 という2段階の設計が採用されました。
#isCursorOnEmptyLastLine の判定ロジックを変更して空白のみの行も「空行」として扱う方法もありえましたが、この変更では判定ロジックを拡張せず、空白のみの行を専用のハンドラで処理することで、既存の判定の意味を変えずに済んでいます。また、insertNewAfter → getLastChild().remove() の組み合わせで「子ノードを削除しつつカーソル位置だけを更新する」副作用を意図的に利用しているのが特徴的です。
if/else if チェーンへのリファクタリングにより、各条件のパスが排他的に明示され、以前の複数の独立したif文よりも制御フローが読みやすくなっています。
まとめ
本修正は、既存の脱出ロジックを書き換えるのではなく、空白のみの末尾行を事前に処理する前段ステップを追加することでバグを解消しています。新しい判定メソッドと既存の脱出フローを組み合わせた最小限の変更で、ユーザーが直感的に期待する「Enterで脱出できる」動作を一貫して実現しています。