Commit: patch 9.2.0739: completion: 'autocompletedelay' blocks the main loop and drops autocommands

0 views
Skip to first unread message

Christian Brabandt

unread,
12:30 PM (2 hours ago) 12:30 PM
to vim...@googlegroups.com
patch 9.2.0739: completion: 'autocompletedelay' blocks the main loop and drops autocommands

Commit: https://github.com/vim/vim/commit/8ce43ea4e3cb315f566bcd9048c254ba58042964
Author: Hirohito Higashi <h.eas...@gmail.com>
Date: Sun Jun 28 16:24:13 2026 +0000

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: #20591
closes: #20598

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 c10654e36..1860ba371 100644
--- a/src/edit.c
+++ b/src/edit.c
@@ -100,12 +100,16 @@ static int ins_need_undo; // call u_save() before inserting a
static int dont_sync_undo = FALSE; // CTRL-G U prevents syncing undo for
// the next left/right cursor key

+// 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 { \
update_screen(UPD_VALID); /* Show char (deletion) immediately */ \
out_flush(); \
ins_compl_enable_autocomplete(); \
- goto docomplete; \
+ if (!ins_compl_arm_autocomplete_delay())\
+ goto docomplete; \
} while (0)

#define MAY_TRIGGER_AUTOCOMPLETE(c) \
@@ -601,7 +605,7 @@ edit(
/*
* Get a character for Insert mode. Ignore K_IGNORE and K_NOP.
*/
- if (c != K_CURSORHOLD)
+ if (c != K_CURSORHOLD && c != K_COMPLETE_DELAY)
lastc = c; // remember the previous char for CTRL-D

// After using CTRL-G U the next cursor key will not break undo.
@@ -632,7 +636,10 @@ edit(
if (p_hkmap)
c = hkmap(c); // Hebrew mode mapping
#endif
- goto docomplete;
+ // Defer until the delay expires (K_COMPLETE_DELAY), or
+ // trigger now when no delay is in effect.
+ if (!ins_compl_arm_autocomplete_delay())
+ goto docomplete;
}
}
}
@@ -1163,6 +1170,19 @@ doESCkey:
dont_sync_undo = MAYBE;
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;
+ c = char_before_cursor();
+ if (!vim_isprintc(c))
+ break;
+ // The completion may have been cleared while waiting, so re-enable
+ // autocomplete to match a zero delay.
+ ins_compl_enable_autocomplete();
+ goto docomplete;
+
#ifdef FEAT_GUI_MSWIN
// On MS-Windows ignore <M-F4>, we get it when closing the window
// was cancelled.
@@ -1408,6 +1428,7 @@ doESCkey:
goto normalchar;

