]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0739: completion: 'autocompletedelay' blocks the main loop and drops autoco... v9.2.0739
authorHirohito Higashi <h.east.727@gmail.com>
Sun, 28 Jun 2026 16:24:13 +0000 (16:24 +0000)
committerChristian Brabandt <cb@256bit.org>
Sun, 28 Jun 2026 16:27:06 +0000 (16:27 +0000)
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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
14 files changed:
src/edit.c
src/getchar.c
src/insexpand.c
src/keymap.h
src/proto/insexpand.pro
src/testdir/dumps/Test_autocompletedelay_2.dump
src/testdir/dumps/Test_autocompletedelay_3.dump
src/testdir/dumps/Test_autocompletedelay_5.dump
src/testdir/dumps/Test_autocompletedelay_longest_1.dump
src/testdir/dumps/Test_autocompletedelay_longest_3.dump
src/testdir/dumps/Test_autocompletedelay_preinsert_1.dump
src/testdir/test_ins_complete.vim
src/ui.c
src/version.c

index c10654e3681779f072c7cd44d9c4e64793eebf3f..1860ba3716c6de83dea3f55319fb44e079c08512 100644 (file)
@@ -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
index 25422be0d1253705f677471a501f7c89e8b7fa32..309c0d7c15b9095ccab1bb713562a707810b65cb 100644 (file)
@@ -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
                    && (
index f5ad9400816b4fcb1766d66ac77ea40da95dc947..0361d9a6e7e339c3de087a5714a76b61b896f214 100644 (file)
@@ -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
  */
index b3be60c48d379789558602ec099603f4274d311a..a8c9e275c033380482fea5e4353028b19fefa8bb 100644 (file)
@@ -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)
index 2c1882dc198cc67412f692cf30a73274f977d452..b574652aaf0608f80e81f15d7fbec181797358f6 100644 (file)
@@ -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 : */
index 65dc8f42ead949f816306c475c1caf9b1a25bff7..0dd4850d0cf88013924222b65fc3a19e7dfb495b 100644 (file)
@@ -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| 
index d3ed65d9028822a182ec9133954e52b07a6ede1d..ffc63d38734f71b0b4d438589f74798b5be16da1 100644 (file)
@@ -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| 
index 0dd4850d0cf88013924222b65fc3a19e7dfb495b..c792e09ea93a15ecf64451d7e14d7c099906e1b9 100644 (file)
@@ -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| 
index 10a50793cc69491cae60865b707974f91e16eea7..6ac0522388e8bb5cb0317f48d59d2376a72a9a7d 100644 (file)
@@ -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
index 52bda2c30cb0a21d9b6c4299277433d1e7ed7010..8149ef5906836feabf2f4962f9db01f3e76bf177 100644 (file)
@@ -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| 
index 3b1bc4bbd53cbc2c659eb3b6f4e3459afcfbc710..8149ef5906836feabf2f4962f9db01f3e76bf177 100644 (file)
@@ -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| 
index 4df2efb0616eee819db90a4df6aa47604a30dd01..d4298aeb967f00954988594c45f6c92d55afdcb7 100644 (file)
@@ -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>")
index f277ba4075ad9ca7d8ea210bd597e151c1fe3e66..3ba656d3a5f31f431117f1e0b4efd28c216593c7 100644 (file)
--- 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
index 7cfc8de77729e9035211f0e3fb1a0ac31835c429..0ca0f16e8df68755db6a96beb73563fe55774134 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    739,
 /**/
     738,
 /**/