Skip to content

Commit e57f7d6

Browse files
committed
feat(tags): enable treesitter
1 parent e364796 commit e57f7d6

8 files changed

Lines changed: 181 additions & 11 deletions

File tree

.emmyrc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
"deps/nvim-web-devicons",
2525
"deps/nvim-cmp",
2626
"deps/nvim-dap",
27+
"$XDG_DATA_HOME/nvim/site/pack/core/opt/mini.nvim",
28+
"$XDG_DATA_HOME/nvim/site/pack/core/opt/nvim-web-devicons",
29+
"$XDG_DATA_HOME/nvim/site/pack/core/opt/nvim-cmp",
30+
"$XDG_DATA_HOME/nvim/site/pack/core/opt/nvim-dap",
2731
"$XDG_DATA_HOME/nvim/lazy/mini.nvim",
2832
"$XDG_DATA_HOME/nvim/lazy/nvim-web-devicons",
2933
"$XDG_DATA_HOME/nvim/lazy/nvim-cmp",

lua/fzf-lua/defaults.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,29 @@ M.defaults.spellcheck = {
13191319
},
13201320
}
13211321

1322+
---@param line string
1323+
---@param include_file boolean?
1324+
---@return string?, string?, { start_col: integer, end_col: integer, text: string? }?
1325+
local function ctag_line_parser(line, include_file)
1326+
local start_col, end_col = utils.ctag_match(line)
1327+
if not start_col or not end_col then return end
1328+
-- remove tag's wrapping slashes
1329+
start_col = start_col + 1
1330+
end_col = end_col - 1
1331+
local tag = line:sub(start_col, end_col)
1332+
local text, had_caret, had_dollar = utils.regex_strip_anchors(tag)
1333+
local file = not include_file and "" or (function()
1334+
-- TODO: how to extract filetype consistently?
1335+
return "foo.lua"
1336+
end)()
1337+
return file, nil, {
1338+
-- NOTE: ts indexes are zero based
1339+
start_col = start_col + had_caret - 1,
1340+
end_col = end_col - had_dollar,
1341+
text = text
1342+
}
1343+
end
1344+
13221345
---@class fzf-lua.config.TagsBase: fzf-lua.config.Base
13231346
---Path to the tags file, default: auto-detect.
13241347
---@field ctags_file? string
@@ -1348,6 +1371,7 @@ M.defaults.tags = {
13481371
-- field_index_expr = "{}", -- For `_fmt.from` to work with `bat_native`
13491372
_actions = function() return M.globals.actions.files end,
13501373
actions = { ["ctrl-g"] = { actions.grep_lgrep } },
1374+
_treesitter = function(line) return ctag_line_parser(line, true) end,
13511375
}
13521376

13531377
---Search current buffer ctags.
@@ -1370,6 +1394,7 @@ M.defaults.btags = vim.tbl_deep_extend("force", M.defaults.tags, {
13701394
fzf_opts = { ["--with-nth"] = "1,3.." },
13711395
actions = { ["ctrl-g"] = false },
13721396
_resume_reload = true,
1397+
_treesitter = function(line) return ctag_line_parser(line) end,
13731398
})
13741399

13751400
---Installed colorschemes.

lua/fzf-lua/make_entry.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ M.tag = function(x, opts)
714714
name,
715715
M.file(file, opts),
716716
line and utils.ansi_codes.green(tostring(line)) .. ";" or "",
717-
tag and utils.ansi_codes.blue(tag) or ""
717+
tag or ""
718718
)
719719
end
720720

lua/fzf-lua/path.lua

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,9 @@ function M.entry_to_ctag(raw, opts)
411411
ctag = tag,
412412
line = line or 0,
413413
col = 0,
414-
stripped = string.format("%s:%s %s", file, line and line .. ":" or "", tag or ""),
414+
stripped = string.format("%s:%s %s", file, line and line .. ":" or "",
415+
-- remove ctag ^$ prefix/postfix so qflist can have ts highlights
416+
utils.regex_strip_anchors(tag) or ""),
415417
debug = opts.debug and raw:match("^%[DEBUG]") and raw or nil,
416418
} ---@as fzf-lua.path.Entry
417419
end

