Skip to content

Commit 4586839

Browse files
committed
fix(grep): align preview highlight stripping with Rust query parser
The Lua heuristic in highlight_grep_matches used a simple prefix check (^[*!/] or ^.) to strip constraints. This diverged from the Rust GrepConfig parser in several ways: - Multi-word queries like 'foo bar *.rs' only highlighted 'foo' - Constraint prefixes like type:rust were not stripped - Tokens starting with '.' were incorrectly treated as constraints - Escaped constraint tokens (e.g. \*.config) were not handled Replace the heuristic with _is_grep_constraint() that matches the Rust parser's actual GrepConfig rules: extensions (*.rs), path segments (/src/), exclusions (!test), type filters (type:rust), and path-oriented globs. Use all text parts joined with space for highlighting, matching grep_text() on the Rust side. Fixes #331
1 parent 29e6480 commit 4586839

1 file changed

Lines changed: 51 additions & 6 deletions

File tree

lua/fff/location_utils.lua

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,45 @@ function M.highlight_location(bufnr, location, namespace)
134134
return #extmarks > 0 and extmarks or nil
135135
end
136136

137+
--- Check if a token is a grep constraint (matches Rust GrepConfig parser rules).
138+
--- Keeps highlight stripping aligned with the actual query parser so preview
139+
--- highlights match the grep results.
140+
--- @param token string A single whitespace-delimited query token
141+
--- @return boolean
142+
function M._is_grep_constraint(token)
143+
if token == '' then return false end
144+
145+
-- Escaped tokens (leading \) are never constraints in the Rust parser
146+
if token:sub(1, 1) == '\\' and #token > 1 then return false end
147+
148+
local first = token:sub(1, 1)
149+
150+
-- Extension: *.rs, *.{ts,tsx} (but not bare "*" or "*.")
151+
if first == '*' then
152+
if token == '*' or token == '*.' then return false end
153+
if token:match('^%*%.') then return true end
154+
return false
155+
end
156+
157+
-- Exclusion: !foo, !*.rs
158+
if first == '!' and #token > 1 then return true end
159+
160+
-- Path segment with leading slash: /src/, /lib
161+
if first == '/' then return true end
162+
163+
-- Path segment with trailing slash: src/, lib/
164+
if token:sub(-1) == '/' then return true end
165+
166+
-- Glob: tokens containing / or brace expansion with letters like {ts,tsx}
167+
if token:find('/') then return true end
168+
if token:match('{%a.*,%a.*}') then return true end
169+
170+
-- Type filter: type:rust
171+
if token:match('^type:') then return true end
172+
173+
return false
174+
end
175+
137176
--- Highlight all occurrences of a grep pattern in the preview buffer.
138177
--- For plain text and regex modes: highlights every match on all loaded lines
139178
--- using Lua string.find with the query text.
@@ -178,16 +217,22 @@ function M.highlight_grep_matches(bufnr, location, namespace)
178217

179218
local query = location.grep_query
180219

181-
-- Extract the actual search text from the grep query (strip file constraints like *.rs /src/)
182-
-- The query parser uses space-separated tokens; the first non-constraint token is the pattern.
183-
-- Simple heuristic: strip tokens that look like constraints (start with *, /, or !)
184-
local search_text = query
220+
-- Extract search text by stripping constraint tokens, matching the Rust
221+
-- query parser's GrepConfig rules (extensions, path segments, globs,
222+
-- exclusions, type filters). Escaped leading backslash means "not a
223+
-- constraint" in the Rust parser, so we keep those tokens too.
185224
local parts = vim.split(query, '%s+')
186225
local text_parts = {}
187226
for _, part in ipairs(parts) do
188-
if part ~= '' and not part:match('^[%*!/]') and not part:match('^%.') then table.insert(text_parts, part) end
227+
if part ~= '' and not M._is_grep_constraint(part) then
228+
-- Strip leading backslash when it escapes a constraint trigger (*, /, !)
229+
-- so that e.g. \*.config highlights "*.config" not "\*.config"
230+
if #part > 1 and part:sub(1, 1) == '\\' and part:sub(2, 2):match('[%*!/]') then part = part:sub(2) end
231+
table.insert(text_parts, part)
232+
end
189233
end
190-
if #text_parts > 0 then search_text = text_parts[1] end
234+
local search_text = table.concat(text_parts, ' ')
235+
if search_text == '' then search_text = query end
191236

192237
if not search_text or search_text == '' then return nil end
193238

0 commit comments

Comments
 (0)