mirror of
https://github.com/neovim/neovim.git
synced 2024-12-19 18:55:14 -07:00
feat(api): add lua C bindings for xdiff (#14536)
* feat(api): add lua C bindings for xdiff * chore: opt.hunk_lines -> opt.result_type opt.on_hunk now takes precedence over opt.result_type * chore: fix indents Fix indents * chore: change how priv is managed Assign priv NULL and unconditionally apply XFREE_CLEAR to it when finished.
This commit is contained in:
parent
8331cd13c4
commit
3d3c0c669d
@ -1581,4 +1581,71 @@ uri_to_fname({uri}) *vim.uri_to_fname()*
|
||||
Return: ~
|
||||
Filename
|
||||
|
||||
==============================================================================
|
||||
Lua module: diff *lua-diff*
|
||||
|
||||
diff({a}, {b}, {opts}) *vim.diff()*
|
||||
Run diff on strings {a} and {b}. Any indices returned by this
|
||||
function, either directly or via callback arguments, are
|
||||
1-based.
|
||||
|
||||
Examples: >
|
||||
vim.diff('a\n', 'b\nc\n')
|
||||
-->
|
||||
@@ -1 +1,2 @@
|
||||
-a
|
||||
+b
|
||||
+c
|
||||
|
||||
vim.diff('a\n', 'b\nc\n', {hunk_lines = true})
|
||||
-->
|
||||
{
|
||||
{1, 1, 1, 2}
|
||||
}
|
||||
<
|
||||
Parameters: ~
|
||||
{a} First string to compare
|
||||
{b} Second string to compare
|
||||
{opts} Optional parameters:
|
||||
• `on_hunk` (callback):
|
||||
Invoked for each hunk in the diff. Return a
|
||||
negative number to cancel the callback for any
|
||||
remaining hunks.
|
||||
Args:
|
||||
• `start_a` (integer): Start line of hunk in {a}.
|
||||
• `count_a` (integer): Hunk size in {a}.
|
||||
• `start_b` (integer): Start line of hunk in {b}.
|
||||
• `count_b` (integer): Hunk size in {b}.
|
||||
• `result_type` (string): Form of the returned diff:
|
||||
• "unified": (default) String in unified format.
|
||||
• "indices": Array of hunk locations.
|
||||
Note this option is ignored if `on_hunk` is
|
||||
used.
|
||||
• `algorithm` (string):
|
||||
Diff algorithm to use. Values:
|
||||
• "myers" the default algorithm
|
||||
• "minimal" spend extra time to generate the
|
||||
smallest possible diff
|
||||
• "patience" patience diff algorithm
|
||||
• "histogram" histogram diff algorithm
|
||||
• `ctxlen` (integer): Context length
|
||||
• `interhunkctxlen` (integer):
|
||||
Inter hunk context length
|
||||
• `ignore_whitespace` (boolean):
|
||||
Ignore whitespace
|
||||
• `ignore_whitespace_change` (boolean):
|
||||
Ignore whitespace change
|
||||
• `ignore_whitespace_change_at_eol` (boolean)
|
||||
Ignore whitespace change at end-of-line.
|
||||
• `ignore_cr_at_eol` (boolean)
|
||||
Ignore carriage return at end-of-line
|
||||
• `ignore_blank_lines` (boolean)
|
||||
Ignore blank lines
|
||||
• `indent_heuristic` (boolean):
|
||||
Use the indent heuristic for the internal
|
||||
diff library.
|
||||
|
||||
Return: ~
|
||||
See {opts.result_type}. nil if {opts.on_hunk} is given.
|
||||
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
|
@ -40,6 +40,7 @@
|
||||
#include "nvim/lua/converter.h"
|
||||
#include "nvim/lua/executor.h"
|
||||
#include "nvim/lua/treesitter.h"
|
||||
#include "nvim/lua/xdiff.h"
|
||||
|
||||
#include "luv/luv.h"
|
||||
|
||||
@ -517,6 +518,10 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL
|
||||
// internal vim._treesitter... API
|
||||
nlua_add_treesitter(lstate);
|
||||
|
||||
// vim.diff
|
||||
lua_pushcfunction(lstate, &nlua_xdl_diff);
|
||||
lua_setfield(lstate, -2, "diff");
|
||||
|
||||
lua_setglobal(lstate, "vim");
|
||||
|
||||
{
|
||||
|
334
src/nvim/lua/xdiff.c
Normal file
334
src/nvim/lua/xdiff.c
Normal file
@ -0,0 +1,334 @@
|
||||
#include <lua.h>
|
||||
#include <lualib.h>
|
||||
#include <lauxlib.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "nvim/vim.h"
|
||||
#include "nvim/xdiff/xdiff.h"
|
||||
#include "nvim/lua/xdiff.h"
|
||||
#include "nvim/lua/converter.h"
|
||||
#include "nvim/lua/executor.h"
|
||||
#include "nvim/api/private/helpers.h"
|
||||
|
||||
typedef enum {
|
||||
kNluaXdiffModeUnified = 0,
|
||||
kNluaXdiffModeOnHunkCB,
|
||||
kNluaXdiffModeLocations,
|
||||
} NluaXdiffMode;
|
||||
|
||||
typedef struct {
|
||||
lua_State *lstate;
|
||||
Error *err;
|
||||
} hunkpriv_t;
|
||||
|
||||
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
||||
# include "lua/xdiff.c.generated.h"
|
||||
#endif
|
||||
|
||||
static int write_string(void *priv, mmbuffer_t *mb, int nbuf)
|
||||
{
|
||||
luaL_Buffer *buf = (luaL_Buffer *)priv;
|
||||
for (int i = 0; i < nbuf; i++) {
|
||||
const long size = mb[i].size;
|
||||
for (long total = 0; total < size; total += LUAL_BUFFERSIZE) {
|
||||
const int tocopy = MIN((int)(size - total), LUAL_BUFFERSIZE);
|
||||
char *p = luaL_prepbuffer(buf);
|
||||
if (!p) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(p, mb[i].ptr + total, (unsigned)tocopy);
|
||||
luaL_addsize(buf, (unsigned)tocopy);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// hunk_func callback used when opts.hunk_lines = true
|
||||
static int hunk_locations_cb(long start_a, long count_a,
|
||||
long start_b, long count_b, void *cb_data)
|
||||
{
|
||||
// Mimic extra offsets done by xdiff, see:
|
||||
// src/nvim/xdiff/xemit.c:284
|
||||
// src/nvim/xdiff/xutils.c:(356,368)
|
||||
if (count_a > 0) {
|
||||
start_a += 1;
|
||||
}
|
||||
if (count_b > 0) {
|
||||
start_b += 1;
|
||||
}
|
||||
|
||||
lua_State * lstate = (lua_State *)cb_data;
|
||||
lua_createtable(lstate, 0, 0);
|
||||
|
||||
lua_pushinteger(lstate, start_a);
|
||||
lua_rawseti(lstate, -2, 1);
|
||||
lua_pushinteger(lstate, count_a);
|
||||
lua_rawseti(lstate, -2, 2);
|
||||
lua_pushinteger(lstate, start_b);
|
||||
lua_rawseti(lstate, -2, 3);
|
||||
lua_pushinteger(lstate, count_b);
|
||||
lua_rawseti(lstate, -2, 4);
|
||||
|
||||
lua_rawseti(lstate, -2, (signed)lua_objlen(lstate, -2)+1);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// hunk_func callback used when opts.on_hunk is given
|
||||
static int call_on_hunk_cb(long start_a, long count_a,
|
||||
long start_b, long count_b, void *cb_data)
|
||||
{
|
||||
// Mimic extra offsets done by xdiff, see:
|
||||
// src/nvim/xdiff/xemit.c:284
|
||||
// src/nvim/xdiff/xutils.c:(356,368)
|
||||
if (count_a > 0) {
|
||||
start_a += 1;
|
||||
}
|
||||
if (count_b > 0) {
|
||||
start_b += 1;
|
||||
}
|
||||
|
||||
hunkpriv_t *priv = (hunkpriv_t *)cb_data;
|
||||
lua_State * lstate = priv->lstate;
|
||||
Error *err = priv->err;
|
||||
const int fidx = lua_gettop(lstate);
|
||||
lua_pushvalue(lstate, fidx);
|
||||
lua_pushinteger(lstate, start_a);
|
||||
lua_pushinteger(lstate, count_a);
|
||||
lua_pushinteger(lstate, start_b);
|
||||
lua_pushinteger(lstate, count_b);
|
||||
|
||||
if (lua_pcall(lstate, 4, 1, 0) != 0) {
|
||||
api_set_error(err, kErrorTypeException,
|
||||
"error running function on_hunk: %s",
|
||||
lua_tostring(lstate, -1));
|
||||
return -1;
|
||||
}
|
||||
|
||||
int r = 0;
|
||||
if (lua_isnumber(lstate, -1)) {
|
||||
r = (int)lua_tonumber(lstate, -1);
|
||||
}
|
||||
|
||||
lua_pop(lstate, 1);
|
||||
lua_settop(lstate, fidx);
|
||||
return r;
|
||||
}
|
||||
|
||||
static mmfile_t get_string_arg(lua_State *lstate, int idx)
|
||||
{
|
||||
if (lua_type(lstate, idx) != LUA_TSTRING) {
|
||||
luaL_argerror(lstate, idx, "expected string");
|
||||
}
|
||||
mmfile_t mf;
|
||||
mf.ptr = (char *)lua_tolstring(lstate, idx, (size_t *)&mf.size);
|
||||
return mf;
|
||||
}
|
||||
|
||||
// Helper function for validating option types
|
||||
static bool check_xdiff_opt(ObjectType actType, ObjectType expType,
|
||||
const char *name, Error *err)
|
||||
{
|
||||
if (actType != expType) {
|
||||
const char * type_str =
|
||||
expType == kObjectTypeString ? "string" :
|
||||
expType == kObjectTypeInteger ? "integer" :
|
||||
expType == kObjectTypeBoolean ? "boolean" :
|
||||
expType == kObjectTypeLuaRef ? "function" :
|
||||
"NA";
|
||||
|
||||
api_set_error(err, kErrorTypeValidation, "%s is not a %s", name,
|
||||
type_str);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static NluaXdiffMode process_xdl_diff_opts(lua_State *lstate,
|
||||
xdemitconf_t *cfg,
|
||||
xpparam_t *params, Error *err)
|
||||
{
|
||||
const DictionaryOf(LuaRef) opts = nlua_pop_Dictionary(lstate, true, err);
|
||||
|
||||
NluaXdiffMode mode = kNluaXdiffModeUnified;
|
||||
|
||||
bool had_on_hunk = false;
|
||||
bool had_result_type_indices = false;
|
||||
for (size_t i = 0; i < opts.size; i++) {
|
||||
String k = opts.items[i].key;
|
||||
Object *v = &opts.items[i].value;
|
||||
if (strequal("on_hunk", k.data)) {
|
||||
if (check_xdiff_opt(v->type, kObjectTypeLuaRef, "on_hunk", err)) {
|
||||
goto exit_1;
|
||||
}
|
||||
had_on_hunk = true;
|
||||
nlua_pushref(lstate, v->data.luaref);
|
||||
} else if (strequal("result_type", k.data)) {
|
||||
if (check_xdiff_opt(v->type, kObjectTypeString, "result_type", err)) {
|
||||
goto exit_1;
|
||||
}
|
||||
if (strequal("unified", v->data.string.data)) {
|
||||
} else if (strequal("indices", v->data.string.data)) {
|
||||
had_result_type_indices = true;
|
||||
} else {
|
||||
api_set_error(err, kErrorTypeValidation, "not a valid result_type");
|
||||
goto exit_1;
|
||||
}
|
||||
} else if (strequal("algorithm", k.data)) {
|
||||
if (check_xdiff_opt(v->type, kObjectTypeString, "algorithm", err)) {
|
||||
goto exit_1;
|
||||
}
|
||||
if (strequal("myers", v->data.string.data)) {
|
||||
// default
|
||||
} else if (strequal("minimal", v->data.string.data)) {
|
||||
cfg->flags |= XDF_NEED_MINIMAL;
|
||||
} else if (strequal("patience", v->data.string.data)) {
|
||||
cfg->flags |= XDF_PATIENCE_DIFF;
|
||||
} else if (strequal("histogram", v->data.string.data)) {
|
||||
cfg->flags |= XDF_HISTOGRAM_DIFF;
|
||||
} else {
|
||||
api_set_error(err, kErrorTypeValidation, "not a valid algorithm");
|
||||
goto exit_1;
|
||||
}
|
||||
} else if (strequal("ctxlen", k.data)) {
|
||||
if (check_xdiff_opt(v->type, kObjectTypeInteger, "ctxlen", err)) {
|
||||
goto exit_1;
|
||||
}
|
||||
cfg->ctxlen = v->data.integer;
|
||||
} else if (strequal("interhunkctxlen", k.data)) {
|
||||
if (check_xdiff_opt(v->type, kObjectTypeInteger, "interhunkctxlen",
|
||||
err)) {
|
||||
goto exit_1;
|
||||
}
|
||||
cfg->interhunkctxlen = v->data.integer;
|
||||
} else {
|
||||
struct {
|
||||
const char *name;
|
||||
unsigned long value;
|
||||
} flags[] = {
|
||||
{ "ignore_whitespace" , XDF_IGNORE_WHITESPACE },
|
||||
{ "ignore_whitespace_change" , XDF_IGNORE_WHITESPACE_CHANGE },
|
||||
{ "ignore_whitespace_change_at_eol", XDF_IGNORE_WHITESPACE_AT_EOL },
|
||||
{ "ignore_cr_at_eol" , XDF_IGNORE_CR_AT_EOL },
|
||||
{ "ignore_blank_lines" , XDF_IGNORE_BLANK_LINES },
|
||||
{ "indent_heuristic" , XDF_INDENT_HEURISTIC },
|
||||
{ NULL , 0 },
|
||||
};
|
||||
bool key_used = false;
|
||||
for (size_t j = 0; flags[j].name; j++) {
|
||||
if (strequal(flags[j].name, k.data)) {
|
||||
if (check_xdiff_opt(v->type, kObjectTypeBoolean, flags[j].name,
|
||||
err)) {
|
||||
goto exit_1;
|
||||
}
|
||||
if (v->data.boolean) {
|
||||
params->flags |= flags[j].value;
|
||||
}
|
||||
key_used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (key_used) {
|
||||
continue;
|
||||
}
|
||||
|
||||
api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data);
|
||||
goto exit_1;
|
||||
}
|
||||
}
|
||||
|
||||
if (had_on_hunk) {
|
||||
mode = kNluaXdiffModeOnHunkCB;
|
||||
cfg->hunk_func = call_on_hunk_cb;
|
||||
} else if (had_result_type_indices) {
|
||||
mode = kNluaXdiffModeLocations;
|
||||
cfg->hunk_func = hunk_locations_cb;
|
||||
}
|
||||
|
||||
exit_1:
|
||||
api_free_dictionary(opts);
|
||||
return mode;
|
||||
}
|
||||
|
||||
int nlua_xdl_diff(lua_State *lstate)
|
||||
{
|
||||
if (lua_gettop(lstate) < 2) {
|
||||
return luaL_error(lstate, "Expected at least 2 arguments");
|
||||
}
|
||||
mmfile_t ma = get_string_arg(lstate, 1);
|
||||
mmfile_t mb = get_string_arg(lstate, 2);
|
||||
|
||||
Error err = ERROR_INIT;
|
||||
|
||||
xdemitconf_t cfg;
|
||||
xpparam_t params;
|
||||
xdemitcb_t ecb;
|
||||
|
||||
memset(&cfg , 0, sizeof(cfg));
|
||||
memset(¶ms, 0, sizeof(params));
|
||||
memset(&ecb , 0, sizeof(ecb));
|
||||
|
||||
NluaXdiffMode mode = kNluaXdiffModeUnified;
|
||||
|
||||
if (lua_gettop(lstate) == 3) {
|
||||
if (lua_type(lstate, 3) != LUA_TTABLE) {
|
||||
return luaL_argerror(lstate, 3, "expected table");
|
||||
}
|
||||
|
||||
mode = process_xdl_diff_opts(lstate, &cfg, ¶ms, &err);
|
||||
|
||||
if (ERROR_SET(&err)) {
|
||||
goto exit_0;
|
||||
}
|
||||
}
|
||||
|
||||
luaL_Buffer buf;
|
||||
hunkpriv_t *priv = NULL;
|
||||
switch (mode) {
|
||||
case kNluaXdiffModeUnified:
|
||||
luaL_buffinit(lstate, &buf);
|
||||
ecb.priv = &buf;
|
||||
ecb.outf = write_string;
|
||||
break;
|
||||
case kNluaXdiffModeOnHunkCB:
|
||||
priv = xmalloc(sizeof(*priv));
|
||||
priv->lstate = lstate;
|
||||
priv->err = &err;
|
||||
ecb.priv = priv;
|
||||
break;
|
||||
case kNluaXdiffModeLocations:
|
||||
lua_createtable(lstate, 0, 0);
|
||||
ecb.priv = lstate;
|
||||
break;
|
||||
}
|
||||
|
||||
if (xdl_diff(&ma, &mb, ¶ms, &cfg, &ecb) == -1) {
|
||||
if (!ERROR_SET(&err)) {
|
||||
api_set_error(&err, kErrorTypeException,
|
||||
"Error while performing diff operation");
|
||||
}
|
||||
}
|
||||
|
||||
XFREE_CLEAR(priv);
|
||||
|
||||
exit_0:
|
||||
if (ERROR_SET(&err)) {
|
||||
luaL_where(lstate, 1);
|
||||
lua_pushstring(lstate, err.msg);
|
||||
api_clear_error(&err);
|
||||
lua_concat(lstate, 2);
|
||||
return lua_error(lstate);
|
||||
} else if (mode == kNluaXdiffModeUnified) {
|
||||
luaL_pushresult(&buf);
|
||||
return 1;
|
||||
} else if (mode == kNluaXdiffModeLocations) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
12
src/nvim/lua/xdiff.h
Normal file
12
src/nvim/lua/xdiff.h
Normal file
@ -0,0 +1,12 @@
|
||||
#ifndef NVIM_LUA_XDIFF_H
|
||||
#define NVIM_LUA_XDIFF_H
|
||||
|
||||
#include <lua.h>
|
||||
#include <lualib.h>
|
||||
#include <lauxlib.h>
|
||||
|
||||
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
||||
# include "lua/xdiff.h.generated.h"
|
||||
#endif
|
||||
|
||||
#endif // NVIM_LUA_XDIFF_H
|
112
test/functional/lua/xdiff_spec.lua
Normal file
112
test/functional/lua/xdiff_spec.lua
Normal file
@ -0,0 +1,112 @@
|
||||
local helpers = require('test.functional.helpers')(after_each)
|
||||
local clear = helpers.clear
|
||||
local exec_lua = helpers.exec_lua
|
||||
local eq = helpers.eq
|
||||
local pcall_err = helpers.pcall_err
|
||||
|
||||
describe('xdiff bindings', function()
|
||||
before_each(function()
|
||||
clear()
|
||||
end)
|
||||
|
||||
describe('can diff text', function()
|
||||
before_each(function()
|
||||
exec_lua[[
|
||||
a1 = 'Hello\n'
|
||||
b1 = 'Helli\n'
|
||||
|
||||
a2 = 'Hello\nbye\nfoo\n'
|
||||
b2 = 'Helli\nbye\nbar\nbaz\n'
|
||||
]]
|
||||
end)
|
||||
|
||||
it('with no callback', function()
|
||||
|
||||
eq(
|
||||
table.concat({
|
||||
'@@ -1 +1 @@',
|
||||
'-Hello',
|
||||
'+Helli',
|
||||
''
|
||||
}, '\n'),
|
||||
exec_lua("return vim.diff(a1, b1)")
|
||||
)
|
||||
|
||||
eq(
|
||||
table.concat({
|
||||
'@@ -1 +1 @@',
|
||||
'-Hello',
|
||||
'+Helli',
|
||||
'@@ -3 +3,2 @@',
|
||||
'-foo',
|
||||
'+bar',
|
||||
'+baz',
|
||||
''
|
||||
}, '\n'),
|
||||
exec_lua("return vim.diff(a2, b2)")
|
||||
)
|
||||
|
||||
end)
|
||||
|
||||
it('with callback', function()
|
||||
exec_lua([[on_hunk = function(sa, ca, sb, cb)
|
||||
exp[#exp+1] = {sa, ca, sb, cb}
|
||||
end]])
|
||||
|
||||
eq({{1, 1, 1, 1}}, exec_lua[[
|
||||
exp = {}
|
||||
assert(vim.diff(a1, b1, {on_hunk = on_hunk}) == nil)
|
||||
return exp
|
||||
]])
|
||||
|
||||
eq({{1, 1, 1, 1}, {3, 1, 3, 2}}, exec_lua[[
|
||||
exp = {}
|
||||
assert(vim.diff(a2, b2, {on_hunk = on_hunk}) == nil)
|
||||
return exp
|
||||
]])
|
||||
|
||||
-- gives higher precedence to on_hunk over result_type
|
||||
eq({{1, 1, 1, 1}, {3, 1, 3, 2}}, exec_lua[[
|
||||
exp = {}
|
||||
assert(vim.diff(a2, b2, {on_hunk = on_hunk, result_type='indices'}) == nil)
|
||||
return exp
|
||||
]])
|
||||
end)
|
||||
|
||||
it('with error callback', function()
|
||||
exec_lua([[on_hunk = function(sa, ca, sb, cb)
|
||||
error('ERROR1')
|
||||
end]])
|
||||
|
||||
eq([[Error executing lua: [string "<nvim>"]:0: error running function on_hunk: [string "<nvim>"]:0: ERROR1]],
|
||||
pcall_err(exec_lua, [[vim.diff(a1, b1, {on_hunk = on_hunk})]]))
|
||||
end)
|
||||
|
||||
it('with hunk_lines', function()
|
||||
eq({{1, 1, 1, 1}},
|
||||
exec_lua([[return vim.diff(a1, b1, {result_type = 'indices'})]]))
|
||||
|
||||
eq({{1, 1, 1, 1}, {3, 1, 3, 2}},
|
||||
exec_lua([[return vim.diff(a2, b2, {result_type = 'indices'})]]))
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
it('can handle bad args', function()
|
||||
eq([[Error executing lua: [string "<nvim>"]:0: Expected at least 2 arguments]],
|
||||
pcall_err(exec_lua, [[vim.diff('a')]]))
|
||||
|
||||
eq([[Error executing lua: [string "<nvim>"]:0: bad argument #1 to 'diff' (expected string)]],
|
||||
pcall_err(exec_lua, [[vim.diff(1, 2)]]))
|
||||
|
||||
eq([[Error executing lua: [string "<nvim>"]:0: bad argument #3 to 'diff' (expected table)]],
|
||||
pcall_err(exec_lua, [[vim.diff('a', 'b', true)]]))
|
||||
|
||||
eq([[Error executing lua: [string "<nvim>"]:0: unexpected key: bad_key]],
|
||||
pcall_err(exec_lua, [[vim.diff('a', 'b', { bad_key = true })]]))
|
||||
|
||||
eq([[Error executing lua: [string "<nvim>"]:0: on_hunk is not a function]],
|
||||
pcall_err(exec_lua, [[vim.diff('a', 'b', { on_hunk = true })]]))
|
||||
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue
Block a user