Merge pull request #72 from barrettruth/feat/vscode-diff
feat(highlight): character-level intra-line diff highlighting
This commit is contained in:
commit
ac2eb657de
10 changed files with 1268 additions and 18 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"runtime.version": "Lua 5.1",
|
||||
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
|
||||
"diagnostics.globals": ["vim"],
|
||||
"diagnostics.globals": ["vim", "jit"],
|
||||
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
|
||||
"workspace.checkThirdParty": false,
|
||||
"completion.callSnippet": "Replace"
|
||||
|
|
|
|||
|
|
@ -62,7 +62,12 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
|||
},
|
||||
vim = {
|
||||
enabled = false,
|
||||
max_lines = 200,
|
||||
max_lines = 500,
|
||||
},
|
||||
intra = {
|
||||
enabled = true,
|
||||
algorithm = 'auto',
|
||||
max_lines = 500,
|
||||
},
|
||||
},
|
||||
fugitive = {
|
||||
|
|
@ -116,6 +121,10 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
|||
Vim syntax highlighting options (experimental).
|
||||
See |diffs.VimConfig| for fields.
|
||||
|
||||
{intra} (table, default: see below)
|
||||
Character-level (intra-line) diff highlighting.
|
||||
See |diffs.IntraConfig| for fields.
|
||||
|
||||
*diffs.TreesitterConfig*
|
||||
Treesitter config fields: ~
|
||||
{enabled} (boolean, default: true)
|
||||
|
|
@ -140,6 +149,26 @@ Configuration is done via `vim.g.diffs`. Set this before the plugin loads:
|
|||
this many lines. Lower than the treesitter default
|
||||
due to the per-character cost of |synID()|.
|
||||
|
||||
*diffs.IntraConfig*
|
||||
Intra config fields: ~
|
||||
{enabled} (boolean, default: true)
|
||||
Enable character-level diff highlighting within
|
||||
changed lines. When a line changes from `local x = 1`
|
||||
to `local x = 2`, only the `1`/`2` characters get
|
||||
an intense background overlay while the rest of the
|
||||
line keeps the softer line-level background.
|
||||
|
||||
{algorithm} (string, default: 'auto')
|
||||
Diff algorithm for character-level analysis.
|
||||
`'auto'`: use libvscodediff if available, else
|
||||
native `vim.diff()`. `'native'`: always use
|
||||
`vim.diff()`. `'vscode'`: require libvscodediff
|
||||
(falls back to native if not available).
|
||||
|
||||
{max_lines} (integer, default: 500)
|
||||
Skip character-level highlighting for hunks larger
|
||||
than this many lines.
|
||||
|
||||
Note: Header context (e.g., `@@ -10,3 +10,4 @@ func()`) is always
|
||||
highlighted with treesitter when a parser is available.
|
||||
|
||||
|
|
@ -259,6 +288,8 @@ Summary / commit detail views: ~
|
|||
- Background extmarks (`DiffsAdd`/`DiffsDelete`) at priority 198
|
||||
- `Normal` extmarks at priority 199 clear underlying diff foreground
|
||||
- Syntax highlights are applied as extmarks at priority 200
|
||||
- Character-level diff extmarks (`DiffsAddText`/`DiffsDeleteText`) at
|
||||
priority 201 overlay changed characters with an intense background
|
||||
- Conceal extmarks hide diff prefixes when `hide_prefix` is enabled
|
||||
4. Re-highlighting occurs on `TextChanged` (debounced) and `Syntax` events
|
||||
|
||||
|
|
@ -349,6 +380,19 @@ Fugitive unified diff highlights: ~
|
|||
DiffsDeleteNr Line number for `-` lines. Foreground from
|
||||
`diffRemoved`, background from `DiffsDelete`.
|
||||
|
||||
*DiffsAddText*
|
||||
DiffsAddText Character-level background for changed characters
|
||||
within `+` lines. Derived by blending `diffAdded`
|
||||
foreground with `Normal` background at 40% alpha.
|
||||
Uses the same base color as `DiffsAddNr` foreground,
|
||||
making changed characters clearly visible. Only sets
|
||||
`bg`, so treesitter foreground colors show through.
|
||||
|
||||
*DiffsDeleteText*
|
||||
DiffsDeleteText Character-level background for changed characters
|
||||
within `-` lines. Derived by blending `diffRemoved`
|
||||
foreground with `Normal` background at 40% alpha.
|
||||
|
||||
Diff mode window highlights: ~
|
||||
These are used for |winhighlight| remapping in `&diff` windows.
|
||||
|
||||
|
|
@ -382,6 +426,7 @@ Run |:checkhealth| diffs to verify your setup.
|
|||
Checks performed:
|
||||
- Neovim version >= 0.9.0
|
||||
- vim-fugitive is installed (optional)
|
||||
- libvscode_diff shared library is available (optional)
|
||||
|
||||
==============================================================================
|
||||
ACKNOWLEDGEMENTS *diffs-acknowledgements*
|
||||
|
|
|
|||
68
lua/diffs/debug.lua
Normal file
68
lua/diffs/debug.lua
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
local M = {}
|
||||
|
||||
local ns = vim.api.nvim_create_namespace('diffs')
|
||||
|
||||
function M.dump()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local marks = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
|
||||
local by_line = {}
|
||||
for _, mark in ipairs(marks) do
|
||||
local id, row, col, details = mark[1], mark[2], mark[3], mark[4]
|
||||
local entry = {
|
||||
id = id,
|
||||
row = row,
|
||||
col = col,
|
||||
end_row = details.end_row,
|
||||
end_col = details.end_col,
|
||||
hl_group = details.hl_group,
|
||||
priority = details.priority,
|
||||
line_hl_group = details.line_hl_group,
|
||||
number_hl_group = details.number_hl_group,
|
||||
virt_text = details.virt_text,
|
||||
}
|
||||
if not by_line[row] then
|
||||
by_line[row] = { text = lines[row + 1] or '', marks = {} }
|
||||
end
|
||||
table.insert(by_line[row].marks, entry)
|
||||
end
|
||||
|
||||
local all_ns_marks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true })
|
||||
local non_diffs = {}
|
||||
for _, mark in ipairs(all_ns_marks) do
|
||||
local details = mark[4]
|
||||
if details.ns_id ~= ns then
|
||||
table.insert(non_diffs, {
|
||||
ns_id = details.ns_id,
|
||||
row = mark[2],
|
||||
col = mark[3],
|
||||
end_row = details.end_row,
|
||||
end_col = details.end_col,
|
||||
hl_group = details.hl_group,
|
||||
priority = details.priority,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local result = {
|
||||
bufnr = bufnr,
|
||||
buf_name = vim.api.nvim_buf_get_name(bufnr),
|
||||
ns_id = ns,
|
||||
total_diffs_marks = #marks,
|
||||
total_all_marks = #all_ns_marks,
|
||||
non_diffs_marks = non_diffs,
|
||||
lines = by_line,
|
||||
}
|
||||
|
||||
local state_dir = vim.fn.stdpath('state')
|
||||
local path = state_dir .. '/diffs_debug.json'
|
||||
local f = io.open(path, 'w')
|
||||
if f then
|
||||
f:write(vim.json.encode(result))
|
||||
f:close()
|
||||
vim.notify('[diffs.nvim] debug dump: ' .. path, vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
353
lua/diffs/diff.lua
Normal file
353
lua/diffs/diff.lua
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
---@class diffs.CharSpan
|
||||
---@field line integer
|
||||
---@field col_start integer
|
||||
---@field col_end integer
|
||||
|
||||
---@class diffs.IntraChanges
|
||||
---@field add_spans diffs.CharSpan[]
|
||||
---@field del_spans diffs.CharSpan[]
|
||||
|
||||
---@class diffs.ChangeGroup
|
||||
---@field del_lines {idx: integer, text: string}[]
|
||||
---@field add_lines {idx: integer, text: string}[]
|
||||
|
||||
local M = {}
|
||||
|
||||
local dbg = require('diffs.log').dbg
|
||||
|
||||
---@param hunk_lines string[]
|
||||
---@return diffs.ChangeGroup[]
|
||||
function M.extract_change_groups(hunk_lines)
|
||||
---@type diffs.ChangeGroup[]
|
||||
local groups = {}
|
||||
---@type {idx: integer, text: string}[]
|
||||
local del_buf = {}
|
||||
---@type {idx: integer, text: string}[]
|
||||
local add_buf = {}
|
||||
|
||||
---@type boolean
|
||||
local in_del = false
|
||||
|
||||
for i, line in ipairs(hunk_lines) do
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == '-' then
|
||||
if not in_del and #add_buf > 0 then
|
||||
if #del_buf > 0 then
|
||||
table.insert(groups, { del_lines = del_buf, add_lines = add_buf })
|
||||
end
|
||||
del_buf = {}
|
||||
add_buf = {}
|
||||
end
|
||||
in_del = true
|
||||
table.insert(del_buf, { idx = i, text = line:sub(2) })
|
||||
elseif prefix == '+' then
|
||||
in_del = false
|
||||
table.insert(add_buf, { idx = i, text = line:sub(2) })
|
||||
else
|
||||
if #del_buf > 0 and #add_buf > 0 then
|
||||
table.insert(groups, { del_lines = del_buf, add_lines = add_buf })
|
||||
end
|
||||
del_buf = {}
|
||||
add_buf = {}
|
||||
in_del = false
|
||||
end
|
||||
end
|
||||
|
||||
if #del_buf > 0 and #add_buf > 0 then
|
||||
table.insert(groups, { del_lines = del_buf, add_lines = add_buf })
|
||||
end
|
||||
|
||||
return groups
|
||||
end
|
||||
|
||||
---@param old_text string
|
||||
---@param new_text string
|
||||
---@return {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[]
|
||||
local function byte_diff(old_text, new_text)
|
||||
local ok, result = pcall(vim.diff, old_text, new_text, { result_type = 'indices' })
|
||||
if not ok or not result then
|
||||
return {}
|
||||
end
|
||||
---@type {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[]
|
||||
local hunks = {}
|
||||
for _, h in ipairs(result) do
|
||||
table.insert(hunks, {
|
||||
old_start = h[1],
|
||||
old_count = h[2],
|
||||
new_start = h[3],
|
||||
new_count = h[4],
|
||||
})
|
||||
end
|
||||
return hunks
|
||||
end
|
||||
|
||||
---@param s string
|
||||
---@return string[]
|
||||
local function split_bytes(s)
|
||||
local bytes = {}
|
||||
for i = 1, #s do
|
||||
table.insert(bytes, s:sub(i, i))
|
||||
end
|
||||
return bytes
|
||||
end
|
||||
|
||||
---@param old_line string
|
||||
---@param new_line string
|
||||
---@param del_idx integer
|
||||
---@param add_idx integer
|
||||
---@return diffs.CharSpan[], diffs.CharSpan[]
|
||||
local function char_diff_pair(old_line, new_line, del_idx, add_idx)
|
||||
---@type diffs.CharSpan[]
|
||||
local del_spans = {}
|
||||
---@type diffs.CharSpan[]
|
||||
local add_spans = {}
|
||||
|
||||
local old_bytes = split_bytes(old_line)
|
||||
local new_bytes = split_bytes(new_line)
|
||||
|
||||
local old_text = table.concat(old_bytes, '\n') .. '\n'
|
||||
local new_text = table.concat(new_bytes, '\n') .. '\n'
|
||||
|
||||
local char_hunks = byte_diff(old_text, new_text)
|
||||
|
||||
for _, ch in ipairs(char_hunks) do
|
||||
if ch.old_count > 0 then
|
||||
table.insert(del_spans, {
|
||||
line = del_idx,
|
||||
col_start = ch.old_start,
|
||||
col_end = ch.old_start + ch.old_count,
|
||||
})
|
||||
end
|
||||
|
||||
if ch.new_count > 0 then
|
||||
table.insert(add_spans, {
|
||||
line = add_idx,
|
||||
col_start = ch.new_start,
|
||||
col_end = ch.new_start + ch.new_count,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return del_spans, add_spans
|
||||
end
|
||||
|
||||
---@param group diffs.ChangeGroup
|
||||
---@return diffs.CharSpan[], diffs.CharSpan[]
|
||||
local function diff_group_native(group)
|
||||
---@type diffs.CharSpan[]
|
||||
local all_del = {}
|
||||
---@type diffs.CharSpan[]
|
||||
local all_add = {}
|
||||
|
||||
local del_count = #group.del_lines
|
||||
local add_count = #group.add_lines
|
||||
|
||||
if del_count == 1 and add_count == 1 then
|
||||
local ds, as = char_diff_pair(
|
||||
group.del_lines[1].text,
|
||||
group.add_lines[1].text,
|
||||
group.del_lines[1].idx,
|
||||
group.add_lines[1].idx
|
||||
)
|
||||
vim.list_extend(all_del, ds)
|
||||
vim.list_extend(all_add, as)
|
||||
return all_del, all_add
|
||||
end
|
||||
|
||||
local old_texts = {}
|
||||
for _, l in ipairs(group.del_lines) do
|
||||
table.insert(old_texts, l.text)
|
||||
end
|
||||
local new_texts = {}
|
||||
for _, l in ipairs(group.add_lines) do
|
||||
table.insert(new_texts, l.text)
|
||||
end
|
||||
|
||||
local old_block = table.concat(old_texts, '\n') .. '\n'
|
||||
local new_block = table.concat(new_texts, '\n') .. '\n'
|
||||
|
||||
local line_hunks = byte_diff(old_block, new_block)
|
||||
|
||||
---@type table<integer, integer>
|
||||
local old_to_new = {}
|
||||
for _, lh in ipairs(line_hunks) do
|
||||
if lh.old_count == lh.new_count then
|
||||
for k = 0, lh.old_count - 1 do
|
||||
old_to_new[lh.old_start + k] = lh.new_start + k
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for old_i, new_i in pairs(old_to_new) do
|
||||
if group.del_lines[old_i] and group.add_lines[new_i] then
|
||||
local ds, as = char_diff_pair(
|
||||
group.del_lines[old_i].text,
|
||||
group.add_lines[new_i].text,
|
||||
group.del_lines[old_i].idx,
|
||||
group.add_lines[new_i].idx
|
||||
)
|
||||
vim.list_extend(all_del, ds)
|
||||
vim.list_extend(all_add, as)
|
||||
end
|
||||
end
|
||||
|
||||
for _, lh in ipairs(line_hunks) do
|
||||
if lh.old_count ~= lh.new_count then
|
||||
local pairs_count = math.min(lh.old_count, lh.new_count)
|
||||
for k = 0, pairs_count - 1 do
|
||||
local oi = lh.old_start + k
|
||||
local ni = lh.new_start + k
|
||||
if group.del_lines[oi] and group.add_lines[ni] then
|
||||
local ds, as = char_diff_pair(
|
||||
group.del_lines[oi].text,
|
||||
group.add_lines[ni].text,
|
||||
group.del_lines[oi].idx,
|
||||
group.add_lines[ni].idx
|
||||
)
|
||||
vim.list_extend(all_del, ds)
|
||||
vim.list_extend(all_add, as)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return all_del, all_add
|
||||
end
|
||||
|
||||
---@param group diffs.ChangeGroup
|
||||
---@param handle table
|
||||
---@return diffs.CharSpan[], diffs.CharSpan[]
|
||||
local function diff_group_vscode(group, handle)
|
||||
---@type diffs.CharSpan[]
|
||||
local all_del = {}
|
||||
---@type diffs.CharSpan[]
|
||||
local all_add = {}
|
||||
|
||||
local ffi = require('ffi')
|
||||
|
||||
local old_texts = {}
|
||||
for _, l in ipairs(group.del_lines) do
|
||||
table.insert(old_texts, l.text)
|
||||
end
|
||||
local new_texts = {}
|
||||
for _, l in ipairs(group.add_lines) do
|
||||
table.insert(new_texts, l.text)
|
||||
end
|
||||
|
||||
local orig_arr = ffi.new('const char*[?]', #old_texts)
|
||||
for i, t in ipairs(old_texts) do
|
||||
orig_arr[i - 1] = t
|
||||
end
|
||||
|
||||
local mod_arr = ffi.new('const char*[?]', #new_texts)
|
||||
for i, t in ipairs(new_texts) do
|
||||
mod_arr[i - 1] = t
|
||||
end
|
||||
|
||||
local opts = ffi.new('DiffsDiffOptions', {
|
||||
ignore_trim_whitespace = false,
|
||||
max_computation_time_ms = 1000,
|
||||
compute_moves = false,
|
||||
extend_to_subwords = false,
|
||||
})
|
||||
|
||||
local result = handle.compute_diff(orig_arr, #old_texts, mod_arr, #new_texts, opts)
|
||||
if result == nil then
|
||||
return all_del, all_add
|
||||
end
|
||||
|
||||
for ci = 0, result.changes.count - 1 do
|
||||
local mapping = result.changes.mappings[ci]
|
||||
for ii = 0, mapping.inner_change_count - 1 do
|
||||
local inner = mapping.inner_changes[ii]
|
||||
|
||||
local orig_line = inner.original.start_line
|
||||
if group.del_lines[orig_line] then
|
||||
table.insert(all_del, {
|
||||
line = group.del_lines[orig_line].idx,
|
||||
col_start = inner.original.start_col,
|
||||
col_end = inner.original.end_col,
|
||||
})
|
||||
end
|
||||
|
||||
local mod_line = inner.modified.start_line
|
||||
if group.add_lines[mod_line] then
|
||||
table.insert(all_add, {
|
||||
line = group.add_lines[mod_line].idx,
|
||||
col_start = inner.modified.start_col,
|
||||
col_end = inner.modified.end_col,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
handle.free_lines_diff(result)
|
||||
|
||||
return all_del, all_add
|
||||
end
|
||||
|
||||
---@param hunk_lines string[]
|
||||
---@param algorithm? string
|
||||
---@return diffs.IntraChanges?
|
||||
function M.compute_intra_hunks(hunk_lines, algorithm)
|
||||
local groups = M.extract_change_groups(hunk_lines)
|
||||
if #groups == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
algorithm = algorithm or 'auto'
|
||||
|
||||
local lib = require('diffs.lib')
|
||||
local vscode_handle = nil
|
||||
if algorithm ~= 'native' then
|
||||
vscode_handle = lib.load()
|
||||
end
|
||||
|
||||
if algorithm == 'vscode' and not vscode_handle then
|
||||
dbg('vscode algorithm requested but library not available, falling back to native')
|
||||
end
|
||||
|
||||
---@type diffs.CharSpan[]
|
||||
local all_add = {}
|
||||
---@type diffs.CharSpan[]
|
||||
local all_del = {}
|
||||
|
||||
dbg(
|
||||
'intra: %d change groups, algorithm=%s, vscode=%s',
|
||||
#groups,
|
||||
algorithm,
|
||||
vscode_handle and 'yes' or 'no'
|
||||
)
|
||||
|
||||
for gi, group in ipairs(groups) do
|
||||
dbg('group %d: %d del lines, %d add lines', gi, #group.del_lines, #group.add_lines)
|
||||
local ds, as
|
||||
if vscode_handle then
|
||||
ds, as = diff_group_vscode(group, vscode_handle)
|
||||
else
|
||||
ds, as = diff_group_native(group)
|
||||
end
|
||||
dbg('group %d result: %d del spans, %d add spans', gi, #ds, #as)
|
||||
for _, s in ipairs(ds) do
|
||||
dbg(' del span: line=%d col=%d..%d', s.line, s.col_start, s.col_end)
|
||||
end
|
||||
for _, s in ipairs(as) do
|
||||
dbg(' add span: line=%d col=%d..%d', s.line, s.col_start, s.col_end)
|
||||
end
|
||||
vim.list_extend(all_del, ds)
|
||||
vim.list_extend(all_add, as)
|
||||
end
|
||||
|
||||
if #all_add == 0 and #all_del == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return { add_spans = all_add, del_spans = all_del }
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.has_vscode()
|
||||
return require('diffs.lib').has_lib()
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -15,6 +15,13 @@ function M.check()
|
|||
else
|
||||
vim.health.warn('vim-fugitive not detected (required for unified diff highlighting)')
|
||||
end
|
||||
|
||||
local lib = require('diffs.lib')
|
||||
if lib.has_lib() then
|
||||
vim.health.ok('libvscode_diff found at ' .. lib.lib_path())
|
||||
else
|
||||
vim.health.info('libvscode_diff not found (optional, using native vim.diff fallback)')
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
local M = {}
|
||||
|
||||
local dbg = require('diffs.log').dbg
|
||||
local diff = require('diffs.diff')
|
||||
|
||||
---@param bufnr integer
|
||||
---@param ns integer
|
||||
|
|
@ -282,6 +283,40 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
|
||||
local syntax_applied = extmark_count > 0
|
||||
|
||||
---@type diffs.IntraChanges?
|
||||
local intra = nil
|
||||
local intra_cfg = opts.highlights.intra
|
||||
if intra_cfg and intra_cfg.enabled and #hunk.lines <= intra_cfg.max_lines then
|
||||
dbg('computing intra for hunk %s:%d (%d lines)', hunk.filename, hunk.start_line, #hunk.lines)
|
||||
intra = diff.compute_intra_hunks(hunk.lines, intra_cfg.algorithm)
|
||||
if intra then
|
||||
dbg('intra result: %d add spans, %d del spans', #intra.add_spans, #intra.del_spans)
|
||||
else
|
||||
dbg('intra result: nil (no change groups)')
|
||||
end
|
||||
elseif intra_cfg and not intra_cfg.enabled then
|
||||
dbg('intra disabled by config')
|
||||
elseif intra_cfg and #hunk.lines > intra_cfg.max_lines then
|
||||
dbg('intra skipped: %d lines > %d max', #hunk.lines, intra_cfg.max_lines)
|
||||
end
|
||||
|
||||
---@type table<integer, diffs.CharSpan[]>
|
||||
local char_spans_by_line = {}
|
||||
if intra then
|
||||
for _, span in ipairs(intra.add_spans) do
|
||||
if not char_spans_by_line[span.line] then
|
||||
char_spans_by_line[span.line] = {}
|
||||
end
|
||||
table.insert(char_spans_by_line[span.line], span)
|
||||
end
|
||||
for _, span in ipairs(intra.del_spans) do
|
||||
if not char_spans_by_line[span.line] then
|
||||
char_spans_by_line[span.line] = {}
|
||||
end
|
||||
table.insert(char_spans_by_line[span.line], span)
|
||||
end
|
||||
end
|
||||
|
||||
for i, line in ipairs(hunk.lines) do
|
||||
local buf_line = hunk.start_line + i - 1
|
||||
local line_len = #line
|
||||
|
|
@ -299,24 +334,47 @@ function M.highlight_hunk(bufnr, ns, hunk, opts)
|
|||
})
|
||||
end
|
||||
|
||||
if opts.highlights.background and is_diff_line then
|
||||
local extmark_opts = {
|
||||
line_hl_group = line_hl,
|
||||
priority = 198,
|
||||
}
|
||||
if opts.highlights.gutter then
|
||||
extmark_opts.number_hl_group = number_hl
|
||||
end
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, extmark_opts)
|
||||
end
|
||||
|
||||
if line_len > 1 and syntax_applied then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 1, {
|
||||
end_col = line_len,
|
||||
hl_group = 'Normal',
|
||||
priority = 198,
|
||||
})
|
||||
end
|
||||
|
||||
if opts.highlights.background and is_diff_line then
|
||||
pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, 0, {
|
||||
end_col = line_len,
|
||||
hl_group = line_hl,
|
||||
hl_eol = true,
|
||||
number_hl_group = opts.highlights.gutter and number_hl or nil,
|
||||
priority = 199,
|
||||
})
|
||||
end
|
||||
|
||||
if char_spans_by_line[i] then
|
||||
local char_hl = prefix == '+' and 'DiffsAddText' or 'DiffsDeleteText'
|
||||
for _, span in ipairs(char_spans_by_line[i]) do
|
||||
dbg(
|
||||
'char extmark: line=%d buf_line=%d col=%d..%d hl=%s text="%s"',
|
||||
i,
|
||||
buf_line,
|
||||
span.col_start,
|
||||
span.col_end,
|
||||
char_hl,
|
||||
line:sub(span.col_start + 1, span.col_end)
|
||||
)
|
||||
local ok, err = pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, buf_line, span.col_start, {
|
||||
end_col = span.col_end,
|
||||
hl_group = char_hl,
|
||||
priority = 201,
|
||||
})
|
||||
if not ok then
|
||||
dbg('char extmark FAILED: %s', err)
|
||||
end
|
||||
extmark_count = extmark_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
dbg('hunk %s:%d applied %d extmarks', hunk.filename, hunk.start_line, extmark_count)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,17 @@
|
|||
---@field enabled boolean
|
||||
---@field max_lines integer
|
||||
|
||||
---@class diffs.IntraConfig
|
||||
---@field enabled boolean
|
||||
---@field algorithm string
|
||||
---@field max_lines integer
|
||||
|
||||
---@class diffs.Highlights
|
||||
---@field background boolean
|
||||
---@field gutter boolean
|
||||
---@field treesitter diffs.TreesitterConfig
|
||||
---@field vim diffs.VimConfig
|
||||
---@field intra diffs.IntraConfig
|
||||
|
||||
---@class diffs.FugitiveConfig
|
||||
---@field horizontal string|false
|
||||
|
|
@ -82,6 +88,11 @@ local default_config = {
|
|||
enabled = false,
|
||||
max_lines = 200,
|
||||
},
|
||||
intra = {
|
||||
enabled = true,
|
||||
algorithm = 'auto',
|
||||
max_lines = 500,
|
||||
},
|
||||
},
|
||||
fugitive = {
|
||||
horizontal = 'du',
|
||||
|
|
@ -172,10 +183,24 @@ local function compute_highlight_groups()
|
|||
local blended_add = blend_color(add_bg, bg, 0.4)
|
||||
local blended_del = blend_color(del_bg, bg, 0.4)
|
||||
|
||||
local blended_add_text = blend_color(add_fg, bg, 0.7)
|
||||
local blended_del_text = blend_color(del_fg, bg, 0.7)
|
||||
|
||||
vim.api.nvim_set_hl(0, 'DiffsAdd', { default = true, bg = blended_add })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDelete', { default = true, bg = blended_del })
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddNr', { default = true, fg = add_fg, bg = blended_add })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDeleteNr', { default = true, fg = del_fg, bg = blended_del })
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddText', { default = true, bg = blended_add_text })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { default = true, bg = blended_del_text })
|
||||
|
||||
dbg('highlight groups: Normal.bg=#%06x DiffAdd.bg=#%06x diffAdded.fg=#%06x', bg, add_bg, add_fg)
|
||||
dbg(
|
||||
'DiffsAdd.bg=#%06x DiffsAddText.bg=#%06x DiffsAddNr.fg=#%06x',
|
||||
blended_add,
|
||||
blended_add_text,
|
||||
add_fg
|
||||
)
|
||||
dbg('DiffsDelete.bg=#%06x DiffsDeleteText.bg=#%06x', blended_del, blended_del_text)
|
||||
|
||||
local diff_change = resolve_hl('DiffChange')
|
||||
local diff_text = resolve_hl('DiffText')
|
||||
|
|
@ -207,6 +232,7 @@ local function init()
|
|||
['highlights.gutter'] = { opts.highlights.gutter, 'boolean', true },
|
||||
['highlights.treesitter'] = { opts.highlights.treesitter, 'table', true },
|
||||
['highlights.vim'] = { opts.highlights.vim, 'table', true },
|
||||
['highlights.intra'] = { opts.highlights.intra, 'table', true },
|
||||
})
|
||||
|
||||
if opts.highlights.treesitter then
|
||||
|
|
@ -226,6 +252,20 @@ local function init()
|
|||
['highlights.vim.max_lines'] = { opts.highlights.vim.max_lines, 'number', true },
|
||||
})
|
||||
end
|
||||
|
||||
if opts.highlights.intra then
|
||||
vim.validate({
|
||||
['highlights.intra.enabled'] = { opts.highlights.intra.enabled, 'boolean', true },
|
||||
['highlights.intra.algorithm'] = {
|
||||
opts.highlights.intra.algorithm,
|
||||
function(v)
|
||||
return v == nil or v == 'auto' or v == 'native' or v == 'vscode'
|
||||
end,
|
||||
"'auto', 'native', or 'vscode'",
|
||||
},
|
||||
['highlights.intra.max_lines'] = { opts.highlights.intra.max_lines, 'number', true },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if opts.fugitive then
|
||||
|
|
@ -266,6 +306,14 @@ local function init()
|
|||
then
|
||||
error('diffs: highlights.vim.max_lines must be >= 1')
|
||||
end
|
||||
if
|
||||
opts.highlights
|
||||
and opts.highlights.intra
|
||||
and opts.highlights.intra.max_lines
|
||||
and opts.highlights.intra.max_lines < 1
|
||||
then
|
||||
error('diffs: highlights.intra.max_lines must be >= 1')
|
||||
end
|
||||
|
||||
config = vim.tbl_deep_extend('force', default_config, opts)
|
||||
log.set_enabled(config.debug)
|
||||
|
|
|
|||
214
lua/diffs/lib.lua
Normal file
214
lua/diffs/lib.lua
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
local M = {}
|
||||
|
||||
local dbg = require('diffs.log').dbg
|
||||
|
||||
---@type table?
|
||||
local cached_handle = nil
|
||||
|
||||
---@type boolean
|
||||
local download_in_progress = false
|
||||
|
||||
---@return string
|
||||
local function get_os()
|
||||
local os_name = jit.os:lower()
|
||||
if os_name == 'osx' then
|
||||
return 'macos'
|
||||
end
|
||||
return os_name
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function get_arch()
|
||||
return jit.arch:lower()
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function get_ext()
|
||||
local os_name = jit.os:lower()
|
||||
if os_name == 'windows' then
|
||||
return 'dll'
|
||||
elseif os_name == 'osx' then
|
||||
return 'dylib'
|
||||
end
|
||||
return 'so'
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function lib_dir()
|
||||
return vim.fn.stdpath('data') .. '/diffs/lib'
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function lib_path()
|
||||
return lib_dir() .. '/libvscode_diff.' .. get_ext()
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function version_path()
|
||||
return lib_dir() .. '/version'
|
||||
end
|
||||
|
||||
local EXPECTED_VERSION = '2.18.0'
|
||||
|
||||
---@return boolean
|
||||
function M.has_lib()
|
||||
if cached_handle then
|
||||
return true
|
||||
end
|
||||
return vim.fn.filereadable(lib_path()) == 1
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.lib_path()
|
||||
return lib_path()
|
||||
end
|
||||
|
||||
---@return table?
|
||||
function M.load()
|
||||
if cached_handle then
|
||||
return cached_handle
|
||||
end
|
||||
|
||||
local path = lib_path()
|
||||
if vim.fn.filereadable(path) ~= 1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ffi = require('ffi')
|
||||
|
||||
ffi.cdef([[
|
||||
typedef struct {
|
||||
int start_line;
|
||||
int end_line;
|
||||
} DiffsLineRange;
|
||||
|
||||
typedef struct {
|
||||
int start_line;
|
||||
int start_col;
|
||||
int end_line;
|
||||
int end_col;
|
||||
} DiffsCharRange;
|
||||
|
||||
typedef struct {
|
||||
DiffsCharRange original;
|
||||
DiffsCharRange modified;
|
||||
} DiffsRangeMapping;
|
||||
|
||||
typedef struct {
|
||||
DiffsLineRange original;
|
||||
DiffsLineRange modified;
|
||||
DiffsRangeMapping* inner_changes;
|
||||
int inner_change_count;
|
||||
} DiffsDetailedMapping;
|
||||
|
||||
typedef struct {
|
||||
DiffsDetailedMapping* mappings;
|
||||
int count;
|
||||
int capacity;
|
||||
} DiffsDetailedMappingArray;
|
||||
|
||||
typedef struct {
|
||||
DiffsLineRange original;
|
||||
DiffsLineRange modified;
|
||||
} DiffsMovedText;
|
||||
|
||||
typedef struct {
|
||||
DiffsMovedText* moves;
|
||||
int count;
|
||||
int capacity;
|
||||
} DiffsMovedTextArray;
|
||||
|
||||
typedef struct {
|
||||
DiffsDetailedMappingArray changes;
|
||||
DiffsMovedTextArray moves;
|
||||
bool hit_timeout;
|
||||
} DiffsLinesDiff;
|
||||
|
||||
typedef struct {
|
||||
bool ignore_trim_whitespace;
|
||||
int max_computation_time_ms;
|
||||
bool compute_moves;
|
||||
bool extend_to_subwords;
|
||||
} DiffsDiffOptions;
|
||||
|
||||
DiffsLinesDiff* compute_diff(
|
||||
const char** original_lines,
|
||||
int original_count,
|
||||
const char** modified_lines,
|
||||
int modified_count,
|
||||
const DiffsDiffOptions* options
|
||||
);
|
||||
|
||||
void free_lines_diff(DiffsLinesDiff* diff);
|
||||
]])
|
||||
|
||||
local ok, handle = pcall(ffi.load, path)
|
||||
if not ok then
|
||||
dbg('failed to load libvscode_diff: %s', handle)
|
||||
return nil
|
||||
end
|
||||
|
||||
cached_handle = handle
|
||||
return handle
|
||||
end
|
||||
|
||||
---@param callback fun(handle: table?)
|
||||
function M.ensure(callback)
|
||||
if cached_handle then
|
||||
callback(cached_handle)
|
||||
return
|
||||
end
|
||||
|
||||
if M.has_lib() then
|
||||
callback(M.load())
|
||||
return
|
||||
end
|
||||
|
||||
if download_in_progress then
|
||||
dbg('download already in progress')
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
|
||||
download_in_progress = true
|
||||
|
||||
local dir = lib_dir()
|
||||
vim.fn.mkdir(dir, 'p')
|
||||
|
||||
local os_name = get_os()
|
||||
local arch = get_arch()
|
||||
local ext = get_ext()
|
||||
local filename = ('libvscode_diff_%s_%s_%s.%s'):format(os_name, arch, EXPECTED_VERSION, ext)
|
||||
local url = ('https://github.com/esmuellert/vscode-diff.nvim/releases/download/v%s/%s'):format(
|
||||
EXPECTED_VERSION,
|
||||
filename
|
||||
)
|
||||
|
||||
local dest = lib_path()
|
||||
vim.notify('[diffs] downloading libvscode_diff...', vim.log.levels.INFO)
|
||||
|
||||
local cmd = { 'curl', '-fSL', '-o', dest, url }
|
||||
|
||||
vim.system(cmd, {}, function(result)
|
||||
download_in_progress = false
|
||||
vim.schedule(function()
|
||||
if result.code ~= 0 then
|
||||
vim.notify('[diffs] failed to download libvscode_diff', vim.log.levels.WARN)
|
||||
dbg('curl failed: %s', result.stderr or '')
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
|
||||
local f = io.open(version_path(), 'w')
|
||||
if f then
|
||||
f:write(EXPECTED_VERSION)
|
||||
f:close()
|
||||
end
|
||||
|
||||
vim.notify('[diffs] libvscode_diff downloaded', vim.log.levels.INFO)
|
||||
callback(M.load())
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
163
spec/diff_spec.lua
Normal file
163
spec/diff_spec.lua
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
require('spec.helpers')
|
||||
local diff = require('diffs.diff')
|
||||
|
||||
describe('diff', function()
|
||||
describe('extract_change_groups', function()
|
||||
it('returns empty for all context lines', function()
|
||||
local groups = diff.extract_change_groups({ ' line1', ' line2', ' line3' })
|
||||
assert.are.equal(0, #groups)
|
||||
end)
|
||||
|
||||
it('returns empty for pure additions', function()
|
||||
local groups = diff.extract_change_groups({ '+line1', '+line2' })
|
||||
assert.are.equal(0, #groups)
|
||||
end)
|
||||
|
||||
it('returns empty for pure deletions', function()
|
||||
local groups = diff.extract_change_groups({ '-line1', '-line2' })
|
||||
assert.are.equal(0, #groups)
|
||||
end)
|
||||
|
||||
it('extracts single change group', function()
|
||||
local groups = diff.extract_change_groups({
|
||||
' context',
|
||||
'-old line',
|
||||
'+new line',
|
||||
' context',
|
||||
})
|
||||
assert.are.equal(1, #groups)
|
||||
assert.are.equal(1, #groups[1].del_lines)
|
||||
assert.are.equal(1, #groups[1].add_lines)
|
||||
assert.are.equal('old line', groups[1].del_lines[1].text)
|
||||
assert.are.equal('new line', groups[1].add_lines[1].text)
|
||||
end)
|
||||
|
||||
it('extracts multiple change groups separated by context', function()
|
||||
local groups = diff.extract_change_groups({
|
||||
'-old1',
|
||||
'+new1',
|
||||
' context',
|
||||
'-old2',
|
||||
'+new2',
|
||||
})
|
||||
assert.are.equal(2, #groups)
|
||||
assert.are.equal('old1', groups[1].del_lines[1].text)
|
||||
assert.are.equal('new1', groups[1].add_lines[1].text)
|
||||
assert.are.equal('old2', groups[2].del_lines[1].text)
|
||||
assert.are.equal('new2', groups[2].add_lines[1].text)
|
||||
end)
|
||||
|
||||
it('tracks correct line indices', function()
|
||||
local groups = diff.extract_change_groups({
|
||||
' context',
|
||||
'-deleted',
|
||||
'+added',
|
||||
})
|
||||
assert.are.equal(2, groups[1].del_lines[1].idx)
|
||||
assert.are.equal(3, groups[1].add_lines[1].idx)
|
||||
end)
|
||||
|
||||
it('handles multiple del lines followed by multiple add lines', function()
|
||||
local groups = diff.extract_change_groups({
|
||||
'-del1',
|
||||
'-del2',
|
||||
'+add1',
|
||||
'+add2',
|
||||
'+add3',
|
||||
})
|
||||
assert.are.equal(1, #groups)
|
||||
assert.are.equal(2, #groups[1].del_lines)
|
||||
assert.are.equal(3, #groups[1].add_lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('compute_intra_hunks', function()
|
||||
it('returns nil for all-addition hunks', function()
|
||||
local result = diff.compute_intra_hunks({ '+line1', '+line2' }, 'native')
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
|
||||
it('returns nil for all-deletion hunks', function()
|
||||
local result = diff.compute_intra_hunks({ '-line1', '-line2' }, 'native')
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
|
||||
it('returns nil for context-only hunks', function()
|
||||
local result = diff.compute_intra_hunks({ ' line1', ' line2' }, 'native')
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
|
||||
it('returns spans for single word change', function()
|
||||
local result = diff.compute_intra_hunks({
|
||||
'-local x = 1',
|
||||
'+local x = 2',
|
||||
}, 'native')
|
||||
assert.is_not_nil(result)
|
||||
assert.is_true(#result.del_spans > 0)
|
||||
assert.is_true(#result.add_spans > 0)
|
||||
end)
|
||||
|
||||
it('identifies correct byte offsets for word change', function()
|
||||
local result = diff.compute_intra_hunks({
|
||||
'-local x = 1',
|
||||
'+local x = 2',
|
||||
}, 'native')
|
||||
assert.is_not_nil(result)
|
||||
|
||||
assert.are.equal(1, #result.del_spans)
|
||||
assert.are.equal(1, #result.add_spans)
|
||||
local del_span = result.del_spans[1]
|
||||
local add_span = result.add_spans[1]
|
||||
local del_text = ('local x = 1'):sub(del_span.col_start, del_span.col_end - 1)
|
||||
local add_text = ('local x = 2'):sub(add_span.col_start, add_span.col_end - 1)
|
||||
assert.are.equal('1', del_text)
|
||||
assert.are.equal('2', add_text)
|
||||
end)
|
||||
|
||||
it('handles multiple change groups separated by context', function()
|
||||
local result = diff.compute_intra_hunks({
|
||||
'-local a = 1',
|
||||
'+local a = 2',
|
||||
' local b = 3',
|
||||
'-local c = 4',
|
||||
'+local c = 5',
|
||||
}, 'native')
|
||||
assert.is_not_nil(result)
|
||||
assert.is_true(#result.del_spans >= 2)
|
||||
assert.is_true(#result.add_spans >= 2)
|
||||
end)
|
||||
|
||||
it('handles uneven line counts (2 old, 1 new)', function()
|
||||
local result = diff.compute_intra_hunks({
|
||||
'-line one',
|
||||
'-line two',
|
||||
'+line combined',
|
||||
}, 'native')
|
||||
assert.is_not_nil(result)
|
||||
end)
|
||||
|
||||
it('handles multi-byte UTF-8 content', function()
|
||||
local result = diff.compute_intra_hunks({
|
||||
'-local x = "héllo"',
|
||||
'+local x = "wörld"',
|
||||
}, 'native')
|
||||
assert.is_not_nil(result)
|
||||
assert.is_true(#result.del_spans > 0)
|
||||
assert.is_true(#result.add_spans > 0)
|
||||
end)
|
||||
|
||||
it('returns nil when del and add are identical', function()
|
||||
local result = diff.compute_intra_hunks({
|
||||
'-local x = 1',
|
||||
'+local x = 1',
|
||||
}, 'native')
|
||||
assert.is_nil(result)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('has_vscode', function()
|
||||
it('returns false in test environment', function()
|
||||
assert.is_false(diff.has_vscode())
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -43,6 +43,11 @@ describe('highlight', function()
|
|||
enabled = false,
|
||||
max_lines = 200,
|
||||
},
|
||||
intra = {
|
||||
enabled = false,
|
||||
algorithm = 'native',
|
||||
max_lines = 500,
|
||||
},
|
||||
},
|
||||
}
|
||||
if overrides then
|
||||
|
|
@ -322,7 +327,7 @@ describe('highlight', function()
|
|||
local extmarks = get_extmarks(bufnr)
|
||||
local has_diff_add = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
|
||||
if mark[4] and mark[4].hl_group == 'DiffsAdd' then
|
||||
has_diff_add = true
|
||||
break
|
||||
end
|
||||
|
|
@ -355,7 +360,7 @@ describe('highlight', function()
|
|||
local extmarks = get_extmarks(bufnr)
|
||||
local has_diff_delete = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].line_hl_group == 'DiffsDelete' then
|
||||
if mark[4] and mark[4].hl_group == 'DiffsDelete' then
|
||||
has_diff_delete = true
|
||||
break
|
||||
end
|
||||
|
|
@ -388,7 +393,7 @@ describe('highlight', function()
|
|||
local extmarks = get_extmarks(bufnr)
|
||||
local has_line_hl = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].line_hl_group then
|
||||
if mark[4] and (mark[4].hl_group == 'DiffsAdd' or mark[4].hl_group == 'DiffsDelete') then
|
||||
has_line_hl = true
|
||||
break
|
||||
end
|
||||
|
|
@ -520,7 +525,7 @@ describe('highlight', function()
|
|||
local extmarks = get_extmarks(bufnr)
|
||||
local has_diff_add = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
|
||||
if mark[4] and mark[4].hl_group == 'DiffsAdd' then
|
||||
has_diff_add = true
|
||||
break
|
||||
end
|
||||
|
|
@ -668,7 +673,7 @@ describe('highlight', function()
|
|||
local extmarks = get_extmarks(bufnr)
|
||||
local has_diff_add = false
|
||||
for _, mark in ipairs(extmarks) do
|
||||
if mark[4] and mark[4].line_hl_group == 'DiffsAdd' then
|
||||
if mark[4] and mark[4].hl_group == 'DiffsAdd' then
|
||||
has_diff_add = true
|
||||
break
|
||||
end
|
||||
|
|
@ -727,6 +732,295 @@ describe('highlight', function()
|
|||
assert.is_true(has_normal)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('uses hl_group not line_hl_group for line backgrounds', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,1 @@',
|
||||
'-local x = 1',
|
||||
'+local y = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '-local x = 1', '+local y = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({ highlights = { background = true } })
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
|
||||
assert.is_true(d.hl_eol == true)
|
||||
assert.is_nil(d.line_hl_group)
|
||||
end
|
||||
end
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('line bg priority > Normal priority', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,1 @@',
|
||||
'-local x = 1',
|
||||
'+local y = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '-local x = 1', '+local y = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({ highlights = { background = true } })
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local normal_priority = nil
|
||||
local line_bg_priority = nil
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
if d and d.hl_group == 'Normal' then
|
||||
normal_priority = d.priority
|
||||
end
|
||||
if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
|
||||
line_bg_priority = d.priority
|
||||
end
|
||||
end
|
||||
assert.is_not_nil(normal_priority)
|
||||
assert.is_not_nil(line_bg_priority)
|
||||
assert.is_true(line_bg_priority > normal_priority)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('char-level extmarks have higher priority than line bg', function()
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
|
||||
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,2 @@',
|
||||
'-local x = 1',
|
||||
'+local x = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '-local x = 1', '+local x = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({
|
||||
highlights = {
|
||||
background = true,
|
||||
intra = { enabled = true, algorithm = 'native', max_lines = 500 },
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local line_bg_priority = nil
|
||||
local char_bg_priority = nil
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
if d and (d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete') then
|
||||
line_bg_priority = d.priority
|
||||
end
|
||||
if d and (d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText') then
|
||||
char_bg_priority = d.priority
|
||||
end
|
||||
end
|
||||
assert.is_not_nil(line_bg_priority)
|
||||
assert.is_not_nil(char_bg_priority)
|
||||
assert.is_true(char_bg_priority > line_bg_priority)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('creates char-level extmarks for changed characters', function()
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
|
||||
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,2 @@',
|
||||
'-local x = 1',
|
||||
'+local x = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '-local x = 1', '+local x = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({
|
||||
highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } },
|
||||
})
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local add_text_marks = {}
|
||||
local del_text_marks = {}
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
if d and d.hl_group == 'DiffsAddText' then
|
||||
table.insert(add_text_marks, mark)
|
||||
end
|
||||
if d and d.hl_group == 'DiffsDeleteText' then
|
||||
table.insert(del_text_marks, mark)
|
||||
end
|
||||
end
|
||||
assert.is_true(#add_text_marks > 0)
|
||||
assert.is_true(#del_text_marks > 0)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('does not create char-level extmarks when intra disabled', function()
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,2 @@',
|
||||
'-local x = 1',
|
||||
'+local x = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '-local x = 1', '+local x = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({
|
||||
highlights = { intra = { enabled = false, algorithm = 'native', max_lines = 500 } },
|
||||
})
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
assert.is_not_equal('DiffsAddText', d and d.hl_group)
|
||||
assert.is_not_equal('DiffsDeleteText', d and d.hl_group)
|
||||
end
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('does not create char-level extmarks for pure additions', function()
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
|
||||
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,0 +1,2 @@',
|
||||
'+local x = 1',
|
||||
'+local y = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '+local x = 1', '+local y = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({
|
||||
highlights = { intra = { enabled = true, algorithm = 'native', max_lines = 500 } },
|
||||
})
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
assert.is_not_equal('DiffsAddText', d and d.hl_group)
|
||||
assert.is_not_equal('DiffsDeleteText', d and d.hl_group)
|
||||
end
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
|
||||
it('enforces priority order: Normal < line bg < syntax < char bg', function()
|
||||
vim.api.nvim_set_hl(0, 'DiffsAddText', { bg = 0x00FF00 })
|
||||
vim.api.nvim_set_hl(0, 'DiffsDeleteText', { bg = 0xFF0000 })
|
||||
|
||||
local bufnr = create_buffer({
|
||||
'@@ -1,2 +1,2 @@',
|
||||
'-local x = 1',
|
||||
'+local x = 2',
|
||||
})
|
||||
|
||||
local hunk = {
|
||||
filename = 'test.lua',
|
||||
lang = 'lua',
|
||||
start_line = 1,
|
||||
lines = { '-local x = 1', '+local x = 2' },
|
||||
}
|
||||
|
||||
highlight.highlight_hunk(
|
||||
bufnr,
|
||||
ns,
|
||||
hunk,
|
||||
default_opts({
|
||||
highlights = {
|
||||
background = true,
|
||||
intra = { enabled = true, algorithm = 'native', max_lines = 500 },
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
local extmarks = get_extmarks(bufnr)
|
||||
local priorities = { normal = {}, line_bg = {}, syntax = {}, char_bg = {} }
|
||||
for _, mark in ipairs(extmarks) do
|
||||
local d = mark[4]
|
||||
if d then
|
||||
if d.hl_group == 'Normal' then
|
||||
table.insert(priorities.normal, d.priority)
|
||||
elseif d.hl_group == 'DiffsAdd' or d.hl_group == 'DiffsDelete' then
|
||||
table.insert(priorities.line_bg, d.priority)
|
||||
elseif d.hl_group == 'DiffsAddText' or d.hl_group == 'DiffsDeleteText' then
|
||||
table.insert(priorities.char_bg, d.priority)
|
||||
elseif d.hl_group and d.hl_group:match('^@.*%.lua$') then
|
||||
table.insert(priorities.syntax, d.priority)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(#priorities.normal > 0)
|
||||
assert.is_true(#priorities.line_bg > 0)
|
||||
assert.is_true(#priorities.syntax > 0)
|
||||
assert.is_true(#priorities.char_bg > 0)
|
||||
|
||||
local max_normal = math.max(unpack(priorities.normal))
|
||||
local min_line_bg = math.min(unpack(priorities.line_bg))
|
||||
local min_syntax = math.min(unpack(priorities.syntax))
|
||||
local min_char_bg = math.min(unpack(priorities.char_bg))
|
||||
|
||||
assert.is_true(max_normal < min_line_bg)
|
||||
assert.is_true(min_line_bg < min_syntax)
|
||||
assert.is_true(min_syntax < min_char_bg)
|
||||
delete_buffer(bufnr)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('diff header highlighting', function()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue