Skip to content

Commit 7a5598c

Browse files
committed
perf(tags): improve parsing logic (#2657)
1 parent 1e866cc commit 7a5598c

8 files changed

Lines changed: 193 additions & 172 deletions

File tree

lua/fzf-lua/actions.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,11 @@ M.vimcmd_entry = function(vimcmd, selected, opts, bufedit)
268268
pcall(utils.jump_to_location, { uri = entry.uri, range = entry.range }, "utf-16",
269269
opts.reuse_win)
270270
elseif entry.line == 0 and entry.ctag then
271-
vim.api.nvim_win_set_cursor(0, { 1, 0 })
272-
vim.fn.search(entry.ctag, "W")
271+
local re = utils.ctag_to_magic(entry.ctag)
272+
if utils.vim_regex(re, opts) then
273+
vim.api.nvim_win_set_cursor(0, { 1, 0 })
274+
vim.fn.search(re, "W")
275+
end
273276
elseif not opts.no_action_set_cursor and entry.line > 0 or entry.col > 0 then
274277
-- Make sure we have valid line/column
275278
-- e.g. qf lists from files (no line/col), dap_breakpoints

lua/fzf-lua/defaults.lua

Lines changed: 83 additions & 92 deletions
Large diffs are not rendered by default.

lua/fzf-lua/make_entry.lua

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -703,32 +703,19 @@ M.file = function(x, opts)
703703
end
704704

705705
M.tag = function(x, opts)
706-
local name, file, text = x:match("([^\t]+)\t([^\t]+)\t(.*)")
707-
if not file or not name or not text then return x end
708-
text = text:match([[(.*);"]]) or text -- remove ctag comments
709-
-- unescape ctags special chars
710-
-- '\/' -> '/'
711-
-- '\\' -> '\'
712-
for _, s in ipairs({ "/", "\\" }) do
713-
text = text:gsub([[\]] .. s, s)
714-
end
706+
local name, file, excmd = x:match("([^\t]+)\t([^\t]+)\t(.*)")
707+
if not file or not name or not excmd then return x end
708+
local line, tag = path.parse_ctag_excmd(excmd, true)
715709
-- different alignment fmt if string contains ansi coloring
716710
-- from rg/grep output when using `tags_grep_xxx`
717-
local align = utils.has_ansi_coloring(name) and 47 or 30
718-
local line, tag = text:match("(%d-);?(/.*/)")
719-
if not tag then
720-
-- lines with a tag located solely by line number contain nothing but the
721-
-- number at this point (e.g. using "ctags -R --excmd=number")
722-
line = text:match("%d+")
723-
end
724-
line = line and #line > 0 and tonumber(line)
725-
return string.format("%-" .. tostring(align) .. "s%s%s%s: %s",
711+
local align = 24
712+
if utils.has_ansi_coloring(name) then align = align + 17 end
713+
return string.format("%-" .. tostring(align) .. "s\t%s\t%s%s",
726714
name,
727-
utils.nbsp,
728715
M.file(file, opts),
729-
not line and "" or ":" .. utils.ansi_codes.green(tostring(line)),
730-
utils.ansi_codes.blue(tag)
731-
), line
716+
line and utils.ansi_codes.green(tostring(line)) .. ";" or "",
717+
tag and utils.ansi_codes.blue(tag) or ""
718+
)
732719
end
733720

734721
M.git_status = function(x, opts)

lua/fzf-lua/path.lua

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -377,21 +377,43 @@ function M.lengthen(path)
377377
or string.format("<glob expand failed for '%s'>", glob_expr)
378378
end
379379

380-
function M.entry_to_ctag(entry, noesc)
381-
local ctag = entry:match("%:.-[/\\]^?\t?(.*)[/\\]")
382-
-- if tag name contains a slash we could
383-
-- have the wrong match, most tags start
384-
-- with ^ so try to match based on that
385-
ctag = ctag and ctag:match("[/\\]^(.*)") or ctag
386-
if ctag and not noesc then
387-
-- required escapes for vim.fn.search()
388-
-- \ ] ~ *
389-
ctag = ctag:gsub("[\\%]~*]",
390-
function(x)
391-
return "\\" .. x
392-
end)
393-
end
394-
return ctag
380+
---@param excmd string
381+
---@param do_not_strip_slashes boolean?
382+
---@return number?, string?
383+
function M.parse_ctag_excmd(excmd, do_not_strip_slashes)
384+
-- remove anything after excmd ;" postfix
385+
excmd = excmd:gsub([[;".-$]], ";")
386+
-- test for "--excmd={number|combine}"
387+
local line = excmd:match("^(%d+);")
388+
-- adjust for line, position cursor after ;
389+
if line then excmd = excmd:sub(#line + 2) end
390+
-- extract ctag
391+
local tag = do_not_strip_slashes
392+
and excmd:match("/.*/")
393+
or excmd:match("/(.*)/")
394+
return tonumber(line), tag
395+
end
396+
397+
---@param raw string
398+
---@param opts table
399+
---@return fzf-lua.path.Entry
400+
function M.entry_to_ctag(raw, opts)
401+
assert(opts._ctag)
402+
local _, file, excmd = raw:match("([^\t]+)\t([^\t]+)\t(.*)")
403+
if not file or not excmd then return {} end
404+
file = file:match(".*" .. utils.nbsp .. "(.+)$") or file
405+
local cwd = opts.cwd or opts._cwd
406+
if cwd and not M.is_absolute(file) then file = M.join({ cwd, file }) end
407+
if opts.path_shorten then file = M.lengthen(file) end
408+
local line, tag = M.parse_ctag_excmd(excmd)
409+
return {
410+
path = file,
411+
ctag = tag,
412+
line = line or 0,
413+
col = 0,
414+
stripped = string.format("%s:%s %s", file, line and line .. ":" or "", tag or ""),
415+
debug = opts.debug and raw:match("^%[DEBUG]") and raw or nil,
416+
} ---@as fzf-lua.path.Entry
395417
end
396418

397419
---@param entry string
@@ -437,6 +459,7 @@ end
437459
function M.entry_to_file(entry, opts, force_uri)
438460
-- NOTE: see note in meta.lua:global regarding alt options
439461
opts = opts and opts.__alt_opts or opts or {}
462+
assert(opts)
440463
if opts._fmt then
441464
if type(opts._fmt._from) == "function" then
442465
entry = opts._fmt._from(entry, opts)
@@ -450,6 +473,10 @@ function M.entry_to_file(entry, opts, force_uri)
450473
if opts.render_crlf then
451474
entry = entry:gsub("", "\n"):gsub("", "\r")
452475
end
476+
-- ctag processing is done separately
477+
if opts._ctag then
478+
return M.entry_to_ctag(entry, opts)
479+
end
453480
local stripped, idx = (function()
454481
-- Returns the first viable path:line?:col? + rest of line
455482
-- stripping until the last occurrence of utils.nbsp may err
@@ -556,7 +583,6 @@ function M.entry_to_file(entry, opts, force_uri)
556583
line = utils.tointeger(type(opts.line_query) == "function" and
557584
(opts.line_query(FzfLua.get_info().query)) or line) or 0,
558585
col = utils.tointeger(col) or 0,
559-
ctag = opts._ctag and M.entry_to_ctag(stripped) or nil,
560586
debug = opts.debug and entry:match("^%[DEBUG]") and entry or nil,
561587
}
562588
end

lua/fzf-lua/previewer/builtin.lua

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,7 @@ function Previewer.buffer_or_file:set_cursor_hl(entry)
14531453

14541454
-- If called from tags previewer, can happen when using ctags cmd
14551455
-- "ctags -R --c++-kinds=+p --fields=+iaS --extras=+q --excmd=combine"
1456+
-- vim.regex is always magic, see `:help vim.regex`
14561457
regex = regex and #regex > 0 and utils.regex_to_magic(regex)
14571458
or entry.ctag and utils.ctag_to_magic(entry.ctag)
14581459

@@ -1481,20 +1482,14 @@ function Previewer.buffer_or_file:set_cursor_hl(entry)
14811482
-- If regex is available (grep/lgrep), match on current line
14821483
if regex and hls.search then
14831484
local regex_start, regex_end
1484-
-- vim.regex is always magic, see `:help vim.regex`
1485-
---@diagnostic disable-next-line: param-type-mismatch
1486-
local reg = vim.F.npcall(vim.regex, regex)
1485+
local reg = utils.vim_regex(regex, { silent = true })
14871486
if reg then
14881487
if regex ~= regex:lower() then
14891488
regex_start, regex_end = reg:match_line(buf, lnum - 1, col - 1)
14901489
else
14911490
local line = api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] or ""
14921491
regex_start, regex_end = reg:match_str(line:sub(col):lower())
14931492
end
1494-
elseif self.opts.silent ~= true then
1495-
utils.warn(
1496-
[[Unable to init vim.regex with "%s", %s. . Add 'silent=true' to hide this message.]],
1497-
regex, reg)
14981493
end
14991494
if regex_start and regex_end then
15001495
extmark = api.nvim_buf_set_extmark(buf, self.ns_previewer, lnum - 1, regex_start + col - 1, {
@@ -1766,7 +1761,9 @@ function Previewer.tags:set_cursor_hl(entry)
17661761
-- didn't reload the buffer (same file)
17671762
api.nvim_win_set_cursor(0, { 1, 0 })
17681763
fn.clearmatches()
1769-
local ctag = assert(entry.ctag)
1764+
local ctag = utils.ctag_to_magic(assert(entry.ctag))
1765+
-- test the regex so we can alert the user of the search fail
1766+
if not utils.vim_regex(ctag, self.opts) then return end
17701767
fn.search(ctag, "W")
17711768
if self.win.hls.search then
17721769
fn.matchadd(self.win.hls.search, ctag)

lua/fzf-lua/previewer/fzf.lua

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -182,34 +182,26 @@ end
182182

183183
local grep_tag = function(file, tag)
184184
local line = 1
185-
local filepath = file
186-
local pattern = utils.rg_escape(vim.trim(tag))
187-
if not pattern or not filepath then return line end
188-
local grep_cmd = vim.fn.executable("rg") == 1
189-
and { "rg", "--line-number" }
190-
or { "grep", "-n", "-P" }
185+
if not tag or not file then return line end
186+
local pattern = utils.ctag_escape(tag)
191187
-- ctags uses '$' at the end of short patterns
192188
-- 'rg|grep' does not match these properly when
193189
-- 'fileformat' isn't set to 'unix', when set to
194190
-- 'dos' we need to prepend '$' with '\r$' with 'rg'
195-
-- it is simpler to just ignore it completely.
196-
--[[ local ff = fileformat(filepath)
197-
if ff == 'dos' then
198-
pattern = pattern:gsub("\\%$$", "\\r%$")
199-
else
200-
pattern = pattern:gsub("\\%$$", "%$")
201-
end --]]
202191
-- equivalent pattern to `rg --crlf`
203192
-- see discussion in #219
204-
pattern = pattern:gsub("\\%$$", "\\r??%$")
193+
pattern = pattern:gsub("%$$", "\\r??%$")
194+
local grep_cmd = vim.fn.executable("rg") == 1
195+
and { "rg", "--line-number" }
196+
or { "grep", "-n", "-P" }
205197
local cmd = utils.tbl_deep_clone(grep_cmd)
206198
table.insert(cmd, pattern)
207-
table.insert(cmd, filepath)
199+
table.insert(cmd, file)
208200
local out, rc = utils.io_system(cmd)
209201
if rc == 0 then
210202
line = utils.tointeger(out:match("[^:]+")) or 1
211203
else
212-
utils.warn(("previewer: unable to find pattern '%s' in file '%s'"):format(pattern, file))
204+
utils.warn("Unable to match pattern '%s' in file '%s'", pattern, file)
213205
end
214206
return line
215207
end
@@ -228,16 +220,9 @@ function Previewer.cmd_async:parse_entry_and_verify(entrystr)
228220
-- make relative for bat's header display
229221
local filepath = path.relative_to(entry.bufname or entry.path or "", utils.cwd())
230222
if self.opts._ctag then
231-
-- NOTE: override `entry.ctag` with the unescaped version
232-
entry.ctag = path.entry_to_ctag(entry.stripped, true)
233-
if not tonumber(entry.line) or tonumber(entry.line) < 1 then
223+
if not entry.line or tonumber(entry.line) < 1 then
234224
-- default tags are without line numbers
235-
-- make sure we don't already have line #
236-
-- (in the case the line no. is actually 1)
237-
local line = entry.stripped:match("[^:]+(%d+):")
238-
if not line and entry.ctag then
239-
entry.line = grep_tag(filepath, entry.ctag)
240-
end
225+
entry.line = grep_tag(filepath, entry.ctag)
241226
end
242227
end
243228
local errcmd = nil
@@ -316,7 +301,7 @@ function Previewer.bat_async:cmdline(o)
316301
local filepath, entry, errcmd = self:parse_entry_and_verify(items[1])
317302
if not filepath or not entry then return utils.shell_nop() end
318303
local line_range = ""
319-
if entry.ctag and entry.line then
304+
if self.opts._ctag and entry.line then
320305
-- this is a ctag without line numbers, since we can't
321306
-- provide the preview file offset to fzf via the field
322307
-- index expression we use '--line-range' instead

lua/fzf-lua/providers/tags.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ M.btags = function(opts)
201201
-- Used as fallback to pipe the tags into fzf from stdout
202202
opts._btags_cmd = string.format("%s %s %s",
203203
opts.ctags_bin or "ctags",
204-
opts.ctags_args or "-f -",
205-
opts.filename)
204+
opts.ctags_args or "-f - --excmd=combine",
205+
libuv.shellescape(opts.filename))
206206
if opts.ctags_autogen then
207207
opts.cmd = opts.cmd or opts._btags_cmd
208208
end

lua/fzf-lua/utils.lua

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,20 @@ function M.is_darwin()
216216
return uv.os_uname().sysname == "Darwin"
217217
end
218218

219+
---@param re string
220+
---@param opts table
221+
---@return vim.regex?
222+
function M.vim_regex(re, opts)
223+
local ok, regex = pcall(vim.regex, re)
224+
if ok then return regex --[[@as vim.regex]] end
225+
if not ok and (not opts or opts.silent ~= true) then
226+
M.warn(
227+
[[Unable to init vim.regex with "%s", %s. . Add 'silent=true' to hide this message.]],
228+
re, regex)
229+
return nil
230+
end
231+
end
232+
219233
---@param str string
220234
---@return string
221235
function M.rg_escape(str)
@@ -230,16 +244,34 @@ function M.rg_escape(str)
230244
return ret
231245
end
232246

247+
---@param str string
248+
---@return string
233249
function M.regex_to_magic(str)
234250
-- Convert regex to "very magic" pattern, basically a regex
235251
-- with special meaning for "%=&<>~", `:help /magic`
236-
return [[\v]] .. str:gsub("[%%=&@<>~]", function(x)
237-
return "\\" .. x
238-
end)
252+
return [[\v]] .. str:gsub("([%%=&<>])", [[\%1]])
253+
-- searching for @ in very magic needs [@]
254+
:gsub("([@])", "[%1]")
239255
end
240256

257+
---@param str string
258+
---@return string
259+
function M.ctag_escape(str)
260+
-- unescape already escaped ctags slashes
261+
-- '\\' -> '\'
262+
-- '\/' -> '/'
263+
str = str:gsub([[\\]], [[\]])
264+
str = str:gsub([[\/]], [[/]])
265+
-- regex escape
266+
return (M.rg_escape(str)
267+
-- unescape ^$ if were positioned in start/end respectively
268+
:gsub([[^\^]], "^"):gsub([[\$$]], "$"))
269+
end
270+
271+
---@param str string
272+
---@return string
241273
function M.ctag_to_magic(str)
242-
return [[\v]] .. str:gsub("[=&@<>{%(%)%.%[]", function(x) return [[\]] .. x end)
274+
return M.regex_to_magic(M.ctag_escape(str))
243275
end
244276

245277
function M.sk_escape(str)

0 commit comments

Comments
 (0)