挿入トリガ前の単語でメンションフィルタが正しく機能するように修正
Lexxy のメンション機能で、既存の単語の前に @ を挿入した際にフィルタが空文字列になるバグが修正されました。これにより、BC4 と同様に @ 直後の単語がクエリとして扱われ、期待通りの絞り込みが行われます。
背景
BC5 (Lexxy) では、ユーザーが "Jack" という名前を書いた後にカーソルを単語の先頭へ移動し @ を挿入すると、すべてのプロジェクトメンバーが表示されました。これは、textBackUntil がカーソル以前のテキストだけを対象にクエリを生成していたため、@ の直後に続く単語が無視されたことが原因です。
この挙動は Basecamp のカード #9939048464 で報告され、BC4 では期待通りに動作していたことから、挙動の統一が求められました。結果として、フィルタクエリが空になることで全員が返され、ユーザー体験が低下していました。
結論:既存のロジックはカーソル位置より前のテキストだけを参照していたため、@ を単語の前に挿入したケースを網羅できていませんでした。
技術的な変更
主な修正は textBackUntil の実装を拡張し、トリガから次の空白までの範囲を取得するようにした点です。新たにプライベートメソッド #endOffsetAt を導入し、カーソル位置から最初の空白文字までのインデックスを計算します。
@@ -197,17 +197,27 @@ export default class Contents {
const fullText = anchorNode.getTextContent()
const offset = anchor.offset
-
- const textBeforeCursor = fullText.slice(0, offset)
-
- const lastIndex = textBeforeCursor.lastIndexOf(string)
+ const lastIndex = fullText.slice(0, offset).lastIndexOf(string)
if (lastIndex !== -1) {
- result = textBeforeCursor.slice(lastIndex + string.length)
+ result = fullText.slice(lastIndex + string.length, this.#endOffsetAt(fullText, offset))
}
})
@@
}
+ // The query runs from the trigger up to the next whitespace, even when the
+ // cursor sits inside an existing word — inserting "@" before "Jack" must
+ // filter by "Jack" rather than treating the prompt as empty.
+ #endOffsetAt(fullText, cursorOffset) {
+ const whitespaceOffset = fullText.slice(cursorOffset).search(/\s/)
+ if (whitespaceOffset === -1) {
+ return fullText.length
+ } else {
+ return cursorOffset + whitespaceOffset
+ }
+ }
replaceTextBackUntil も同様に更新され、置換開始位置を #findReplacementStart に変更し、トリガとその後ろの単語全体を対象にしました。置換ロジックは lastIndex(=トリガ開始位置)から stringToReplace の長さ分だけではなく、実際に取得したクエリ全体を置換対象とします。
@@ -238,10 +248,10 @@ export default class Contents {
const { anchorNode, offset } = this.#getTextAnchorData()
if (!anchorNode) return
-
- const lastIndex = this.#findLastIndexBeforeCursor(anchorNode, offset, stringToReplace)
+ const lastIndex = this.#findReplacementStart(anchorNode, offset, stringToReplace)
if (lastIndex === -1) return
-
- this.#performTextReplacement(anchorNode, selection, offset, lastIndex, replacementNodes)
+ this.#performTextReplacement(anchorNode, selection, lastIndex, stringToReplace, replacementNodes)
}
テストとして at_inserted_before_word.test.js が追加され、@ を単語前に挿入した際のフィルタ結果と置換動作の両方を検証しています。テストは Playwright を用いたブラウザベースで、メンションメニューが正しい人数・名前を表示し、選択後に @ が除去されて期待通りのテキストが残ることを確認しています。
結論:クエリ取得ロジックの拡張と置換ロジックの調整により、@ 前に単語が存在するケースでも正しい絞り込みと置換が行われるようになりました。
設計判断
今回の修正は既存 API をできるだけそのまま残しつつ、#endOffsetAt という小さなユーティリティを追加するというミニマルな変更に留めました。これにより、他のプロンプトやエディタ機能への影響は最小限に抑えられ、既存コードベースとの後方互換性が確保されています。
置換ロジックはトリガから取得したクエリ全体を対象にすることで、replaceTextBackUntil が「@ だけ」ではなく「@+単語」全体を置き換えるよう統一しました。この設計は UI と内部テキスト操作の整合性を高め、将来的に同様のトリガ拡張が必要になった場合の拡張ポイントを明確にしています。
結論:変更点は局所的である一方、ユーザー体験の根本的な不整合を解消し、コードベースの一貫性を保ちつつ拡張しやすい構造を実現しています。
まとめ
textBackUntil がトリガ後の単語まで検索できるようになり、replaceTextBackUntil がその全体を正しく置換するよう修正されたことで、Lexxy のメンションフィルタは BC4 と同等の動作を示すようになりました。最小限のコード追加で既存機能を保護しつつ、UX の欠陥を根本的に解消した点が本変更の価値です。