Skip to content

Commit 70b958b

Browse files
committed
feat(ui): apply treesitter highlights to stack preview lines
1 parent 83727be commit 70b958b

1 file changed

Lines changed: 212 additions & 11 deletions

File tree

lua/peekstack/ui/stack_view.lua

Lines changed: 212 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ local str = require("peekstack.util.str")
66
local M = {}
77

88
local NS = vim.api.nvim_create_namespace("PeekstackStackView")
9+
local TS_HL_PRIORITY = 150
10+
11+
---@type table<string, vim.treesitter.Query|false>
12+
local TS_HIGHLIGHT_QUERY_CACHE = {}
913

1014
---Map title highlight groups to their stack view equivalents
1115
---@type table<string, string>
@@ -192,28 +196,221 @@ end
192196
---@field col_end integer
193197
---@field hl_group string
194198

199+
---@class PeekstackStackViewPreviewLine
200+
---@field line string
201+
---@field source_bufnr integer
202+
---@field source_line integer
203+
---@field source_col_start integer
204+
---@field source_col_end integer
205+
---@field preview_col_start integer
206+
207+
---@param lang string
208+
---@return vim.treesitter.Query?
209+
local function get_treesitter_highlight_query(lang)
210+
local cached = TS_HIGHLIGHT_QUERY_CACHE[lang]
211+
if cached ~= nil then
212+
return cached or nil
213+
end
214+
local ok, query = pcall(vim.treesitter.query.get, lang, "highlights")
215+
if not ok or not query then
216+
TS_HIGHLIGHT_QUERY_CACHE[lang] = false
217+
return nil
218+
end
219+
TS_HIGHLIGHT_QUERY_CACHE[lang] = query
220+
return query
221+
end
222+
223+
---@param bufnr integer
224+
---@param line integer
225+
---@return TSNode?, string?
226+
local function treesitter_root_for_line(bufnr, line)
227+
local ok, parser = pcall(vim.treesitter.get_parser, bufnr)
228+
if not ok or not parser then
229+
return nil, nil
230+
end
231+
local ok_parse = pcall(function()
232+
parser:parse()
233+
end)
234+
if not ok_parse then
235+
return nil, nil
236+
end
237+
local trees = parser:trees()
238+
if not trees or vim.tbl_isempty(trees) then
239+
return nil, nil
240+
end
241+
242+
local root = nil
243+
local lang = nil
244+
local smallest_range = math.huge
245+
for _, tree in pairs(trees) do
246+
local candidate = tree:root()
247+
if candidate then
248+
local sr, _, er, _ = candidate:range()
249+
if line >= sr and line <= er then
250+
local range_size = er - sr
251+
if range_size < smallest_range then
252+
smallest_range = range_size
253+
root = candidate
254+
local ok_lang, tree_lang = pcall(function()
255+
return tree:lang()
256+
end)
257+
if ok_lang and type(tree_lang) == "string" and tree_lang ~= "" then
258+
lang = tree_lang
259+
else
260+
lang = nil
261+
end
262+
end
263+
end
264+
end
265+
end
266+
if not root then
267+
return nil, nil
268+
end
269+
if not lang then
270+
local ft = vim.bo[bufnr].filetype
271+
if ft ~= "" then
272+
local ok_map, mapped = pcall(vim.treesitter.language.get_lang, ft)
273+
if ok_map and type(mapped) == "string" and mapped ~= "" then
274+
lang = mapped
275+
else
276+
lang = ft
277+
end
278+
end
279+
end
280+
return root, lang
281+
end
282+
283+
---@param name string
284+
---@return boolean
285+
local function highlight_exists(name)
286+
if name == "" then
287+
return false
288+
end
289+
return vim.fn.hlexists(name) == 1
290+
end
291+
292+
---@param capture_name string?
293+
---@param lang string?
294+
---@return string?
295+
local function capture_hl_group(capture_name, lang)
296+
if type(capture_name) ~= "string" or capture_name == "" then
297+
return nil
298+
end
299+
if lang and lang ~= "" then
300+
local lang_group = string.format("@%s.%s", capture_name, lang)
301+
if highlight_exists(lang_group) then
302+
return lang_group
303+
end
304+
end
305+
local base_group = "@" .. capture_name
306+
if highlight_exists(base_group) then
307+
return base_group
308+
end
309+
return nil
310+
end
311+
312+
---@param target_bufnr integer
313+
---@param preview_line_nr integer
314+
---@param preview PeekstackStackViewPreviewLine
315+
local function apply_preview_treesitter_highlight(target_bufnr, preview_line_nr, preview)
316+
local source_bufnr = preview.source_bufnr
317+
if not source_bufnr or not vim.api.nvim_buf_is_valid(source_bufnr) then
318+
return
319+
end
320+
321+
local source_line = preview.source_line
322+
local source_start = preview.source_col_start
323+
local source_end = preview.source_col_end
324+
if source_end <= source_start then
325+
return
326+
end
327+
328+
local root, lang = treesitter_root_for_line(source_bufnr, source_line)
329+
if not root or not lang then
330+
return
331+
end
332+
333+
local query = get_treesitter_highlight_query(lang)
334+
if not query then
335+
return
336+
end
337+
338+
local ok_iter = pcall(function()
339+
for capture_id, node in query:iter_captures(root, source_bufnr, source_line, source_line + 1) do
340+
local hl_group = capture_hl_group(query.captures[capture_id], lang)
341+
if hl_group then
342+
local sr, sc, er, ec = node:range()
343+
if source_line >= sr and source_line <= er then
344+
local node_start = (sr == source_line) and sc or 0
345+
local node_end = (er == source_line) and ec or source_end
346+
local start_col = math.max(node_start, source_start)
347+
local end_col = math.min(node_end, source_end)
348+
if end_col > start_col then
349+
local view_start = preview.preview_col_start + (start_col - source_start)
350+
local view_end = preview.preview_col_start + (end_col - source_start)
351+
vim.api.nvim_buf_set_extmark(target_bufnr, NS, preview_line_nr - 1, view_start, {
352+
end_col = view_end,
353+
hl_group = hl_group,
354+
priority = TS_HL_PRIORITY,
355+
})
356+
end
357+
end
358+
end
359+
end
360+
end)
361+
if not ok_iter then
362+
return
363+
end
364+
end
365+
366+
---@param target_bufnr integer
367+
---@param previews table<integer, PeekstackStackViewPreviewLine>
368+
local function apply_preview_treesitter_highlights(target_bufnr, previews)
369+
for preview_line_nr, preview in pairs(previews) do
370+
apply_preview_treesitter_highlight(target_bufnr, preview_line_nr, preview)
371+
end
372+
end
373+
195374
--- Get a trimmed preview line from a source buffer
196375
---@param source_bufnr integer?
197376
---@param line integer
198377
---@param max_width integer
199-
---@return string
378+
---@return PeekstackStackViewPreviewLine?
200379
local function get_preview_line(source_bufnr, line, max_width)
201380
if not source_bufnr or not vim.api.nvim_buf_is_valid(source_bufnr) then
202-
return ""
381+
return nil
203382
end
204383
local ok, buf_lines = pcall(vim.api.nvim_buf_get_lines, source_bufnr, line, line + 1, false)
205384
if not ok or not buf_lines or #buf_lines == 0 then
206-
return ""
385+
return nil
207386
end
208-
local text = vim.trim(buf_lines[1] or "")
209-
if text == "" then
210-
return ""
387+
local source_text = buf_lines[1] or ""
388+
if source_text == "" then
389+
return nil
390+
end
391+
392+
local source_col_start = #(source_text:match("^%s*") or "")
393+
local source_col_end = #source_text - #(source_text:match("%s*$") or "")
394+
if source_col_end <= source_col_start then
395+
return nil
211396
end
397+
398+
local text = source_text:sub(source_col_start + 1, source_col_end)
212399
local available = math.max(10, max_width - 4)
213400
if vim.fn.strdisplaywidth(text) > available then
214-
text = vim.fn.strcharpart(text, 0, available - 3) .. "..."
401+
local keep_chars = math.max(available - 3, 0)
402+
local kept = vim.fn.strcharpart(text, 0, keep_chars)
403+
text = kept .. "..."
404+
source_col_end = source_col_start + #kept
215405
end
216-
return " " .. text
406+
return {
407+
line = " " .. text,
408+
source_bufnr = source_bufnr,
409+
source_line = line,
410+
source_col_start = source_col_start,
411+
source_col_end = source_col_end,
412+
preview_col_start = 4,
413+
}
217414
end
218415

