Repository navigation
feat: Find in Files — search file contents across the workspace from a ⌘⇧F window and open the hit at its line - #933
danyaberezun wants to merge 7 commits into
Conversation
832abc6 to
9166732
Compare
9166732 to
c3d5051
Compare
|
@jetbrains-air please review — all 13 threads addressed: every match git finds is kept (git config, Unicode case incl. non-ASCII pairs, whole-word retry, record cap, newline in names), the palette's file set (hidden folders) and no git in a chat folder, failures logged with codes, the window re-asks after a reconnect or failure and is a combobox, open at a line takes the latest hit and keeps a tab's reading position; lone-CR numbering documented as a v1 limit |
|
@jetbrains-air please review the current head |
c3d5051 to
fccf7f8
Compare
|
@jetbrains-air please review the current head |
…r file name, and keep git out of a chat
Find in Files dropped or garbled matches git had found, and reported the search complete:
- a user's `grep.column` / `grep.fullName` reshaped the output the parser reads; both are now
overridden with `-c` for the one command;
- the host recomputed ranges with a lowercase that changes length (`İ`), throwing the whole line away,
and a whole-word candidate rejected at a word edge skipped its overlapping neighbour; ranges now come
from a Unicode case-insensitive regex over the original text, retried one character later after a
rejected edge, as git's `-w` does;
- git folds only ASCII case under `LC_ALL=C`, so `äpfel` missed `Äpfel`; a case-insensitive query with
a non-ASCII cased letter is now sent as an extended regex spelling out each letter's case variants;
- a 64 KiB record cap cut long lines of small files before matching; a record now holds a whole line of
any file under the size limit;
- a newline in a file name split the record; the path and line number now end at their NULs and only
the text ends at the newline.
The search now shares the palette's file set: `node_modules`, `.hg` and `.svn` are excluded as
pathspecs (and dropped if they slip through), and a chat's folder is never handed to git: the walked
file list the palette already uses is read in-process, skipping oversized and binary files, so a chat
stays git-free and no enclosing or global ignore rule hides its files.
A failed search is logged through `logger("search")` with `TEXT_SEARCH_TIMEOUT` or
`TEXT_SEARCH_FAILED` (new `WsErrorCode`s, never git's stderr); git's no-match exit and cancellation
are not failures. The first match is sent at once and later ones within `flushMs`, even while git is
still scanning, and a client's search is cancelled when its socket closes.
…h, and announce its matches The window skipped a request identical to the last one asked, even when that search could never finish: its batches only reach the socket that asked, so a reconnect mid-search left it "Searching…", and a search that ended `failed` kept its key, so the same query was never asked again. The shell now hands the dialog the welcome generation: a request asked on an earlier connection that is still searching is asked again with a fresh `seq`, while a finished one keeps its results; a `failed` end forgets the key like a rejected request. `search.textResults` is never replayed to a late subscriber, as the contract says. The window is now a combobox over a listbox of one labelled group per file, with option ids for `aria-activedescendant` and a polite live status, so a screen reader hears the selected match and the counts while focus stays in the field.
…'s reading position after its reveal A second open at a line while the file's read was in flight returned at once, found no tab to reveal, and the first hit won; `openReadTab` now hands every caller the in-flight read, so each reveals after the tab is installed, in call order. A tab's reveal stayed on the tab and every remount scrolled back to the hit over the saved view state; the code views now report the tick they applied (`onRevealHandled`) and the pane clears it (`clearFileTabReveal`). Monaco reveals with an immediate scroll every time: a smooth scroll to a far line was cut short by the next layout.
…eeze the clock to prove silence The open-at-a-line scenario only revealed lines near the top, so a reveal that never reached a far line passed; a kept tab now opens at line 150, keeps the position the user scrolled to across a tab switch, and the older-host case plays its debounce out on a paused page clock instead of a 500 ms sleep.
fccf7f8 to
6b7a26d
Compare
|
@jetbrains-air please review the current head |
Problem
ThinkRail could find a file by name (the palette's
/scope) but nothing searched file contents: finding where a symbol or a string occurs in the workspace meant a terminal andgrep, and opening the hit at its line meant scrolling by hand. #644 asked for workspace-wide text search behind⌘⇧F— results grouped by file, one row per matching line, a click opening the file at that line,.git/ binary / oversized files skipped, a stated cap. Its proposed design is taken as the approved v1 and is not re-asked; the interview closed what it left open (own window with a code preview, smart-case +Aa/W, the palette's file set, 2 000-line cap,↵preview tab /⌘↵kept tab, the window restoring on the next⌘⇧F). Design in the originating chat'sTASK-find-in-files.md.Sixth PR of the global-search stack; stacked on #888 so it reuses what the palette's host search already owns (one search root, per-client
seqsuperseding, addressed pushes, the command catalog) instead of building a second copy.Rebased on
main(df7cb6d2) with the stack.TEXT_SEARCH_PROTOCOL_VERSIONis v102 (main v98, search v99, chats v100, continue v101).Approach
git grepon the host, streamed. Atextprovider in the server'ssearchmodule runs onegit -c grep.column=false -c grep.fullName=false grep --no-color -n -z -I --fixed-strings [-i] [-w] --untracked --exclude-standard -- . <hidden folders>per search in the same root the palette's/file names use (the active worktree; Project Home → the project's Default record). That is the palette's file set minus binaries (-I) —node_modules,.hgand.svnexcluded as pathspecs, as the palette hides them — with files over 1 MB dropped after onestat, lines cut to 400 characters around the first match, and the process killed at 2 000 matching lines (done.reason = "limit"). A chat without a project (Chats without a project: a built-in Chats container with one scratch folder per chat #887) is never a repository and never runs git: its folder's walked file list is read in-process. Rejected: ripgrep through pi'sensureTool(not a public export), bundling ripgrep (+~5 MB per artifact),rgfromPATH(hermeticPATH), an in-process TypeScript scan (re-reads every file per keystroke and re-implements.gitignore), and atextkind insidesearch.query(ranked ≤ 50-per-group groups cannot carry 2 000 lines grouped by file and cannot stream). On this repository a symbol search answers in ~40 ms.search.text { seq, query, context, caseSensitive, wholeWord }returns{ seq }immediately (or{ seq, superseded }for aseqolder than the client's latest); the matches follow asTextSearchBatches on the addressedsearch.textResultspush through asetSearchTextPublisherseamserver.tsinstalls — the terminal / MCP-login pattern, never a broadcast. A newersearch.textfrom the same client aborts the older process (cancelling the bounded stream kills the process tree —streamBoundedalready did that, so no newsignalplumbing was needed), and an aborted search emits nothing more.Aatoggle, makes the search exact; the host only ever sees an explicitcaseSensitive.Wadds-w; the host recomputes match ranges itself with a Unicode case-insensitive regex over the original text (git only reports lines), so highlighting is exact. Git folds only ASCII case underLC_ALL=C, so a case-insensitive query with a non-ASCII letter is sent as an extended regex spelling out each letter's case variants.⌘⇧F/Ctrl+Shift+F(one owner:useGlobalHotkeys; double Shift stays the palette's) opens a newfindInFilesmodule's dialog over the workbench: the query withAa/W(⌥C/⌥W), the scope and counts, results grouped by file with the match marked, a Monaco preview of the selected match below, and a keycap footer. The palette's>commands gain Find in files… with the chord as its display shortcut. The module keeps the query, toggles, results and selection in a module-level store so the next⌘⇧Frestores them (results from another workspace are hidden, not cleared, and the same query re-runs there); it imports neitherstorenorpanels—shell/FindInFilesHostinjects what↵/⌘↵do and how the preview renders. The window tellsready/offline/unsupportedapart, so against an older host it says the host needs an update rather than "offline", and the palette hides the command.ResourceViewProps.reveal({ line, column?, length?, tick }, exported asResourceRevealwithSOURCE_RENDERER_ID) is honoured by the code renderer's two views — Pierre scrolls with its virtualizer'srevealLineand marks the line; Monaco usesrevealLineNowon a fresh mount (the review-focus rule) and selectscolumn..column+length— and ignored by the rich ones.openTabs.openFileAtLineis the ordinaryopenFileInTabfollowed by the store's newrevealFileTab, which stamps the tab'sreveal(atickrepeats the same line) and switches the tab to the source renderer so a Markdown or HTML file opens at its line rather than as its rendered document.ResourcePreviewforwards the samereveal, so the window's preview and the opened tab share the behaviour.Keycaps,Hintand rangehighlightmoved tocomponents(Keycaps,KeyHint,highlightRanges) and both surfaces use them; the footer keys go throughcommands'shortcutKeys, so Windows/Linux readCtrl+↵where macOS reads⌘↵.Changes
packages/contracts:PROTOCOL_VERSION = 102=TEXT_SEARCH_PROTOCOL_VERSION;search.textmethod,search.textResultschannel;TextSearchMatch/TextSearchFile/TextSearchBatch/TextSearchEnd,TEXT_SEARCH_LIMITS(2 000 lines, 1 MB, 400 chars, 2–200 query chars); error codesTEXT_SEARCH_TIMEOUT/TEXT_SEARCH_FAILED; the v101 pin now asserts>=.packages/server:search/text.ts(args, NUL/newline record parser with a capped record buffer, ranges, line cut, the streaming search),SearchService.text(per-clientseq+AbortController,forgetClientaborts),host/search.tspublisher seam,host/server.ts(addressed send ≥ v102, cleared on close),handlers.ts. Tests:search/text.test.tson a temp repository — tracked and untracked files, ignored / binary / oversized files never match, the limit, case and whole-word flags reaching git, empty and non-git folders, abort emits nothing more, supersede per client.packages/resource-renderers:ResourceReveal,ResourceViewProps.reveal,ResourcePreview.reveal,SOURCE_RENDERER_ID; Pierre and Monaco views honour a reveal.apps/web: newfindInFiles/(SPEC, barrel,state.tsstore,runner.tscontroller,FindInFilesDialog);shell/FindInFilesHost.tsx,Mod+Shift+FinuseGlobalHotkeys,files.findInFilesinpaletteCatalog(gated onsupportsTextSearch+ a scope);storeFileTab.reveal+revealFileTab;panels/openTabs.openFileAtLine,FilePaneforwardsreveal;transport.supportsTextSearch;components/Keycaps.tsx,components/MatchHighlight.tsx(the palette now imports them); 25findInFiles.*keys +commands.files.findInFilesinen.json. Tests:findInFiles/state.test.ts,runner.test.ts(mocked transport), the hotkey table,revealFileTab.apps/web/src/findInFiles/SPEC.md;apps/web/SPEC.md(module table, edges),shell,commands(coverage table),store,panels,components,transport,search(server: the Text section, host composition, the fifth addressed channel),contracts,resource-renderers,e2e/SPEC.md; README gains the capability sentence.find-in-files.spec.ts(5 scenarios) — chord, grouping and counts, the preview,↵opening Monaco at the line with the match selected, the restored window; smart-case and the toggles, the limit banner, "Nothing found"; the palette command from Project Home searching the Default workspace and entering it, double Shift inert over the window; an older host (welcome rewritten to v101) hides the command and the window says so with nosearch.texton the wire.grep.*config overridden, Unicode case-insensitive ranges (a length-changingİno longer drops the line), the whole-word retry one character later, non-ASCII case variants asked of git underLC_ALL=C, a record holding a whole line of any kept file, a newline in a file name;TEXT_SEARCH_TIMEOUT/TEXT_SEARCH_FAILED; the first match sent at once; a search cancelled when its socket closes;Deferred, as #644 scopes v1: regex, include/exclude globs, replace, find inside rendered previews and diffs (#599), a permanent search panel, search history.
Screenshots
Before (the same workbench on the base —
⌘⇧Fdoes nothing there):After —
⌘⇧F,renameSessiontyped, the second match selected and previewed:After —
↵opened the file as a preview tab at line 1 with the match selected:Testing
On the final head (
6b7a26dba, rebased ontomaindf7cb6d2):bunx turbo run typecheck --filter='!@thinkrail/desktop'→ 24/24;bun run lint(one existing warning),check:deps,check:boundaries,check:seams,check:spec-surface,check:i18n,check:compiler→ green;apps/webbun test→ 2099 passbun run test→ 25/25 tasksbun run e2e(full no-agent suite, 8 shards) → all 699 passed. It ran in a private network namespace (loopback plus an empty bridge): this machine's Wi-Fi re-associates every ~5 minutes and Chromium then aborts in-flight loads withnet::ERR_NETWORK_CHANGED, which fails plainmaintoo.Earlier (
c3d5051df, on #8885b93701a4andmain940d15ca5; the same tree ase789849b0, where these runs were made):bunx turbo run typecheck --filter='!@thinkrail/desktop'→ 24/24;bun run lint(one existing warning),check:deps,check:seams,check:boundaries,check:spec-surface,check:i18n,check:compiler→ greenbun run test→ 25/25 tasks;packages/server/src/search/text.test.ts→ 14 pass, and each round-1 test fails with its fix removed (git config, hidden folders, the record cap, non-ASCII case variants, failure logging, a chat folder listed afresh); the webfindInFiles,openTabs,transportand store reveal tests likewise fail without their fixesbun run e2e e2e/find-in-files.spec.ts e2e/search-palette.spec.ts e2e/chats.spec.ts --repeat-each=3→ 87 passedbun run e2e(full no-agent suite, 8 shards) → all 692 passed. The suite ran in a private network namespace (loopback plus an empty bridge): this machine's Wi-Fi re-associates every ~5 minutes and Chromium then aborts in-flight page loads withnet::ERR_NETWORK_CHANGED, which fails plainmaintoo0d22c1256) too: the hit at line 150 never came into viewRelated issues
Closes #644. Part of the global-search stack (#804 → #805 → #806 → #887 → #888 → this).
Checklist
bun run lint,bun run typecheck,bun run testbun run e2e, orbun run e2e:fullwhen touching agent behavior)SPEC.md/ top-level specs updated to reflect any boundary, contract, or behavior change