From 14f2a86cd6bb589c29b2f78bc39e4994e128e8c6 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 1 Jul 2026 17:00:08 +0800 Subject: [PATCH] vim-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: vim/vim#20591 closes: vim/vim#20598 https://github.com/vim/vim/commit/8ce43ea4e3cb315f566bcd9048c254ba58042964 Also include some insexpand.c and ui.c changes from patch 9.2.0750. Co-authored-by: Hirohito Higashi Co-Authored-By: Claude Opus 4.8 (1M context) --- src/nvim/edit.c | 39 ++++++++++--- src/nvim/insexpand.c | 64 ++++++++++++---------- src/nvim/keycodes.h | 4 ++ src/nvim/os/input.c | 33 ++++++++++- test/functional/editor/completion_spec.lua | 27 ++++----- test/old/testdir/test_ins_complete.vim | 6 +- 6 files changed, 117 insertions(+), 56 deletions(-) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 1b9320c73a..db244f095a 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -146,14 +146,19 @@ static linenr_T o_lnum = 0; static kvec_t(char) replace_stack = KV_INITIAL_VALUE; +// 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 { \ redraw_later(curwin, UPD_VALID); \ update_screen(); /* Show char deletion immediately */ \ ui_flush(); \ ins_compl_enable_autocomplete(); \ - insert_do_complete(s); \ - break; \ + if (!ins_compl_arm_autocomplete_delay()) { \ + insert_do_complete(s); \ + break; \ + } \ } while (0) #define MAY_TRIGGER_AUTOCOMPLETE(c) \ @@ -519,8 +524,8 @@ static int insert_check(VimState *state) s->old_topline = curwin->w_topline; s->old_topfill = curwin->w_topfill; - if (s->c != K_EVENT) { - s->lastc = s->c; // remember previous char for CTRL-D + if (s->c != K_EVENT && s->c != K_COMPLETE_DELAY) { + s->lastc = s->c; // remember the previous char for CTRL-D } // After using CTRL-G U the next cursor key will not break undo. @@ -540,9 +545,13 @@ static int insert_check(VimState *state) if (vim_isprintc(s->c)) { ins_compl_enable_autocomplete(); ins_compl_init_get_longest(); - insert_do_complete(s); - insert_handle_key_post(s); - return 1; + // Defer until the delay expires (K_COMPLETE_DELAY), or + // trigger now when no delay is in effect. + if (!ins_compl_arm_autocomplete_delay()) { + insert_do_complete(s); + insert_handle_key_post(s); + return 1; + } } } } @@ -995,6 +1004,21 @@ static int insert_handle_key(InsertState *s) 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; + } + s->c = char_before_cursor(); + if (!vim_isprintc(s->c)) { + break; + } + // The completion may have been cleared while waiting, so re-enable + // autocomplete to match a zero delay. + ins_compl_enable_autocomplete(); + insert_do_complete(s); + break; + case K_HOME: // case K_KHOME: case K_S_HOME: @@ -1271,6 +1295,7 @@ normalchar: static void insert_do_complete(InsertState *s) { + ins_compl_clear_autocomplete_delay(); compl_busy = true; disable_fold_update++; // don't redraw folds here if (ins_complete(s->c, true) == FAIL) { diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index e9f79e1145..d79834114a 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -294,6 +294,8 @@ 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 bool compl_autocomplete = false; ///< whether autocompletion is active +static bool compl_autocomplete_pending = false; +static uint64_t compl_autocomplete_start_tv; ///< when the delay was armed static uint64_t compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS; static bool compl_time_slice_expired = false; ///< time budget exceeded for current source static bool compl_from_nonkeyword = false; ///< completion started from non-keyword @@ -2371,7 +2373,9 @@ static void 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); } @@ -6269,10 +6273,6 @@ static void ins_compl_show_statusmsg(void) /// Returns OK if completion was done, FAIL if something failed. int ins_complete(int c, bool enable_pum) { - const bool disable_ac_delay = compl_started && ctrl_x_mode_normal() - && (c == Ctrl_N || c == Ctrl_P || c == Ctrl_R - || ins_compl_pum_key(c)); - compl_direction = ins_compl_key2dir(c); int insert_match = ins_compl_use_match(c); @@ -6284,10 +6284,6 @@ int ins_complete(int c, bool enable_pum) return FAIL; } - uint64_t compl_start_tv = 0; ///< Time when match collection starts - if (compl_autocomplete && p_acl > 0 && !disable_ac_delay) { - compl_start_tv = os_hrtime(); - } compl_curr_win = curwin; compl_curr_buf = curwin->w_buffer; compl_shown_match = compl_curr_match; @@ -6343,26 +6339,6 @@ int ins_complete(int c, bool enable_pum) ins_compl_show_statusmsg(); } - // Wait for the autocompletion delay to expire - if (compl_autocomplete && p_acl > 0 && !disable_ac_delay && !no_matches_found - && (os_hrtime() - compl_start_tv) / 1000000 < (uint64_t)p_acl) { - setcursor(); - ui_flush(); - do { - if (char_avail()) { - if (ins_compl_preinsert_effect() && ins_compl_win_active(curwin)) { - ins_compl_delete(false); // Remove pre-inserted text - compl_ins_end_col = compl_col; - } - ins_compl_restart(); - compl_interrupted = true; - break; - } else { - os_delay(2L, true); - } - } while ((os_hrtime() - compl_start_tv) / 1000000 < (uint64_t)p_acl); - } - // Show the popup menu, unless we got interrupted. if (enable_pum && !compl_interrupted) { show_pum(save_w_wrow, save_w_leftcol); @@ -6380,6 +6356,36 @@ void ins_compl_enable_autocomplete(void) compl_get_longest = false; } +/// 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) +{ + if (p_acl > 0) { + compl_autocomplete_start_tv = os_hrtime(); + compl_autocomplete_pending = true; + return true; + } + 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; +} + +/// Return the time in msec since the 'autocompletedelay' was armed. +int64_t ins_compl_autocomplete_elapsed(void) +{ + return (int64_t)((os_hrtime() - compl_autocomplete_start_tv) / 1000000); +} + /// Remove (if needed) and show the popup menu static void show_pum(int prev_w_wrow, int prev_w_leftcol) { diff --git a/src/nvim/keycodes.h b/src/nvim/keycodes.h index bc3cf4e041..2fa0226c7e 100644 --- a/src/nvim/keycodes.h +++ b/src/nvim/keycodes.h @@ -224,6 +224,8 @@ enum key_extra { // KE_SID = 106, // KE_ESC = 107, KE_WILD = 108, // triggers wildmode completion + // KE_OSC = 109, + KE_COMPLETE_DELAY = 110, // 'autocompletedelay' expired }; // the three byte codes are replaced with the following int when using vgetc() @@ -451,6 +453,8 @@ enum key_extra { #define K_DROP TERMCAP2KEY(KS_EXTRA, KE_DROP) +#define K_COMPLETE_DELAY TERMCAP2KEY(KS_EXTRA, KE_COMPLETE_DELAY) + #define K_EVENT TERMCAP2KEY(KS_EXTRA, KE_EVENT) #define K_COMMAND TERMCAP2KEY(KS_EXTRA, KE_COMMAND) #define K_LUA TERMCAP2KEY(KS_EXTRA, KE_LUA) diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index e042ffdb87..b52e18f91b 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -21,6 +21,7 @@ #include "nvim/getchar.h" #include "nvim/gettext_defs.h" #include "nvim/globals.h" +#include "nvim/insexpand.h" #include "nvim/keycodes.h" #include "nvim/log.h" #include "nvim/macros_defs.h" @@ -111,6 +112,7 @@ static void reset_cursorhold_wait(int tb_change_cnt) /// @return Bytes read into buf, or 0 if no input was read int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *events) { + assert((maxlen > 0 && buf != NULL) || maxlen == 0); // This check is needed so that feeding typeahead from RPC can prevent CursorHold. if (tb_change_cnt != cursorhold_tb_change_cnt) { reset_cursorhold_wait(tb_change_cnt); @@ -120,7 +122,6 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e do { \ if (maxlen && input_available()) { \ reset_cursorhold_wait(tb_change_cnt); \ - assert(maxlen >= 0); \ size_t to_read = MIN((size_t)maxlen, input_available()); \ memcpy(buf, input_read_pos, to_read); \ input_read_pos += to_read; \ @@ -145,11 +146,39 @@ int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e } else { uint64_t wait_start = os_hrtime(); cursorhold_time = MIN(cursorhold_time, (int)p_ut); - if ((result = inbuf_poll((int)p_ut - cursorhold_time, events)) == kFalse) { + // 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; + // Measure the delay from when it was armed (the keystroke), so a + // CursorHold returning in between does not push the popup back. + int64_t wait_time = p_ut - cursorhold_time; + if (delay_pending) { + int64_t delay_left = p_acl - ins_compl_autocomplete_elapsed(); + wait_time = MIN(MAX(delay_left, 0), wait_time); + } + if ((result = inbuf_poll((int)wait_time, events)) == kFalse) { if (read_stream.s.closed && silent_mode) { // Drained eventloop & initial input; exit silent/batch-mode (-es/-Es). read_error_exit(); } + // The 'autocompletedelay' expired: trigger the popup. When + // 'updatetime' is shorter, fall through to CursorHold instead. + if (delay_pending && ins_compl_autocomplete_elapsed() >= p_acl + && (buf == NULL || maxlen >= 3) && !typebuf_changed(tb_change_cnt)) { + if (buf == NULL) { + uint8_t ibuf[3]; + ibuf[0] = K_SPECIAL; + ibuf[1] = KS_EXTRA; + ibuf[2] = KE_COMPLETE_DELAY; + input_enqueue_raw((char *)ibuf, 3); + return 0; + } + buf[0] = K_SPECIAL; + buf[1] = KS_EXTRA; + buf[2] = KE_COMPLETE_DELAY; + return 3; + } reset_cursorhold_wait(tb_change_cnt); if (trigger_cursorhold() && !typebuf_changed(tb_change_cnt)) { create_cursorhold_event(events == main_loop.events); diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index d21fe79c6c..76db2ba479 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -1552,15 +1552,6 @@ describe('completion', function() {5:-- INSERT --} | ]]) feed('') - screen:expect([[ - foo | - foobar | - foobarbaz | - f^ | - {1:~ }|*5 - {5:-- INSERT --} | - ]]) - vim.uv.sleep(500) screen:expect([[ foo | foobar | @@ -1572,6 +1563,8 @@ describe('completion', function() {1:~ }|*2 {5:-- INSERT --} | ]]) + vim.uv.sleep(500) + screen:expect_unchanged() -- During delay wait, user can open menu using CTRL_N completion feed('') @@ -1589,7 +1582,9 @@ describe('completion', function() {5:-- Keyword completion (^N^P) }{6:match 1 of 3} | ]]) - -- 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. feed('') command('set completeopt=menu') feed('Sf') @@ -1601,7 +1596,7 @@ describe('completion', function() {1:~ }|*5 {5:-- INSERT --} | ]]) - vim.uv.sleep(500) + vim.uv.sleep(600) screen:expect([[ foo | foobar | @@ -1734,7 +1729,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - au{102:^tocom} | + au^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1742,7 +1737,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - autoc{102:^om} | + autoc^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1762,7 +1757,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - aut{102:^ocom} | + aut^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1770,7 +1765,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - au{102:^tocom} | + au^ | {1:~ }|*4 {5:-- INSERT --} | ]]) @@ -1793,7 +1788,7 @@ describe('completion', function() screen:expect([[ autocomplete | autocomxxx | - au{102:^tocomplete} | + au^ | {1:~ }|*4 {5:-- INSERT --} | ]]) diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 93d28e91a3..af4b7ccdc6 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6046,10 +6046,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, "\")