From 8c292c96dd8f901264b23697bfdb5e7e2441ef5d Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 10 Sep 2026 05:57:03 -0400 Subject: [PATCH] fix(autocmd): dot-repeat triggers InsertCharPre #41806 Problem: InsertCharPre is triggered for stuffed text (".", i_CTRL-R, i_CTRL-A). But that text was already transformed when it was typed: the `v:char` result is appended to the redo buffer literally (`redo_append_lit()`). So "." transforms it again, e.g. an autopair handler turns "()" into "())". Solution: Skip InsertCharPre for stuffed text. Precedent: `vgetorpeek()` disables abbreviations for stuffed text for the same reason (it is post-expansion). Macros are unaffected. --- runtime/doc/autocmd.txt | 2 + runtime/doc/vim_diff.txt | 2 + src/nvim/insert.c | 10 ++--- test/functional/editor/mode_insert_spec.lua | 50 +++++++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 9d018012f0..cec6cb3c1d 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -903,6 +903,8 @@ InsertCharPre When a character is typed in Insert mode, to more than one character this text is inserted literally. + Not triggered for |.| redo, |i_CTRL-R|, |i_CTRL-A|. + Cannot change the text. |textlock| *InsertEnter* diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index f1b92016e0..62341dca42 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -349,6 +349,8 @@ Editor: Events (autocommands): - Fixed inconsistent behavior in execution of nested autocommands #23368 +- |InsertCharPre| is not triggered for stuffed text (|.| redo, |i_CTRL-R|, + |i_CTRL-A|), which also skips mappings and abbreviations. - |OptionSet| is triggered for 'modified' when writing a file, undoing changes, or using |:set| modified. - |Progress| diff --git a/src/nvim/insert.c b/src/nvim/insert.c index d5cd4879e1..e6a698d505 100644 --- a/src/nvim/insert.c +++ b/src/nvim/insert.c @@ -4384,14 +4384,14 @@ static char *do_insert_char_pre(int c) char buf[MB_MAXBYTES + 1]; const int save_State = State; - if (c == Ctrl_RSB) { + if (c == Ctrl_RSB // i_CTRL-] only triggers abbreviations. + // Stuffed text was transformed when typed and appended to redobuf (redo_append_lit). + // Like abbreviations (vgetorpeek()), don't transform it again. + || KeyStuffed + || !has_event(EVENT_INSERTCHARPRE)) { return NULL; } - // Return quickly when there is nothing to do. - if (!has_event(EVENT_INSERTCHARPRE)) { - return NULL; - } size_t buflen = (size_t)utf_char2bytes(c, buf); buf[buflen] = NUL; diff --git a/test/functional/editor/mode_insert_spec.lua b/test/functional/editor/mode_insert_spec.lua index c0897e17b2..6f9a585298 100644 --- a/test/functional/editor/mode_insert_spec.lua +++ b/test/functional/editor/mode_insert_spec.lua @@ -49,6 +49,56 @@ describe('insert-mode', function() expect('hellhellhellhelloxo') end) + it('InsertCharPre is not triggered for stuffed text (redo/dot-repeat) #25296', function() + n.exec_lua([[ + _G.n = 0 + vim.api.nvim_create_autocmd('InsertCharPre', { + callback = function() + _G.n = _G.n + 1 + if vim.v.char == '(' then + vim.v.char = '()' + end + end, + }) + ]]) + local function calls() + return n.exec_lua('return _G.n') + end + api.nvim_buf_set_lines(0, 0, -1, true, { 'a', 'b', 'c', 'd' }) + feed('gg0i(') + eq(1, calls()) + + -- Dot-repeat ("redo") inserts literally (no InsertCharPre). + feed('j.') + expect([[ + ()a + ()b + c + d]]) + eq(1, calls()) + + -- i_CTRL-R, i_CTRL-A inserts as stuffed text (no InsertCharPre). + n.fn.setreg('a', '(') + feed('jia') + feed('ji') + expect([[ + ()a + ()b + (c + (d]]) + eq(1, calls()) + + -- Macro keys are typeahead, not stuffed, thus trigger InsertCharPre. + n.fn.setreg('q', 'A(\27') + feed('gg@q') + expect([[ + ()a() + ()b + (c + (d]]) + eq(2, calls()) + end) + describe('Ctrl-R', function() it('works', function() command("let @@ = 'test'")