docomplete:
+ ins_compl_clear_autocomplete_delay();
compl_busy = TRUE;
#ifdef FEAT_FOLDING
disable_fold_update++; // don't redraw folds here
diff --git a/src/getchar.c b/src/getchar.c
index 25422be0d..309c0d7c1 100644
--- a/src/getchar.c
+++ b/src/getchar.c
@@ -4195,8 +4195,11 @@ fix_input_buffer(char_u *buf, int len)
else
#endif
if (p[0] == NUL || (p[0] == K_SPECIAL
- // timeout may generate K_CURSORHOLD
- && (i < 2 || p[1] != KS_EXTRA || p[2] != (int)KE_CURSORHOLD)
+ // timeout may generate K_CURSORHOLD,
+ // 'autocompletedelay' timeout K_COMPLETE_DELAY
+ && (i < 2 || p[1] != KS_EXTRA
+ || (p[2] != (int)KE_CURSORHOLD
+ && p[2] != (int)KE_COMPLETE_DELAY))
#if defined(MSWIN) && (!defined(FEAT_GUI) || defined(VIMDLL))
// Win32 console passes modifiers
&& (
diff --git a/src/insexpand.c b/src/insexpand.c
index f5ad94008..0361d9a6e 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -205,6 +205,7 @@ 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 int compl_autocomplete = FALSE; // whether autocompletion is active
+static bool compl_autocomplete_pending = false;
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
@@ -2609,7 +2610,9 @@ 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);

@@ -7295,13 +7298,6 @@ ins_complete(int c, int enable_pum)
int save_w_leftcol;
int insert_match;
int no_matches_found;
-#ifdef ELAPSED_FUNC
- elapsed_T compl_start_tv = {0}; // Time when match collection starts
- int disable_ac_delay;
-
- disable_ac_delay = compl_started && ctrl_x_mode_normal()
- && (c == Ctrl_N || c == Ctrl_P || c == Ctrl_R || ins_compl_pum_key(c));
-#endif

compl_direction = ins_compl_key2dir(c);
insert_match = ins_compl_use_match(c);
@@ -7314,10 +7310,6 @@ ins_complete(int c, int enable_pum)
else if (insert_match && stop_arrow() == FAIL)
return FAIL;

-#ifdef ELAPSED_FUNC
- if (compl_autocomplete && p_acl > 0 && !disable_ac_delay)
- ELAPSED_INIT(compl_start_tv);
-#endif
compl_curr_win = curwin;
compl_curr_buf = curwin->w_buffer;
compl_shown_match = compl_curr_match;
@@ -7373,34 +7365,6 @@ ins_complete(int c, int enable_pum)
if (!shortmess(SHM_COMPLETIONMENU) && !compl_autocomplete)
ins_compl_show_statusmsg();

- // Wait for the autocompletion delay to expire
-#ifdef ELAPSED_FUNC
- if (compl_autocomplete && p_acl > 0 && !disable_ac_delay
- && !no_matches_found && ELAPSED_FUNC(compl_start_tv) < p_acl)
- {
- cursor_on();
- setcursor();
- out_flush_cursor(FALSE, FALSE);
- do
- {
- if (char_avail())
- {
- if (ins_compl_preinsert_effect()
- && ins_compl_win_active(curwin))
- {
- ins_compl_delete(); // Remove pre-inserted text
- compl_ins_end_col = compl_col;
- }
- ins_compl_restart();
- compl_interrupted = TRUE;
- break;
- }
- else
- ui_delay(2L, TRUE);
- } while (ELAPSED_FUNC(compl_start_tv) < p_acl);
- }
-#endif
-
// Show the popup menu, unless we got interrupted.
if (enable_pum && !compl_interrupted)
show_pum(save_w_wrow, save_w_leftcol);
@@ -7423,6 +7387,41 @@ ins_compl_enable_autocomplete(void)
#endif
}

+/*
+ * 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)
+{
+#ifdef ELAPSED_FUNC
+ if (p_acl > 0)
+ {
+ compl_autocomplete_pending = true;
+ return true;
+ }
+#endif
+ 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;
+}
+
/*
* Remove (if needed) and show the popup menu
*/
diff --git a/src/keymap.h b/src/keymap.h
index b3be60c48..a8c9e275c 100644
--- a/src/keymap.h
+++ b/src/keymap.h
@@ -281,6 +281,7 @@ enum key_extra
, KE_ESC = 107 // used for K_ESC
, KE_WILD = 108 // triggers wildmode completion
, KE_OSC = 109 // finished OSC sequence
+ , KE_COMPLETE_DELAY = 110 // 'autocompletedelay' expired
};

