From c822a2657c78e4b9d3798dfdf93e365460b5b885 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sun, 1 Mar 2026 12:05:59 +0000 Subject: [PATCH] refactor(lua): move vim.wait into runtime Lua Move vim.wait into runtime/lua/vim/_core/editor.lua and replace the C entrypoint with narrow vim._core helpers for polling, UI flushing, and interrupt checks. Keep the existing interval semantics by retaining the dummy timer that wakes the loop while it is otherwise idle. Update the docs to describe the success return values correctly, and adjust the test expectation for the new vim.validate() callback error. AI-assisted: Codex --- runtime/doc/lua.txt | 95 ++++++++--------- runtime/lua/vim/_core/editor.lua | 131 +++++++++++++++++++++++ runtime/lua/vim/_meta/builtin.lua | 58 ++++------- src/nvim/lua/executor.c | 168 ++++++------------------------ test/functional/lua/vim_spec.lua | 7 +- 5 files changed, 233 insertions(+), 226 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 403ca58beb..7a11c4f3b1 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -853,53 +853,6 @@ vim.ui_detach({ns}) *vim.ui_detach()* Parameters: ~ • {ns} (`integer`) Namespace ID -vim.wait({time}, {callback}, {interval}, {fast_only}) *vim.wait()* - Waits up to `time` milliseconds, until `callback` returns `true` - (success). Executes `callback` immediately, then on user events, internal - events, and approximately every `interval` milliseconds (default 200). - Returns all `callback` results on success. - - Nvim processes other events while waiting. Cannot be called during an - |api-fast| event. - - Examples: >lua - -- Wait for 100 ms, allowing other events to process. - vim.wait(100) - - -- Wait up to 1000 ms or until `vim.g.foo` is true, at intervals of ~500 ms. - vim.wait(1000, function() return vim.g.foo end, 500) - - -- Wait indefinitely until `vim.g.foo` is true, and get the callback results. - local ok, rv1, rv2, rv3 = vim.wait(math.huge, function() - return vim.g.foo, 'a', 42, { ok = { 'yes' } } - end) - - -- Schedule a function to set a value in 100ms. This would wait 10s if blocked, but actually - -- only waits 100ms because `vim.wait` processes other events while waiting. - vim.defer_fn(function() vim.g.timer_result = true end, 100) - if vim.wait(10000, function() return vim.g.timer_result end) then - print('Only waiting a little bit of time!') - end -< - - Parameters: ~ - • {time} (`number`) Number of milliseconds to wait. Must be - non-negative number, any fractional part is truncated. - • {callback} (`fun(): boolean, ...?`) Optional callback. Waits until - {callback} returns true - • {interval} (`integer?`) (Approximate) number of milliseconds to wait - between polls - • {fast_only} (`boolean?`) If true, only |api-fast| events will be - processed. - - Return (multiple): ~ - (`boolean`) - (`-1|-2?`) - • If callback returns `true` before timeout: `true, nil, ...` - • On timeout: `false, -1` - • On interrupt: `false, -2` - • On error: the error is raised. - ============================================================================== LUA-VIMSCRIPT BRIDGE *lua-vimscript* @@ -1519,6 +1472,54 @@ vim.str_utfindex({s}, {encoding}, {index}, {strict_indexing}) Return: ~ (`integer`) +vim.wait({time}, {callback}, {interval}, {fast_only}) *vim.wait()* + Waits up to `time` milliseconds, until `callback` returns `true` + (success). Executes `callback` immediately, then on user events, internal + events, and approximately every `interval` milliseconds (default 200). + Returns `true` plus any remaining callback results on success. + + Nvim processes other events while waiting. Cannot be called during an + |api-fast| event. + + Examples: >lua + -- Wait for 100 ms, allowing other events to process. + vim.wait(100) + + -- Wait up to 1000 ms or until `vim.g.foo` is true, at intervals of ~500 ms. + vim.wait(1000, function() return vim.g.foo end, 500) + + -- Wait indefinitely until `vim.g.foo` is true, and get the callback results. + local ok, rv1, rv2, rv3 = vim.wait(math.huge, function() + return vim.g.foo, 'a', 42, { ok = { 'yes' } } + end) + + -- Schedule a function to set a value in 100ms. This would wait 10s if blocked, but actually + -- only waits 100ms because `vim.wait` processes other events while waiting. + vim.defer_fn(function() vim.g.timer_result = true end, 100) + if vim.wait(10000, function() return vim.g.timer_result end) then + print('Only waiting a little bit of time!') + end +< + + Parameters: ~ + • {time} (`number`) Number of milliseconds to wait. Must be + non-negative number, any fractional part is truncated. + • {callback} (`fun(): boolean, ...?`) Optional callback. Waits until + {callback} returns true + • {interval} (`integer?`) (Approximate) number of milliseconds to wait + between polls + • {fast_only} (`boolean?`) If true, only |api-fast| events will be + processed. + + Return (multiple): ~ + (`boolean`) + (`-1|-2?`) + • If callback returns `true` before timeout: `true, ...` (remaining + callback results). + • On timeout: `false, -1` + • On interrupt: `false, -2` + • On error: the error is raised. + ============================================================================== Lua module: vim.inspector *vim.inspector* diff --git a/runtime/lua/vim/_core/editor.lua b/runtime/lua/vim/_core/editor.lua index e3c08c54db..412459a7dc 100644 --- a/runtime/lua/vim/_core/editor.lua +++ b/runtime/lua/vim/_core/editor.lua @@ -40,6 +40,137 @@ vim._extra = { inspect_pos = true, } +--- Waits up to `time` milliseconds, until `callback` returns `true` (success). Executes +--- `callback` immediately, then on user events, internal events, and approximately every +--- `interval` milliseconds (default 200). Returns `true` plus any remaining callback +--- results on success. +--- +--- Nvim processes other events while waiting. +--- Cannot be called during an |api-fast| event. +--- +--- Examples: +--- +--- ```lua +--- -- Wait for 100 ms, allowing other events to process. +--- vim.wait(100) +--- +--- -- Wait up to 1000 ms or until `vim.g.foo` is true, at intervals of ~500 ms. +--- vim.wait(1000, function() return vim.g.foo end, 500) +--- +--- -- Wait indefinitely until `vim.g.foo` is true, and get the callback results. +--- local ok, rv1, rv2, rv3 = vim.wait(math.huge, function() +--- return vim.g.foo, 'a', 42, { ok = { 'yes' } } +--- end) +--- +--- -- Schedule a function to set a value in 100ms. This would wait 10s if blocked, but actually +--- -- only waits 100ms because `vim.wait` processes other events while waiting. +--- vim.defer_fn(function() vim.g.timer_result = true end, 100) +--- if vim.wait(10000, function() return vim.g.timer_result end) then +--- print('Only waiting a little bit of time!') +--- end +--- ``` +--- +--- @param time number Number of milliseconds to wait. Must be non-negative number, any fractional +--- part is truncated. +--- @param callback? fun(): boolean, ... Optional callback. Waits until {callback} returns true +--- @param interval? integer (Approximate) number of milliseconds to wait between polls +--- @param fast_only? boolean If true, only |api-fast| events will be processed. +--- @return boolean, nil|-1|-2, ... +--- - If callback returns `true` before timeout: `true, ...` (remaining callback results). +--- - On timeout: `false, -1` +--- - On interrupt: `false, -2` +--- - On error: the error is raised. +function vim.wait(time, callback, interval, fast_only) + if vim.in_fast_event() then + error('E5560: vim.wait must not be called in a fast event context', 0) + end + + vim.validate('time', time, 'number') + if time < 0 then + error('timeout must be >= 0') + end + local has_deadline = time == time and time ~= math.huge + if has_deadline then + time = math.floor(time) + end + + vim.validate('callback', callback, 'callable', true) + + vim.validate('interval', interval, 'number', true) + if interval then + interval = math.floor(interval) + if interval < 0 then + error('interval must be >= 0') + end + else + interval = 200 + end + + vim.validate('fast_only', fast_only, 'boolean', true) + if fast_only == nil then + fast_only = false + end + + local start = vim.uv.hrtime() + local dummy_timer --- @type uv.uv_timer_t? + + local function cleanup() + if dummy_timer and not dummy_timer:is_closing() then + dummy_timer:stop() + dummy_timer:close() + end + end + + if interval > 0 then + dummy_timer = assert(vim.uv.new_timer()) + dummy_timer:start(interval, interval, function() + -- If Nvim exits while loop_poll() is blocked, vim.wait() does not + -- resume. + if vim.v.exiting ~= vim.NIL then + cleanup() + end + end) + end + + -- Flush screen updates before blocking. + vim._core.ui_flush() + + while true do + if vim._core.check_interrupt() then + cleanup() + return false, -2 + end + + if callback then + local results = vim.F.pack_len(pcall(callback)) + if not results[1] then + cleanup() + error(results[2], 0) + elseif results[2] then + cleanup() + return true, unpack(results, 3, results.n) + end + end + + local poll_timeout = -1 + if has_deadline then + local remaining_ms = time - (vim.uv.hrtime() - start) / 1e6 + if remaining_ms <= 0 then + cleanup() + return false, -1 + end + + -- loop_poll() takes an integer timeout, so cap each individual poll + -- while still honoring larger overall waits. + poll_timeout = math.min(math.ceil(remaining_ms), vim._maxint) + end + + -- The dummy timer wakes `vim._core.loop_poll()` on interval boundaries. Without it, + -- polling would only resume for unrelated events or the final timeout. + vim._core.loop_poll(poll_timeout, fast_only) + end +end + --- @nodoc vim.log = { --- @enum vim.log.levels diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index 65ec0ae8ed..2700bccd27 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -183,46 +183,24 @@ function vim.iconv(str, from, to, opts) end --- @return string? err Error message if scheduling failed, `nil` otherwise. function vim.schedule(fn) end ---- Waits up to `time` milliseconds, until `callback` returns `true` (success). Executes ---- `callback` immediately, then on user events, internal events, and approximately every ---- `interval` milliseconds (default 200). Returns all `callback` results on success. ---- ---- Nvim processes other events while waiting. ---- Cannot be called during an |api-fast| event. ---- ---- Examples: ---- ---- ```lua ---- -- Wait for 100 ms, allowing other events to process. ---- vim.wait(100) ---- ---- -- Wait up to 1000 ms or until `vim.g.foo` is true, at intervals of ~500 ms. ---- vim.wait(1000, function() return vim.g.foo end, 500) ---- ---- -- Wait indefinitely until `vim.g.foo` is true, and get the callback results. ---- local ok, rv1, rv2, rv3 = vim.wait(math.huge, function() ---- return vim.g.foo, 'a', 42, { ok = { 'yes' } } ---- end) ---- ---- -- Schedule a function to set a value in 100ms. This would wait 10s if blocked, but actually ---- -- only waits 100ms because `vim.wait` processes other events while waiting. ---- vim.defer_fn(function() vim.g.timer_result = true end, 100) ---- if vim.wait(10000, function() return vim.g.timer_result end) then ---- print('Only waiting a little bit of time!') ---- end ---- ``` ---- ---- @param time number Number of milliseconds to wait. Must be non-negative number, any fractional ---- part is truncated. ---- @param callback? fun(): boolean, ... Optional callback. Waits until {callback} returns true ---- @param interval? integer (Approximate) number of milliseconds to wait between polls ---- @param fast_only? boolean If true, only |api-fast| events will be processed. ---- @return boolean, nil|-1|-2, ... ---- - If callback returns `true` before timeout: `true, nil, ...` ---- - On timeout: `false, -1` ---- - On interrupt: `false, -2` ---- - On error: the error is raised. -function vim.wait(time, callback, interval, fast_only) end +---@nodoc +---@class vim._core +vim._core = {} + +--- @nodoc +--- Polls the main event loop for up to {timeout} milliseconds. +--- @param timeout integer +--- @param fast_only boolean +function vim._core.loop_poll(timeout, fast_only) end + +--- @nodoc +--- Flushes pending UI updates. +function vim._core.ui_flush() end + +--- @nodoc +--- Checks for interrupt, clears it, and consumes input if present. +--- @return boolean +function vim._core.check_interrupt() end --- Subscribe to |ui-events|, similar to |nvim_ui_attach()| but receive events in a Lua callback. --- Used to implement screen elements like popupmenu or message handling in Lua. diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 8a31a5dd2b..4a747848f3 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -520,148 +520,36 @@ static int nlua_schedule(lua_State *const lstate) return 2; } -// Dummy timer callback. Used by vim.wait(). -static void dummy_timer_due_cb(TimeWatcher *tw, void *data) -{ - // If the main loop is closing, the condition won't be checked again. - // Close the timer to avoid leaking resources. - if (main_loop.closing) { - time_watcher_stop(tw); - time_watcher_close(tw, dummy_timer_close_cb); - } -} - -// Dummy timer close callback. Used by vim.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, - int *nresults) -{ - int top = lua_gettop(lstate); - lua_pushvalue(lstate, 2); - *status = nlua_pcall(lstate, 0, LUA_MULTRET); - if (*status) { - return true; // break on error, but keep error on stack - } - *nresults = lua_gettop(lstate) - top; - if (*nresults == 0) { - *callback_result = false; - return false; - } - *callback_result = lua_toboolean(lstate, top + 1); - if (!*callback_result) { - lua_settop(lstate, top); - return false; - } - lua_remove(lstate, top + 1); - (*nresults)--; - return true; // break if true -} - -/// "vim.wait(timeout, condition[, interval])" function -static int nlua_wait(lua_State *lstate) +static int nlua_loop_poll(lua_State *lstate) FUNC_ATTR_NONNULL_ALL { - if (in_fast_callback) { - return luaL_error(lstate, e_fast_api_disabled, "vim.wait"); - } - - double timeout_number = luaL_checknumber(lstate, 1); - if (timeout_number < 0) { - return luaL_error(lstate, "timeout must be >= 0"); - } - int64_t timeout = (isnan(timeout_number) || timeout_number > (double)INT64_MAX) - ? INT64_MAX - : (int64_t)timeout_number; - - int lua_top = lua_gettop(lstate); - - // Check if condition can be called. - bool is_function = false; - if (lua_top >= 2 && !lua_isnil(lstate, 2)) { - 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: callback must be callable"); - return lua_error(lstate); - } - } - - intptr_t interval = 200; - if (lua_top >= 3 && !lua_isnil(lstate, 3)) { - interval = luaL_checkinteger(lstate, 3); - if (interval < 0) { - return luaL_error(lstate, "interval must be >= 0"); - } - } - - bool fast_only = false; - if (lua_top >= 4) { - fast_only = lua_toboolean(lstate, 4); - } + int64_t timeout = (int64_t)luaL_checkinteger(lstate, 1); + bool fast_only = lua_toboolean(lstate, 2); MultiQueue *loop_events = fast_only ? main_loop.fast_events : main_loop.events; + LOOP_PROCESS_EVENTS(&main_loop, loop_events, timeout); - TimeWatcher *tw = xmalloc(sizeof(TimeWatcher)); + return 0; +} - // Start dummy timer. - time_watcher_init(&main_loop, tw, NULL); - // Don't schedule the due callback, as that'll lead to two different types of events - // on each interval, causing the condition to be checked twice. - tw->events = NULL; - time_watcher_start(tw, dummy_timer_due_cb, (uint64_t)interval, (uint64_t)interval); - - int pcall_status = 0; - bool callback_result = false; - int nresults = 0; - - // Flush screen updates before blocking. +static int nlua_ui_flush(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ ui_flush(); + return 0; +} - LOOP_PROCESS_EVENTS_UNTIL(&main_loop, - loop_events, - timeout, - got_int || (is_function ? nlua_wait_condition(lstate, - &pcall_status, - &callback_result, - &nresults) - : false)); - - // 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); - if (nresults == 0) { - lua_pushnil(lstate); - nresults = 1; - } else { - lua_insert(lstate, -1 - nresults); - } - return nresults + 1; - } else if (got_int) { +static int nlua_check_interrupt(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + bool interrupted = got_int; + if (got_int) { got_int = false; vgetc(); - lua_pushboolean(lstate, 0); - lua_pushinteger(lstate, -2); - return 2; - } else { - lua_pushboolean(lstate, 0); - lua_pushinteger(lstate, -1); - return 2; } + + lua_pushboolean(lstate, interrupted); + return 1; } static nlua_ref_state_t *nlua_new_ref_state(lua_State *lstate, bool is_thread) @@ -718,6 +606,10 @@ static void nlua_common_vim_init(lua_State *lstate, bool is_thread, bool is_stan lua_pushcfunction(lstate, &nlua_is_thread); lua_setfield(lstate, -2, "is_thread"); + // vim._core + lua_createtable(lstate, 0, 0); + lua_setfield(lstate, -2, "_core"); + // vim.NIL lua_newuserdata(lstate, 0); lua_createtable(lstate, 0, 0); @@ -929,10 +821,6 @@ static bool 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"); - // ui_attach lua_pushcfunction(lstate, &nlua_ui_attach); lua_setfield(lstate, -2, "ui_attach"); @@ -943,6 +831,16 @@ static bool nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL nlua_common_vim_init(lstate, false, false); + // vim._core wait helpers + lua_getfield(lstate, -1, "_core"); + lua_pushcfunction(lstate, &nlua_loop_poll); + lua_setfield(lstate, -2, "loop_poll"); + lua_pushcfunction(lstate, &nlua_ui_flush); + lua_setfield(lstate, -2, "ui_flush"); + lua_pushcfunction(lstate, &nlua_check_interrupt); + lua_setfield(lstate, -2, "check_interrupt"); + lua_pop(lstate, 1); + // patch require() (only for --startuptime) if (time_fd != NULL) { lua_getglobal(lstate, "require"); diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 00ac46eb88..ce5341ad15 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -2371,10 +2371,9 @@ describe('lua stdlib', function() end) it('callback must be a function', function() - eq( - { false, 'vim.wait: callback must be callable' }, - exec_lua [[return {pcall(function() vim.wait(1000, 13) end)}]] - ) + local result = exec_lua [[return {pcall(function() vim.wait(1000, 13) end)}]] + eq(false, result[1]) + matches('callback: expected callable, got number$', remove_trace(result[2])) end) it('waits if callback arg is nil', function()