(q: string)
| 95 | }, [focused]); |
| 96 | |
| 97 | const handleQueryChange = (q: string) => { |
| 98 | setQuery(q); |
| 99 | if (timeoutRef.current) clearTimeout(timeoutRef.current); |
| 100 | abortRef.current?.abort(); |
| 101 | |
| 102 | if (!q.trim()) { |
| 103 | setMatches(m => (m.length ? [] : m)); |
| 104 | setIsSearching(false); |
| 105 | setTruncated(false); |
| 106 | return; |
| 107 | } |
| 108 | const controller = new AbortController(); |
| 109 | abortRef.current = controller; |
| 110 | setIsSearching(true); |
| 111 | setTruncated(false); |
| 112 | // Client-filter existing results while rg walks — keeps something on |
| 113 | // screen instead of flashing blank. rg results are merged in (deduped by |
| 114 | // file:line) rather than replaced, so the count is monotonic within a |
| 115 | // query: it only grows as rg streams, never dips to the first chunk's |
| 116 | // size. Narrowing (new query extends old): filter is exact — any line |
| 117 | // that matched the old -F -i literal contains the new one iff its text |
| 118 | // includes the new query lowered. Non-narrowing (broadening/different): |
| 119 | // filter is best-effort — may briefly show a subset until rg fills in |
| 120 | // the rest. |
| 121 | const queryLower = q.toLowerCase(); |
| 122 | setMatches(m => { |
| 123 | const filtered = m.filter(match => match.text.toLowerCase().includes(queryLower)); |
| 124 | return filtered.length === m.length ? m : filtered; |
| 125 | }); |
| 126 | |
| 127 | timeoutRef.current = setTimeout( |
| 128 | (query, controller, setMatches, setTruncated, setIsSearching) => { |
| 129 | // ripgrep outputs absolute paths when given an absolute target, so |
| 130 | // relativize against cwd to preserve directory context in the truncated |
| 131 | // display (otherwise the cwd prefix eats the width budget). |
| 132 | // relativePath() returns POSIX-normalized output so truncatePathMiddle |
| 133 | // (which uses lastIndexOf('/')) works on Windows too. |
| 134 | const cwd = getCwd(); |
| 135 | let collected = 0; |
| 136 | void ripGrepStream( |
| 137 | // -e disambiguates pattern from options when the query starts with '-' |
| 138 | // (e.g. searching for "--verbose" or "-rf"). See GrepTool.ts for the |
| 139 | // same precaution. |
| 140 | ['-n', '--no-heading', '-i', '-m', String(MAX_MATCHES_PER_FILE), '-F', '-e', query], |
| 141 | cwd, |
| 142 | controller.signal, |
| 143 | lines => { |
| 144 | if (controller.signal.aborted) return; |
| 145 | const parsed: Match[] = []; |
| 146 | for (const line of lines) { |
| 147 | const m = parseRipgrepLine(line); |
| 148 | if (!m) continue; |
| 149 | const rel = relativePath(cwd, m.file); |
| 150 | parsed.push({ ...m, file: rel.startsWith('..') ? m.file : rel }); |
| 151 | } |
| 152 | if (!parsed.length) return; |
| 153 | collected += parsed.length; |
| 154 | setMatches(prev => { |
nothing calls this directly
no test coverage detected