/*
@@ -489,6 +490,7 @@ enum key_extra
#define K_FOCUSLOST TERMCAP2KEY(KS_EXTRA, KE_FOCUSLOST)

#define K_CURSORHOLD TERMCAP2KEY(KS_EXTRA, KE_CURSORHOLD)
+#define K_COMPLETE_DELAY TERMCAP2KEY(KS_EXTRA, KE_COMPLETE_DELAY)

#define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND)
#define K_SCRIPT_COMMAND TERMCAP2KEY(KS_EXTRA, KE_SCRIPT_COMMAND)
diff --git a/src/proto/insexpand.pro b/src/proto/insexpand.pro
index 2c1882dc1..b574652aa 100644
--- a/src/proto/insexpand.pro
+++ b/src/proto/insexpand.pro
@@ -76,6 +76,9 @@ void ins_compl_insert(int move_cursor, int insert_prefix);
void ins_compl_check_keys(int frequency, int in_compl_func);
int ins_complete(int c, int enable_pum);
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);
void free_insexpand_stuff(void);
void f_preinserted(typval_T *argvars, typval_T *rettv);
/* vim: set ft=c : */
diff --git a/src/testdir/dumps/Test_autocompletedelay_2.dump b/src/testdir/dumps/Test_autocompletedelay_2.dump
index 65dc8f42e..0dd4850d0 100644
--- a/src/testdir/dumps/Test_autocompletedelay_2.dump
+++ b/src/testdir/dumps/Test_autocompletedelay_2.dump
@@ -7,4 +7,4 @@
|~| @73
|~| @73
|~| @73
-|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|1| @10|A|l@1|
+|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|2| @10|A|l@1|
diff --git a/src/testdir/dumps/Test_autocompletedelay_3.dump b/src/testdir/dumps/Test_autocompletedelay_3.dump
index d3ed65d90..ffc63d387 100644
--- a/src/testdir/dumps/Test_autocompletedelay_3.dump
+++ b/src/testdir/dumps/Test_autocompletedelay_3.dump
@@ -7,4 +7,4 @@
|~| @73
|~| @73
|~| @73
-|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|1| @10|A|l@1|
+|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|3| @10|A|l@1|
diff --git a/src/testdir/dumps/Test_autocompletedelay_5.dump b/src/testdir/dumps/Test_autocompletedelay_5.dump
index 0dd4850d0..c792e09ea 100644
--- a/src/testdir/dumps/Test_autocompletedelay_5.dump
+++ b/src/testdir/dumps/Test_autocompletedelay_5.dump
@@ -2,9 +2,9 @@
|f|o@1|b|a|r| @68
|f|o@1|b|a|r|b|a|z| @65
|f> @73
-|~+0#4040ff13&| @73
-|~| @73
-|~| @73
+|f+0#0000001#ffd7ff255|o@1|b|a|r|b|a|z| @5| +0#4040ff13#ffffff0@59
+|f+0#0000001#ffd7ff255|o@1|b|a|r| @8| +0#4040ff13#ffffff0@59
+|f+0#0000001#ffd7ff255|o@1| @11| +0#4040ff13#ffffff0@59
|~| @73
|~| @73
|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|4|,|2| @10|A|l@1|
diff --git a/src/testdir/dumps/Test_autocompletedelay_longest_1.dump b/src/testdir/dumps/Test_autocompletedelay_longest_1.dump
index 10a50793c..6ac052238 100644
--- a/src/testdir/dumps/Test_autocompletedelay_longest_1.dump
+++ b/src/testdir/dumps/Test_autocompletedelay_longest_1.dump
@@ -1,10 +1,10 @@
|a+0&#ffffff0|u|t|o|c|o|m|p|l|e|t|e| @62
|a|u|t|o|c|o|m|x@2| @64
-|a|u|t|o|c>o+0#00e0003&|m| +0#0000000&@67
+|a|u|t|o|c> @69
|~+0#4040ff13&| @73
|~| @73
|~| @73
|~| @73
|~| @73
|~| @73
-|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|1| @10|T|o|p|
+|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|6| @10|A|l@1|
diff --git a/src/testdir/dumps/Test_autocompletedelay_longest_3.dump b/src/testdir/dumps/Test_autocompletedelay_longest_3.dump
index 52bda2c30..8149ef590 100644
--- a/src/testdir/dumps/Test_autocompletedelay_longest_3.dump
+++ b/src/testdir/dumps/Test_autocompletedelay_longest_3.dump
@@ -1,10 +1,10 @@
|a+0&#ffffff0|u|t|o|c|o|m|p|l|e|t|e| @62
|a|u|t|o|c|o|m|x@2| @64
-|a|u>t+0#00e0003&|o|c|o|m| +0#0000000&@67
+|a|u> @72
|~+0#4040ff13&| @73
|~| @73
|~| @73
|~| @73
|~| @73
|~| @73
-|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|1| @10|A|l@1|
+|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|3| @10|A|l@1|
diff --git a/src/testdir/dumps/Test_autocompletedelay_preinsert_1.dump b/src/testdir/dumps/Test_autocompletedelay_preinsert_1.dump
index 3b1bc4bbd..8149ef590 100644
--- a/src/testdir/dumps/Test_autocompletedelay_preinsert_1.dump
+++ b/src/testdir/dumps/Test_autocompletedelay_preinsert_1.dump
@@ -1,10 +1,10 @@
|a+0&#ffffff0|u|t|o|c|o|m|p|l|e|t|e| @62
|a|u|t|o|c|o|m|x@2| @64
-|a|u>t+0#00e0003&|o|c|o|m|p|l|e|t|e| +0#0000000&@62
+|a|u> @72
|~+0#4040ff13&| @73
|~| @73
|~| @73
|~| @73
|~| @73
|~| @73
-|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|1| @10|A|l@1|
+|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@44|3|,|3| @10|A|l@1|
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index 4df2efb06..d4298aeb9 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -5954,10 +5954,12 @@ func Test_autocompletedelay()
call term_sendkeys(buf, "Sf\<C-N>")
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, "\<Esc>:set completeopt=menu noruler\<CR>")
call term_sendkeys(buf, "\<Esc>Sf")
- sleep 500ms
+ sleep 600ms
call term_sendkeys(buf, "\<C-N>")
call VerifyScreenDump(buf, 'Test_autocompletedelay_8', {})
call term_sendkeys(buf, "\<Down>")
diff --git a/src/ui.c b/src/ui.c
index f277ba407..3ba656d3a 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -301,13 +301,20 @@ inchar_loop(
return 0;
}
# endif
- if (wtime < 0 && did_start_blocking)
+ // 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;
+
+ if (wtime < 0 && did_start_blocking && !delay_pending)
// blocking and already waited for p_ut
wait_time = -1;
else
{
if (wtime >= 0)
wait_time = wtime;
+ else if (delay_pending)
+ wait_time = did_start_blocking ? p_acl : MIN(p_acl, p_ut);
else
// going to block after p_ut
wait_time = p_ut;
@@ -325,6 +332,29 @@ inchar_loop(
// no character available within "wtime"
return 0;

+ // The 'autocompletedelay' expired: trigger the popup. When
+ // 'updatetime' is shorter, fall through to CursorHold instead.
+ if (delay_pending && elapsed_time >= p_acl && maxlen >= 3
+ && !typebuf_changed(tb_change_cnt))
+ {
+ if (buf == NULL)
+ {
+ char_u ibuf[3];
+
+ ibuf[0] = CSI;
+ ibuf[1] = KS_EXTRA;
+ ibuf[2] = (int)KE_COMPLETE_DELAY;
+ add_to_input_buf(ibuf, 3);
+ }
+ else
+ {
+ buf[0] = K_SPECIAL;
+ buf[1] = KS_EXTRA;
+ buf[2] = (int)KE_COMPLETE_DELAY;
+ }
+ return 3;
+ }
+
// No character available within 'updatetime'.
did_start_blocking = TRUE;
if (trigger_cursorhold() && maxlen >= 3
diff --git a/src/version.c b/src/version.c
index 7cfc8de77..0ca0f16e8 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 */
+/**/
+ 739,
/**/
738,
/**/
Reply all
Reply to author
Forward
0 new messages