Skip to content

Commit 14e8ac0

Browse files
committed
feat(beta): use ts highlights in main fzf win
Will only work with file-like entries, e.g. grep_xxx, blines, etc: ``` file:line:col:text (grep_xxx) file:line:text (grep_project or missing "--column" flag) line:col:text (grep_curbuf) line:text (blines) ``` Enable globally via `setup`: ```lua require("fzf-lua").setup({ winopts = { treesitter = true } }) ``` Or call individually per picker: ```vim :FzfLua blines winopts.treesitter=true ``` or ```lua :lua require("fzf-lua").blines({ winopts = { treesitter = true } }) ``` Feedback appreciated: #1485
1 parent 5e8f0e2 commit 14e8ac0

3 files changed

Lines changed: 158 additions & 4 deletions

File tree

OPTIONS.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,19 @@ Backdrop opacity value, 0 for fully opaque, 100 for fully transparent (i.e. disa
265265

266266
#### globals.winopts.fullscreen
267267

268-
Type: `fullscreen`, Default: `false`
268+
Type: `boolean`, Default: `false`
269269

270270
Use fullscreen for the fzf-load floating window.
271271

272+
#### globals.winopts.treesitter
273+
274+
Type: `boolean`, Default: `false`
275+
276+
Use treesitter highlighting in fzf's main window.
277+
278+
> **NOTE**: Only works for file-like entires where treesitter parser exists and is loaded
279+
> for the filetype.
280+
272281
#### globals.winopts.on_create
273282

274283
Type: `function`, Default: `nil`

doc/fzf-lua-opts.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,23 @@ disabled).
346346

347347
globals.winopts.fullscreen *fzf-lua-opts-globals.winopts.fullscreen*
348348

349-
Type: `fullscreen`, Default: `false`
349+
Type: `boolean`, Default: `false`
350350

351351
Use fullscreen for the fzf-load floating window.
352352

353353

354354

355+
globals.winopts.treesitter *fzf-lua-opts-globals.winopts.treesitter*
356+
357+
Type: `boolean`, Default: `false`
358+
359+
Use treesitter highlighting in fzf's main window.
360+
361+
**NOTE**: Only works for file-like entires where treesitter parser exists and
362+
is loaded for the filetype.
363+
364+
365+
355366
globals.winopts.on_create *fzf-lua-opts-globals.winopts.on_create*
356367

357368
Type: `function`, Default: `nil`

lua/fzf-lua/win.lua

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,90 @@
1+
local path = require "fzf-lua.path"
12
local utils = require "fzf-lua.utils"
23
local config = require "fzf-lua.config"
34
local actions = require "fzf-lua.actions"
45

56
local api = vim.api
67
local fn = vim.fn
78

9+
local TSInjector = {}
10+
11+
---@type table<number, table<string,{parser: vim.treesitter.LanguageTree, highlighter:vim.treesitter.highlighter, enabled:boolean}>>
12+
TSInjector.cache = {}
13+
14+
function TSInjector.setup()
15+
if TSInjector._setup then return true end
16+
17+
TSInjector._setup = true
18+
TSInjector._ns = TSInjector._ns or vim.api.nvim_create_namespace("fzf-lua.win.highlighter")
19+
20+
local function wrap_ts_hl_callback(name)
21+
return function(_, win, buf, ...)
22+
-- print(name, buf, win, TSInjector.cache[buf])
23+
if not TSInjector.cache[buf] then
24+
return false
25+
end
26+
for _, hl in pairs(TSInjector.cache[buf] or {}) do
27+
if hl.enabled then
28+
vim.treesitter.highlighter.active[buf] = hl.highlighter
29+
vim.treesitter.highlighter[name](_, win, buf, ...)
30+
end
31+
end
32+
vim.treesitter.highlighter.active[buf] = nil
33+
end
34+
end
35+
36+
vim.api.nvim_set_decoration_provider(TSInjector._ns, {
37+
on_win = wrap_ts_hl_callback("_on_win"),
38+
on_line = wrap_ts_hl_callback("_on_line"),
39+
})
40+
41+
return true
42+
end
43+
44+
function TSInjector.deregister()
45+
if not TSInjector._ns then return end
46+
vim.api.nvim_set_decoration_provider(TSInjector._ns, { on_win = nil, on_line = nil })
47+
TSInjector._setup = nil
48+
end
49+
50+
function TSInjector.clear_cache(buf, noassert)
51+
TSInjector.cache[buf] = nil
52+
assert(noassert or utils.tbl_isempty(TSInjector.cache))
53+
end
54+
55+
---@param buf number
56+
function TSInjector.attach(buf, regions)
57+
if not TSInjector.setup() then return end
58+
59+
TSInjector.cache[buf] = TSInjector.cache[buf] or {}
60+
for lang, _ in pairs(TSInjector.cache[buf]) do
61+
TSInjector.cache[buf][lang].enabled = regions[lang] ~= nil
62+
end
63+
64+
for lang, _ in pairs(regions) do
65+
TSInjector._attach_lang(buf, lang, regions[lang])
66+
end
67+
end
68+
69+
---@param buf number
70+
---@param lang? string
71+
function TSInjector._attach_lang(buf, lang, regions)
72+
if not TSInjector.cache[buf][lang] then
73+
local ok, parser = pcall(vim.treesitter.languagetree.new, buf, lang)
74+
if not ok then return end
75+
TSInjector.cache[buf][lang] = {
76+
parser = parser,
77+
highlighter = vim.treesitter.highlighter.new(parser),
78+
}
79+
end
80+
81+
local parser = TSInjector.cache[buf][lang].parser
82+
if not parser then return end
83+
84+
TSInjector.cache[buf][lang].enabled = true
85+
parser:set_included_regions(regions)
86+
end
87+
888
local FzfWin = {}
989

