diff options
-rw-r--r-- | src/nvim/api/buffer.c | 21 | ||||
-rw-r--r-- | src/nvim/buffer_defs.h | 3 | ||||
-rw-r--r-- | src/nvim/buffer_updates.c | 23 | ||||
-rw-r--r-- | src/nvim/lua/executor.c | 11 | ||||
-rw-r--r-- | test/functional/lua/buffer_updates_spec.lua | 92 |
5 files changed, 140 insertions, 10 deletions
diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 81b3851c53..b0b65545ab 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -12,6 +12,7 @@ #include "nvim/api/buffer.h" #include "nvim/api/private/helpers.h" #include "nvim/api/private/defs.h" +#include "nvim/lua/executor.h" #include "nvim/vim.h" #include "nvim/buffer.h" #include "nvim/charset.h" @@ -137,24 +138,38 @@ Boolean nvim_buf_attach(uint64_t channel_id, if (is_lua && strequal("on_lines", k.data)) { if (v->type != kObjectTypeLuaRef) { api_set_error(err, kErrorTypeValidation, "callback is not a function"); - return false; + goto error; } cb.on_lines = v->data.luaref; v->data.integer = LUA_NOREF; } else if (is_lua && strequal("on_changedtick", k.data)) { if (v->type != kObjectTypeLuaRef) { api_set_error(err, kErrorTypeValidation, "callback is not a function"); - return false; + goto error; } cb.on_changedtick = v->data.luaref; v->data.integer = LUA_NOREF; + } else if (is_lua && strequal("on_detach", k.data)) { + if (v->type != kObjectTypeLuaRef) { + api_set_error(err, kErrorTypeValidation, "callback is not a function"); + goto error; + } + cb.on_detach = v->data.luaref; + v->data.integer = LUA_NOREF; } else { api_set_error(err, kErrorTypeValidation, "unexpected key: %s", k.data); - return false; + goto error; } } return buf_updates_register(buf, channel_id, cb, send_buffer); + +error: + // TODO(bfredl): ASAN build should check that the ref table is empty? + executor_free_luaref(cb.on_lines); + executor_free_luaref(cb.on_changedtick); + executor_free_luaref(cb.on_detach); + return false; } /// Deactivates buffer-update events on the channel. diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 117a9183a4..69cefe2247 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -456,8 +456,9 @@ typedef TV_DICTITEM_STRUCT(sizeof("changedtick")) ChangedtickDictItem; typedef struct { LuaRef on_lines; LuaRef on_changedtick; + LuaRef on_detach; } BufUpdateCallbacks; -#define BUF_UPDATE_CALLBACKS_INIT { LUA_NOREF, LUA_NOREF } +#define BUF_UPDATE_CALLBACKS_INIT { LUA_NOREF, LUA_NOREF, LUA_NOREF } #define BUF_HAS_QF_ENTRY 1 #define BUF_HAS_LL_ENTRY 2 diff --git a/src/nvim/buffer_updates.c b/src/nvim/buffer_updates.c index 2515e3f8aa..21efda9fd9 100644 --- a/src/nvim/buffer_updates.c +++ b/src/nvim/buffer_updates.c @@ -143,7 +143,21 @@ void buf_updates_unregister_all(buf_T *buf) } for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) { - free_update_callbacks(kv_A(buf->update_callbacks, i)); + BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i); + if (cb.on_detach != LUA_NOREF) { + Array args = ARRAY_DICT_INIT; + Object items[1]; + args.size = 1; + args.items = items; + + // the first argument is always the buffer handle + args.items[0] = BUFFER_OBJ(buf->handle); + + textlock++; + executor_exec_lua_cb(cb.on_detach, "detach", args, false); + textlock--; + } + free_update_callbacks(cb); } kv_destroy(buf->update_callbacks); kv_init(buf->update_callbacks); @@ -237,13 +251,14 @@ void buf_updates_send_changes(buf_T *buf, args.items[4] = INTEGER_OBJ(firstline - 1 + num_added); textlock++; - Object res = executor_exec_lua_cb(cb.on_lines, "lines", args); + Object res = executor_exec_lua_cb(cb.on_lines, "lines", args, true); textlock--; if (res.type == kObjectTypeBoolean && res.data.boolean == true) { free_update_callbacks(cb); keep = false; } + api_free_object(res); } if (keep) { kv_A(buf->update_callbacks, j++) = kv_A(buf->update_callbacks, i); @@ -276,13 +291,15 @@ void buf_updates_changedtick(buf_T *buf) args.items[1] = INTEGER_OBJ(buf_get_changedtick(buf)); textlock++; - Object res = executor_exec_lua_cb(cb.on_changedtick, "changedtick", args); + Object res = executor_exec_lua_cb(cb.on_changedtick, "changedtick", + args, true); textlock--; if (res.type == kObjectTypeBoolean && res.data.boolean == true) { free_update_callbacks(cb); keep = false; } + api_free_object(res); } if (keep) { kv_A(buf->update_callbacks, j++) = kv_A(buf->update_callbacks, i); diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 8cb282356a..ae872c1540 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -520,7 +520,8 @@ Object executor_exec_lua_api(const String str, const Array args, Error *err) return nlua_pop_Object(lstate, false, err); } -Object executor_exec_lua_cb(LuaRef ref, const char *name, Array args) +Object executor_exec_lua_cb(LuaRef ref, const char *name, Array args, + bool retval) { lua_State *const lstate = nlua_enter(); nlua_pushref(lstate, ref); @@ -529,7 +530,7 @@ Object executor_exec_lua_cb(LuaRef ref, const char *name, Array args) nlua_push_Object(lstate, args.items[i]); } - if (lua_pcall(lstate, (int)args.size+1, 1, 0)) { + if (lua_pcall(lstate, (int)args.size+1, retval ? 1 : 0, 0)) { // TODO(bfredl): callbacks:s might not always be msg-safe, for instance // lua callbacks for redraw events. Later on let the caller deal with the // error instead. @@ -538,7 +539,11 @@ Object executor_exec_lua_cb(LuaRef ref, const char *name, Array args) } Error err = ERROR_INIT; - return nlua_pop_Object(lstate, false, &err); + if (retval) { + return nlua_pop_Object(lstate, false, &err); + } else { + return NIL; + } } /// Run lua string diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua new file mode 100644 index 0000000000..c419d89be3 --- /dev/null +++ b/test/functional/lua/buffer_updates_spec.lua @@ -0,0 +1,92 @@ +-- Test suite for testing interactions with API bindings +local helpers = require('test.functional.helpers')(after_each) + +local command = helpers.command +local meths = helpers.meths +local clear = helpers.clear +local eq = helpers.eq + +local origlines = {"original line 1", + "original line 2", + "original line 3", + "original line 4", + "original line 5", + "original line 6"} + +describe('lua: buffer event callbacks', function() + before_each(function() + clear() + meths.execute_lua([[ + local events = {} + + function test_register(bufnr, id, changedtick) + local function callback(...) + table.insert(events, {id, ...}) + if test_unreg == id then + return true + end + end + local opts = {on_lines=callback, on_detach=callback} + if changedtick then + opts.on_changedtick = callback + end + vim.api.nvim_buf_attach(bufnr, false, opts) + end + + function get_events() + local ret_events = events + events = {} + return ret_events + end + ]], {}) + end) + + it('works', function() + meths.buf_set_lines(0, 0, -1, true, origlines) + meths.execute_lua("return test_register(...)", {0, "test1"}) + local tick = meths.buf_get_changedtick(0) + + command('normal! GyyggP') + tick = tick + 1 + eq({{ "test1", "lines", 1, tick, 0, 0, 1 }}, + meths.execute_lua("return get_events(...)", {})) + + meths.buf_set_lines(0, 3, 5, true, {"changed line"}) + tick = tick + 1 + eq({{ "test1", "lines", 1, tick, 3, 5, 4 }}, + meths.execute_lua("return get_events(...)", {})) + + meths.execute_lua("return test_register(...)", {0, "test2", true}) + tick = tick + 1 + command('undo') + + -- plugins can opt in to receive changedtick events, or choose + -- to only recieve actual changes. + eq({{ "test1", "lines", 1, tick, 3, 4, 5 }, + { "test2", "lines", 1, tick, 3, 4, 5 }, + { "test2", "changedtick", 1, tick+1 } }, + meths.execute_lua("return get_events(...)", {})) + tick = tick + 1 + + -- simulate next callback returning true + meths.execute_lua("test_unreg = 'test1'", {}) + + meths.buf_set_lines(0, 6, 7, true, {"x1","x2","x3"}) + tick = tick + 1 + + -- plugins can opt in to receive changedtick events, or choose + -- to only recieve actual changes. + eq({{ "test1", "lines", 1, tick, 6, 7, 9 }, + { "test2", "lines", 1, tick, 6, 7, 9 }}, + meths.execute_lua("return get_events(...)", {})) + + meths.buf_set_lines(0, 1, 1, true, {"added"}) + tick = tick + 1 + eq({{ "test2", "lines", 1, tick, 1, 1, 2 }}, + meths.execute_lua("return get_events(...)", {})) + + command('bwipe!') + eq({{ "test2", "detach", 1 }}, + meths.execute_lua("return get_events(...)", {})) + end) +end) |