lua/fzf-lua/types.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ local FzfLua = require("fzf-lua")
202202
---@field _actions? fun():fzf-lua.config.Actions?
203203
---@field __ACT_TO? function
204204
---@field _start? boolean
205-
---@field _treesitter? (fun(line: string):string?,string?,string?,string?)|boolean?
205+
---@field _treesitter? (fun(line: string):string?,string?,string|table?,string?)|boolean?
206206
---@field help_open_win? fun(buf: integer, enter: boolean, config: vim.api.keyset.win_config): integer
207207
---Auto close fzf-lua interface when a terminal is opened, set to `false` to keep the interface open.
208208
---@field autoclose? boolean

lua/fzf-lua/utils.lua

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,53 @@ function M.regex_to_magic(str)
254254
:gsub("([@])", "[%1]")
255255
end
256256

257+
---Remove ^ $ regex anchors if they appear at start/end respectively
258+
-- used with ctags to convert the tag to a valid code block
259+
---@param s string?
260+
---@return string?, integer, integer
261+
function M.regex_strip_anchors(s)
262+
local had_caret, had_dollar = 0, 0
263+
if not s then return nil, had_caret, had_dollar end
264+
---@format disable
265+
if string.byte(s, 1) == 94 then s = s:sub(2); had_caret = 1 end -- remove ^
266+
---@format disable
267+
if string.byte(s, #s) == 36 then s = s:sub(1, #s - 1); had_dollar = 1 end -- remove $
268+
return s, had_caret, had_dollar
269+
end
270+
271+
---@param str string
272+
---@return integer?, integer?
273+
function M.ctag_match(str)
274+
---@param s string
275+
---@param i integer?
276+
---@return integer?
277+
local rfind_slash = function(s, i)
278+
-- assert(string.byte("/", 1) == 47)
279+
-- assert(string.byte([[\]], 1) == 92)
280+
local SLASH, BSLASH = 47, 92
281+
local len = #s
282+
i = i or len
283+
while i > 0 do
284+
if string.byte(s, i) == SLASH then
285+
local bs_count, j = 0, i - 1
286+
while j > 0 and string.byte(s, j) == BSLASH do
287+
bs_count = bs_count + 1
288+
j = j - 1
289+
end
290+
if bs_count % 2 == 0 then
291+
return i
292+
end
293+
end
294+
i = i - 1
295+
end
296+
end
297+
local start_col, end_col = nil, rfind_slash(str)
298+
if end_col then
299+
start_col = rfind_slash(str, end_col - 1)
300+
end
301+
if start_col then return start_col, end_col end
302+
end
303+
257304
---@param str string
258305
---@return string
259306
function M.ctag_escape(str)
@@ -1298,7 +1345,7 @@ function M.getbufinfo(bufnr)
12981345
if M.__HAS_AUTOLOAD_FNS then
12991346
return vim.fn["fzf_lua#getbufinfo"](bufnr)
13001347
else
1301-
return vim.fn.getbufinfo(bufnr)[1] or {}
1348+
return vim.fn.getbufinfo(bufnr)[1] or {} ---@as vim.fn.getbufinfo.ret.item
13021349
end
13031350
end
13041351

lua/fzf-lua/win/tsinjector.lua

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,14 @@ end
127127

128128
---@param self fzf-lua.Win
129129
---@param buf integer
130-
---@param line_parser (fun(line: string):string?,string?,string?,string?)|boolean?
130+
---@param line_parser (fun(line: string):string?,string?,string|table?,string?)|boolean?
131131
---@return function detach
132132
function M.attach(self, buf, line_parser)
133133
-- local utf8 = require("fzf-lua.lib.utf8")
134134
local function trim(s) return (string.gsub(s, "^%s*(.-)%s*$", "%1")) end
135135
---@type fun(line: string):string?,string?,string?,string?
136136
local default_line_parser = function(line) return line:match("(.-):?(%d+)[: ](.+)$") end
137+
---@type (fun(line: string):string?,string?,string|table?,string?)
137138
line_parser = vim.is_callable(line_parser) and line_parser or default_line_parser
138139
M.cache[buf] = {}
139140
api.nvim_buf_attach(buf, false, {
@@ -171,13 +172,23 @@ function M.attach(self, buf, line_parser)
171172
-- file:line:text (grep_project or missing "--column" flag)
172173
-- line:col:text (grep_curbuf)
173174
-- line<U+00A0>text (lines|blines)
174-
local filepath, _lnum, text, _ft = line_parser(line:sub(min_col))
175-
if not text or text == 0 then return end
175+
local line_slice = line:sub(min_col)
176+
local col_offset = #line - #line_slice
177+
local filepath, _lnum, info, _ft = line_parser(line_slice)
178+
179+
-- info can be a string or `{ text = ..., start_col, end_col }`
180+
info = type(info) == "table" and info or { text = info }
181+
182+
-- line_parser can return text with start_col/end_col
183+
local text = info.text
184+
if not text or #text == 0 then return end
176185

177186
text = text:gsub("^%d+:", "") -- remove col nr if exists
178187
filepath = trim(filepath) -- trim spaces
179188

180189
local ft_bufnr = (function()
190+
-- we only need this as fallback if _ft is nil
191+
if _ft then return nil end
181192
-- blines|lines: U+00A0 (decimal: 160) follows the lnum
182193
-- grep_curbuf: formats as line:col:text` thus `#filepath == 0`
183194
if #filepath == 0 or string.byte(text, 1) == 160 then
@@ -190,6 +201,8 @@ function M.attach(self, buf, line_parser)
190201
end
191202
end)()
192203

204+
-- _ft should nullify ft_bufnr
205+
assert(not _ft or not ft_bufnr)
193206
local ft = _ft or (ft_bufnr and vim.bo[ft_bufnr].ft
194207
or vim.filetype.match({ filename = path.tail(filepath) }))
195208
if not ft then return end
@@ -206,13 +219,18 @@ function M.attach(self, buf, line_parser)
206219
-- we use `max_col` instead (assuming our code isn't unicode)
207220
local line_idx = i - 1
208221
local line_len = #line
209-
local start_col = math.max(min_col, line_len - #text)
210-
local end_col = max_col and math.min(max_col, line_len) or (line_len - trim_right)
222+
local start_col = info.start_col and (info.start_col + col_offset) or (line_len - #text)
223+
local end_col = info.end_col and (info.end_col + col_offset) or (line_len - trim_right)
224+
-- clamp min/max columns
225+
start_col = math.max(min_col, start_col)
226+
if max_col then end_col = math.min(max_col, end_col) end
211227
regions[lang] = regions[lang] or {}
212228
empty_regions[lang] = empty_regions[lang] or {}
213229
table.insert(regions[lang], { { line_idx, start_col, line_idx, end_col } })
214-
-- print(lang, string.format("%d:%d [%d] %d:%s",
215-
-- start_col, end_col, line_idx, _lnum, line:sub(start_col + 1, end_col)))
230+
-- uncomment to debug:
231+
-- print(lang, string.format("%d:%d +%d [%d] %s:%s",
232+
-- start_col, end_col, col_offset, line_idx, tostring(_lnum),
233+
-- line:sub(start_col + 1, end_col)))
216234
end)()
217235
end
218236
attach(bufnr, empty_regions)

tests/utils_spec.lua

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,78 @@ describe("Testing utils module", function()
135135
eq(utils.strsplit(",foo,bar,", ","), { "", "foo", "bar", "" })
136136
eq(utils.strsplit("foobar", ","), { "foobar" })
137137
end)
138+
139+
it("regex_strip_anchors", function()
140+
-- nil input
141+
eq({ utils.regex_strip_anchors(nil) }, { [2] = 0, [3] = 0 })
142+
-- empty string
143+
eq({ utils.regex_strip_anchors("") }, { "", 0, 0 })
144+
-- no anchors
145+
eq({ utils.regex_strip_anchors("foobar") }, { "foobar", 0, 0 })
146+
-- only start anchor
147+
eq({ utils.regex_strip_anchors("^foobar") }, { "foobar", 1, 0 })
148+
-- only end anchor
149+
eq({ utils.regex_strip_anchors("foobar$") }, { "foobar", 0, 1 })
150+
-- both anchors
151+
eq({ utils.regex_strip_anchors("^foobar$") }, { "foobar", 1, 1 })
152+
-- anchors in middle (should not be stripped)
153+
eq({ utils.regex_strip_anchors("foo^bar$baz") }, { "foo^bar$baz", 0, 0 })
154+
-- only anchors
155+
eq({ utils.regex_strip_anchors("^") }, { "", 1, 0 })
156+
eq({ utils.regex_strip_anchors("$") }, { "", 0, 1 })
157+
eq({ utils.regex_strip_anchors("^$") }, { "", 1, 1 })
158+
-- single character with anchors
159+
eq({ utils.regex_strip_anchors("^x$") }, { "x", 1, 1 })
160+
end)
161+
162+
it("ctag_match", function()
163+
-- no slashes
164+
eq(utils.ctag_match("foobar"), nil)
165+
-- single slash (needs at least 2 slashes)
166+
eq(utils.ctag_match("foo/bar"), nil)
167+
-- anchored slahes
168+
eq({ utils.ctag_match("/foo/") }, { 1, 5 })
169+
-- two slashes (finds the two rightmost unescaped slashes)
170+
eq({ utils.ctag_match("/foo/bar/baz") }, { 5, 9 })
171+
-- escaped slash only (should ignore escaped ones)
172+
eq(utils.ctag_match([[/foo\/bar]]), nil)
173+
eq(utils.ctag_match([[/foo\\\/bar]]), nil)
174+
-- escaped slash backslash before slash
175+
eq({ utils.ctag_match([[/foo\\/bar]]) }, { 1, 7 })
176+
eq({ utils.ctag_match([[/foo\\\\/bar]]) }, { 1, 9 })
177+
-- mixed escaped and unescaped
178+
eq({ utils.ctag_match([[foo/bar\/baz/qux]]) }, { 4, 13 })
179+
-- reverse search from end
180+
eq({ utils.ctag_match("/a/b/c/d") }, { 5, 7 })
181+
end)
182+
183+
it("ctag_escape", function()
184+
-- empty string
185+
eq(utils.ctag_escape(""), "")
186+
-- no special characters
187+
eq(utils.ctag_escape("foobar"), "foobar")
188+
-- escaped backslash (\\ becomes \, then rg_escape doubles it back)
189+
eq(utils.ctag_escape("foo\\bar"), "foo\\\\bar")
190+
-- unescape escaped slash (\/ becomes /)
191+
eq(utils.ctag_escape("foo\\/bar"), "foo/bar")
192+
-- regex escape (rg_escape behavior)
193+
eq(utils.ctag_escape("foo.bar"), "foo\\.bar")
194+
eq(utils.ctag_escape("foo*bar"), "foo\\*bar")
195+
-- ^ at start gets unescaped (was escaped by rg_escape)
196+
eq(utils.ctag_escape("^foobar"), "^foobar")
197+
-- $ at end gets unescaped (was escaped by rg_escape)
198+
eq(utils.ctag_escape("foobar$"), "foobar$")
199+
-- already escaped ^ at start stays escaped (rg_escape adds another backslash)
200+
eq(utils.ctag_escape("\\^foobar"), "\\\\\\^foobar")
201+
-- already escaped $ at end stays escaped
202+
eq(utils.ctag_escape("foobar\\$"), "foobar\\\\$")
203+
-- ^ in middle stays escaped
204+
eq(utils.ctag_escape("foo^bar"), "foo\\^bar")
205+
-- $ in middle stays escaped
206+
eq(utils.ctag_escape("foo$bar"), "foo\\$bar")
207+
-- combination of patterns
208+
eq(utils.ctag_escape("^foo.bar$"), "^foo\\.bar$")
209+
-- escaped backslash before dot (\. becomes ., then rg_escape escapes both)
210+
eq(utils.ctag_escape("foo\\.bar"), "foo\\\\\\.bar")
211+
end)
138212
end)

0 commit comments

Comments
 (0)