aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/lua.txt56
-rw-r--r--runtime/lua/vim/lsp.lua37
-rw-r--r--src/nvim/lua/executor.c102
-rw-r--r--test/functional/lua/vim_spec.lua186
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)