@@ -6,6 +6,10 @@ local str = require("peekstack.util.str")
66local M = {}
77
88local 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 ?
200379local 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+ }
217414end
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