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.
This commit is contained in:
Justin M. Keyes
2026-09-10 05:57:03 -04:00
committed by GitHub
parent 0c9012f295
commit 8c292c96dd
4 changed files with 59 additions and 5 deletions

View File

@@ -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*

View File

@@ -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|

View File

@@ -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;

View File

@@ -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(<Esc>')
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('ji<C-R>a<Esc>')
feed('ji<C-A><Esc>')
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'")