Add support for vim.xdl_diff

Requires PR neovim/neovim/#14536
This commit is contained in:
Lewis Russell 2021-07-24 12:44:54 +01:00
parent 368c7f65aa
commit 1ddb1f64f5
14 changed files with 308 additions and 265 deletions

View File

@ -17,10 +17,14 @@ on:
jobs:
# This workflow contains a single job called "build"
build:
strategy:
fail-fast: true
matrix:
neovim_branch: ['v0.5.0', 'nightly']
# The type of runner that the job will run on
runs-on: ubuntu-latest
env:
NEOVIM_BRANCH: v0.5.0
NEOVIM_BRANCH: ${{ matrix.neovim_branch }}
# Steps represent a sequence of tasks that will be executed as part of the job
steps:

View File

@ -153,7 +153,7 @@ Feature | gitsigns
---------------------------------------------------------|----------------------|-----------------------------------------------|--------
Shows signs for added, modified, and removed lines | :white_check_mark: | :white_check_mark: |
Asynchronous | :white_check_mark: | :white_check_mark: |
Runs diffs in-process (no IO or pipes) | :white_check_mark: * | | * Via FFI and soon via [lua](https://github.com/neovim/neovim/pull/14536)
Runs diffs in-process (no IO or pipes) | :white_check_mark: * | | * Via [lua](https://github.com/neovim/neovim/pull/14536) or FFI.
Only adds signs for drawn lines | :white_check_mark: * | | * Via Neovims decoration API
Updates immediately | :white_check_mark: | * | * Triggered on CursorHold
Ensures signs are always up to date | :white_check_mark: * | | * Watches the git index to do so

View File

@ -14,13 +14,13 @@ feature set which includes (but not limited to):
• Provides signs in the |signcolumn| to show changed/added/removed lines.
• Mappings to operate on hunks to stage, undo or reset against Git's index.
Gitsigns is implemented entirely in Lua which is built into Neovim and because
of this requires no external dependencies. This is unlike other plugins that
require python, node, etc, which need to communicate with Neovim using |RPC|.
By default, Gitsigns also uses Neovim's built-in diff library and runs
in-process via LuaJIT's FFI module. This is unlike other similar plugins that
need to run `git-diff` as an external process which is less efficient, has
tighter bottlenecks and requires file IO.
Gitsigns is implemented entirely in Lua which is built into Neovim and
requires no external dependencies other than git. This is unlike other plugins
that require python, node, etc, which need to communicate with Neovim using
|RPC|. By default, Gitsigns also uses Neovim's built-in diff library
(`vim.diff`) unlike other similar plugins that need to run `git-diff` as an
external process which is less efficient, has tighter bottlenecks and requires
file IO.
==============================================================================
USAGE *gitsigns-usage*
@ -519,11 +519,12 @@ update_debounce *gitsigns-config-update_debounce*
Debounce time for updates (in milliseconds).
use_internal_diff *gitsigns-config-use_internal_diff*
Type: `boolean`, Default: `true` if luajit is present (windows unsupported)
Type: `boolean`, Default: `true` if `vim.diff` or luajit is present. Windows unsupported on v0.5
Use Neovim's built in xdiff library for running diffs.
This uses LuaJIT's FFI interface.
Note Neovim v0.5 uses LuaJIT's FFI interface, whereas v0.5+ uses
`vim.diff`.
current_line_blame *gitsigns-config-current_line_blame*
Type: `boolean`, Default: `false`

View File

@ -361,7 +361,7 @@ M.preview_hunk = function()
vim.cmd([[autocmd CursorMoved,CursorMovedI <buffer> ++once silent! unlet b:_gitsigns_preview_open]])
if config.use_internal_diff then
local regions = require('gitsigns.diff_ffi').run_word_diff(hunk.lines)
local regions = require('gitsigns.diff_int').run_word_diff(hunk.lines)
local offset = #lines - #hunk.lines
for _, region in ipairs(regions) do
local line, scol, ecol = region[1], region[3], region[4]
@ -537,7 +537,7 @@ end
local function run_diff(a, b)
if config.use_internal_diff then
return require('gitsigns.diff_ffi').run_diff(a, b, config.diff_algorithm)
return require('gitsigns.diff_int').run_diff(a, b, config.diff_algorithm)
else
return require('gitsigns.diff_ext').run_diff(a, b, config.diff_algorithm)
end

View File

@ -353,17 +353,20 @@ M.schema = {
use_internal_diff = {
type = 'boolean',
default = function()
if not jit or jit.os == "Windows" then
if vim.diff then
return true
elseif not jit or jit.os == "Windows" then
return false
else
return true
end
end,
default_help = "`true` if luajit is present (windows unsupported)",
default_help = "`true` if `vim.diff` or luajit is present. Windows unsupported on v0.5",
description = [[
Use Neovim's built in xdiff library for running diffs.
This uses LuaJIT's FFI interface.
Note Neovim v0.5 uses LuaJIT's FFI interface, whereas v0.5+ uses
`vim.diff`.
]],
},

View File

@ -1,152 +1,22 @@
local create_hunk = require("gitsigns.hunks").create_hunk
local Hunk = require('gitsigns.hunks').Hunk
local ffi = require("ffi")
ffi.cdef([[
typedef struct s_mmbuffer { const char *ptr; long size; } mmbuffer_t;
typedef struct s_xpparam {
unsigned long flags;
// See Documentation/diff-options.txt.
char **anchors;
size_t anchors_nr;
} xpparam_t;
typedef long (__stdcall *find_func_t)(
const char *line,
long line_len,
char *buffer,
long buffer_size,
void *priv
);
typedef int (__stdcall *xdl_emit_hunk_consume_func_t)(
long start_a, long count_a, long start_b, long count_b,
void *cb_data
);
typedef struct s_xdemitconf {
long ctxlen;
long interhunkctxlen;
unsigned long flags;
find_func_t find_func;
void *find_func_priv;
xdl_emit_hunk_consume_func_t hunk_func;
} xdemitconf_t;
typedef struct s_xdemitcb {
void *priv;
int (__stdcall *outf)(void *, mmbuffer_t *, int);
} xdemitcb_t;
int xdl_diff(
mmbuffer_t *mf1,
mmbuffer_t *mf2,
xpparam_t const *xpp,
xdemitconf_t const *xecfg,
xdemitcb_t *ecb
);
]])
local MMBuffer = {}
local function setup_mmbuffer(lines)
local text = vim.tbl_isempty(lines) and '' or table.concat(lines, '\n') .. '\n'
return text, #text
end
local XPParam = {}
local function get_xpparam_flag(diff_algo)
local daflag = 0
if diff_algo == 'minimal' then daflag = 1
elseif diff_algo == 'patience' then daflag = math.floor(2 ^ 14)
elseif diff_algo == 'histogram' then daflag = math.floor(2 ^ 15)
end
return daflag
end
local Long = {}
local XDEmitConf = {}
local M = {}
local DiffResult = {}
local mmba = ffi.new('mmbuffer_t')
local mmbb = ffi.new('mmbuffer_t')
local xpparam = ffi.new('xpparam_t')
local emitcb = ffi.new('xdemitcb_t')
local run_diff_xdl
local function run_diff_xdl(fa, fb, diff_algo)
mmba.ptr, mmba.size = setup_mmbuffer(fa)
mmbb.ptr, mmbb.size = setup_mmbuffer(fb)
xpparam.flags = get_xpparam_flag(diff_algo)
local results = {}
local hunk_func = ffi.cast('xdl_emit_hunk_consume_func_t', function(
start_a, count_a, start_b, count_b)
local ca = tonumber(count_a)
local cb = tonumber(count_b)
local sa = tonumber(start_a)
local sb = tonumber(start_b)
if ca > 0 then sa = sa + 1 end
if cb > 0 then sb = sb + 1 end
results[#results + 1] = { sa, ca, sb, cb }
return 0
end)
local emitconf = ffi.new('xdemitconf_t')
emitconf.hunk_func = hunk_func
local ok = ffi.C.xdl_diff(mmba, mmbb, xpparam, emitconf, emitcb)
hunk_func:free()
return ok == 0 and results
if vim.diff then
run_diff_xdl = function(fa, fb, algorithm)
local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n') .. '\n'
local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n') .. '\n'
return vim.diff(a, b, { result_type = 'indices', algorithm = algorithm })
end
else
run_diff_xdl = require('gitsigns.diff_int.xdl_diff_ffi')
end
jit.off(run_diff_xdl)
function M.run_diff(fa, fb, diff_algo)
local results = run_diff_xdl(fa, fb, diff_algo)

145
lua/gitsigns/diff_int/xdl_diff_ffi.lua generated Normal file
View File

@ -0,0 +1,145 @@
local ffi = require("ffi")
ffi.cdef([[
typedef struct s_mmbuffer { const char *ptr; long size; } mmbuffer_t;
typedef struct s_xpparam {
unsigned long flags;
// See Documentation/diff-options.txt.
char **anchors;
size_t anchors_nr;
} xpparam_t;
typedef long (__stdcall *find_func_t)(
const char *line,
long line_len,
char *buffer,
long buffer_size,
void *priv
);
typedef int (__stdcall *xdl_emit_hunk_consume_func_t)(
long start_a, long count_a, long start_b, long count_b,
void *cb_data
);
typedef struct s_xdemitconf {
long ctxlen;
long interhunkctxlen;
unsigned long flags;
find_func_t find_func;
void *find_func_priv;
xdl_emit_hunk_consume_func_t hunk_func;
} xdemitconf_t;
typedef struct s_xdemitcb {
void *priv;
int (__stdcall *outf)(void *, mmbuffer_t *, int);
} xdemitcb_t;
int xdl_diff(
mmbuffer_t *mf1,
mmbuffer_t *mf2,
xpparam_t const *xpp,
xdemitconf_t const *xecfg,
xdemitcb_t *ecb
);
]])
local MMBuffer = {}
local function setup_mmbuffer(lines)
local text = vim.tbl_isempty(lines) and '' or table.concat(lines, '\n') .. '\n'
return text, #text
end
local XPParam = {}
local function get_xpparam_flag(diff_algo)
local daflag = 0
if diff_algo == 'minimal' then daflag = 1
elseif diff_algo == 'patience' then daflag = math.floor(2 ^ 14)
elseif diff_algo == 'histogram' then daflag = math.floor(2 ^ 15)
end
return daflag
end
local Long = {}
local XDEmitConf = {}
local DiffResult = {}
local mmba = ffi.new('mmbuffer_t')
local mmbb = ffi.new('mmbuffer_t')
local xpparam = ffi.new('xpparam_t')
local emitcb = ffi.new('xdemitcb_t')
local function run_diff_xdl(fa, fb, diff_algo)
mmba.ptr, mmba.size = setup_mmbuffer(fa)
mmbb.ptr, mmbb.size = setup_mmbuffer(fb)
xpparam.flags = get_xpparam_flag(diff_algo)
local results = {}
local hunk_func = ffi.cast('xdl_emit_hunk_consume_func_t', function(
start_a, count_a, start_b, count_b)
local ca = tonumber(count_a)
local cb = tonumber(count_b)
local sa = tonumber(start_a)
local sb = tonumber(start_b)
if ca > 0 then sa = sa + 1 end
if cb > 0 then sb = sb + 1 end
results[#results + 1] = { sa, ca, sb, cb }
return 0
end)
local emitconf = ffi.new('xdemitconf_t')
emitconf.hunk_func = hunk_func
local ok = ffi.C.xdl_diff(mmba, mmbb, xpparam, emitconf, emitcb)
hunk_func:free()
return ok == 0 and results
end
jit.off(run_diff_xdl)
return run_diff_xdl

View File

@ -143,7 +143,7 @@ M.apply_word_diff = function(bufnr, row)
for _, hunk in ipairs(cache[bufnr].hunks) do
if lnum >= hunk.start and lnum <= hunk.vend then
local size = #hunk.lines / 2
local regions = require('gitsigns.diff_ffi').run_word_diff(hunk.lines)
local regions = require('gitsigns.diff_int').run_word_diff(hunk.lines)
for _, region in ipairs(regions) do
local line = region[1]
if lnum == hunk.start + line - size - 1 and
@ -192,7 +192,7 @@ local update0 = function(bufnr, bcache)
local run_diff
if config.use_internal_diff then
run_diff = require('gitsigns.diff_ffi').run_diff
run_diff = require('gitsigns.diff_int').run_diff
else
run_diff = require('gitsigns.diff_ext').run_diff
end

View File

@ -361,7 +361,7 @@ M.preview_hunk = function()
vim.cmd[[autocmd CursorMoved,CursorMovedI <buffer> ++once silent! unlet b:_gitsigns_preview_open]]
if config.use_internal_diff then
local regions = require('gitsigns.diff_ffi').run_word_diff(hunk.lines)
local regions = require('gitsigns.diff_int').run_word_diff(hunk.lines)
local offset = #lines - #hunk.lines
for _, region in ipairs(regions) do
local line, scol, ecol = region[1], region[3], region[4]
@ -537,7 +537,7 @@ end
local function run_diff(a: {string}, b: {string}): {Hunk}
if config.use_internal_diff then
return require('gitsigns.diff_ffi').run_diff(a, b, config.diff_algorithm)
return require('gitsigns.diff_int').run_diff(a, b, config.diff_algorithm)
else
return require('gitsigns.diff_ext').run_diff(a, b, config.diff_algorithm)
end

View File

@ -353,17 +353,20 @@ M.schema = {
use_internal_diff = {
type = 'boolean',
default = function(): boolean
if not jit or jit.os == "Windows" then
if vim.diff then
return true
elseif not jit or jit.os == "Windows" then
return false
else
return true
end
end,
default_help = "`true` if luajit is present (windows unsupported)",
default_help = "`true` if `vim.diff` or luajit is present. Windows unsupported on v0.5",
description = [[
Use Neovim's built in xdiff library for running diffs.
This uses LuaJIT's FFI interface.
Note Neovim v0.5 uses LuaJIT's FFI interface, whereas v0.5+ uses
`vim.diff`.
]]
},

115
teal/gitsigns/diff_int.tl Normal file
View File

@ -0,0 +1,115 @@
local create_hunk = require("gitsigns.hunks").create_hunk
local Hunk = require('gitsigns.hunks').Hunk
local M = {}
local type DiffResult = {integer, integer, integer, integer}
local run_diff_xdl: function({string}, {string}, diff_algo: string): {DiffResult}
if vim.diff then
run_diff_xdl = function(fa: {string}, fb: {string}, algorithm: string): {DiffResult}
local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n')..'\n'
local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n')..'\n'
return vim.diff(a, b, { result_type = 'indices', algorithm = algorithm })
end
else
run_diff_xdl = require('gitsigns.diff_int.xdl_diff_ffi')
end
function M.run_diff(fa: {string}, fb: {string}, diff_algo: string): {Hunk}
local results = run_diff_xdl(fa, fb, diff_algo)
local hunks: {Hunk} = {}
for _, r in ipairs(results) do
local rs, rc, as, ac = unpack(r)
local hunk = create_hunk(rs, rc, as, ac)
hunk.head = ('@@ -%d%s +%d%s @@'):format(
rs, rc > 0 and ','..rc or '',
as, ac > 0 and ','..ac or ''
)
if rc > 0 then
for i = rs, rs+rc-1 do
table.insert(hunk.lines, '-'..(fa[i] or ''))
end
end
if ac > 0 then
for i = as, as+ac-1 do
table.insert(hunk.lines, '+'..(fb[i] or ''))
end
end
table.insert(hunks, hunk)
end
return hunks
end
local type Region = {integer, string, integer, integer}
local gaps_between_regions = 5
function M.run_word_diff(hunk_body: {string}): {Region}
local removed, added = 0, 0
for _, line in ipairs(hunk_body) do
if line:sub(1, 1) == '-' then
removed = removed + 1
elseif line:sub(1, 1) == '+' then
added = added + 1
end
end
if removed ~= added then
return {}
end
local ret: {Region} = {}
for i = 1, removed do
-- pair lines by position
local rline = hunk_body[i]:sub(2)
local aline = hunk_body[i + removed]:sub(2)
local a, b = vim.split(rline, ''), vim.split(aline, '')
local hunks0: {Hunk} = {}
for _, r in ipairs(run_diff_xdl(a, b)) do
local rs, rc, as, ac = unpack(r)
-- Balance of the unknown offset done in hunk_func
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)
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
for _, h in ipairs(hunks) do
local rem = {i , h.type, h.removed.start, h.removed.start + h.removed.count}
local add = {i+removed, h.type, h.added.start , h.added.start + h.added.count}
ret[#ret+1] = rem
ret[#ret+1] = add
end
end
return ret
end
return M

View File

@ -1,6 +1,3 @@
local create_hunk = require("gitsigns.hunks").create_hunk
local Hunk = require('gitsigns.hunks').Hunk
local ffi = require("ffi")
ffi.cdef[[
@ -102,8 +99,6 @@ end
-- local DIFF_CLOSE_OFF = 0x400 -- diffoff when closing window
-- local DIFF_FOLLOWWRAP = 0x800 -- follow the wrap option
local M = {}
local type DiffResult = {integer, integer, integer, integer}
local mmba = ffi.new('mmbuffer_t') as MMBuffer
@ -147,99 +142,4 @@ end
jit.off(run_diff_xdl)
function M.run_diff(fa: {string}, fb: {string}, diff_algo: string): {Hunk}
local results = run_diff_xdl(fa, fb, diff_algo)
local hunks: {Hunk} = {}
for _, r in ipairs(results) do
local rs, rc, as, ac = unpack(r)
local hunk = create_hunk(rs, rc, as, ac)
hunk.head = ('@@ -%d%s +%d%s @@'):format(
rs, rc > 0 and ','..rc or '',
as, ac > 0 and ','..ac or ''
)
if rc > 0 then
for i = rs, rs+rc-1 do
table.insert(hunk.lines, '-'..(fa[i] or ''))
end
end
if ac > 0 then
for i = as, as+ac-1 do
table.insert(hunk.lines, '+'..(fb[i] or ''))
end
end
table.insert(hunks, hunk)
end
return hunks
end
local type Region = {integer, string, integer, integer}
local gaps_between_regions = 5
function M.run_word_diff(hunk_body: {string}): {Region}
local removed, added = 0, 0
for _, line in ipairs(hunk_body) do
if line:sub(1, 1) == '-' then
removed = removed + 1
elseif line:sub(1, 1) == '+' then
added = added + 1
end
end
if removed ~= added then
return {}
end
local ret: {Region} = {}
for i = 1, removed do
-- pair lines by position
local rline = hunk_body[i]:sub(2)
local aline = hunk_body[i + removed]:sub(2)
local a, b = vim.split(rline, ''), vim.split(aline, '')
local hunks0: {Hunk} = {}
for _, r in ipairs(run_diff_xdl(a, b)) do
local rs, rc, as, ac = unpack(r)
-- Balance of the unknown offset done in hunk_func
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)
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
for _, h in ipairs(hunks) do
local rem = {i , h.type, h.removed.start, h.removed.start + h.removed.count}
local add = {i+removed, h.type, h.added.start , h.added.start + h.added.count}
ret[#ret+1] = rem
ret[#ret+1] = add
end
end
return ret
end
return M
return run_diff_xdl

View File

@ -143,7 +143,7 @@ M.apply_word_diff = function(bufnr: integer, row: integer)
for _, hunk in ipairs(cache[bufnr].hunks) do
if lnum >= hunk.start and lnum <= hunk.vend then
local size = #hunk.lines / 2
local regions = require('gitsigns.diff_ffi').run_word_diff(hunk.lines)
local regions = require('gitsigns.diff_int').run_word_diff(hunk.lines)
for _, region in ipairs(regions) do
local line = region[1]
if lnum == hunk.start + line - size - 1
@ -192,7 +192,7 @@ local update0 = function(bufnr: integer, bcache: CacheEntry)
-- See https://github.com/neovim/neovim/issues/15147
local run_diff: function({string}, {string}, string): {Hunk}
if config.use_internal_diff then
run_diff = require('gitsigns.diff_ffi').run_diff
run_diff = require('gitsigns.diff_int').run_diff
else
run_diff = require('gitsigns.diff_ext').run_diff
end

View File

@ -227,6 +227,8 @@ global record vim
deepcopy: function<T>(T): T
diff: function(string|{string}, string|{string}, table)
record o
diffopt: string
wrapscan: boolean