Skip to content

Add global project content search - #4007

Closed
jakeleventhal wants to merge 3 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/add-global-project-search
Closed

Add global project content search#4007
jakeleventhal wants to merge 3 commits into
pingdotgg:mainfrom
jakeleventhal:t3code/add-global-project-search

Conversation

@jakeleventhal

@jakeleventhal jakeleventhal commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Screenshot 2026-07-15 at 9 32 39 AM Screenshot 2026-07-15 at 9 33 00 AM
  • add project-wide content search with Cmd/Ctrl+Shift+F
  • group matches by file with case-sensitive, whole-word, and regex options
  • syntax-highlight result lines and preserve precise match highlighting
  • open results at the matching line and reveal the selected file in the explorer

Why

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

  • adds shared contracts and RPC handling for content-search results
  • enables FFF content indexing and uses its native grep implementation with result limits and gitignore support
  • adds the default project.searchContents keybinding and search dialog
  • reuses the shared Shiki highlighter for language-aware result snippets
  • synchronizes opened search results with the right-side file explorer
  • handles punctuation-ended whole-word queries without including boundary characters in highlighted ranges

Validation

  • vp check
  • vp run typecheck
  • 101 focused tests across workspace search, contracts, keybindings, and right-panel state
  • punctuation whole-word regression tests
  • live browser verification of search options, syntax highlighting, result navigation, line reveal, and explorer selection

Note

[!NOTE]

Add project-wide content search with mod+shift+f keybinding

  • Adds a ProjectContentSearchDialog component accessible via mod+shift+f (outside terminal focus) that searches file contents across the active project with options for case sensitivity, whole word, and regex modes.
  • Adds a projects.searchContents WebSocket RPC endpoint backed by WorkspaceSearchIndex.searchContents, which delegates to FileFinder.grep and returns per-line matches with byte-accurate ranges; requires orchestration read scope.
  • Renders syntax-highlighted result lines with match highlighting via a new HighlightedSearchLine component and a shared getSyntaxHighlighterPromise utility.
  • Extends FileBrowserPanel to auto-reveal and scroll the currently open file into view when selectedPath or selectedPathRevealId props change.
  • Behavioral Change: FileFinder is 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.searchContents RPC and WorkspaceEntries.searchContentsWorkspaceSearchIndex.searchContents, which runs FileFinder.grep with limits, gitignore behavior, and structured errors (query text not leaked). Content indexing is turned on for workspace indexes (disableContentIndexing: false). Whole-word matching uses \b or non-word boundary fallbacks for punctuation-ended patterns; match ranges are mapped from byte offsets and trimmed for literal whole-word hits.

UI: ProjectContentSearchDialog debounces queries, groups results by file, syntax-highlights lines via shared getSyntaxHighlighterPromise, and opens the right panel at the matching line. FileBrowserPanel syncs 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.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 15e03ea3-4aaa-4df8-8801-4b8d5350cffc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 15, 2026
Comment thread apps/server/src/workspace/WorkspaceSearchIndex.ts Outdated
@jakeleventhal
jakeleventhal marked this pull request as ready for review July 15, 2026 13:43
Comment thread apps/web/src/components/ProjectContentSearchDialog.tsx
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
Comment thread apps/server/src/workspace/WorkspaceSearchIndex.ts
Comment thread apps/web/src/components/files/FilePreviewPanel.tsx
Comment thread apps/web/src/components/files/FileBrowserPanel.tsx
@macroscopeapp

macroscopeapp Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread apps/server/src/workspace/WorkspaceSearchIndex.ts
}

model.getItem(selectedPath)?.select();
model.scrollToPath(selectedPath, { focus: true, offset: "center" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6ae2b29. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)"}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@jakeleventhal
jakeleventhal force-pushed the t3code/add-global-project-search branch from 6ae2b29 to 663a8c8 Compare July 20, 2026 19:46
Comment thread apps/web/src/components/ProjectContentSearchDialog.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +338 to +342
finder.grep(searchQuery, {
mode: regexMode ? "regex" : "plain",
smartCase: !input.caseSensitive && !regexMode,
maxMatchesPerFile: input.limit,
pageSize: input.limit,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@jakeleventhal
jakeleventhal force-pushed the t3code/add-global-project-search branch from 663a8c8 to 35a1864 Compare July 25, 2026 01:29

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

❌ 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]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 35a1864. Configure here.

@jakeleventhal

Copy link
Copy Markdown
Contributor Author

Closing in favor of #4855

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant