diff --git a/runtime/doc/dev_arch.txt b/runtime/doc/dev_arch.txt index 1ced08f361..82840239c0 100644 --- a/runtime/doc/dev_arch.txt +++ b/runtime/doc/dev_arch.txt @@ -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 diff --git a/runtime/doc/lua-guide.txt b/runtime/doc/lua-guide.txt index 994e1f292c..8563ae2f5c 100644 --- a/runtime/doc/lua-guide.txt +++ b/runtime/doc/lua-guide.txt @@ -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* diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 27449ec419..ddddc7e320 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -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: ~ diff --git a/runtime/doc/pattern.txt b/runtime/doc/pattern.txt index 8e68258a8c..c0bf53be70 100644 --- a/runtime/doc/pattern.txt +++ b/runtime/doc/pattern.txt @@ -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 diff --git a/runtime/doc/various.txt b/runtime/doc/various.txt index 10ce8de1ca..a72d52bf83 100644 --- a/runtime/doc/various.txt +++ b/runtime/doc/various.txt @@ -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. diff --git a/runtime/lua/vim/_core/editor.lua b/runtime/lua/vim/_core/editor.lua index 8f8cdf2beb..8a46c0537e 100644 --- a/runtime/lua/vim/_core/editor.lua +++ b/runtime/lua/vim/_core/editor.lua @@ -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 diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 4d896b9558..bf2e5e7cea 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -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()') + eq({ 'notification', 'ready', {} }, next_msg(500)) + feed('') + eq({ 'notification', 'wait', { -2 } }, next_msg(500)) + end) + it('fails in fast callbacks #26122', function() local screen = Screen.new(80, 10) exec_lua([[