Commit: patch 9.2.0750: completion: 'autocompletedelay' deferral leaks state

1 view
Skip to first unread message

Christian Brabandt

unread,
4:15 PM (5 hours ago) 4:15 PM
to vim...@googlegroups.com
patch 9.2.0750: completion: 'autocompletedelay' deferral leaks state

Commit: https://github.com/vim/vim/commit/1f0f14bc2fbe278690c2b1510c9d0f3b5061020c
Author: Hirohito Higashi <h.eas...@gmail.com>
Date: Sun Jun 28 19:48:02 2026 +0000

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) <nor...@anthropic.com>
Signed-off-by: Hirohito Higashi <h.eas...@gmail.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/src/edit.c b/src/edit.c
index bc8ce4128..3b0cfcc88 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 309c0d7c1..a192d1462 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 75e55a60e..331a50011 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 b574652aa..ebc8e25ac 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 cdcdb217e..4165002cd 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("\<Esc>q", 't') })
+ call feedkeys("qaSf", 'tx!')
+ call assert_equal("Sf\<Esc>", @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 3ba656d3a..ee6837d8d 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 a42f2bd83..218fdc163 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,
/**/
Reply all
Reply to author
Forward
0 new messages