fix(lua): vim.wait(0) does not call loop_poll #39679

Problem:
Regression from c822a2657c: `vim.wait(0)` does not call `loop_poll`,
so `vim.wait(1)` is needed to "yield" from Lua.

Solution:
- Ensure that `vim._core.loop_poll()` is always called, even when `time=0`.
- Document how to interrupt Lua code (ctrl-c).

ref https://github.com/neovim/neovim/issues/6800
This commit is contained in:
Justin M. Keyes
2026-05-10 14:22:31 -04:00
committed by GitHub
parent c2d7dd781a
commit 5370eb0146
7 changed files with 81 additions and 14 deletions

View File

@@ -459,8 +459,8 @@ typed. The `vgetc()` function is used for this. It also handles mapping.
What we consider the "Nvim event loop" is actually a wrapper around `uv_run` to
handle both the `fast_events` queue and possibly (a suitable subset of) deferred
events. Therefore "raw" `vim.uv.run()` is often not enough to "yield" from Lua
plugins; instead they can call `vim.wait(0)`.
events. Therefore "raw" `vim.uv.run()` is often not enough to yield from Lua;
instead call `vim.wait(0)` and check its result.
Updating the screen is mostly postponed until a command or a sequence of
commands has finished. The work is done by `update_screen()`, which calls

View File

@@ -87,6 +87,22 @@ Finally, you can include Lua code in a Vimscript file by putting it inside a
end
EOF
<
*lua-guide-interrupt*
Lua cannot normally be interrupted by |CTRL-C| (Lua does not implicitly
"yield" to Nvim). To allow the user to interrupt potentially slow loops,
periodically call |vim.wait()|. Passing `interval=0` skips the dummy timer
setup, minimizing per-call overhead in a tight loop: >lua
while true do
-- ...work...
local _, code = vim.wait(0, nil, 0)
if code == -2 then -- CTRL-C.
break
end
end
<
See also: https://github.com/neovim/neovim/issues/6800
------------------------------------------------------------------------------
Using Lua files on startup *lua-guide-config*

View File

@@ -1499,6 +1499,15 @@ vim.wait({time}, {callback}, {interval}, {fast_only}) *vim.wait()*
if vim.wait(10000, function() return vim.g.timer_result end) then
print('Only waiting a little bit of time!')
end
-- Yield via vim.wait() to allow CTRL-C to interrupt Lua code.
while true do
-- ...work...
local _, code = vim.wait(0, nil, 0)
if code == -2 then -- CTRL-C.
break
end
end
<
Parameters: ~

View File

@@ -128,10 +128,6 @@ gD Goto global Declaration. When the cursor is on a
1gD Like "gD", but ignore matches inside a {} block that
ends before the cursor position.
*CTRL-C*
CTRL-C Interrupt current (search) command.
In Normal mode, any pending command is aborted.
*:noh* *:nohlsearch*
:noh[lsearch] Stop the highlighting for the 'hlsearch' option. It
is automatically turned back on when using a search

View File

@@ -11,6 +11,16 @@ Various commands *various*
==============================================================================
1. Various commands *various-cmds*
*CTRL-C*
CTRL-C Interrupt the current command, script, mapping,
prompt, or search.
In Normal mode, any pending command is aborted. During
a search, interrupts the search.
Lua code cannot be interrupted unless it "yields" via
|vim.wait()|.
*CTRL-L*
CTRL-L Clears and redraws the screen. The redraw may happen
later, after processing typeahead.

View File

@@ -69,6 +69,15 @@ vim._extra = {
--- if vim.wait(10000, function() return vim.g.timer_result end) then
--- print('Only waiting a little bit of time!')
--- end
---
--- -- Yield via vim.wait() to allow CTRL-C to interrupt Lua code.
--- while true do
--- -- ...work...
--- local _, code = vim.wait(0, nil, 0)
--- if code == -2 then -- CTRL-C.
--- break
--- end
--- end
--- ```
---
--- @param time number Number of milliseconds to wait. Must be non-negative number, any fractional
@@ -156,19 +165,26 @@ function vim.wait(time, callback, interval, fast_only)
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)
-- loop_poll() takes an integer timeout, so cap each individual poll while still honoring
-- larger overall waits. Always poll at least once, so vim.wait(0) still pumps the event loop
-- (lets user code "yield" to CTRL-C).
poll_timeout = math.max(0, 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)
-- Check deadline AFTER polling (mirrors LOOP_PROCESS_EVENTS_UNTIL behavior).
-- `got_int` may have been set during poll; let check_interrupt() catch it.
if has_deadline and time - (vim.uv.hrtime() - start) / 1e6 <= 0 then
if vim._core.check_interrupt() then
cleanup()
return false, -2
end
cleanup()
return false, -1
end
end
end

View File

@@ -2567,6 +2567,26 @@ describe('lua stdlib', function()
end)
end)
it('lets CTRL-C interrupt a Lua loop', function()
api.nvim_set_var('channel', api.nvim_get_chan_info(0).id)
exec_lua([[
function _G.Loop()
vim.rpcnotify(vim.g.channel, 'ready')
while true do
local _, code = vim.wait(0, nil, 0)
if code == -2 then
vim.rpcnotify(vim.g.channel, 'wait', code)
return
end
end
end
]])
feed(':lua _G.Loop()<CR>')
eq({ 'notification', 'ready', {} }, next_msg(500))
feed('<C-C>')
eq({ 'notification', 'wait', { -2 } }, next_msg(500))
end)
it('fails in fast callbacks #26122', function()
local screen = Screen.new(80, 10)
exec_lua([[