コードブロック先頭でのEnterキー操作を修正

basecamp/lexxy

コードブロックの先頭にカーソルがある状態でEnterを押したとき、ブロックの前に段落が挿入されるように修正されました。これにより、コードブロックの上に新しい内容を追加する操作が直感的に行えるようになります。

背景

Lexxyのコードブロック(EarlyEscapeCodeNode)は、末尾の空行でEnterを押したときにコードブロックを「脱出」してその後ろに段落を挿入する機能を持っていました。しかし、ブロックの先頭でEnterを押した場合の挙動は考慮されておらず、super.insertNewAfter()が呼ばれてコード内で改行が挿入されてしまうという問題がありました。

エディタのUXとして、ブロック要素の先頭でEnterを押す操作は「その要素の前に新しいブロックを追加したい」という意図と解釈されるのが一般的です。ドキュメントの冒頭にコードブロックが置かれた場合など、コードの前にテキストを書き足したいときにこの問題が顕在化します。

技術的な変更

EarlyEscapeCodeNodeinsertNewAfter メソッドに、カーソルがノード先頭にある場合の分岐が追加されました。

変更前:

insertNewAfter(selection, restoreSelection) {
  if (!selection.isCollapsed()) return super.insertNewAfter(selection, restoreSelection)

  if (this.#isCursorOnEmptyLastLine(selection)) {
    // ...
  }

  return super.insertNewAfter(selection, restoreSelection)
}

変更後:

insertNewAfter(selection, restoreSelection) {
  if (!selection.isCollapsed()) return super.insertNewAfter(selection, restoreSelection)

  if (this.#isCursorAtStart(selection)) {
    this.insertBefore($createParagraphNode())
    return null
  }

  if (this.#isCursorOnEmptyLastLine(selection)) {
    // ...
  }

  return super.insertNewAfter(selection, restoreSelection)
}

#isCursorAtStart(selection) {
  const { anchor } = selection
  if (!$isAtNodeStart(anchor)) return false

  const anchorNode = anchor.getNode()
  return this.is(anchorNode) || this.getFirstChild()?.is(anchorNode)
}

先頭判定には既存のヘルパー $isAtNodeStart が活用されています。さらに、アンカーノードが EarlyEscapeCodeNode 自身か、その最初の子ノードかを確認することで、コードブロック内の構造(コードノードが子要素として存在するケース)にも対応しています。先頭と判定された場合は this.insertBefore($createParagraphNode()) でブロックの前に空の段落を挿入し、null を返して super.insertNewAfter の呼び出しを防ぎます。

合わせて、ブラウザテスト test/browser/tests/formatting/code_block_navigation.test.js が新たに追加されました。テストでは、コードブロック先頭でEnterを押したあとにArrowUpで移動してテキストを入力し、段落とコードブロックの両方が期待通りの内容を保持しているかをPlaywrightで検証しています。

設計判断

insertBefore + return null という組み合わせが採用されています。insertNewAfter の戻り値はLexical内部で「挿入後のカーソル移動先ノード」として利用されますが、先頭へのブロック挿入ではカーソルをコードブロック内にとどめる必要があるため、null を返して追加のカーソル操作を抑制しています。

また、#isCursorAtStart の判定を独立したプライベートメソッドとして分離した点も注目に値します。末尾判定の #isCursorOnEmptyLastLine と同じパターンに揃えることで、insertNewAfter 本体はポリシーの列挙に集中した読みやすい構造を維持しています。

まとめ

本PRは「コードブロック末尾の脱出」と対称的な「コードブロック先頭への挿入」を実装し、Enterキーの挙動をコードブロックの両端で一貫させた変更です。既存の $isAtNodeStart ヘルパーと統一されたプライベートメソッドの設計パターンを活用することで、コードの変更量を最小限に抑えながらUXの穴を塞いでいます。

記事メタデータ

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

この記事は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:path/to/file.js)とPR番号のリンク記法([#963](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Lexicalフレームワークの内部実装に関するトピックを、専門用語を前提として解説しており、対象読者であるエンジニアに適した技術レベルです。

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

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

各セクションが総論→各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。非常に読みやすい構成です。

Diff内容との照合 ⚠ WARNING

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

「変更後」のコード引用はDiffと正確に一致していますが、「変更前」のコードはDiffに直接存在するものではなく、変更後のコードから類推して作成されたものです。読者の理解を助けるための表現であり許容範囲ですが、厳密にはDiffの内容と異なります。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

EarlyEscapeCodeNode, insertNewAfter, $isAtNodeStartなど、Lexicalフレームワークに関連する技術用語が正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

カーソル位置の判定、insertBeforeによるノード挿入、return nullによるカーソル操作の抑制など、コード変更に関する説明が技術的に正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのTitle, Description, Diffの内容に基づいており、根拠のない推測や捏造(ハルシネーション)は見られません。「設計判断」もコード構造から導かれる合理的な解説です。

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

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

PR番号(#963)、ファイルパス、メソッド名などの固有名詞はすべて正確です。

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

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

記事タイトル「コードブロック先頭でのEnterキー操作を修正」は、PRのタイトル「Fix code block insert before」の内容を的確に反映しています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPRの範囲に限定されており、バージョン情報やリリース予定など、PR外の知識を持ち込んでいる箇所はありません。

時間表現の正確性 ✓ PASS

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

「〜修正されました」「〜問題がありました」といった過去形・完了形の表現が使われており、既に完了した変更であることを正しく伝えています。