挿入トリガ前の単語でメンションフィルタが正しく機能するように修正

basecamp/lexxy

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 の欠陥を根本的に解消した点が本変更の価値です。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
7e819292

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文がタイトル直下に配置され、背景・技術的変更・設計判断・まとめの各セクションがあり、まとめで結論が示されています。構成は『総論→各論→結論』の流れに従っています。

カスタムMarkdown構文 ⚠ WARNING

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

コードブロックは正しい ```diff 形式で記載されています。ファイル名付きハイライトは使用していないため問題なし。PRリンクは [PR #1135](URL) という形式で、#付きリンクとしては要件の [#123](URL) とは微妙に異なるため警告としました。

対象読者への適合性 ✓ PASS

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

専門的なエンジニア向けの内容で、前提知識があることを前提した記述となっており、初心者向けの余計な説明はありません。

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

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

各セクションは総論パラグラフ→各論パラグラフ→結論パラグラフの構成になり、段落はトピックセンテンスで始まり、1段落1トピックで、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

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

記事中の diff ブロックは提供された PR の diff と完全に一致しており、ファイル名・行番号・変更内容すべてが正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語(textBackUntil、#endOffsetAt、trigger など)は PR の記述と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

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

根本原因と修正内容の説明が PR の Description と一致しており、技術的に正確です。

事実の突合 ✓ PASS

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

記事の全ての主張は PR の情報(カード番号、BC4 の挙動、修正内容)で裏付けられており、捏造や推測はありません。

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

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

PR 番号 #1135、カード番号 9939048464 など数値は正しく記載されています。

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

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

記事タイトルは PR タイトル「Filter prompts by the word following an inserted trigger」の意味を正しく日本語に置き換えており、一致しています。

外部知識の正確性 ✓ PASS

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

バージョンサポート状況やリリース日程といった PR 之外の外部情報は含まれていません。

時間表現の正確性 ✓ PASS

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

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