]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0750: completion: 'autocompletedelay' deferral leaks state v9.2.0750
authorHirohito Higashi <h.east.727@gmail.com>
Sun, 28 Jun 2026 19:48:02 +0000 (19:48 +0000)
committerChristian Brabandt <cb@256bit.org>
Sun, 28 Jun 2026 19:48:02 +0000 (19:48 +0000)
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) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/edit.c
src/getchar.c
src/insexpand.c
src/proto/insexpand.pro
src/testdir/test_ins_complete.vim
src/ui.c
src/version.c

index bc8ce4128d257f62179b5b952dae1046c4e4d7b6..3b0cfcc882bd8b981b782f7c29a6bc0728550430 100644 (file)
@@ -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()
index 309c0d7c15b9095ccab1bb713562a707810b65cb..a192d146286b547822a3b988eb46e36e44b20773 100644 (file)
@@ -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;
index 75e55a60eb09b7717fb2d6b08f482f0ea77657ae..331a50011e8ebe23c0efda1dbc7bca6f134d1322 100644 (file)
@@ -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
  */
index b574652aaf0608f80e81f15d7fbec181797358f6..ebc8e25ac49174dd1a4d49beef0d8dac63e362cc 100644 (file)
@@ -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 : */
index cdcdb217e39524a5b0457f8cfc0ee49cb73f8fd7..4165002cd8a77f26134b5cb6d31f2f5f4455e572 100644 (file)
@@ -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()
index 3ba656d3a5f31f431117f1e0b4efd28c216593c7..ee6837d8d6a2353fe3a58179f756e57e0dac3f3b 100644 (file)
--- 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)
index a42f2bd83935e2e56ddd23196ad6a36b9e24a5e2..218fdc163a157c0837c941518771e91caa99b666 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    750,
 /**/
     749,
 /**/