feat(highlight): wire highlights.context into treesitter pipeline (#151)
## Problem `highlights.context.enabled` and `highlights.context.lines` were defined, validated, and range-checked but never read during highlighting. Hunks inside incomplete constructs (e.g., a table literal or function body whose opening is beyond the hunk's own context lines) parsed incorrectly because treesitter had no surrounding code. ## Solution `compute_hunk_context` in `init.lua` reads the working tree file using the hunk's `@@ +start,count @@` line numbers to collect up to `lines` (default 25) surrounding code lines in each direction. Files are read once via `io.open` and cached across hunks in the same file. `highlight_treesitter` in `highlight.lua` accepts an optional context parameter that prepends/appends context lines to the parse string and offsets capture rows by the prefix count, so extmarks only land on actual hunk lines. Wired through `highlight_hunk` for the two code-language treesitter calls (not headers, not `highlight_text`, not vim syntax). Closes #148.
This commit is contained in:
parent
29e624d9f0
commit
e7d56e3bbe
5 changed files with 534 additions and 17 deletions
|
|
@ -67,6 +67,10 @@ end
|
|||
---@field defer_vim_syntax? boolean
|
||||
---@field syntax_only? boolean
|
||||
|
||||
---@class diffs.TSContext
|
||||
---@field before string[]?
|
||||
---@field after string[]?
|
||||
|
||||
---@param bufnr integer
|
||||
---@param ns integer
|
||||
---@param code_lines string[]
|
||||
|
|
@ -76,6 +80,7 @@ end
|
|||
---@param covered_lines? table<integer, true>
|
||||
---@param priorities diffs.PrioritiesConfig
|
||||
---@param force_high_priority? boolean
|
||||
---@param context? diffs.TSContext
|
||||
---@return integer
|
||||
local function highlight_treesitter(
|
||||
bufnr,
|
||||
|
|
@ -86,9 +91,34 @@ local function highlight_treesitter(
|
|||
col_offset,
|
||||
covered_lines,
|
||||
priorities,
|
||||
force_high_priority
|
||||
force_high_priority,
|
||||
context
|
||||
)
|
||||
local code = table.concat(code_lines, '\n')
|
||||
local prefix_count = 0
|
||||
local parse_lines = code_lines
|
||||
if context then
|
||||
local before = context.before
|
||||
local after = context.after
|
||||
if (before and #before > 0) or (after and #after > 0) then
|
||||
parse_lines = {}
|
||||
if before then
|
||||
prefix_count = #before
|
||||
for _, l in ipairs(before) do
|
||||
parse_lines[#parse_lines + 1] = l
|
||||
end
|
||||
end
|
||||
for _, l in ipairs(code_lines) do
|
||||
parse_lines[#parse_lines + 1] = l
|
||||
end
|
||||
if after then
|
||||
for _, l in ipairs(after) do
|
||||
parse_lines[#parse_lines + 1] = l
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local code = table.concat(parse_lines, '\n')
|
||||
if code == '' then
|
||||
return 0
|
||||
end
|
||||
|
|
@ -118,6 +148,8 @@ local function highlight_treesitter(
|
|||
if capture ~= 'spell' and capture ~= 'nospell' then
|
||||
local capture_name = '@' .. capture .. '.' .. tree_lang
|
||||
local sr, sc, er, ec = node:range()
|
||||
sr = sr - prefix_count
|
||||
er = er - prefix_count
|
||||
|
||||
local buf_sr = line_map[sr]
|
||||
if buf_sr then
|
||||
|
|
@ -329,10 +361,36 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
end
|
||||
end
|
||||
|
||||
extmark_count =
|
||||
highlight_treesitter(bufnr, ns, new_code, hunk.lang, new_map, pw + qw, covered_lines, p)
|
||||
local ts_context = nil
|
||||
if opts.highlights.context.enabled and (hunk.context_before or hunk.context_after) then
|
||||
ts_context = { before = hunk.context_before, after = hunk.context_after }
|
||||
end
|
||||
|
||||
extmark_count = highlight_treesitter(
|
||||
bufnr,
|
||||
ns,
|
||||
new_code,
|
||||
hunk.lang,
|
||||
new_map,
|
||||
pw + qw,
|
||||
covered_lines,
|
||||
p,
|
||||
nil,
|
||||
ts_context
|
||||
)
|
||||
extmark_count = extmark_count
|
||||
+ highlight_treesitter(bufnr, ns, old_code, hunk.lang, old_map, pw + qw, covered_lines, p)
|
||||
+ highlight_treesitter(
|
||||
bufnr,
|
||||
ns,
|
||||
old_code,
|
||||
hunk.lang,
|
||||
old_map,
|
||||
pw + qw,
|
||||
covered_lines,
|
||||
p,
|
||||
nil,
|
||||
ts_context
|
||||
)
|
||||
|
||||
if hunk.header_context and hunk.header_context_col then
|
||||
local header_extmarks = highlight_text(
|
||||
|
|
|
|||
|
|
@ -297,6 +297,69 @@ local function carry_forward_highlighted(old_entry, new_hunks)
|
|||
return highlighted
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string[]?
|
||||
local function read_file_lines(path)
|
||||
local f = io.open(path, 'r')
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
local lines = {}
|
||||
for line in f:lines() do
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
f:close()
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param hunks diffs.Hunk[]
|
||||
---@param max_lines integer
|
||||
local function compute_hunk_context(hunks, max_lines)
|
||||
---@type table<string, string[]|false>
|
||||
local file_cache = {}
|
||||
|
||||
for _, hunk in ipairs(hunks) do
|
||||
if not hunk.repo_root or not hunk.filename or not hunk.file_new_start then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local path = vim.fs.joinpath(hunk.repo_root, hunk.filename)
|
||||
local file_lines = file_cache[path]
|
||||
if file_lines == nil then
|
||||
file_lines = read_file_lines(path) or false
|
||||
file_cache[path] = file_lines
|
||||
end
|
||||
if not file_lines then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local new_start = hunk.file_new_start
|
||||
local new_count = hunk.file_new_count or 0
|
||||
local total = #file_lines
|
||||
|
||||
local before_start = math.max(1, new_start - max_lines)
|
||||
if before_start < new_start then
|
||||
local before = {}
|
||||
for i = before_start, new_start - 1 do
|
||||
before[#before + 1] = file_lines[i]
|
||||
end
|
||||
hunk.context_before = before
|
||||
end
|
||||
|
||||
local after_start = new_start + new_count
|
||||
local after_end = math.min(total, after_start + max_lines - 1)
|
||||
if after_start <= total then
|
||||
local after = {}
|
||||
for i = after_start, after_end do
|
||||
after[#after + 1] = file_lines[i]
|
||||
end
|
||||
hunk.context_after = after
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
local function ensure_cache(bufnr)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
|
|
@ -321,6 +384,9 @@ local function ensure_cache(bufnr)
|
|||
local lc = vim.api.nvim_buf_line_count(bufnr)
|
||||
local bc = vim.api.nvim_buf_get_offset(bufnr, lc)
|
||||
dbg('parsed %d hunks in buffer %d (tick %d)', #hunks, bufnr, tick)
|
||||
if config.highlights.context.enabled then
|
||||
compute_hunk_context(hunks, config.highlights.context.lines)
|
||||
end
|
||||
local carried = entry and not entry.pending_clear and carry_forward_highlighted(entry, hunks)
|
||||
hunk_cache[bufnr] = {
|
||||
hunks = hunks,
|
||||
|
|
@ -941,6 +1007,7 @@ M._test = {
|
|||
hunks_eq = hunks_eq,
|
||||
process_pending_clear = process_pending_clear,
|
||||
ft_retry_pending = ft_retry_pending,
|
||||
compute_hunk_context = compute_hunk_context,
|
||||
}
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
---@field prefix_width integer
|
||||
---@field quote_width integer
|
||||
---@field repo_root string?
|
||||
---@field context_before string[]?
|
||||
---@field context_after string[]?
|
||||
|
||||
local M = {}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue