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