diff options
-rw-r--r-- | runtime/doc/lua.txt | 56 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 37 | ||||
-rw-r--r-- | src/nvim/lua/executor.c | 102 | ||||
-rw-r--r-- | test/functional/lua/vim_spec.lua | 186 |
4 files changed, 343 insertions, 38 deletions
diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 2b83c35c90..19579a2a3c 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -827,6 +827,62 @@ vim.schedule({callback}) *vim.schedule()* Schedules {callback} to be invoked soon by the main event-loop. Useful to avoid |textlock| or other temporary restrictions. + +vim.defer_fn({fn}, {timeout}) *vim.defer_fn* + Defers calling {fn} until {timeout} ms passes. Use to do a one-shot timer + that calls {fn}. + + Parameters: ~ + {fn} Callback to call once {timeout} expires + {timeout} Time in ms to wait before calling {fn} + + Returns: ~ + |vim.loop|.new_timer() object + +vim.wait({time}, {callback} [, {interval}]) *vim.wait()* + Wait for {time} in milliseconds until {callback} returns `true`. + + Executes {callback} immediately and at approximately {interval} + milliseconds (default 200). Nvim still processes other events during + this time. + + + Returns: ~ + If {callback} returns `true` during the {time}: + `true, nil` + + If {callback} never returns `true` during the {time}: + `false, -1` + + If {callback} is interrupted during the {time}: + `false, -2` + + If {callback} errors, the error is raised. + + Examples: > + + --- + -- Wait for 100 ms, allowing other events to process + vim.wait(100, function() end) + + --- + -- Wait for 100 ms or until global variable set. + vim.wait(100, function() return vim.g.waiting_for_var end) + + --- + -- Wait for 1 second or until global variable set, checking every ~500 ms + vim.wait(1000, function() return vim.g.waiting_for_var end, 500) + + --- + -- Schedule a function to set a value in 100ms + vim.defer_fn(function() vim.g.timer_result = true end, 100) + + -- Would wait ten seconds if results blocked. Actually only waits 100 ms + if vim.wait(10000, function() return vim.g.timer_result end) then + print('Only waiting a little bit of time!') + end +< + vim.fn.{func}({...}) *vim.fn* Invokes |vim-function| or |user-function| {func} with arguments {...}. To call autoload functions, use the syntax: > diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7135d2c5b6..84812b8c64 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -46,31 +46,6 @@ local function is_dir(filename) return stat and stat.type == 'directory' or false end --- TODO Use vim.wait when that is available, but provide an alternative for now. -local wait = vim.wait or function(timeout_ms, condition, interval) - validate { - timeout_ms = { timeout_ms, 'n' }; - condition = { condition, 'f' }; - interval = { interval, 'n', true }; - } - assert(timeout_ms > 0, "timeout_ms must be > 0") - local _ = log.debug() and log.debug("wait.fallback", timeout_ms) - interval = interval or 200 - local interval_cmd = "sleep "..interval.."m" - local timeout = timeout_ms + uv.now() - -- TODO is there a better way to sync this? - while true do - uv.update_time() - if condition() then - return 0 - end - if uv.now() >= timeout then - return -1 - end - nvim_command(interval_cmd) - -- vim.loop.sleep(10) - end -end local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" } local valid_encodings = { @@ -810,8 +785,8 @@ function lsp._vim_exit_handler() for _, client in pairs(active_clients) do client.stop() end - local wait_result = wait(500, function() return tbl_isempty(active_clients) end, 50) - if wait_result ~= 0 then + + if not vim.wait(500, function() return tbl_isempty(active_clients) end, 50) then for _, client in pairs(active_clients) do client.stop(true) end @@ -889,12 +864,14 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) for _ in pairs(client_request_ids) do expected_result_count = expected_result_count + 1 end - local wait_result = wait(timeout_ms or 100, function() + + local wait_result, reason = vim.wait(timeout_ms or 100, function() return result_count >= expected_result_count end, 10) - if wait_result ~= 0 then + + if not wait_result then cancel() - return nil, wait_result_reason[wait_result] + return nil, wait_result_reason[reason] end return request_results end diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 144646fca2..327ed6d6b7 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -28,6 +28,8 @@ #include "nvim/ascii.h" #include "nvim/change.h" #include "nvim/eval/userfunc.h" +#include "nvim/event/time.h" +#include "nvim/event/loop.h" #ifdef WIN32 #include "nvim/os/os.h" @@ -255,6 +257,101 @@ static struct luaL_Reg regex_meta[] = { { NULL, NULL } }; +// Dummy timer callback. Used by f_wait(). +static void dummy_timer_due_cb(TimeWatcher *tw, void *data) +{ +} + +// Dummy timer close callback. Used by f_wait(). +static void dummy_timer_close_cb(TimeWatcher *tw, void *data) +{ + xfree(tw); +} + +static bool nlua_wait_condition(lua_State *lstate, int *status, + bool *callback_result) +{ + lua_pushvalue(lstate, 2); + *status = lua_pcall(lstate, 0, 1, 0); + if (*status) { + return true; // break on error, but keep error on stack + } + *callback_result = lua_toboolean(lstate, -1); + lua_pop(lstate, 1); + return *callback_result; // break if true +} + +/// "vim.wait(timeout, condition[, interval])" function +static int nlua_wait(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + intptr_t timeout = luaL_checkinteger(lstate, 1); + if (timeout < 0) { + return luaL_error(lstate, "timeout must be > 0"); + } + + // Check if condition can be called. + bool is_function = (lua_type(lstate, 2) == LUA_TFUNCTION); + + // Check if condition is callable table + if (!is_function && luaL_getmetafield(lstate, 2, "__call") != 0) { + is_function = (lua_type(lstate, -1) == LUA_TFUNCTION); + lua_pop(lstate, 1); + } + + if (!is_function) { + lua_pushliteral(lstate, "vim.wait: condition must be a function"); + return lua_error(lstate); + } + + intptr_t interval = 200; + if (lua_gettop(lstate) >= 3) { + interval = luaL_checkinteger(lstate, 3); + if (interval < 0) { + return luaL_error(lstate, "interval must be > 0"); + } + } + + TimeWatcher *tw = xmalloc(sizeof(TimeWatcher)); + + // Start dummy timer. + time_watcher_init(&main_loop, tw, NULL); + tw->events = main_loop.events; + tw->blockable = true; + time_watcher_start(tw, dummy_timer_due_cb, + (uint64_t)interval, (uint64_t)interval); + + int pcall_status = 0; + bool callback_result = false; + + LOOP_PROCESS_EVENTS_UNTIL( + &main_loop, + main_loop.events, + (int)timeout, + nlua_wait_condition(lstate, &pcall_status, &callback_result) || got_int); + + // Stop dummy timer + time_watcher_stop(tw); + time_watcher_close(tw, dummy_timer_close_cb); + + if (pcall_status) { + return lua_error(lstate); + } else if (callback_result) { + lua_pushboolean(lstate, 1); + lua_pushnil(lstate); + } else if (got_int) { + got_int = false; + vgetc(); + lua_pushboolean(lstate, 0); + lua_pushinteger(lstate, -2); + } else { + lua_pushboolean(lstate, 0); + lua_pushinteger(lstate, -1); + } + + return 2; +} + /// Initialize lua interpreter state /// /// Called by lua interpreter itself to initialize state. @@ -305,7 +402,6 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL // regex lua_pushcfunction(lstate, &nlua_regex); lua_setfield(lstate, -2, "regex"); - luaL_newmetatable(lstate, "nvim_regex"); luaL_register(lstate, NULL, regex_meta); lua_pushvalue(lstate, -1); // [meta, meta] @@ -320,6 +416,10 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL lua_pushcfunction(lstate, &nlua_rpcnotify); lua_setfield(lstate, -2, "rpcnotify"); + // wait + lua_pushcfunction(lstate, &nlua_wait); + lua_setfield(lstate, -2, "wait"); + // vim.loop luv_set_loop(lstate, &main_loop.uv); luv_set_callback(lstate, nlua_luv_cfpcall); diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index c68c05dffa..596b960419 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -1048,13 +1048,13 @@ describe('lua stdlib', function() end) it('vim.defer_fn', function() - exec_lua [[ - vim.g.test = 0 - vim.defer_fn(function() vim.g.test = 1 end, 50) - ]] - eq(0, exec_lua[[return vim.g.test]]) - exec_lua [[vim.cmd("sleep 1000m")]] - eq(1, exec_lua[[return vim.g.test]]) + eq(false, exec_lua [[ + vim.g.test = false + vim.defer_fn(function() vim.g.test = true end, 150) + return vim.g.test + ]]) + exec_lua [[vim.wait(1000, function() return vim.g.test end)]] + eq(true, exec_lua[[return vim.g.test]]) end) it('vim.region', function() @@ -1066,4 +1066,176 @@ describe('lua stdlib', function() eq({5,15}, exec_lua[[ return vim.region(0,{1,5},{1,14},'v',true)[1] ]]) end) + describe('vim.wait', function() + before_each(function() + exec_lua[[ + -- high precision timer + get_time = function() + return vim.fn.reltimefloat(vim.fn.reltime()) + end + ]] + end) + + it('should run from lua', function() + exec_lua[[vim.wait(100, function() return true end)]] + end) + + it('should wait the expected time if false', function() + eq({time = true, wait_result = {false, -1}}, exec_lua[[ + start_time = get_time() + wait_succeed, wait_fail_val = vim.wait(200, function() return false end) + + return { + -- 150ms waiting or more results in true. Flaky tests are bad. + time = (start_time + 0.15) < get_time(), + wait_result = {wait_succeed, wait_fail_val} + } + ]]) + end) + + + it('should not block other events', function() + eq({time = true, wait_result = true}, exec_lua[[ + start_time = get_time() + + vim.g.timer_result = false + timer = vim.loop.new_timer() + timer:start(100, 0, vim.schedule_wrap(function() + vim.g.timer_result = true + end)) + + -- Would wait ten seconds if results blocked. + wait_result = vim.wait(10000, function() return vim.g.timer_result end) + + return { + time = (start_time + 5) > get_time(), + wait_result = wait_result, + } + ]]) + end) + + it('should work with vim.defer_fn', function() + eq({time = true, wait_result = true}, exec_lua[[ + start_time = get_time() + + vim.defer_fn(function() vim.g.timer_result = true end, 100) + wait_result = vim.wait(10000, function() return vim.g.timer_result end) + + return { + time = (start_time + 5) > get_time(), + wait_result = wait_result, + } + ]]) + end) + + it('should require functions to be passed', function() + local pcall_result = exec_lua [[ + return {pcall(function() vim.wait(1000, 13) end)} + ]] + + eq(pcall_result[1], false) + matches('condition must be a function', pcall_result[2]) + end) + + it('should not crash when callback errors', function() + local pcall_result = exec_lua [[ + return {pcall(function() vim.wait(1000, function() error("As Expected") end) end)} + ]] + + eq(pcall_result[1], false) + matches('As Expected', pcall_result[2]) + end) + + it('should call callbacks exactly once if they return true immediately', function() + eq(true, exec_lua [[ + vim.g.wait_count = 0 + vim.wait(1000, function() + vim.g.wait_count = vim.g.wait_count + 1 + return true + end, 20) + return vim.g.wait_count == 1 + ]]) + end) + + it('should call callbacks few times with large `interval`', function() + eq(true, exec_lua [[ + vim.g.wait_count = 0 + vim.wait(50, function() vim.g.wait_count = vim.g.wait_count + 1 end, 200) + return vim.g.wait_count < 5 + ]]) + end) + + it('should call callbacks more times with small `interval`', function() + eq(true, exec_lua [[ + vim.g.wait_count = 0 + vim.wait(50, function() vim.g.wait_count = vim.g.wait_count + 1 end, 5) + return vim.g.wait_count > 5 + ]]) + end) + + it('should play nice with `not` when fails', function() + eq(true, exec_lua [[ + if not vim.wait(50, function() end) then + return true + end + + return false + ]]) + end) + + it('should play nice with `if` when success', function() + eq(true, exec_lua [[ + if vim.wait(50, function() return true end) then + return true + end + + return false + ]]) + end) + + it('should return immediately with false if timeout is 0', function() + eq({false, -1}, exec_lua [[ + return { + vim.wait(0, function() return false end) + } + ]]) + end) + + it('should work with tables with __call', function() + eq(true, exec_lua [[ + local t = setmetatable({}, {__call = function(...) return true end}) + return vim.wait(100, t, 10) + ]]) + end) + + it('should work with tables with __call that change', function() + eq(true, exec_lua [[ + local t = {count = 0} + setmetatable(t, { + __call = function() + t.count = t.count + 1 + return t.count > 3 + end + }) + + return vim.wait(1000, t, 10) + ]]) + end) + + it('should not work with negative intervals', function() + local pcall_result = exec_lua [[ + return pcall(function() vim.wait(1000, function() return false end, -1) end) + ]] + + eq(false, pcall_result) + end) + + it('should not work with weird intervals', function() + local pcall_result = exec_lua [[ + return pcall(function() vim.wait(1000, function() return false end, 'a string value') end) + ]] + + eq(false, pcall_result) + end) end) +end) |