diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index d65aa2349e..c4312c8e5d 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -843,32 +843,29 @@ vim.ui_detach({ns}) *vim.ui_detach()* • {ns} (`integer`) Namespace ID vim.wait({time}, {callback}, {interval}, {fast_only}) *vim.wait()* - Wait for {time} in milliseconds until {callback} returns `true`. + Waits up to `time` milliseconds, until `callback` returns `true` + (success). Executes `callback` immediately, then at intervals of + approximately `interval` milliseconds (default 200). Returns all + `callback` results on success. - Executes {callback} immediately and at approximately {interval} - milliseconds (default 200). Nvim still processes other events during this - time. - - Cannot be called while in an |api-fast| event. + 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, function() end) + -- Wait for 100 ms, allowing other events to process. + vim.wait(100) - --- - -- Wait for 100 ms or until global variable set. - vim.wait(100, function() return vim.g.waiting_for_var end) + -- 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 for 1 second or until global variable set, checking every ~500 ms - vim.wait(1000, function() return vim.g.waiting_for_var end, 500) + -- Wait up to 100 ms or until `vim.g.foo` is true, and get the callback results. + local ok, rv1, rv2, rv3 = vim.wait(100, function() + return vim.g.foo, 'a', 42, { ok = { 'yes' } } + end) - --- - -- Schedule a function to set a value in 100ms + -- 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) - - -- 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 @@ -886,10 +883,10 @@ vim.wait({time}, {callback}, {interval}, {fast_only}) *vim.wait()* Return (multiple): ~ (`boolean`) (`-1|-2?`) - • 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. + • If callback returns `true` before timeout: `true, nil, ...` + • On timeout: `false, -1` + • On interrupt: `false, -2` + • On error: the error is raised. ============================================================================== diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 6fe29b2cab..81bb726a00 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -247,6 +247,7 @@ LSP LUA +• |vim.wait()| returns the callback results. • Lua type annotations for `vim.uv`. • |vim.hl.range()| now allows multiple timed highlights. • |vim.tbl_extend()| and |vim.tbl_deep_extend()| now accept a function behavior argument. diff --git a/runtime/lua/vim/_meta/builtin.lua b/runtime/lua/vim/_meta/builtin.lua index b676522566..899229edef 100644 --- a/runtime/lua/vim/_meta/builtin.lua +++ b/runtime/lua/vim/_meta/builtin.lua @@ -179,34 +179,30 @@ function vim.iconv(str, from, to, opts) end --- @param fn fun() function vim.schedule(fn) end ---- Wait for {time} in milliseconds until {callback} returns `true`. +--- Waits up to `time` milliseconds, until `callback` returns `true` (success). Executes +--- `callback` immediately, then at intervals of approximately `interval` milliseconds (default +--- 200). Returns all `callback` results on success. --- ---- Executes {callback} immediately and at approximately {interval} ---- milliseconds (default 200). Nvim still processes other events during ---- this time. ---- ---- Cannot be called while in an |api-fast| event. +--- 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, function() end) +--- -- Wait for 100 ms, allowing other events to process. +--- vim.wait(100) --- ---- --- ---- -- Wait for 100 ms or until global variable set. ---- vim.wait(100, function() return vim.g.waiting_for_var end) +--- -- 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 for 1 second or until global variable set, checking every ~500 ms ---- vim.wait(1000, function() return vim.g.waiting_for_var end, 500) +--- -- Wait up to 100 ms or until `vim.g.foo` is true, and get the callback results. +--- local ok, rv1, rv2, rv3 = vim.wait(100, function() +--- return vim.g.foo, 'a', 42, { ok = { 'yes' } } +--- end) --- ---- --- ---- -- Schedule a function to set a value in 100ms +--- -- 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) ---- ---- -- 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 @@ -216,11 +212,11 @@ function vim.schedule(fn) end --- @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` 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. +--- @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 --- Subscribe to |ui-events|, similar to |nvim_ui_attach()| but receive events in a Lua callback. diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 0860c753ad..00cc73ccdb 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -175,11 +175,18 @@ int nlua_pcall(lua_State *lstate, int nargs, int nresults) lua_getfield(lstate, -1, "traceback"); lua_remove(lstate, -2); lua_insert(lstate, -2 - nargs); + int pre_top = lua_gettop(lstate); int status = lua_pcall(lstate, nargs, nresults, -2 - nargs); if (status) { lua_remove(lstate, -2); } else { - lua_remove(lstate, -1 - nresults); + if (nresults == LUA_MULTRET) { + int new_top = lua_gettop(lstate); + int actual_nres = new_top - pre_top + nargs + 1; + lua_remove(lstate, -1 - actual_nres); + } else { + lua_remove(lstate, -1 - nresults); + } } return status; } @@ -415,16 +422,28 @@ 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) +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, 1); + *status = nlua_pcall(lstate, 0, LUA_MULTRET); 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 + *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 @@ -454,8 +473,7 @@ static int nlua_wait(lua_State *lstate) } if (!is_function) { - lua_pushliteral(lstate, - "vim.wait: if passed, condition must be a function"); + lua_pushliteral(lstate, "vim.wait: callback must be callable"); return lua_error(lstate); } } @@ -488,6 +506,7 @@ static int nlua_wait(lua_State *lstate) int pcall_status = 0; bool callback_result = false; + int nresults = 0; // Flush screen updates before blocking. ui_flush(); @@ -497,7 +516,8 @@ static int nlua_wait(lua_State *lstate) (int)timeout, got_int || (is_function ? nlua_wait_condition(lstate, &pcall_status, - &callback_result) + &callback_result, + &nresults) : false)); // Stop dummy timer @@ -508,18 +528,26 @@ static int nlua_wait(lua_State *lstate) return lua_error(lstate); } else if (callback_result) { lua_pushboolean(lstate, 1); - lua_pushnil(lstate); + if (nresults == 0) { + lua_pushnil(lstate); + nresults = 1; + } else { + lua_insert(lstate, -1 - nresults); + } + return nresults + 1; } else 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; } - return 2; + abort(); } static nlua_ref_state_t *nlua_new_ref_state(lua_State *lstate, bool is_thread) diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 9372f5c7b7..7d2db461b6 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -2096,6 +2096,31 @@ stack traceback: exec_lua [[vim.wait(100, function() return true end)]] end) + it('returns all (multiple) callback results', function() + eq({ true, false }, exec_lua [[return { vim.wait(200, function() return true, false end) }]]) + eq( + { true, 'a', 42, { ok = { 'yes' } } }, + exec_lua [[ + local ok, rv1, rv2, rv3 = vim.wait(200, function() + return true, 'a', 42, { ok = { 'yes' } } + end) + + return { ok, rv1, rv2, rv3 } + ]] + ) + end) + + it('does not return callback results on timeout', function() + eq( + { false, -1 }, + exec_lua [[ + return { vim.wait(1, function() + return false, 'a', 42, { ok = { 'yes' } } + end) } + ]] + ) + end) + it('waits the expected time if false', function() eq( { time = true, wait_result = { false, -1 } }, @@ -2184,38 +2209,36 @@ stack traceback: eq({ false, '[string ""]:1: As Expected' }, { result[1], remove_trace(result[2]) }) end) - it('if callback is passed, it must be a function', function() + it('callback must be a function', function() eq( - { false, 'vim.wait: if passed, condition must be a function' }, - exec_lua [[ - return {pcall(function() vim.wait(1000, 13) end)} - ]] + { false, 'vim.wait: callback must be callable' }, + exec_lua [[return {pcall(function() vim.wait(1000, 13) end)}]] ) end) - it('allows waiting with no callback, explicit', function() + it('waits if callback arg is nil', function() eq( true, exec_lua [[ local start_time = vim.uv.hrtime() - vim.wait(50, nil) + vim.wait(50, nil) -- select('#', ...) == 1 return vim.uv.hrtime() - start_time > 25000 ]] ) end) - it('allows waiting with no callback, implicit', function() + it('waits if callback arg is omitted', function() eq( true, exec_lua [[ local start_time = vim.uv.hrtime() - vim.wait(50) + vim.wait(50) -- select('#', ...) == 0 return vim.uv.hrtime() - start_time > 25000 ]] ) end) - it('calls callbacks exactly once if they return true immediately', function() + it('invokes callback exactly once if it returns true immediately', function() eq( true, exec_lua [[