From: Hirohito Higashi Date: Sun, 28 Jun 2026 19:48:02 +0000 (+0000) Subject: patch 9.2.0750: completion: 'autocompletedelay' deferral leaks state X-Git-Tag: v9.2.0750^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=1f0f14bc2fbe278690c2b1510c9d0f3b5061020c;p=thirdparty%2Fvim.git 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: #20669 Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- diff --git a/src/edit.c b/src/edit.c index bc8ce4128d..3b0cfcc882 100644 --- a/src/edit.c +++ b/src/edit.c @@ -901,6 +901,8 @@ do_intr: break; } doESCkey: + // Drop a pending autocomplete so it does not outlive Insert mode. + ins_compl_clear_autocomplete_delay(); /* * This is the ONLY return from edit()! */ @@ -1539,8 +1541,9 @@ normalchar: break; } // end of switch (c) - // If typed something may trigger CursorHoldI again. - if (c != K_CURSORHOLD + // If typed something may trigger CursorHoldI again; K_COMPLETE_DELAY is + // injected, not typed. + if (c != K_CURSORHOLD && c != K_COMPLETE_DELAY #ifdef FEAT_COMPL_FUNC // but not in CTRL-X mode, a script can't restore the state && ctrl_x_mode_normal() diff --git a/src/getchar.c b/src/getchar.c index 309c0d7c15..a192d14628 100644 --- a/src/getchar.c +++ b/src/getchar.c @@ -2175,7 +2175,8 @@ vgetc(void) #endif #ifdef FEAT_BEVAL_TERM - if (c != K_MOUSEMOVE && c != K_IGNORE && c != K_CURSORHOLD) + if (c != K_MOUSEMOVE && c != K_IGNORE && c != K_CURSORHOLD + && c != K_COMPLETE_DELAY) { // Don't trigger 'balloonexpr' unless only the mouse was moved. bevalexpr_due_set = FALSE; diff --git a/src/insexpand.c b/src/insexpand.c index 75e55a60eb..331a50011e 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -206,6 +206,9 @@ static buf_T *compl_curr_buf = NULL; // buf where completion is active // COMPL_FUNC_TIMEOUT_NON_KW_MS). - girish static int compl_autocomplete = FALSE; // whether autocompletion is active static bool compl_autocomplete_pending = false; +#ifdef ELAPSED_FUNC +static elapsed_T compl_autocomplete_start_tv; // when the delay was armed +#endif static int compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS; static int compl_time_slice_expired = FALSE; // time budget exceeded for current source static int compl_from_nonkeyword = FALSE; // completion started from non-keyword @@ -7398,6 +7401,7 @@ ins_compl_arm_autocomplete_delay(void) #ifdef ELAPSED_FUNC if (p_acl > 0) { + ELAPSED_INIT(compl_autocomplete_start_tv); compl_autocomplete_pending = true; return true; } @@ -7423,6 +7427,19 @@ ins_compl_autocomplete_pending(void) return compl_autocomplete_pending; } +/* + * Return the time in msec since the 'autocompletedelay' was armed. + */ + long +ins_compl_autocomplete_elapsed(void) +{ +#ifdef ELAPSED_FUNC + return ELAPSED_FUNC(compl_autocomplete_start_tv); +#else + return 0; +#endif +} + /* * Remove (if needed) and show the popup menu */ diff --git a/src/proto/insexpand.pro b/src/proto/insexpand.pro index b574652aaf..ebc8e25ac4 100644 --- a/src/proto/insexpand.pro +++ b/src/proto/insexpand.pro @@ -79,6 +79,7 @@ void ins_compl_enable_autocomplete(void); bool ins_compl_arm_autocomplete_delay(void); void ins_compl_clear_autocomplete_delay(void); bool ins_compl_autocomplete_pending(void); +long ins_compl_autocomplete_elapsed(void); void free_insexpand_stuff(void); void f_preinserted(typval_T *argvars, typval_T *rettv); /* vim: set ft=c : */ diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index cdcdb217e3..4165002cd8 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -6021,6 +6021,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() diff --git a/src/ui.c b/src/ui.c index 3ba656d3a5..ee6837d8d6 100644 --- a/src/ui.c +++ b/src/ui.c @@ -304,24 +304,40 @@ inchar_loop( // 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. + long acl_elapsed = delay_pending ? ins_compl_autocomplete_elapsed() : 0; if (wtime < 0 && did_start_blocking && !delay_pending) // blocking and already waited for p_ut wait_time = -1; else { +# ifdef ELAPSED_FUNC + elapsed_time = ELAPSED_FUNC(start_tv); +# endif if (wtime >= 0) - wait_time = wtime; + wait_time = wtime - elapsed_time; else if (delay_pending) - wait_time = did_start_blocking ? p_acl : MIN(p_acl, p_ut); + { + long delay_left = p_acl - acl_elapsed; + long ut_left = p_ut - elapsed_time; + + if (did_start_blocking) + wait_time = delay_left; + else + wait_time = MIN(delay_left, ut_left); + } else // going to block after p_ut - wait_time = p_ut; -# ifdef ELAPSED_FUNC - elapsed_time = ELAPSED_FUNC(start_tv); -# endif - wait_time -= elapsed_time; + wait_time = p_ut - elapsed_time; // If the waiting time is now zero or less, we timed out. However, // loop at least once to check for characters and events. Matters @@ -334,7 +350,7 @@ inchar_loop( // The 'autocompletedelay' expired: trigger the popup. When // 'updatetime' is shorter, fall through to CursorHold instead. - if (delay_pending && elapsed_time >= p_acl && maxlen >= 3 + if (delay_pending && acl_elapsed >= p_acl && maxlen >= 3 && !typebuf_changed(tb_change_cnt)) { if (buf == NULL) diff --git a/src/version.c b/src/version.c index a42f2bd839..218fdc163a 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 750, /**/ 749, /**/