プロンプトポップオーバーの自動非表示改善
Lexxy のメンション入力時に表示されるプロンプトポップオーバーは、カーソルがトリガー文字列から外れたときに閉じるべきですが、以前はトリガーの後方へ移動したケースで閉じませんでした。本稿ではその根本原因と修正内容、そして設計上の判断ポイントを解説します。
背景
既存実装では #addCursorPositionListener がカーソル位置を監視し、最後に出現したトリガー がカーソルより前にあるかどうかでポップオーバーの表示を決めていました。カーソルがトリガーの直前に戻った場合は閉じる動作が正しく機能していましたが、カーソルがトリガーの後方(テキストの右側)へ移動すると、トリガーは依然として「カーソルより前」にあると判定され、ポップオーバーが残り続けました。この挙動は Basecamp のカード #9941546721 で報告されたバグです。
技術的な変更
変更ポイント
-
判定ロジックを新メソッド
#cursorIsTypingSearchTermに委譲し、カーソルがトリガーと検索語の連続領域の末端にあるかどうかを評価するようにしました。 -
検索語抽出: カーソル直前のテキストから最後のトリガー位置を取得し、トリガー直後からカーソルまでの文字列を
searchTermとして切り出します。 -
改行・空白の判定:
supportsSpaceInSearchesが true の場合は改行のみをブレーク文字とし、false の場合は空白と改行の両方をブレーク文字として扱います。ブレーク文字が見つかればポップオーバーは非表示になります。 -
既存ロジックの簡素化: 以前の
lastTriggerIndexとtriggerEndIndexに基づく比較を削除し、#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 で判定を集中させたことで、コードの保守性と拡張性が向上しました。追加テストにより、既存の動作と新たなスペース対応検索語の挙動が網羅的に確認されています。