From 3d3c0c669d054ef49304223e3b91ac820996d1e0 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 22 Aug 2021 11:22:04 +0100 Subject: [PATCH] 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. --- runtime/doc/lua.txt | 67 ++++++ src/nvim/lua/executor.c | 5 + src/nvim/lua/xdiff.c | 334 +++++++++++++++++++++++++++++ src/nvim/lua/xdiff.h | 12 ++ test/functional/lua/xdiff_spec.lua | 112 ++++++++++ 5 files changed, 530 insertions(+) create mode 100644 src/nvim/lua/xdiff.c create mode 100644 src/nvim/lua/xdiff.h create mode 100644 test/functional/lua/xdiff_spec.lua diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index fd1bedd8ef..a684110b11 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -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: diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 5799c3ee98..7fa7a7d94e 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -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"); { diff --git a/src/nvim/lua/xdiff.c b/src/nvim/lua/xdiff.c new file mode 100644 index 0000000000..d50f874a82 --- /dev/null +++ b/src/nvim/lua/xdiff.c @@ -0,0 +1,334 @@ +#include +#include +#include + +#include +#include +#include +#include + +#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; +} diff --git a/src/nvim/lua/xdiff.h b/src/nvim/lua/xdiff.h new file mode 100644 index 0000000000..cae7c98e81 --- /dev/null +++ b/src/nvim/lua/xdiff.h @@ -0,0 +1,12 @@ +#ifndef NVIM_LUA_XDIFF_H +#define NVIM_LUA_XDIFF_H + +#include +#include +#include + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "lua/xdiff.h.generated.h" +#endif + +#endif // NVIM_LUA_XDIFF_H diff --git a/test/functional/lua/xdiff_spec.lua b/test/functional/lua/xdiff_spec.lua new file mode 100644 index 0000000000..4f28f84c01 --- /dev/null +++ b/test/functional/lua/xdiff_spec.lua @@ -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 ""]:0: error running function on_hunk: [string ""]: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 ""]:0: Expected at least 2 arguments]], + pcall_err(exec_lua, [[vim.diff('a')]])) + + eq([[Error executing lua: [string ""]:0: bad argument #1 to 'diff' (expected string)]], + pcall_err(exec_lua, [[vim.diff(1, 2)]])) + + eq([[Error executing lua: [string ""]:0: bad argument #3 to 'diff' (expected table)]], + pcall_err(exec_lua, [[vim.diff('a', 'b', true)]])) + + eq([[Error executing lua: [string ""]:0: unexpected key: bad_key]], + pcall_err(exec_lua, [[vim.diff('a', 'b', { bad_key = true })]])) + + eq([[Error executing lua: [string ""]:0: on_hunk is not a function]], + pcall_err(exec_lua, [[vim.diff('a', 'b', { on_hunk = true })]])) + + end) +end)