219416
---Render the stack view list
@@ -232,6 +429,8 @@ local function render(s)
232429
local lines = {}
233430
---@type PeekstackStackViewHighlight[][]
234431
local highlights = {}
432+
---@type table<integer, PeekstackStackViewPreviewLine>
433+
local preview_lines = {}
235434
s.line_to_id = {}
236435
s.header_lines = 0
237436

@@ -340,12 +539,13 @@ local function render(s)
340539
and popup.location.range.start.line
341540
if source_line then
342541
local preview = get_preview_line(popup.source_bufnr or popup.bufnr, source_line, win_width)
343-
if preview ~= "" then
344-
table.insert(lines, preview)
542+
if preview then
543+
table.insert(lines, preview.line)
345544
local preview_line_nr = #lines
346545
table.insert(highlights, {
347-
{ col_start = 0, col_end = #preview, hl_group = "PeekstackStackViewPreview" },
546+
{ col_start = 0, col_end = #preview.line, hl_group = "PeekstackStackViewPreview" },
348547
})
548+
preview_lines[preview_line_nr] = preview
349549
s.line_to_id[preview_line_nr] = popup.id
350550
end
351551
end
@@ -364,6 +564,7 @@ local function render(s)
364564
})
365565
end
366566
end
567+
apply_preview_treesitter_highlights(s.bufnr, preview_lines)
367568

368569
if s.winid and vim.api.nvim_win_is_valid(s.winid) and #visible > 0 then
369570
local cursor = vim.api.nvim_win_get_cursor(s.winid)[1]

0 commit comments

Comments
 (0)