## 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.
382 lines
10 KiB
Lua
382 lines
10 KiB
Lua
require('spec.helpers')
|
|
local diffs = require('diffs')
|
|
local highlight = require('diffs.highlight')
|
|
local compute_hunk_context = diffs._test.compute_hunk_context
|
|
|
|
describe('context', function()
|
|
describe('compute_hunk_context', function()
|
|
local tmpdir
|
|
|
|
before_each(function()
|
|
tmpdir = vim.fn.tempname()
|
|
vim.fn.mkdir(tmpdir, 'p')
|
|
end)
|
|
|
|
after_each(function()
|
|
vim.fn.delete(tmpdir, 'rf')
|
|
end)
|
|
|
|
local function write_file(filename, lines)
|
|
local path = vim.fs.joinpath(tmpdir, filename)
|
|
local dir = vim.fn.fnamemodify(path, ':h')
|
|
if vim.fn.isdirectory(dir) == 0 then
|
|
vim.fn.mkdir(dir, 'p')
|
|
end
|
|
local f = io.open(path, 'w')
|
|
f:write(table.concat(lines, '\n') .. '\n')
|
|
f:close()
|
|
end
|
|
|
|
local function make_hunk(filename, opts)
|
|
return {
|
|
filename = filename,
|
|
ft = 'lua',
|
|
lang = 'lua',
|
|
start_line = opts.start_line or 1,
|
|
lines = opts.lines,
|
|
prefix_width = opts.prefix_width or 1,
|
|
quote_width = 0,
|
|
repo_root = tmpdir,
|
|
file_new_start = opts.file_new_start,
|
|
file_new_count = opts.file_new_count,
|
|
}
|
|
end
|
|
|
|
it('reads context_before from file lines preceding the hunk', function()
|
|
write_file('a.lua', {
|
|
'local M = {}',
|
|
'function M.foo()',
|
|
' local x = 1',
|
|
' local y = 2',
|
|
'end',
|
|
'return M',
|
|
})
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = 3,
|
|
file_new_count = 3,
|
|
lines = { ' local x = 1', '+local new = true', ' local y = 2' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.same({ 'local M = {}', 'function M.foo()' }, hunks[1].context_before)
|
|
end)
|
|
|
|
it('reads context_after from file lines following the hunk', function()
|
|
write_file('a.lua', {
|
|
'local M = {}',
|
|
'function M.foo()',
|
|
' local x = 1',
|
|
'end',
|
|
'return M',
|
|
})
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = 2,
|
|
file_new_count = 2,
|
|
lines = { ' function M.foo()', '+ local x = 1' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.same({ 'end', 'return M' }, hunks[1].context_after)
|
|
end)
|
|
|
|
it('caps context_before to max_lines', function()
|
|
write_file('a.lua', {
|
|
'line1',
|
|
'line2',
|
|
'line3',
|
|
'line4',
|
|
'line5',
|
|
'target',
|
|
})
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = 6,
|
|
file_new_count = 1,
|
|
lines = { '+target' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 2)
|
|
|
|
assert.same({ 'line4', 'line5' }, hunks[1].context_before)
|
|
end)
|
|
|
|
it('caps context_after to max_lines', function()
|
|
write_file('a.lua', {
|
|
'target',
|
|
'after1',
|
|
'after2',
|
|
'after3',
|
|
'after4',
|
|
})
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = 1,
|
|
file_new_count = 1,
|
|
lines = { '+target' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 2)
|
|
|
|
assert.same({ 'after1', 'after2' }, hunks[1].context_after)
|
|
end)
|
|
|
|
it('skips hunks without file_new_start', function()
|
|
write_file('a.lua', { 'line1', 'line2' })
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = nil,
|
|
file_new_count = nil,
|
|
lines = { '+something' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.is_nil(hunks[1].context_before)
|
|
assert.is_nil(hunks[1].context_after)
|
|
end)
|
|
|
|
it('skips hunks without repo_root', function()
|
|
local hunks = {
|
|
{
|
|
filename = 'a.lua',
|
|
ft = 'lua',
|
|
lang = 'lua',
|
|
start_line = 1,
|
|
lines = { '+x' },
|
|
prefix_width = 1,
|
|
quote_width = 0,
|
|
repo_root = nil,
|
|
file_new_start = 1,
|
|
file_new_count = 1,
|
|
},
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.is_nil(hunks[1].context_before)
|
|
assert.is_nil(hunks[1].context_after)
|
|
end)
|
|
|
|
it('skips when file does not exist on disk', function()
|
|
local hunks = {
|
|
make_hunk('nonexistent.lua', {
|
|
file_new_start = 1,
|
|
file_new_count = 1,
|
|
lines = { '+x' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.is_nil(hunks[1].context_before)
|
|
assert.is_nil(hunks[1].context_after)
|
|
end)
|
|
|
|
it('returns nil context_before for hunk at line 1', function()
|
|
write_file('a.lua', { 'first', 'second' })
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = 1,
|
|
file_new_count = 1,
|
|
lines = { '+first' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.is_nil(hunks[1].context_before)
|
|
end)
|
|
|
|
it('returns nil context_after for hunk at end of file', function()
|
|
write_file('a.lua', { 'first', 'last' })
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = 1,
|
|
file_new_count = 2,
|
|
lines = { ' first', '+last' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.is_nil(hunks[1].context_after)
|
|
end)
|
|
|
|
it('reads file once for multiple hunks in same file', function()
|
|
write_file('a.lua', {
|
|
'local M = {}',
|
|
'function M.foo()',
|
|
' return 1',
|
|
'end',
|
|
'function M.bar()',
|
|
' return 2',
|
|
'end',
|
|
'return M',
|
|
})
|
|
|
|
local hunks = {
|
|
make_hunk('a.lua', {
|
|
file_new_start = 2,
|
|
file_new_count = 3,
|
|
lines = { ' function M.foo()', '+ return 1', ' end' },
|
|
}),
|
|
make_hunk('a.lua', {
|
|
file_new_start = 5,
|
|
file_new_count = 3,
|
|
lines = { ' function M.bar()', '+ return 2', ' end' },
|
|
}),
|
|
}
|
|
compute_hunk_context(hunks, 25)
|
|
|
|
assert.same({ 'local M = {}' }, hunks[1].context_before)
|
|
assert.same({ 'function M.bar()', ' return 2', 'end', 'return M' }, hunks[1].context_after)
|
|
assert.same({
|
|
'local M = {}',
|
|
'function M.foo()',
|
|
' return 1',
|
|
'end',
|
|
}, hunks[2].context_before)
|
|
assert.same({ 'return M' }, hunks[2].context_after)
|
|
end)
|
|
end)
|
|
|
|
describe('highlight_treesitter with context', function()
|
|
local ns
|
|
|
|
before_each(function()
|
|
ns = vim.api.nvim_create_namespace('diffs_context_test')
|
|
local normal = vim.api.nvim_get_hl(0, { name = 'Normal' })
|
|
vim.api.nvim_set_hl(0, 'DiffsClear', { fg = normal.fg or 0xc0c0c0 })
|
|
end)
|
|
|
|
local function create_buffer(lines)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
return bufnr
|
|
end
|
|
|
|
local function delete_buffer(bufnr)
|
|
if vim.api.nvim_buf_is_valid(bufnr) then
|
|
vim.api.nvim_buf_delete(bufnr, { force = true })
|
|
end
|
|
end
|
|
|
|
local function get_extmarks(bufnr)
|
|
return vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
|
|
end
|
|
|
|
local function default_opts(overrides)
|
|
local opts = {
|
|
hide_prefix = false,
|
|
highlights = {
|
|
background = false,
|
|
gutter = false,
|
|
context = { enabled = true, lines = 25 },
|
|
treesitter = { enabled = true, max_lines = 500 },
|
|
vim = { enabled = false, max_lines = 200 },
|
|
intra = { enabled = false, algorithm = 'default', max_lines = 500 },
|
|
priorities = { clear = 198, syntax = 199, line_bg = 200, char_bg = 201 },
|
|
},
|
|
}
|
|
if overrides then
|
|
if overrides.highlights then
|
|
opts.highlights = vim.tbl_deep_extend('force', opts.highlights, overrides.highlights)
|
|
end
|
|
end
|
|
return opts
|
|
end
|
|
|
|
it('applies extmarks only to hunk lines, not context lines', function()
|
|
local bufnr = create_buffer({
|
|
'@@ -1,2 +1,3 @@',
|
|
' local x = 1',
|
|
' local y = 2',
|
|
'+local z = 3',
|
|
})
|
|
|
|
local hunk = {
|
|
filename = 'test.lua',
|
|
lang = 'lua',
|
|
start_line = 2,
|
|
lines = { ' local x = 1', ' local y = 2', '+local z = 3' },
|
|
prefix_width = 1,
|
|
quote_width = 0,
|
|
context_before = { 'local function foo()' },
|
|
context_after = { 'end' },
|
|
}
|
|
|
|
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
|
|
|
|
local extmarks = get_extmarks(bufnr)
|
|
for _, mark in ipairs(extmarks) do
|
|
local row = mark[2]
|
|
assert.is_true(row >= 1 and row <= 3, 'extmark row ' .. row .. ' outside hunk range')
|
|
end
|
|
assert.is_true(#extmarks > 0)
|
|
delete_buffer(bufnr)
|
|
end)
|
|
|
|
it('does not pass context when context.enabled = false', function()
|
|
local bufnr = create_buffer({
|
|
'@@ -1,1 +1,2 @@',
|
|
' local x = 1',
|
|
'+local y = 2',
|
|
})
|
|
|
|
local hunk = {
|
|
filename = 'test.lua',
|
|
lang = 'lua',
|
|
start_line = 2,
|
|
lines = { ' local x = 1', '+local y = 2' },
|
|
prefix_width = 1,
|
|
quote_width = 0,
|
|
context_before = { 'local function foo()' },
|
|
context_after = { 'end' },
|
|
}
|
|
|
|
local opts_enabled = default_opts({ highlights = { context = { enabled = true } } })
|
|
highlight.highlight_hunk(bufnr, ns, hunk, opts_enabled)
|
|
local extmarks_with = get_extmarks(bufnr)
|
|
|
|
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
|
|
|
local opts_disabled = default_opts({ highlights = { context = { enabled = false } } })
|
|
highlight.highlight_hunk(bufnr, ns, hunk, opts_disabled)
|
|
local extmarks_without = get_extmarks(bufnr)
|
|
|
|
assert.is_true(#extmarks_with > 0)
|
|
assert.is_true(#extmarks_without > 0)
|
|
delete_buffer(bufnr)
|
|
end)
|
|
|
|
it('skips context fields that are nil', function()
|
|
local bufnr = create_buffer({
|
|
'@@ -1,1 +1,2 @@',
|
|
' local x = 1',
|
|
'+local y = 2',
|
|
})
|
|
|
|
local hunk = {
|
|
filename = 'test.lua',
|
|
lang = 'lua',
|
|
start_line = 2,
|
|
lines = { ' local x = 1', '+local y = 2' },
|
|
prefix_width = 1,
|
|
quote_width = 0,
|
|
}
|
|
|
|
highlight.highlight_hunk(bufnr, ns, hunk, default_opts())
|
|
|
|
local extmarks = get_extmarks(bufnr)
|
|
assert.is_true(#extmarks > 0)
|
|
delete_buffer(bufnr)
|
|
end)
|
|
end)
|
|
end)
|