mirror of
https://github.com/neovim/neovim.git
synced 2026-07-09 10:59:38 +00:00
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
8ce43ea4e3
Also include some insexpand.c and ui.c changes from patch 9.2.0750.
Co-authored-by: Hirohito Higashi <h.east.727@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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: // <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) {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1552,15 +1552,6 @@ describe('completion', function()
|
||||
{5:-- INSERT --} |
|
||||
]])
|
||||
feed('<BS>')
|
||||
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('<Esc>')
|
||||
@@ -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('<Esc>')
|
||||
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 --} |
|
||||
]])
|
||||
|
||||
@@ -6046,10 +6046,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>")
|
||||
|
||||
Reference in New Issue
Block a user