refactor(diff): make word diff more efficient

Resolves #467
This commit is contained in:
Lewis Russell 2022-02-07 12:06:43 +00:00 committed by Lewis Russell
parent 672482df3e
commit 11425117a9
6 changed files with 148 additions and 114 deletions

10
lua/gitsigns.lua generated
View File

@ -521,11 +521,11 @@ M.setup = void(function(cfg)
end
manager.apply_win_signs(bufnr, bcache.pending_signs, top + 1, bot + 1)
return config.word_diff and config.diff_opts.internal
end,
on_line = function(_, _, bufnr, row)
manager.apply_word_diff(bufnr, row)
if config.word_diff and config.diff_opts.internal then
for i = top, bot do
manager.apply_word_diff(bufnr, i)
end
end
end,
})

View File

@ -49,6 +49,26 @@ local Region = {}
local gaps_between_regions = 5
local function denoise_hunks(hunks)
local ret = { hunks[1] }
for j = 2, #hunks do
local h, n = ret[#ret], hunks[j]
if not h or not n then break end
if n.added.start - h.added.start - h.added.count < gaps_between_regions then
h.added.count = n.added.start + n.added.count - h.added.start
h.removed.count = n.removed.start + n.removed.count - h.removed.start
if h.added.count > 0 or h.removed.count > 0 then
h.type = 'change'
end
else
ret[#ret + 1] = n
end
end
return ret
end
function M.run_word_diff(removed, added)
local adds = {}
local rems = {}
@ -59,12 +79,9 @@ function M.run_word_diff(removed, added)
for i = 1, #removed do
local rline = removed[i]
local aline = added[i]
local a, b = vim.split(removed[i], ''), vim.split(added[i], '')
local a, b = vim.split(rline, ''), vim.split(aline, '')
local hunks0 = {}
local hunks = {}
for _, r in ipairs(run_diff_xdl(a, b)) do
local rs, rc, as, ac = unpack(r)
@ -72,26 +89,10 @@ function M.run_word_diff(removed, added)
if rc == 0 then rs = rs + 1 end
if ac == 0 then as = as + 1 end
hunks0[#hunks0 + 1] = create_hunk(rs, rc, as, ac)
hunks[#hunks + 1] = create_hunk(rs, rc, as, ac)
end
local hunks = { hunks0[1] }
for j = 2, #hunks0 do
local h, n = hunks[#hunks], hunks0[j]
if not h or not n then break end
if n.added.start - h.added.start - h.added.count < gaps_between_regions then
h.added.count = n.added.start + n.added.count - h.added.start
h.removed.count = n.removed.start + n.removed.count - h.removed.start
if h.added.count > 0 or h.removed.count > 0 then
h.type = 'change'
end
else
hunks[#hunks + 1] = n
end
end
hunks = denoise_hunks(hunks)
for _, h in ipairs(hunks) do
adds[#adds + 1] = { i + #removed, h.type, h.added.start, h.added.start + h.added.count }

View File

@ -175,40 +175,56 @@ end
local ns = api.nvim_create_namespace('gitsigns')
M.apply_word_diff = function(bufnr, row)
if not cache[bufnr] or not cache[bufnr].hunks then return end
if not cache[bufnr] or not cache[bufnr].hunks then
return
end
local line = api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1]
if not line then
return
end
local lnum = row + 1
local cols = #api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1]
for _, hunk in ipairs(cache[bufnr].hunks) do
if lnum >= hunk.start and lnum <= hunk.vend then
local size = (#hunk.added.lines + #hunk.removed.lines) / 2
local removed_regions, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines)
local regions = vim.list_extend(removed_regions or {}, added_regions or {})
for _, region in ipairs(regions) do
local line = region[1]
if lnum == hunk.start + line - size - 1 then
local hunk = gs_hunks.find_hunk(lnum, cache[bufnr].hunks)
if not hunk then
local rtype, scol, ecol = region[2], region[3], region[4]
if scol <= cols then
if ecol > cols then
ecol = cols
elseif ecol == scol then
return
end
ecol = scol + 1
end
api.nvim_buf_set_extmark(bufnr, ns, row, scol - 1, {
end_col = ecol - 1,
hl_group = rtype == 'add' and 'GitSignsAddLnInline' or
rtype == 'change' and 'GitSignsChangeLnInline' or
'GitSignsDeleteLnInline',
ephemeral = true,
priority = 1000,
})
end
end
if hunk.added.count ~= hunk.removed.count then
return
end
local pos = lnum - hunk.start + 1
local added_line = hunk.added.lines[pos]
local removed_line = hunk.removed.lines[pos]
local _, added_regions = require('gitsigns.diff_int').run_word_diff({ removed_line }, { added_line })
local cols = #line
for _, region in ipairs(added_regions) do
local rtype, scol, ecol = region[2], region[3], region[4]
if scol <= cols then
if ecol > cols then
ecol = cols
elseif ecol == scol then
ecol = scol + 1
end
break
api.nvim_buf_set_extmark(bufnr, ns, row, scol - 1, {
end_col = ecol - 1,
hl_group = rtype == 'add' and 'GitSignsAddLnInline' or
rtype == 'change' and 'GitSignsChangeLnInline' or
'GitSignsDeleteLnInline',
ephemeral = true,
priority = 1000,
})
api.nvim__buf_redraw_range(bufnr, row, row + 1)
end
end
end

View File

@ -521,11 +521,11 @@ M.setup = void(function(cfg: Config)
end
manager.apply_win_signs(bufnr, bcache.pending_signs, top+1, bot+1)
-- Returning false prevents the on_line callbacks
return config.word_diff and config.diff_opts.internal
end,
on_line = function(_, _, bufnr: integer, row: integer)
manager.apply_word_diff(bufnr, row)
if config.word_diff and config.diff_opts.internal then
for i = top, bot do
manager.apply_word_diff(bufnr, i)
end
end
end
})

View File

@ -49,6 +49,26 @@ local type Region = {integer, string, integer, integer}
local gaps_between_regions = 5
local function denoise_hunks(hunks: {Hunk}): {Hunk}
-- Denoise the hunks
local ret = {hunks[1]}
for j = 2, #hunks do
local h, n = ret[#ret], hunks[j]
if not h or not n then break end
if n.added.start - h.added.start - h.added.count < gaps_between_regions then
h.added.count = n.added.start + n.added.count - h.added.start
h.removed.count = n.removed.start + n.removed.count - h.removed.start
if h.added.count > 0 or h.removed.count > 0 then
h.type = 'change'
end
else
ret[#ret+1] = n
end
end
return ret
end
function M.run_word_diff(removed: {string}, added: {string}): {Region}, {Region}
local adds: {Region} = {}
local rems: {Region} = {}
@ -59,12 +79,9 @@ function M.run_word_diff(removed: {string}, added: {string}): {Region}, {Region}
for i = 1, #removed do
-- pair lines by position
local rline = removed[i]
local aline = added[i]
local a, b = vim.split(removed[i], ''), vim.split(added[i], '')
local a, b = vim.split(rline, ''), vim.split(aline, '')
local hunks0: {Hunk} = {}
local hunks: {Hunk} = {}
for _, r in ipairs(run_diff_xdl(a, b)) do
local rs, rc, as, ac = unpack(r)
@ -72,26 +89,10 @@ function M.run_word_diff(removed: {string}, added: {string}): {Region}, {Region}
if rc == 0 then rs = rs + 1 end
if ac == 0 then as = as + 1 end
-- print(string.format('-%d,%d +%d,%d', rs, rc, as, ac))
hunks0[#hunks0+1] = create_hunk(rs, rc, as, ac)
hunks[#hunks+1] = create_hunk(rs, rc, as, ac)
end
-- Denoise the hunks
local hunks = {hunks0[1]}
for j = 2, #hunks0 do
local h, n = hunks[#hunks], hunks0[j]
if not h or not n then break end
if n.added.start - h.added.start - h.added.count < gaps_between_regions then
h.added.count = n.added.start + n.added.count - h.added.start
h.removed.count = n.removed.start + n.removed.count - h.removed.start
if h.added.count > 0 or h.removed.count > 0 then
h.type = 'change'
end
else
hunks[#hunks+1] = n
end
end
hunks = denoise_hunks(hunks)
for _, h in ipairs(hunks) do
adds[#adds+1] = {i+#removed, h.type, h.added.start , h.added.start + h.added.count}

View File

@ -175,40 +175,56 @@ end
local ns = api.nvim_create_namespace('gitsigns')
M.apply_word_diff = function(bufnr: integer, row: integer)
if not cache[bufnr] or not cache[bufnr].hunks then return end
if not cache[bufnr] or not cache[bufnr].hunks then
return
end
local line = api.nvim_buf_get_lines(bufnr, row, row+1, false)[1]
if not line then
-- Invalid line
return
end
local lnum = row + 1
local cols = #api.nvim_buf_get_lines(bufnr, lnum-1, lnum, false)[1]
for _, hunk in ipairs(cache[bufnr].hunks) do
if lnum >= hunk.start and lnum <= hunk.vend then
local size = (#hunk.added.lines +#hunk.removed.lines) / 2
local removed_regions, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines)
local regions = vim.list_extend(removed_regions or {}, added_regions or {})
for _, region in ipairs(regions) do
local line = region[1]
if lnum == hunk.start + line - size - 1 then
-- and vim.startswith(hunk.lines[line], '+') then
local rtype, scol, ecol = region[2], region[3], region[4]
if scol <= cols then
if ecol > cols then
ecol = cols
elseif ecol == scol then
-- Make sure region is at least 1 column width
ecol = scol + 1
end
api.nvim_buf_set_extmark(bufnr, ns, row, scol-1, {
end_col = ecol-1,
hl_group = rtype == 'add' and 'GitSignsAddLnInline'
or rtype == 'change' and 'GitSignsChangeLnInline'
or 'GitSignsDeleteLnInline',
ephemeral = true,
priority = 1000
})
end
end
local hunk = gs_hunks.find_hunk(lnum, cache[bufnr].hunks)
if not hunk then
-- No hunk at line
return
end
if hunk.added.count ~= hunk.removed.count then
-- Only word diff if added count == removed
return
end
local pos = lnum - hunk.start + 1
local added_line = hunk.added.lines[pos]
local removed_line = hunk.removed.lines[pos]
local _, added_regions = require('gitsigns.diff_int').run_word_diff({removed_line}, {added_line})
local cols = #line
for _, region in ipairs(added_regions) do
local rtype, scol, ecol = region[2], region[3], region[4]
if scol <= cols then
if ecol > cols then
ecol = cols
elseif ecol == scol then
-- Make sure region is at least 1 column width
ecol = scol + 1
end
break
api.nvim_buf_set_extmark(bufnr, ns, row, scol-1, {
end_col = ecol-1,
hl_group = rtype == 'add' and 'GitSignsAddLnInline'
or rtype == 'change' and 'GitSignsChangeLnInline'
or 'GitSignsDeleteLnInline',
ephemeral = true,
priority = 1000
})
api.nvim__buf_redraw_range(bufnr, row, row+1)
end
end
end