From 8c440c469bb6f075fe26a1199ae6dcd2314394c9 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 24 Aug 2026 10:18:47 -0400 Subject: [PATCH] fix(ui2): ui2 misinterprets getchar() input as pager_char (CR) #41465 Problem: `vim.on_key()` callbacks are invoked for keys that `getchar()` consumes, which never reach the main loop, and there is no way to tell such a key apart. With ui2 the `on_key` handler activated after interactive cmdline, handles it as `pager_char` (thus enters the ui2 pager). Solution: Skip `vim.on_key()` callbacks during `getchar()`/`getcharstr()`. --- runtime/doc/lua.txt | 17 +++++++++-------- runtime/doc/news.txt | 1 + runtime/lua/vim/_core/editor.lua | 11 +++++------ runtime/lua/vim/_core/ui2.lua | 4 ++-- src/nvim/input.c | 6 +++++- test/functional/lua/vim_spec.lua | 17 ++++++++++++++++- 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 0796f1306b..58bb1ad6b6 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -1347,24 +1347,26 @@ vim.notify_once({msg}, {level}, {opts}) *vim.notify_once()* (`boolean`) true if message was displayed, else false vim.on_key({fn}, {ns_id}, {opts}) *vim.on_key()* - Registers function {fn} with |namespace| {ns_id} as a listener to every, - yes EVERY, input key. + Registers function {fn} with |namespace| {ns_id} as a listener to EVERY + input key (except keys consumed by |getchar()|, by {fn} itself, or by a + payload). To parse |key-chord|s, see |vim.keycode()|. Example: >lua local keychords = vim.keycode(vim.fn.keytrans(key), true) < - The |-w| command-line option is related but does not support callbacks and - cannot be toggled dynamically. - Note: ~ • If {fn} returns an empty string, {key} is discarded/ignored; if {key} is then the "" sequence is discarded as a whole. • Non-recursive: if {fn} itself consumes input, it won't be invoked for those keys. + • Not invoked for keys consumed by getchar()/getcharstr(): they never + reach the main loop. • To UNregister a given {ns_id}, pass `nil` {fn}. • {fn} will be removed on error. • {fn} will not be cleared by |nvim_buf_clear_namespace()| + • The |-w| command-line option is related but does not support callbacks + and cannot be toggled dynamically. Parameters: ~ • {fn} (`fun(key: string, typed: string): string??`) Function @@ -6123,8 +6125,8 @@ To enable this feature (default opts shown): >lua pager = { -- Options related to message window. height = 0.999, -- Maximum height. }, - pager_char = nil, -- Key checked after interactive messages. }, + pager_char = nil, -- Enters the pager after confirming an interactive ":" cmd. }) < @@ -6143,9 +6145,8 @@ and their respective buffers. Unlike the legacy |hit-enter| prompt, messages exceeding 'cmdheight' are instead "collapsed", followed by a `[+x]` "spill" indicator, where `x` indicates the spilled lines. To see the full messages, do either: -• ENTER immediately after interactive |:| cmdline shows a message and returns - to |Normal-mode|. • |g<| at any time. +• Type `pager_char` immediately after an interactive |:| command. If you'd like behavior similar to the old hit-enter prompt, pass `pager_char = ""` on the `cfg` table. When the pager is shown, hitting diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 30464fd3f7..ec65e7ba71 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -141,6 +141,7 @@ EDITOR EVENTS +• |vim.on_key()| callbacks are not invoked for keys consumed by |getchar()|. • |BufModifiedSet| has been removed. Use the |OptionSet| event with pattern "modified" instead. • |ui-messages| `msg_show.bufwrite` and `msg_show.completion` messages are now diff --git a/runtime/lua/vim/_core/editor.lua b/runtime/lua/vim/_core/editor.lua index 348679b794..9f39e563a2 100644 --- a/runtime/lua/vim/_core/editor.lua +++ b/runtime/lua/vim/_core/editor.lua @@ -711,22 +711,21 @@ end local on_key_cbs = {} --- @type table ---- Registers function {fn} with [namespace] {ns_id} as a listener to every, yes EVERY, input key. +--- Registers function {fn} with [namespace] {ns_id} as a listener to EVERY input key (except keys +--- consumed by [getchar()], by {fn} itself, or by a [] payload). --- --- To parse [key-chord]s, see |vim.keycode()|. Example: --- ```lua --- local keychords = vim.keycode(vim.fn.keytrans(key), true) --- ``` --- ---- The |-w| command-line option is related but does not support callbacks and cannot be toggled ---- dynamically. ---- ----@note If {fn} returns an empty string, {key} is discarded/ignored; if {key} is [] then the ---- "[]…[]" sequence is discarded as a whole. +---@note If {fn} returns an empty string, {key} is discarded/ignored; if {key} is [] then the "[]…[]" sequence is discarded as a whole. ---@note Non-recursive: if {fn} itself consumes input, it won't be invoked for those keys. +---@note Not invoked for keys consumed by getchar()/getcharstr(): they never reach the main loop. ---@note To UNregister a given {ns_id}, pass `nil` {fn}. ---@note {fn} will be removed on error. ---@note {fn} will not be cleared by |nvim_buf_clear_namespace()| +---@note The |-w| command-line option is related but does not support callbacks and cannot be toggled dynamically. --- ---@param fn nil|fun(key: string, typed: string): string? Function invoked for every input key, --- after mappings have been applied but before further processing. Arguments {key} and diff --git a/runtime/lua/vim/_core/ui2.lua b/runtime/lua/vim/_core/ui2.lua index b1281ee85d..d70714f007 100644 --- a/runtime/lua/vim/_core/ui2.lua +++ b/runtime/lua/vim/_core/ui2.lua @@ -28,8 +28,8 @@ --- pager = { -- Options related to message window. --- height = 0.999, -- Maximum height. --- }, ---- pager_char = nil, -- Key checked after interactive messages. --- }, +--- pager_char = nil, -- Enters the pager after confirming an interactive ":" cmd. --- }) --- ``` --- @@ -46,8 +46,8 @@ --- Unlike the legacy |hit-enter| prompt, messages exceeding 'cmdheight' are --- instead "collapsed", followed by a `[+x]` "spill" indicator, where `x` --- indicates the spilled lines. To see the full messages, do either: ---- - ENTER immediately after interactive |:| cmdline shows a message and returns to |Normal-mode|. --- - |g<| at any time. +--- - Type `pager_char` immediately after an interactive |:| command. --- --- If you'd like behavior similar to the old hit-enter prompt, pass `pager_char = ""` on --- the `cfg` table. When the pager is shown, hitting `` (in this example) will enter the diff --git a/src/nvim/input.c b/src/nvim/input.c index ece5da2b65..73c15e9522 100644 --- a/src/nvim/input.c +++ b/src/nvim/input.c @@ -132,6 +132,8 @@ static kvec_withinit_t(char, MAXMAPLEN + 1) on_key_buf = KVI_INITIAL_VALUE(on_ke /// Number of following bytes that should not be stored for vim.on_key(). static size_t on_key_ignore_len = 0; +/// Nesting depth of getchar()/getcharstr(). Will skip vim.on_key() callbacks. #40010 +static int getchar_depth = 0; static int typeahead_char = 0; ///< typeahead char that's not flushed @@ -1811,7 +1813,7 @@ int vgetc(void) // Execute Lua on_key callbacks. kvi_push(on_key_buf, NUL); - if (nlua_exec_on_key(c, on_key_buf.items)) { + if (getchar_depth == 0 && nlua_exec_on_key(c, on_key_buf.items)) { // Keys following K_COMMAND/K_LUA/K_PASTE_START aren't normally received by // vim.on_key() callbacks, so discard them along with the current key. if (c == K_COMMAND) { @@ -1948,6 +1950,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number no_mapping++; allow_keys++; + getchar_depth++; if (!simplify) { no_reduce_keys++; } @@ -1994,6 +1997,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv, bool allow_number atom_payload_end(); no_mapping--; allow_keys--; + getchar_depth--; if (!simplify) { no_reduce_keys--; } diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index f91df88238..485ded7e47 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -2102,7 +2102,7 @@ describe('lua stdlib', function() eq('/', exec_lua([[return _G.ctrl_c_cmdtype]])) end) - it('callback is not invoked recursively #30752', function() + it('not invoked recursively #30752', function() local screen = Screen.new(60, 10) exec_lua([[ vim.on_key(function(key, typed) @@ -2140,6 +2140,21 @@ describe('lua stdlib', function() ]]) end) + it('not invoked for keys consumed by getchar() #40010', function() + exec_lua(function() + _G.typed = {} + vim.on_key(function(_, typed) + table.insert(_G.typed, vim.fn.keytrans(typed)) + end) + vim.keymap.set('n', '', function() + _G.got = vim.fn.getchar() + end) + end) + feed('') + eq(13, exec_lua('return _G.got')) + eq({ '' }, exec_lua('return _G.typed')) + end) + it('can discard input', function() -- discard the first key produced by every other 'x' key typed exec_lua [[