feat(desktop): Search タブの全文検索を VSCode 準拠に拡張 (#359 第2弾)#369
Conversation
PR #365 に続く第2段として、右サイドバー Search タブ (ripgrep ベースの ファイル内容検索) に VSCode で一般的に期待される機能を追加した。 ## 追加機能 - **Match whole word (`\b...\b`)**: ripgrep の `--word-regexp` を使い、 部分一致ではなく単語単位でのみマッチさせる。置換も `\b(?:query)\b` を適用するのでインライン置換が `foo` にマッチして `foobar` を 誤置換することはない - **Multiline regex**: 正規表現モード時のみ表示される。ripgrep の `--multiline --multiline-dotall` を使い、パターンが改行をまたいで マッチできるようにする (`.` も改行に一致) - **scopeId による cancellation 分離**: searchContent も searchFiles と 同じく `${rootPath}::${scopeId}` で AbortController のキーを分離。 Search タブ / Cmd+P / Files タブが同時に走っても互いを abort しない - **外部 AbortSignal のサポート**: `SearchContentOptions.signal` を追加 し、内部 controller と連結する。cleanup で listener も撤去 ## 挙動改善 - `searchContent` のデフォルト `includeHidden` を `true` → `false` に 変更。searchFiles と揃えつつ、UI は従来どおり明示的に `false` を 渡しているので既存パスに影響しない - ripgrep の出力に含まれる `./` プレフィックスを除去して relativePath を fast-glob 経路と揃えた (テストで fragile な `"./a.ts"` 比較を 避けるため) - `replaceContent` にも `wholeWord` / `multiline` を導入 ## UI - `SearchToolbar` に Whole Word / Multiline のトグルを追加 (Multiline は Regex モード時のみ表示) - 置換・ハイライトの全パスに wholeWord / multiline を propagate ## テスト (packages/workspace-fs/src/search.test.ts) 新規 7 件: - searchContent が .gitignore を尊重するか - includeHidden=true で ignored ファイルも出るか - wholeWord で部分一致を除外できるか - multiline で改行をまたいだ regex がマッチするか - scopeId 分離で並行呼び出しが互いを cancel しないか - replaceContent のキャプチャグループ置換 ($1 等) - replaceContent の wholeWord 置換 テストヘルパーも強化: - createTempRoot が fs.realpath を通す (macOS の /var → /private/var symlink で writeFile の root 境界チェックが誤検出するのを回避) - @vscode/ripgrep を devDependency に追加し、bundledRunRipgrep という テスト用ランナーを提供 (CI に rg が入っていない環境でも ripgrep 固有機能のテストが走るように) Refs: Issue #359 (第2弾)
📝 WalkthroughWalkthroughCI ワークフローに ripgrep バイナリのセットアップステップを追加し、デスクトップ検索機能にストリーミング API を実装。「全単語一致」「複数行」フラグの対応を追加し、UI コンポーネント、検索ユーティリティ、バックエンドサービス層全体で拡張。新しいテストカバレッジも含む。 Changes
Sequence DiagramsequenceDiagram
participant User as ユーザー
participant SearchView as SearchView<br/>(フロントエンド)
participant useContentSearch as useContentSearch<br/>(フック)
participant tRPC as tRPC<br/>Subscription
participant SearchService as SearchService<br/>(バックエンド)
participant Ripgrep as Ripgrep<br/>(バイナリ)
User->>SearchView: 検索クエリを入力<br/>(wholeWord/multiline)
SearchView->>useContentSearch: query & flags を渡す
useContentSearch->>tRPC: searchContentStream を<br/>subscribe開始
tRPC->>SearchService: 検索パラメータを転送
SearchService->>Ripgrep: ripgrep プロセスを spawn<br/>(--json フラグ付き)
loop ストリーミング処理
Ripgrep->>SearchService: JSON マッチ行を出力
SearchService->>SearchService: JSON をパース<br/>重複を排除
SearchService->>tRPC: FsContentMatch を emit
tRPC->>useContentSearch: マッチイベント受信
useContentSearch->>useContentSearch: searchResults に蓄積
useContentSearch->>SearchView: 結果を更新
SearchView->>User: マッチを段階的に表示
end
User->>SearchView: (リセット)<br/>キャンセル信号
SearchView->>useContentSearch: subscription クリア
useContentSearch->>SearchService: AbortSignal 発火
SearchService->>Ripgrep: プロセス終了 (SIGTERM)
Ripgrep->>SearchService: 終了
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e5358b9e93
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
VSCode Search View と UX を揃えるための2機能を追加した。どちらも本 PR の
既存機能 (whole word / multiline / scopeId 分離) と地続きの挙動改善。
## 1. ストリーミング検索結果表示
- workspace-fs に `searchContentStream` を追加。`spawn` ベースで ripgrep
`--json` を逐次 parse し、AsyncIterable で1件ずつ yield する
- `RunRipgrepStream` type と `spawnRipgrep` オプションを導入し、batched
版 `runRipgrep` と平行して注入できるようにした
- FsService.searchContentStream と FsSubscriptionMap に登録
- apps/desktop の tRPC に `searchContentStream` subscription を追加
(observable パターン、AGENTS.md の trpc-electron 制約準拠)
- apps/desktop で `@vscode/ripgrep` の bundled バイナリを spawn する
実装を `spawnBundledRipgrep` として追加
- useContentSearch を `useSubscription` ベースに書き換え、match が届く
たびに state に append。巨大リポでも最初の数件が即座に表示される
## 2. 置換プレビュー (適用前 inline diff)
- `buildLineReplacementSegments` を追加: 1 行に対しクエリを適用し
`{kind, text}[]` のセグメント列 (元テキスト / 削除対象 / 追加テキスト)
を返す純粋関数。regex の `.replace(replacement)` で `$1` 等の置換
パターンを backend と同じ semantics で解決
- SearchMatchItem に `replacement` prop を追加。replaceOpen + replacement
非空のとき、既存のハイライト描画を <del>(赤取消) + <ins>(緑) の
before/after 表示に切り替える
- SearchFileGroup / SearchTreeNode にも propagate
## テスト (packages/workspace-fs/src/search.test.ts)
新規 3 件 (24 tests total, all pass):
- searchContentStream が match を逐次 yield するか (5 ファイル確認)
- limit 指定で runaway queries を止められるか
- AbortSignal で途中停止しつつ runaway を防げるか
Refs: Issue #359 (第2弾の追加機能)
CI (Linux runner) は \`bun install\` 時に \`@vscode/ripgrep\` の postinstall を走らせないケースがあり、bin/rg バイナリが node_modules に存在しない。 その結果、bundledRgPath を spawn/execFile した瞬間に ENOENT になり、以下 のテストが失敗していた: - searchContent > multiline=true lets regex span newlines - searchContentStream > yields each match incrementally ... - searchContentStream > honors limit ... - searchContentStream > cancels streaming ... これらのテストは「ripgrep が実際に走った場合の挙動」を固定するもので、 fallback (searchContentWithScan) では再現できない。rg が無いなら本番 desktop でも @vscode/ripgrep が同梱されているので CI での実動は担保 できないが、ロジックの回帰検出には他のテストで十分。 bun:test の \`it.skipIf\` を使い、bundledRgPath が存在しない環境では 当該テストを skip する。type 定義 (bun-test.d.ts) にも追加。
\`bun install --frozen --ignore-scripts\` は安全のため全パッケージの postinstall を止めているので、\`@vscode/ripgrep\` が platform-specific な ripgrep バイナリをダウンロードする処理も走らない。結果として Test ジョブで bundledRgPath が ENOENT になり、streaming / multiline のテストが落ちて いた。 当該パッケージの postinstall だけを後から明示的に走らせるステップを 追加し、CI でも実際に ripgrep を呼ぶテストが通るようにする。
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8124e3a4f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
## P1: クエリ編集時にストリーム結果がリセットされない useContentSearch のリセット効果が deps=[] で一度しか走らず、検索テキストや オプションを編集しても古い match が残り、新しい stream event が append されてしまっていた。結果としてユーザーが "Replace match" を押すと古い 結果に対して置換を適用してしまう。クエリ identity を primitive string (subscriptionKey) に落とし込み、useEffect のキーとして使うことで確実に リセットされるよう修正。 ## P2: 長時間ストリーム中に完了ヒューリスティックが誤発火 完了判定の 400ms タイマーが isStreaming=true になった時点で開始される だけで、以降の onData では更新されなかった。400ms を超えて emit が続く 検索では途中で isStreaming が false に倒れ、UI が「完了」表示になる。 idleTimerRef + resetIdleTimer useCallback に書き換え、each event ごと にタイマーを再スタート。unmount 時の cleanup も追加。 ## P2: multi-line regex モードで単行 replace が空振り replaceSearchMatchesInLineInContent は1物理行しか扱えないため、 foo\nbar 系の multi-line 検索では match は出るが置換できずに "out-of-date" エラーになる。canInlineReplace を \`!(isRegex && multiline)\` で落とし、該当モードでは per-match の Replace ボタンを無効化 (Replace All はバックエンドが処理するので健在)。 Refs: PR #369 (Codex review comments)
Summary
PR #365 (Cmd+P / Files タブの .gitignore 尊重と MRU boost) の続編。右サイドバー Search タブ (ripgrep による全文検索 + 置換) を VSCode Search View と同等に拡張する。
Issue: #359
追加機能
コア
--word-regexp+ 置換/ハイライトで\b(?:...)\b--multiline --multiline-dotall${rootPath}::${scopeId}で AbortController キーを分離SearchContentOptions.signalを内部 controller に forwardストリーミング検索結果表示
workspace-fs/searchContentStreamを新設:spawnベースで ripgrep--jsonを逐次 parse、AsyncIterable で match を 1 件ずつ yieldRunRipgrepStream型とspawnRipgrepオプションを導入、batchedrunRipgrepと並行して注入可能FsSubscriptionMapにsearchContentStreamを追加@vscode/ripgrepbundled バイナリを spawn するspawnBundledRipgrepを追加useContentSearchをuseSubscriptionベースに書き換え、match 到着のたびに state に append。巨大リポでも数件目から UI に表示される置換プレビュー (適用前 inline diff)
buildLineReplacementSegments純粋関数: 1 行に対して{kind, text}[](元テキスト / 削除対象 / 追加テキスト) のセグメント列を返す。$1等のキャプチャグループは backend と同じ semanticsSearchMatchItemにreplacementprop 追加、<del>(赤取消し) +<ins>(緑) の diff 表示挙動改善
searchContentのデフォルトincludeHiddenをtrue→falseに変更 (searchFiles と揃える)./プレフィックスをnormalizePathForGlobで除去し、fast-glob 経路と relativePath を一致replaceContentにもwholeWord/multilineを追加@vscode/ripgrepの postinstall を明示実行するステップを追加 (--ignore-scripts下でも bundled rg が展開される)テスト (packages/workspace-fs/src/search.test.ts)
新規 10 件 (24 tests total, all pass):
.gitignore尊重 /includeHidden=trueで reveal / whole word / multiline / scopeId 分離$1) / whole wordテストヘルパー強化
createTempRootでfs.realpathを通す (macOS の/var → /private/varsymlink でwriteFileの root 境界チェックが誤検出するのを回避)@vscode/ripgrepをdevDependencyに追加、bundledRunRipgrep/bundledSpawnRipgrepを提供it.skipIf(!BUNDLED_RG_AVAILABLE)で ripgrep バイナリ未展開の環境では rg 依存テストを skipレビュー対応 (Codex)
subscriptionKey(primitive identity) をuseEffectdeps に入れて確実にリセットidleTimerRef+resetIdleTimerに書き換え、各onDataでタイマー再スタートcanInlineReplace = !(isRegex && multiline)で該当モードでは単行 Replace ボタンを無効化 (Replace All は健在)非スコープ
files.exclude/search.exclude設定連携 (そもそも Superset に settings 機構が薄い)Test plan
function\s+foo[\s\S]*returnのようなパターンがマッチすること.gitignoreされたファイルが結果に出ないこと$1を使った正規表現置換が動くことbun run typecheck/bun run lint/bun test全通過