From: Hirohito Higashi Date: Sun, 28 Jun 2026 16:24:13 +0000 (+0000) Subject: patch 9.2.0739: completion: 'autocompletedelay' blocks the main loop and drops autoco... X-Git-Tag: v9.2.0739^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8ce43ea4e3cb315f566bcd9048c254ba58042964;p=thirdparty%2Fvim.git 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) Signed-off-by: Hirohito Higashi Signed-off-by: Christian Brabandt --- diff --git a/src/edit.c b/src/edit.c index c10654e368..1860ba3716 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 , 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 25422be0d1..309c0d7c15 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 f5ad940081..0361d9a6e7 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 b3be60c48d..a8c9e275c0 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 2c1882dc19..b574652aaf 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 65dc8f42ea..0dd4850d0c 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 d3ed65d902..ffc63d3873 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 0dd4850d0c..c792e09ea9 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 10a50793cc..6ac0522388 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 52bda2c30c..8149ef5906 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 3b1bc4bbd5..8149ef5906 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 4df2efb061..d4298aeb96 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\") 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, "\") diff --git a/src/ui.c b/src/ui.c index f277ba4075..3ba656d3a5 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 7cfc8de777..0ca0f16e8d 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, /**/