プロンプトポップオーバーの自動非表示改善

basecamp/lexxy

Lexxy のメンション入力時に表示されるプロンプトポップオーバーは、カーソルがトリガー文字列から外れたときに閉じるべきですが、以前はトリガーの後方へ移動したケースで閉じませんでした。本稿ではその根本原因と修正内容、そして設計上の判断ポイントを解説します。

背景

既存実装では #addCursorPositionListener がカーソル位置を監視し、最後に出現したトリガー がカーソルより前にあるかどうかでポップオーバーの表示を決めていました。カーソルがトリガーの直前に戻った場合は閉じる動作が正しく機能していましたが、カーソルがトリガーの後方(テキストの右側)へ移動すると、トリガーは依然として「カーソルより前」にあると判定され、ポップオーバーが残り続けました。この挙動は Basecamp のカード #9941546721 で報告されたバグです。

技術的な変更

変更ポイント

  1. 判定ロジックを新メソッド #cursorIsTypingSearchTerm に委譲し、カーソルがトリガーと検索語の連続領域の末端にあるかどうかを評価するようにしました。
  2. 検索語抽出: カーソル直前のテキストから最後のトリガー位置を取得し、トリガー直後からカーソルまでの文字列を searchTerm として切り出します。
  3. 改行・空白の判定: supportsSpaceInSearches が true の場合は改行のみをブレーク文字とし、false の場合は空白と改行の両方をブレーク文字として扱います。ブレーク文字が見つかればポップオーバーは非表示になります。
  4. 既存ロジックの簡素化: 以前の lastTriggerIndextriggerEndIndex に基づく比較を削除し、#cursorIsTypingSearchTerm が true を返したときは何もしない(ポップオーバーを保持)とし、それ以外は #hidePopover を呼び出す形に統一しました。

変更前後のコード比較(インデントブロック)

// 変更前
if ($isTextNode(node) && offset > 0) {
  const fullText = node.getTextContent()
  const textBeforeCursor = fullText.slice(0, offset)
  const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger)
  const triggerEndIndex = lastTriggerIndex + this.trigger.length - 1

  // If trigger is not found, or cursor is at or before the trigger end position, hide popover
  if (lastTriggerIndex === -1 || offset <= triggerEndIndex) {
    this.#hidePopover()
  }
} else {
  this.#hidePopover()
}

// 変更後
if (this.#cursorIsTypingSearchTerm(node, offset)) {
  return
} else {
  this.#hidePopover()
}

// 新メソッド実装
#cursorIsTypingSearchTerm(node, offset) {
  if (!$isTextNode(node) || offset === 0) return false

  const textBeforeCursor = node.getTextContent().slice(0, offset)
  const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger)
  if (lastTriggerIndex === -1) return false

  const searchTerm = textBeforeCursor.slice(lastTriggerIndex + this.trigger.length)
  const breakPattern = this.supportsSpaceInSearches ? /\n/ : /[ \n]/
  return !breakPattern.test(searchTerm)
}

テスト追加

  • test/browser/tests/prompts/cursor_moves_away.test.js に、カーソルがトリガーの 後方前方 に移動した際にポップオーバーが閉じることを検証する2ケースを実装しました。
  • test/browser/tests/prompts/multiword_search_term.test.js では、supports-space-in-searches が有効なプロンプトでスペースを含む検索語が入力されたときにポップオーバーが保持されることを確認しています。

設計判断

後方互換性の確保

元の trigger キーだけで判定していたロジックを拡張し、新しい判定メソッド を内部で呼び出す形にしたため、外部からの API 変更はありません。既存の @# トリガーを使用したエディタはそのまま動作し続けます。

トリガー拡張の方針

supportsSpaceInSearches フラグを活用し、スペースを許容する検索語(例: person:)と許容しない検索語(例: @)を同一ロジックで分岐させる設計としました。これにより、将来的に別トリガーでスペースサポートを追加する際も、フラグを設定するだけで既存ロジックが再利用できます。

ロジックの単一責務化

カーソル位置判定を専用メソッドに切り出したことで、#addCursorPositionListener の可読性が向上し、テスト対象も明確になりました。テストはこのメソッドの振る舞いを直接検証できるため、回帰防止が容易です。

まとめ

本修正は、Lexxy のプロンプトポップオーバーが「カーソルがトリガー文字列の前後どちらに移動しても」適切に閉じるようロジックを刷新したものです。検索語の連続性とブレーク文字の判定に注目し、#cursorIsTypingSearchTerm で判定を集中させたことで、コードの保守性と拡張性が向上しました。追加テストにより、既存の動作と新たなスペース対応検索語の挙動が網羅的に確認されています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
24ed47f9

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

リード文、背景(Context)、技術的な変更(Technical Detail)、設計判断(任意)、まとめ(Conclusion)が揃っており、3部構成が明確に示されている。

カスタムMarkdown構文 ✓ PASS

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

コードブロックは正しい fenced format(diff, markdown)で記述され、GitHubリンクは PR #1133 の形式で正しくリンク化されている。ファイル名付きハイライトは使用していないが誤りではない。

対象読者への適合性 ✓ PASS

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

エンジニア向けの技術的記述が中心で、初心者向けの過度な解説はなく、対象読者に適合している。

パラグラフ・ライティング ⚠ WARNING

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

各セクションの冒頭は要点を示すが、セクションごとの結論パラグラフが明示的に欠けている箇所がある。全体としては読みやすいが、総論→各論→結論の明確な区切りが不足している。

Diff内容との照合 ✓ PASS

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

記事中のコードスニペットは提供された Diff と内容・ファイル名・ロジックが一致しており、改変や省略がなく正確に反映されている。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「trigger」「search term」「supportsSpaceInSearches」「#cursorIsTypingSearchTerm」などの用語は PR と一致し、誤用は見られない。

説明の技術的正確性 ✓ PASS

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

変更の根本原因、修正内容、テスト追加の説明が PR の記述と合致しており、技術的に正確である。

事実の突合 ✓ PASS

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

記事の主張はすべて PR のタイトル・説明・Diff に裏付けられており、推測や捏造はない。

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

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

PR番号 #1133、カード番号 9941546721 などの数値は正確に記載されている。

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

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

記事タイトルは PR の趣旨(ポップオーバーの自動非表示改善)を適切に要約しており、内容と矛盾しない。

外部知識の正確性 ✓ PASS

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

LTS やリリース日程など、PR に記載のない外部知識は一切含まれていない。

時間表現の正確性 ✓ PASS

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

時間表現の歪曲はなく、PR の記述と一致している。