'default' inherits algorithm and linematch from diffopt, 'vscode' uses the FFI library. Removes the need for diffs.nvim to duplicate settings that users already control globally.
391 lines
10 KiB
Lua
391 lines
10 KiB
Lua
---@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
|
|
|
|
---@return {algorithm?: string, linematch?: integer}
|
|
local function parse_diffopt()
|
|
local opts = {}
|
|
for _, item in ipairs(vim.split(vim.o.diffopt, ',')) do
|
|
local key, val = item:match('^(%w+):(.+)$')
|
|
if key == 'algorithm' then
|
|
opts.algorithm = val
|
|
elseif key == 'linematch' then
|
|
opts.linematch = tonumber(val)
|
|
end
|
|
end
|
|
return opts
|
|
end
|
|
|
|
---@param old_text string
|
|
---@param new_text string
|
|
---@param diff_opts? {algorithm?: string, linematch?: integer}
|
|
---@return {old_start: integer, old_count: integer, new_start: integer, new_count: integer}[]
|
|
local function byte_diff(old_text, new_text, diff_opts)
|
|
local vim_opts = { result_type = 'indices' }
|
|
if diff_opts then
|
|
if diff_opts.algorithm then
|
|
vim_opts.algorithm = diff_opts.algorithm
|
|
end
|
|
if diff_opts.linematch then
|
|
vim_opts.linematch = diff_opts.linematch
|
|
end
|
|
end
|
|
local ok, result = pcall(vim.diff, old_text, new_text, vim_opts)
|
|
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
|
|
---@param diff_opts? {algorithm?: string, linematch?: integer}
|
|
---@return diffs.CharSpan[], diffs.CharSpan[]
|
|
local function char_diff_pair(old_line, new_line, del_idx, add_idx, diff_opts)
|
|
---@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, diff_opts)
|
|
|
|
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
|
|
---@param diff_opts? {algorithm?: string, linematch?: integer}
|
|
---@return diffs.CharSpan[], diffs.CharSpan[]
|
|
local function diff_group_native(group, diff_opts)
|
|
---@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,
|
|
diff_opts
|
|
)
|
|
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, diff_opts)
|
|
|
|
---@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,
|
|
diff_opts
|
|
)
|
|
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,
|
|
diff_opts
|
|
)
|
|
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 'default'
|
|
|
|
local vscode_handle = nil
|
|
if algorithm == 'vscode' then
|
|
vscode_handle = require('diffs.lib').load()
|
|
if not vscode_handle then
|
|
dbg('vscode algorithm requested but library not available, falling back to default')
|
|
end
|
|
end
|
|
|
|
local diff_opts = nil
|
|
if not vscode_handle then
|
|
diff_opts = parse_diffopt()
|
|
if diff_opts.algorithm then
|
|
dbg('diffopt algorithm: %s', diff_opts.algorithm)
|
|
end
|
|
if diff_opts.linematch then
|
|
dbg('diffopt linematch: %d', diff_opts.linematch)
|
|
end
|
|
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, diff_opts)
|
|
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
|