Add global project content search - #4007
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 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 |
ApprovabilityVerdict: Needs human review This PR adds a new global project content search feature with new UI components, backend search functionality, and RPC endpoints. Multiple unresolved review comments (including a high-severity race condition) require human attention before merging. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f52cf27096
ℹ️ 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".
| : { start: start + queryIndex, end: start + queryIndex + input.query.length }; | ||
| }; | ||
| return { | ||
| matches: result.value.items.map((match) => ({ |
There was a problem hiding this comment.
Suppress fallback matches for invalid regexes
The fff grep API reports regexFallbackError after falling back to literal matching, but this code still maps result.value.items into normal search results. With regex enabled, an invalid pattern like foo[ can therefore render and open literal foo[ hits under the dialog's “Invalid regular expression” banner, which is not the requested regex result set. Treat regexFallbackError as an empty/error result before exposing matches.
Useful? React with 👍 / 👎.
| ); | ||
|
|
||
| return { | ||
| matches: result.data?.matches ?? EMPTY_CONTENT_MATCHES, |
There was a problem hiding this comment.
Hide stale matches while a new query is debouncing
When the input changes from one non-empty query to another, debouncedQuery still points at the previous request for 120ms, but this returns the previous result.data?.matches unconditionally. During that window the dialog can show old rows while saying it is searching the new text, and pressing Enter opens matches[selectedIndex] from the previous search. Gate the returned data on query === debouncedQuery or clear matches while the new query is pending.
Useful? React with 👍 / 👎.
| let ancestorPath = ""; | ||
| for (const segment of segments.slice(0, -1)) { | ||
| ancestorPath = ancestorPath ? `${ancestorPath}/${segment}` : segment; | ||
| const item = model.getItem(ancestorPath); |
There was a problem hiding this comment.
Use directory item ids when expanding ancestors
For a nested file opened from search while its parent folders are collapsed, this lookup uses ids like src or src/components, but the tree is populated with directory paths ending in / via treePath (for example src/). Those ancestor items are not found or expanded, so the subsequent select/scroll can target a hidden file and the explorer does not reveal the search result.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| model.getItem(selectedPath)?.select(); | ||
| model.scrollToPath(selectedPath, { focus: true, offset: "center" }); |
There was a problem hiding this comment.
Explorer reveal steals editor focus
Medium Severity
After opening a search hit, the file-tree sync calls scrollToPath with focus: true, which moves keyboard focus into the explorer instead of leaving it in the file preview the user just opened.
Reviewed by Cursor Bugbot for commit 6ae2b29. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ae2b295f3
ℹ️ 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".
| // match (e.g. query "foo-" → \b(?:foo-)\b never matches "foo- "). Fall back to | ||
| // (?:^|\W)/(?:$|\W) for non-word edges, same as the literal whole-word path. | ||
| const wrapWholeWord = (pattern: string) => | ||
| `${wordCharacter.test(firstCharacter) ? "\\b" : "(?:^|\\W)"}(?:${pattern})${wordCharacter.test(lastCharacter) ? "\\b" : "(?:$|\\W)"}`; |
There was a problem hiding this comment.
Use non-consuming whole-word boundaries
When whole-word search is enabled for a query that both starts and ends with non-word characters, these fallback boundaries consume the separator characters around each hit. In a line like -foo- -foo-, searching whole-word for -foo- matches the first hit plus the trailing space, then the regex engine resumes at the second - where the leading (?:^|\W) can no longer see the separator, so the second hit is skipped. Use lookaround-style/non-consuming boundaries or otherwise avoid consuming delimiters before passing the pattern to grep.
Useful? React with 👍 / 👎.
|
|
||
| export const ProjectSearchContentsInput = Schema.Struct({ | ||
| cwd: TrimmedNonEmptyString, | ||
| query: TrimmedNonEmptyString.check(Schema.isMaxLength(256)), |
There was a problem hiding this comment.
Preserve literal whitespace in content queries
For content search, leading/trailing spaces are part of the string being searched, but this schema decodes the query through TrimmedNonEmptyString (and the web hook trims before sending), so a query like TODO or foo is silently sent as TODO/foo and returns broader, incorrectly highlighted matches. Use a schema that rejects all-whitespace queries without transforming the value, and stop trimming in the caller.
Useful? React with 👍 / 👎.
6ae2b29 to
663a8c8
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 663a8c8840
ℹ️ 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".
| finder.grep(searchQuery, { | ||
| mode: regexMode ? "regex" : "plain", | ||
| smartCase: !input.caseSensitive && !regexMode, | ||
| maxMatchesPerFile: input.limit, | ||
| pageSize: input.limit, |
There was a problem hiding this comment.
Keep search matches within previewable bytes
When FFF uses its default grep file-size cap (10 MB), this can return matches from bytes that the app cannot display: WorkspaceFileSystem only reads the first 1 MB for ProjectReadFileResult, and FilePreviewPanel then renders a truncated preview and clamps reveal requests to the loaded content. In files between 1 MB and 10 MB with a match after the first megabyte, selecting a search result opens the file but reveals the wrong/truncated location. Cap maxFileSize to the preview read limit or make the preview load enough content for searched matches.
Useful? React with 👍 / 👎.
| finder.grep(searchQuery, { | ||
| mode: regexMode ? "regex" : "plain", | ||
| smartCase: !input.caseSensitive && !regexMode, | ||
| maxMatchesPerFile: input.limit, |
There was a problem hiding this comment.
Report truncation when one file hits the match cap
When a single file has more than limit matching lines, this per-file cap silently drops the rest, but truncated is computed only from nextCursor. If that capped file is the last or only eligible file, the cursor can still be null, so the dialog reports an untruncated result set even though matches were omitted. Avoid a separate per-file cap here or include this cap in the truncation signal.
Useful? React with 👍 / 👎.
- Search indexed project files with regex and match options - Open matches in the file preview at the selected line
When useRegex and wholeWord are both enabled, wrap patterns with the same edge-aware boundaries as literal whole-word search so queries like "foo-" still match.
663a8c8 to
35a1864
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 35a1864. Configure here.
|
|
||
| useEffect(() => { | ||
| setSelectedIndex(0); | ||
| }, [matches]); |
There was a problem hiding this comment.
Selection resets on revalidation
Medium Severity
The dialog resets the highlighted result to the first match whenever the matches array reference changes. Background refetches from the content-search query (5s stale cache) return a new array with the same hits, so keyboard or mouse selection jumps back to result zero while the dialog stays open.
Reviewed by Cursor Bugbot for commit 35a1864. Configure here.
|
Closing in favor of #4855 |


Summary
Cmd/Ctrl+Shift+FWhy
T3 Code could locate files by name, but it did not provide a fast way to search file contents across an entire project. This adds a VS Code-style global search flow for navigating directly from a text match to its source file.
Implementation
project.searchContentskeybinding and search dialogValidation
vp checkvp run typecheckNote
[!NOTE]
Add project-wide content search with
mod+shift+fkeybindingProjectContentSearchDialogcomponent accessible viamod+shift+f(outside terminal focus) that searches file contents across the active project with options for case sensitivity, whole word, and regex modes.projects.searchContentsWebSocket RPC endpoint backed byWorkspaceSearchIndex.searchContents, which delegates toFileFinder.grepand returns per-line matches with byte-accurate ranges; requires orchestration read scope.HighlightedSearchLinecomponent and a sharedgetSyntaxHighlighterPromiseutility.FileBrowserPanelto auto-reveal and scroll the currently open file into view whenselectedPathorselectedPathRevealIdprops change.FileFinderis now created with content indexing enabled (disableContentIndexing: false).📊 Macroscope summarized 35a1864. 19 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.
Note
Medium Risk
Enabling FileFinder content indexing changes workspace index cost/behavior for all projects; grep runs locally on the workspace with read-scope auth only, but search still touches full tree contents.
Overview
Adds VS Code-style global content search for the active project via
mod+shift+f(project.searchContents), opening a dialog with case-sensitive, whole-word, and regex options.Backend: New
projects.searchContentsRPC andWorkspaceEntries.searchContents→WorkspaceSearchIndex.searchContents, which runsFileFinder.grepwith limits, gitignore behavior, and structured errors (query text not leaked). Content indexing is turned on for workspace indexes (disableContentIndexing: false). Whole-word matching uses\bor non-word boundary fallbacks for punctuation-ended patterns; match ranges are mapped from byte offsets and trimmed for literal whole-word hits.UI:
ProjectContentSearchDialogdebounces queries, groups results by file, syntax-highlights lines via sharedgetSyntaxHighlighterPromise, and opens the right panel at the matching line.FileBrowserPanelsyncs selection/reveal when a file is opened from search. Shortcut handling treats the search dialog like the command palette for “palette open” checks.Reviewed by Cursor Bugbot for commit 35a1864. Bugbot is set up for automated code reviews on this repo. Configure here.