From 14f2a86cd6bb589c29b2f78bc39e4994e128e8c6 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 1 Jul 2026 17:00:08 +0800 Subject: [PATCH 1/6] vim-patch:9.2.0739: completion: 'autocompletedelay' blocks the main loop and drops autocommands Problem: With a non-zero 'autocompletedelay', Insert-mode autocommands (TextChangedI, TextChangedP, CursorMovedI) are delayed, and while typing faster than the delay they are dropped entirely, because the delay blocks the main loop. Solution: Make 'autocompletedelay' non-blocking: instead of busy-waiting before showing the popup menu, defer it with an input-wait timeout (K_COMPLETE_DELAY) modeled on CursorHoldI, so typing stays responsive and the Insert-mode autocommands fire normally. The delay timer coexists with 'updatetime': the main loop waits for the sooner of the two and triggers the event whose deadline was reached, so 'autocompletedelay' no longer shadows CursorHold timing. Changing the completion leader, for example with Backspace, updates the visible popup immediately like a zero delay; only the first popup is deferred. Update the 'autocompletedelay' screendumps for the non-blocking display. One test opened the menu with CTRL-N right after the delay expired and could race with the deferred popup, so it now waits a little longer than the delay before sending the key. fixes: vim/vim#20591 closes: vim/vim#20598 https://github.com/vim/vim/commit/8ce43ea4e3cb315f566bcd9048c254ba58042964 Also include some insexpand.c and ui.c changes from patch 9.2.0750. Co-authored-by: Hirohito Higashi Co-Authored-By: Claude Opus 4.8 (1M context) --- src/nvim/edit.c | 39 ++++++++++--- src/nvim/insexpand.c | 64 ++++++++++++---------- src/nvim/keycodes.h | 4 ++ src/nvim/os/input.c | 33 ++++++++++- test/functional/editor/completion_spec.lua | 27 ++++----- test/old/testdir/test_ins_complete.vim | 6 +- 6 files changed, 117 insertions(+), 56 deletions(-) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 1b9320c73a..db244f095a 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -146,14 +146,19 @@ static linenr_T o_lnum = 0; static kvec_t(char) replace_stack = KV_INITIAL_VALUE; +// With 'autocompletedelay' set, arm the delay and let the main loop fire +// Insert-mode autocommands; the popup is shown later on K_COMPLETE_DELAY. +// Otherwise trigger completion right away. #define TRIGGER_AUTOCOMPLETE() \ do { \ redraw_later(curwin, UPD_VALID); \ update_screen(); /* Show char deletion immediately */ \ ui_flush(); \ ins_compl_enable_autocomplete(); \ - insert_do_complete(s); \ - break; \ + if (!ins_compl_arm_autocomplete_delay()) { \ + insert_do_complete(s); \ + break; \ + } \ } while (0) #define MAY_TRIGGER_AUTOCOMPLETE(c) \ @@ -519,8 +524,8 @@ static int insert_check(VimState *state) s->old_topline = curwin->w_topline; s->old_topfill = curwin->w_topfill; - if (s->c != K_EVENT) { - s->lastc = s->c; // remember previous char for CTRL-D + if (s->c != K_EVENT && s->c != K_COMPLETE_DELAY) { + s->lastc = s->c; // remember the previous char for CTRL-D } // After using CTRL-G U the next cursor key will not break undo. @@ -540,9 +545,13 @@ static int insert_check(VimState *state) if (vim_isprintc(s->c)) { ins_compl_enable_autocomplete(); ins_compl_init_get_longest(); - insert_do_complete(s); - insert_handle_key_post(s); - return 1; + // Defer until the delay expires (K_COMPLETE_DELAY), or + // trigger now when no delay is in effect. + if (!ins_compl_arm_autocomplete_delay()) { + insert_do_complete(s); + insert_handle_key_post(s); + return 1; + } } } } @@ -995,6 +1004,21 @@ static int insert_handle_key(InsertState *s) break; } + case K_COMPLETE_DELAY: // 'autocompletedelay' expired + ins_compl_clear_autocomplete_delay(); + if (!ins_compl_has_autocomplete() || char_avail() || curwin->w_cursor.col == 0) { + break; + } + s->c = char_before_cursor(); + if (!vim_isprintc(s->c)) { + break; + } + // The completion may have been cleared while waiting, so re-enable + // autocomplete to match a zero delay. + ins_compl_enable_autocomplete(); + insert_do_complete(s); + break; + case K_HOME: // case K_KHOME: case K_S_HOME: @@ -1271,6 +1295,7 @@ normalchar: static void insert_do_complete(InsertState *s) { + ins_compl_clear_autocomplete_delay(); compl_busy = true; disable_fold_update++; // don't redraw folds here if (ins_complete(s->c, true) == FAIL) { diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index e9f79e1145..d79834114a 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -294,6 +294,8 @@ static buf_T *compl_curr_buf = NULL; ///< buf where completion is active // longer fixed timeout is used (COMPL_FUNC_TIMEOUT_MS or // COMPL_FUNC_TIMEOUT_NON_KW_MS). - girish static bool compl_autocomplete = false; ///< whether autocompletion is active +static bool compl_autocomplete_pending = false; +static uint64_t compl_autocomplete_start_tv; ///< when the delay was armed static uint64_t compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS; static bool compl_time_slice_expired = false; ///< time budget exceeded for current source static bool compl_from_nonkeyword = false; ///< completion started from non-keyword @@ -2371,7 +2373,9 @@ static void ins_compl_new_leader(void) compl_enter_selects = !compl_used_match && compl_selected_item != -1; - // Show the popup menu with a different set of matches. + // Show the popup menu with a different set of matches. With + // 'autocompletedelay' the menu is already visible here, so update it + // immediately rather than re-arming the delay, like a zero delay does. if (!compl_interrupted) { show_pum(save_w_wrow, save_w_leftcol); } @@ -6269,10 +6273,6 @@ static void ins_compl_show_statusmsg(void) /// Returns OK if completion was done, FAIL if something failed. int ins_complete(int c, bool enable_pum) { - const bool disable_ac_delay = compl_started && ctrl_x_mode_normal() - && (c == Ctrl_N || c == Ctrl_P || c == Ctrl_R - || ins_compl_pum_key(c)); - compl_direction = ins_compl_key2dir(c); int insert_match = ins_compl_use_match(c); @@ -6284,10 +6284,6 @@ int ins_complete(int c, bool enable_pum) return FAIL; } - uint64_t compl_start_tv = 0; ///< Time when match collection starts - if (compl_autocomplete && p_acl > 0 && !disable_ac_delay) { - compl_start_tv = os_hrtime(); - } compl_curr_win = curwin; compl_curr_buf = curwin->w_buffer; compl_shown_match = compl_curr_match; @@ -6343,26 +6339,6 @@ int ins_complete(int c, bool enable_pum) ins_compl_show_statusmsg(); } - // Wait for the autocompletion delay to expire - if (compl_autocomplete && p_acl > 0 && !disable_ac_delay && !no_matches_found - && (os_hrtime() - compl_start_tv) / 1000000 < (uint64_t)p_acl) { - setcursor(); - ui_flush(); - do { - if (char_avail()) { - if (ins_compl_preinsert_effect() && ins_compl_win_active(curwin)) { - ins_compl_delete(false); // Remove pre-inserted text - compl_ins_end_col = compl_col; - } - ins_compl_restart(); - compl_interrupted = true; - break; - } else { - os_delay(2L, true); - } - } while ((os_hrtime() - compl_start_tv) / 1000000 < (uint64_t)p_acl); - } - // Show the popup menu, unless we got interrupted. if (enable_pum && !compl_interrupted) { show_pum(save_w_wrow, save_w_leftcol); @@ -6380,6 +6356,36 @@ void ins_compl_enable_autocomplete(void) compl_get_longest = false; } +/// Arm the 'autocompletedelay' timer when the delay is in effect. +/// Return true when the popup should be deferred, false to trigger it now. +bool ins_compl_arm_autocomplete_delay(void) +{ + if (p_acl > 0) { + compl_autocomplete_start_tv = os_hrtime(); + compl_autocomplete_pending = true; + return true; + } + return false; +} + +/// Clear the pending 'autocompletedelay' state. +void ins_compl_clear_autocomplete_delay(void) +{ + compl_autocomplete_pending = false; +} + +/// Return true while waiting for 'autocompletedelay' to expire. +bool ins_compl_autocomplete_pending(void) +{ + return compl_autocomplete_pending; +} + +/// Return the time in msec since the 'autocompletedelay' was armed. +int64_t ins_compl_autocomplete_elapsed(void) +{ + return (int64_t)((os_hrtime() - compl_autocomplete_start_tv) / 1000000); +} + /// Remove (if needed) and show the popup menu static void show_pum(int prev_w_wrow, int prev_w_leftcol) { diff --git a/src/nvim/keycodes.h b/src/nvim/keycodes.h index bc3cf4e041..2fa0226c7e 100644 --- a/src/nvim/keycodes.h +++ b/src/nvim/keycodes.h @@ -224,6 +224,8 @@ enum key_extra { // KE_SID = 106, // KE_ESC = 107, KE_WILD = 108, // triggers wildmode completion + // KE_OSC = 109, + KE_COMPLETE_DELAY = 110, // 'autocompletedelay' expired }; // the three byte codes are replaced with the following int when using vgetc() @@ -451,6 +453,8 @@ enum key_extra { #define K_DROP TERMCAP2KEY(KS_EXTRA, KE_DROP) +#define K_COMPLETE_DELAY TERMCAP2KEY(KS_EXTRA, KE_COMPLETE_DELAY) + #define K_EVENT TERMCAP2KEY(KS_EXTRA, KE_EVENT) #define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND) #define K_LUA TERMCAP2KEY(KS_EXTRA, KE_LUA) diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index e042ffdb87..b52e18f91b 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -21,6 +21,7 @@ #include "nvim/getchar.h" #include "nvim/gettext_defs.h" #include "nvim/globals.h" +#include "nvim/insexpand.h" #include "nvim/keycodes.h" #include "nvim/log.h" #include "nvim/macros_defs.h" @@ -111,6 +112,7 @@ static void reset_cursorhold_wait(int tb_change_cnt) /// @return Bytes read into buf, or 0 if no input was read int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *events) { + assert((maxlen > 0 && buf != NULL) || maxlen == 0); // This check is needed so that feeding typeahead from RPC can prevent CursorHold. if (tb_change_cnt != cursorhold_tb_change_cnt) { reset_cursorhold_wait(tb_change_cnt); @@ -120,7 +122,6 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e do { \ if (maxlen && input_available()) { \ reset_cursorhold_wait(tb_change_cnt); \ - assert(maxlen >= 0); \ size_t to_read = MIN((size_t)maxlen, input_available()); \ memcpy(buf, input_read_pos, to_read); \ input_read_pos += to_read; \ @@ -145,11 +146,39 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e } else { uint64_t wait_start = os_hrtime(); cursorhold_time = MIN(cursorhold_time, (int)p_ut); - if ((result = inbuf_poll((int)p_ut - cursorhold_time, events)) == kFalse) { + // 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; + // 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; + if (delay_pending) { + int64_t delay_left = p_acl - ins_compl_autocomplete_elapsed(); + wait_time = MIN(MAX(delay_left, 0), wait_time); + } + if ((result = inbuf_poll((int)wait_time, events)) == kFalse) { if (read_stream.s.closed && silent_mode) { // Drained eventloop & initial input; exit silent/batch-mode (-es/-Es). read_error_exit(); } + // The 'autocompletedelay' expired: trigger the popup. When + // 'updatetime' is shorter, fall through to CursorHold instead. + if (delay_pending && ins_compl_autocomplete_elapsed() >= p_acl + && (buf == NULL || maxlen >= 3) && !typebuf_changed(tb_change_cnt)) { + if (buf == NULL) { + uint8_t ibuf[3]; + ibuf[0] = K_SPECIAL; + ibuf[1] = KS_EXTRA; + ibuf[2] = KE_COMPLETE_DELAY; + input_enqueue_raw((char *)ibuf, 3); + return 0; + } + buf[0] = K_SPECIAL; + buf[1] = KS_EXTRA; + buf[2] = KE_COMPLETE_DELAY; + return 3; + } reset_cursorhold_wait(tb_change_cnt); if (trigger_cursorhold() && !typebuf_changed(tb_change_cnt)) { create_cursorhold_event(events == main_loop.events); diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index d21fe79c6c..76db2ba479 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -1552,15 +1552,6 @@ describe('completion', function() {5:-- INSERT --} | ]]) feed('') - screen:expect([[ - foo | - foobar | - foobarbaz | - f^ | - {1:~ }|*5 - {5:-- INSERT --} | - ]]) - vim.uv.sleep(500) screen:expect([[ foo | foobar | @@ -1572,6 +1563,8 @@ describe('completion', function() {1:~ }|*2 {5:-- INSERT --} | ]]) + vim.uv.sleep(500) + screen:expect_unchanged() -- During delay wait, user can open menu using CTRL_N completion feed('') @@ -1589,7 +1582,9 @@ describe('completion', function() {5:-- Keyword completion (^N^P) }{6:match 1 of 3} | ]]) - -- After the menu is open, ^N/^P and Up/Down should not delay + -- After the menu is open, ^N/^P and Up/Down should not delay. + -- Wait a bit longer than 'autocompletedelay' so the popup is surely shown + -- before sending CTRL-N, otherwise the keys race with the deferred popup. feed('') command('set completeopt=menu') feed('Sf') @@ -1601,7 +1596,7 @@ describe('completion', function() {1:~ }|*5 {5:-- INSERT --} | ]]) - vim.uv.sleep(500) + vim.uv.sleep(600) screen:expect([[ foo | foobar | @@ -1734,7 +1729,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - au{102:^tocom} | + au^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1742,7 +1737,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - autoc{102:^om} | + autoc^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1762,7 +1757,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - aut{102:^ocom} | + aut^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1770,7 +1765,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - au{102:^tocom} | + au^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1793,7 +1788,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - au{102:^tocomplete} | + au^ | {1:~ }|*4 {5:-- INSERT --} | ]]) diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 93d28e91a3..af4b7ccdc6 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6046,10 +6046,12 @@ func Test_autocompletedelay() call term_sendkeys(buf, "Sf\") call VerifyScreenDump(buf, 'Test_autocompletedelay_7', {}) - " After the menu is open, ^N/^P and Up/Down should not delay + " After the menu is open, ^N/^P and Up/Down should not delay. + " Wait a bit longer than 'autocompletedelay' so the popup is surely shown + " before sending CTRL-N, otherwise the keys race with the deferred popup. call term_sendkeys(buf, "\:set completeopt=menu noruler\") call term_sendkeys(buf, "\Sf") - sleep 500ms + sleep 600ms call term_sendkeys(buf, "\") call VerifyScreenDump(buf, 'Test_autocompletedelay_8', {}) call term_sendkeys(buf, "\") From f2af45127f5bd6a811c6fd3be3f2ec258654db23 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 1 Jul 2026 20:46:40 +0800 Subject: [PATCH 2/6] vim-patch:9.2.0748: 'autocompletedelay' interferes with CTRL-G U Problem: 'autocompletedelay' interferes with CTRL-G U (after 9.2.0739). Solution: Restore the flag for CTRL-G U. related: vim/vim#8937 related: vim/vim#20666 https://github.com/vim/vim/commit/9fb5b5d8765e39dafc88f178ad9fd1879c683daf --- src/nvim/edit.c | 4 ++++ test/old/testdir/test_ins_complete.vim | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index db244f095a..e7d5ba728a 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1005,6 +1005,10 @@ static int insert_handle_key(InsertState *s) } case K_COMPLETE_DELAY: // 'autocompletedelay' expired + // If CTRL-G U was used apply it to the next typed key. + if (dont_sync_undo == kTrue) { + dont_sync_undo = kNone; + } ins_compl_clear_autocomplete_delay(); if (!ins_compl_has_autocomplete() || char_avail() || curwin->w_cursor.col == 0) { break; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index af4b7ccdc6..2acb1d0b40 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6068,6 +6068,30 @@ func Test_autocompletedelay() call StopVimInTerminal(buf) endfunc +func Run_test_autocompletedelay_ctrl_g_U(delay1, delay2) + new + call setline(1, 'foo bar baz') + inoremap ( ()U + set autocomplete autocompletedelay=200 + + call timer_start(a:delay1, { -> feedkeys('(', 't') }) + call timer_start(a:delay2, { -> feedkeys("\a\", 't') }) + call feedkeys('ob', 'tx!') + call assert_equal(['foo bar baz', 'b(a)'], getline(1, '$')) + undo + call assert_equal(['foo bar baz'], getline(1, '$')) + + set autocomplete& autocompletedelay& + bwipe! +endfunc + +func Test_autocompletedelay_ctrl_g_U() + " '(' typed after 'autocompletedelay' expires + call Run_test_autocompletedelay_ctrl_g_U(250, 500) + " '(' typed before 'autocompletedelay' expires + call Run_test_autocompletedelay_ctrl_g_U(150, 500) +endfunc + " Preinsert longest prefix when autocomplete func Test_autocomplete_longest() func GetLine() From 5f47d7ac08326877ed9b4972a85872d44e3c1b72 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 1 Jul 2026 20:48:07 +0800 Subject: [PATCH 3/6] vim-patch:9.2.0749: 'autocompletedelay' interferes with i_CTRL-K Problem: 'autocompletedelay' interferes with i_CTRL-K (after 9.2.0739). Solution: Clear the pending autocompltion from the previous key when a new key is typed. closes: vim/vim#20666 https://github.com/vim/vim/commit/0d292e20676cb399fa6bee8300d7ae3bebe97a8f --- src/nvim/edit.c | 4 +++ test/functional/editor/completion_spec.lua | 29 ++++++++++++++++++++++ test/old/testdir/test_ins_complete.vim | 22 ++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index e7d5ba728a..ed723520f3 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -582,6 +582,10 @@ static int insert_execute(VimState *state, int key) if (key != K_EVENT) { did_cursorhold = true; } + if (key != K_EVENT && key != K_COMPLETE_DELAY) { + // Don't want delayed autocompletion from the previous key either. + ins_compl_clear_autocomplete_delay(); + } // Special handling of keys while the popup menu is visible or wanted // and the cursor is still in the completed word. Only when there is diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index 76db2ba479..be6810ff9a 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -1665,6 +1665,35 @@ describe('completion', function() feed('') end) + local function run_test_autocompletedelay_ctrl_k(delay1, delay2) + source([[ + new + call setline(1, 'foo bar baz') + set autocomplete autocompletedelay=200 + ]]) + + feed('ob') + vim.uv.sleep(delay1) + feed('') + vim.uv.sleep(delay2) + feed('.,') + poke_eventloop() + expect('foo bar baz\nb…') + + source([[ + set autocomplete& autocompletedelay& + bwipe! + ]]) + end + + -- oldtest: Test_autocompletedelay_ctrl_k() + it("'autocompletedelay' doesn't interfere with i_CTRL-K", function() + -- Ctrl-K typed after 'autocompletedelay' expires + run_test_autocompletedelay_ctrl_k(250, 500) + -- Ctrl-K typed before 'autocompletedelay' expires + run_test_autocompletedelay_ctrl_k(150, 500) + end) + -- oldtest: Test_fuzzy_select_item_when_acl() it([[first item isn't selected with "fuzzy" and 'acl']], function() screen:try_resize(60, 10) diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 2acb1d0b40..036706f025 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6092,6 +6092,28 @@ func Test_autocompletedelay_ctrl_g_U() call Run_test_autocompletedelay_ctrl_g_U(150, 500) endfunc +func Run_test_autocompletedelay_ctrl_k(delay1, delay2) + new + call setline(1, 'foo bar baz') + set autocomplete autocompletedelay=200 + + call timer_start(a:delay1, { -> feedkeys("\", 't') }) + call timer_start(a:delay2, { -> feedkeys(".,\", 't') }) + call feedkeys('ob', 'tx!') + call assert_equal(['foo bar baz', 'b…'], getline(1, '$')) + + set autocomplete& autocompletedelay& + bwipe! +endfunc + +func Test_autocompletedelay_ctrl_k() + throw 'Skipped: use test/functional/editor/completion_spec.lua' + " Ctrl-K typed after 'autocompletedelay' expires + call Run_test_autocompletedelay_ctrl_k(250, 500) + " Ctrl-K typed before 'autocompletedelay' expires + call Run_test_autocompletedelay_ctrl_k(150, 500) +endfunc + " Preinsert longest prefix when autocomplete func Test_autocomplete_longest() func GetLine() From 826ca0ccfec949ee61d4f6baffd4e22e4ffc0076 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 1 Jul 2026 20:59:25 +0800 Subject: [PATCH 4/6] 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() From cef0dcd54e1f6bec12ea2fdd3a3bbe03845999e0 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 1 Jul 2026 21:02:18 +0800 Subject: [PATCH 5/6] vim-patch:9.2.0755: 'autocomplete' behaves inconsistently when recording Problem: If 'autocompletedelay' is non-zero, 'autocomplete' doesn't work when recording a register (after 9.2.0750). Solution: Still produce K_COMPLETE_DELAY when recording a register, and drop it in gotchars_add_byte() instead (zeertzjq). This patch only changes the behavior when recording a register. Replaying a register with 'autocomplete' still doesn't fully work regardless of 'autocompletedelay', as 'autocomplete' isn't triggered when there is pending input. closes: vim/vim#20675 https://github.com/vim/vim/commit/a658728918405294e134fbc81f13b50311924db5 --- src/nvim/getchar.c | 4 ++++ src/nvim/os/input.c | 4 ++-- test/old/testdir/test_ins_complete.vim | 12 +++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index c90d8b418c..025f48d001 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -1194,6 +1194,10 @@ static bool gotchars_add_byte(gotchars_state_T *state, uint8_t byte) goto ret_false; } c = TO_SPECIAL(state->prev_c, c); + // Drop K_COMPLETE_DELAY, it's not useful in a recording. + if (c == K_COMPLETE_DELAY) { + state->buflen = 0; + } } // When receiving a multibyte character, store it until we have all // the bytes, so that it won't be split between two buffer blocks, diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 84475d4443..29aa6d0cad 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -150,9 +150,9 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e // 'autocompletedelay' and 'updatetime' so the delay does not postpone // CursorHold. Once CursorHold has fired, only the delay is left. // Gate the injection like trigger_cursorhold() so the deferred key - // cannot fire while recording or outside Insert mode. + // cannot fire while outside Insert mode, but do still fire the key + // when recording a register (gotchars_add_byte() will ignore it). 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; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index d5b60c789d..72e2a11f1a 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6124,10 +6124,20 @@ func Test_autocompletedelay_no_record() 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 timer_start(300, { -> feedkeys("\q", 't') }) call feedkeys("qaSf", 'tx!') call assert_equal("Sf\", @a) + " Delayed autocompletion still works when recording. + if !has('win32') || has('nvim') + call setline(1, 'foobar foofoo') + call timer_start(300, { -> feedkeys("\\\q", 't') }) + call feedkeys("qaof", 'tx!') + call assert_equal('foobar', getline('.')) + " XXX: This doesn't produce the same result when replaying. + call assert_equal("of\\\", @a) + endif + set autocomplete& autocompletedelay& bwipe! endfunc From a243486a4638d223d25dec65751aabf1ba965319 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 2 Jul 2026 03:55:58 +0800 Subject: [PATCH 6/6] vim-patch:9.2.0759: Some code for 'autocompletedelay' is no longer needed Problem: Some code for 'autocompletedelay' is no longer needed now that 'autocompletedelay' doesn't block redraw (after 9.2.0739). Solution: Remove unnecessary code. Also remove a duplicate screendump and an outdated comment in test (zeertzjq) closes: vim/vim#20686 https://github.com/vim/vim/commit/0b86b97cc9de013483875d0fab14d9040bcbe279 --- src/nvim/insexpand.c | 17 ++--------------- test/functional/editor/completion_spec.lua | 2 -- test/old/testdir/test_ins_complete.vim | 4 +--- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index d79834114a..53c903d8d7 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -2338,13 +2338,6 @@ static void ins_compl_new_leader(void) ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); compl_used_match = false; - if (p_acl > 0) { - pum_undisplay(true); - redraw_later(curwin, UPD_VALID); - update_screen(); // Show char (deletion) immediately - ui_flush(); - } - int save_w_wrow = curwin->w_wrow; int save_w_leftcol = curwin->w_leftcol; if (compl_started) { @@ -5422,7 +5415,6 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0 || (compl_autocomplete && !ins_compl_has_preinsert()); bool compl_preinsert = ins_compl_has_preinsert(); - bool has_autocomplete_delay = (compl_autocomplete && p_acl > 0); // When user complete function return -1 for findstart which is next // time of 'always', compl_shown_match become NULL. @@ -5469,9 +5461,6 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match // Insert the text of the new completion, or the compl_leader. if (!started && ins_compl_preinsert_longest()) { ins_compl_insert(true, true); - if (has_autocomplete_delay) { - update_screen(); // Show the inserted text right away - } } else if (compl_no_insert && !started && !compl_preinsert) { ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1); compl_used_match = false; @@ -5496,10 +5485,8 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match // redraw to show the user what was inserted update_screen(); // TODO(bfredl): no! - if (!has_autocomplete_delay) { - // display the updated popup menu - ins_compl_show_pum(); - } + // display the updated popup menu + ins_compl_show_pum(); // Delete old text to be replaced, since we're still searching and // don't want to match ourselves! diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index be6810ff9a..a75cc33697 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -1811,8 +1811,6 @@ describe('completion', function() -- Preinsert command('set completeopt& completeopt+=preinsert') - - -- Show preinserted text right away but display popup later feed('Sau') screen:expect([[ autocomplete | diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 72e2a11f1a..11431bfcc2 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6039,7 +6039,7 @@ func Test_autocompletedelay() call term_sendkeys(buf, "\") call VerifyScreenDump(buf, 'Test_autocompletedelay_5', {}) sleep 500m - call VerifyScreenDump(buf, 'Test_autocompletedelay_6', {}) + call VerifyScreenDump(buf, 'Test_autocompletedelay_5', {}) " During delay wait, user can open menu using CTRL_N completion call term_sendkeys(buf, "\:set completeopt=menuone\") @@ -6442,8 +6442,6 @@ func Test_autocompletedelay_longest_preinsert() " Preinsert call term_sendkeys(buf, "\:set completeopt& completeopt+=preinsert\") - - " Show preinserted text right away but display popup later call term_sendkeys(buf, "\Sau") sleep 100m call VerifyScreenDump(buf, 'Test_autocompletedelay_preinsert_1', {})