1090
-- singleton instance used in win_leave
@@ -738,6 +818,50 @@ function FzfWin:set_winleave_autocmd()
738818
self:_nvim_create_autocmd("WinLeave", self.win_leave, [[require('fzf-lua.win').win_leave()]])
739819
end
740820

821+
function FzfWin:treesitter_attach()
822+
if not utils.__HAS_NVIM_09 then return end
823+
if not self._o.winopts.treesitter then return end
824+
local function trim(s) return (string.gsub(s, "^%s*(.-)%s*$", "%1")) end
825+
vim.api.nvim_buf_attach(self.fzf_bufnr, false, {
826+
on_lines = function(_, bufnr, _, first_changed, last_changed, last_updated, bc)
827+
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
828+
local regions = {}
829+
local empty_regions = {}
830+
for i, line in ipairs(lines) do
831+
(function()
832+
-- Lines with code can be of the following formats:
833+
-- file:line:col:text (grep_xxx)
834+
-- file:line:text (grep_project or missing "--column" flag)
835+
-- line:col:text (grep_curbuf)
836+
-- line:text (blines)
837+
local filepath, _lnum, text = line:match("(.-):?(%d+):(.+)$")
838+
if not text or text == 0 then return end
839+
840+
filepath = trim(filepath)
841+
local ft = #filepath == 0 and vim.bo[utils.CTX().bufnr].ft
842+
or vim.filetype.match({ filename = path.tail(filepath) })
843+
if not ft then return end
844+
845+
local lang = vim.treesitter.language.get_lang(ft)
846+
local loaded = lang and utils.has_ts_parser(lang)
847+
if not loaded then return end
848+
849+
-- With the above line match text can start with "%d+:", remove it
850+
text = text:gsub("^%d+:", "")
851+
852+
local line_idx, text_pos = i - 1, #line - #text
853+
regions[lang] = regions[lang] or {}
854+
empty_regions[lang] = empty_regions[lang] or {}
855+
table.insert(regions[lang], { { line_idx, text_pos, line_idx, line:len() } })
856+
-- print(lang, string.format("[%d]%d:%s", line_idx, _lnum, line:sub(text_pos + 1)))
857+
end)()
858+
end
859+
TSInjector.attach(bufnr, empty_regions)
860+
TSInjector.attach(bufnr, regions)
861+
end
862+
})
863+
end
864+
741865
function FzfWin:set_tmp_buffer(no_wipe)
742866
if not self:validate() then return end
743867
-- Store the [would be] detached buffer number
@@ -749,11 +873,16 @@ function FzfWin:set_tmp_buffer(no_wipe)
749873
vim.api.nvim_win_set_buf(self.fzf_winid, self.fzf_bufnr)
750874
-- close the previous fzf term buffer without triggering autocmds
751875
-- this also kills the previous fzf process if its still running
752-
if not no_wipe then utils.nvim_buf_delete(detached, { force = true }) end
876+
if not no_wipe then
877+
utils.nvim_buf_delete(detached, { force = true })
878+
TSInjector.clear_cache(detached)
879+
end
753880
-- in case buffer exists prematurely
754881
self:set_winleave_autocmd()
755882
-- automatically resize fzf window
756883
self:set_redraw_autocmd()
884+
-- Use treesitter to highlight results on the main fzf window
885+
self:treesitter_attach()
757886
-- since we have the cursorline workaround from
758887
-- issue #254, resume shows an ugly cursorline.
759888
-- remove it, nvim_win API is better than vim.wo?
@@ -795,7 +924,7 @@ function FzfWin:create()
795924
-- also recall the user's 'on_create' (#394)
796925
if self.winopts.on_create and
797926
type(self.winopts.on_create) == "function" then
798-
self.winopts.on_create()
927+
self.winopts.on_create({ winid = self.fzf_winid, bufnr = self.fzf_bufnr })
799928
end
800929
-- not sure why but when using a split and reusing the window,
801930
-- fzf will not use all the available width until 'redraw' is
@@ -842,6 +971,8 @@ function FzfWin:create()
842971
self:set_winleave_autocmd()
843972
-- automatically resize fzf window
844973
self:set_redraw_autocmd()
974+
-- Use treesitter to highlight results on the main fzf window
975+
self:treesitter_attach()
845976

846977
self:reset_win_highlights(self.fzf_winid)
847978

@@ -916,6 +1047,9 @@ function FzfWin:close(fzf_bufnr)
9161047
if self.fzf_bufnr and vim.api.nvim_buf_is_valid(self.fzf_bufnr) then
9171048
vim.api.nvim_buf_delete(self.fzf_bufnr, { force = true })
9181049
end
1050+
-- Clear treesitter buffer cache and deregister decoration callbacks
1051+
TSInjector.clear_cache(self.fzf_bufnr, self._hidden_fzf_bufnr)
1052+
TSInjector.deregister()
9191053
-- when using `split = "belowright new"` closing the fzf
9201054
-- window may not always return to the correct source win
9211055
-- depending on the user's split configuration (#397)

0 commit comments

Comments
 (0)