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
This commit is contained in:
Lewis Russell
2026-03-01 12:05:59 +00:00
committed by Lewis Russell
parent 4431713285
commit c822a2657c
5 changed files with 233 additions and 226 deletions

View File

@@ -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*

View File

@@ -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

View File

@@ -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.

View File

@@ -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");

View File

@@ -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()