From 826ca0ccfec949ee61d4f6baffd4e22e4ffc0076 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 1 Jul 2026 20:59:25 +0800 Subject: [PATCH] vim-patch:9.2.0750: completion: 'autocompletedelay' deferral leaks state Problem: After 'autocompletedelay' was made non-blocking, the deferred popup can misbehave: a pending autocomplete survives leaving Insert mode and then keeps waking the editor in Normal mode, the deferral is recorded into registers while recording a macro, the popup appears an extra 'updatetime' late when 'autocompletedelay' is larger and a CursorHoldI autocommand exists, CursorHoldI can fire twice without an intervening keypress, and an open balloon is dismissed (after v9.2.0739) Solution: Treat the deferral like CursorHold: only keep it pending in Insert mode and not while recording, with pending typeahead, or when completion is already active; drop it when Insert mode ends; measure the delay from when the user typed so a CursorHold in between does not push the popup back; and do not let the deferral re-enable CursorHoldI or dismiss the balloon. (Hirohito Higashi). closes: vim/vim#20669 https://github.com/vim/vim/commit/1f0f14bc2fbe278690c2b1510c9d0f3b5061020c Co-authored-by: Hirohito Higashi Co-Authored-By: Claude Opus 4.8 (1M context) --- src/nvim/edit.c | 7 +++++-- src/nvim/os/input.c | 8 +++++++- test/old/testdir/test_ins_complete.vim | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index ed723520f3..5e159c4cfe 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -372,6 +372,8 @@ static void insert_enter(InsertState *s) // and return false, causing `state_enter` to be called again. } while (!ins_esc(&s->count, s->cmdchar, s->nomove)); + // Drop a pending autocomplete so it does not outlive Insert mode. + ins_compl_clear_autocomplete_delay(); // Always update o_lnum, so that a "CTRL-O ." that adds a line // still puts the cursor back after the inserted text. if (ins_at_eol) { @@ -1327,8 +1329,9 @@ static void insert_do_cindent(InsertState *s) static void insert_handle_key_post(InsertState *s) { - // If typed something may trigger CursorHoldI again. - if (s->c != K_EVENT + // If typed something may trigger CursorHoldI again; K_COMPLETE_DELAY is + // injected, not typed. + if (s->c != K_EVENT && s->c != K_COMPLETE_DELAY // but not in CTRL-X mode, a script can't restore the state && ctrl_x_mode_normal()) { did_cursorhold = false; diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index b52e18f91b..84475d4443 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -149,7 +149,13 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e // When an autocomplete is pending, wake at the sooner of // 'autocompletedelay' and 'updatetime' so the delay does not postpone // CursorHold. Once CursorHold has fired, only the delay is left. - bool delay_pending = ins_compl_autocomplete_pending() && p_acl > 0; + // Gate the injection like trigger_cursorhold() so the deferred key + // cannot fire while recording or outside Insert mode. + bool delay_pending = ins_compl_autocomplete_pending() && p_acl > 0 + && reg_recording == 0 + && typebuf.tb_len == 0 + && !ins_compl_active() + && (get_real_state() & MODE_INSERT) != 0; // Measure the delay from when it was armed (the keystroke), so a // CursorHold returning in between does not push the popup back. int64_t wait_time = p_ut - cursorhold_time; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 036706f025..d5b60c789d 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6114,6 +6114,24 @@ func Test_autocompletedelay_ctrl_k() call Run_test_autocompletedelay_ctrl_k(150, 500) endfunc +func Test_autocompletedelay_no_record() + " The K_COMPLETE_DELAY pseudo key must not be recorded into a register while + " recording a macro, like K_CURSORHOLD. + new + call setline(1, 'foobar') + set autocomplete autocompletedelay=100 + + let @a = '' + " Type a char that arms the delay, idle past 'autocompletedelay' so a + " K_COMPLETE_DELAY would be injected, then end Insert mode and stop recording. + call timer_start(200, { -> feedkeys("\q", 't') }) + call feedkeys("qaSf", 'tx!') + call assert_equal("Sf\", @a) + + set autocomplete& autocompletedelay& + bwipe! +endfunc + " Preinsert longest prefix when autocomplete func Test_autocomplete_longest() func GetLine()