Merge pull request #40525 from zeertzjq/vim-9.2.0739

vim-patch:9.2.{0739,0748,0749,0750,0755,0759}: 'autocompletedelay' fixes
This commit is contained in:
zeertzjq
2026-07-02 04:12:54 +08:00
committed by GitHub
7 changed files with 246 additions and 78 deletions

View File

@@ -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) \
@@ -367,6 +372,8 @@ static void insert_enter(InsertState *s)
// and return false, causing `state_enter` to be called again.
} while (!ins_esc(&s->count, s->cmdchar, s->nomove));
// Drop a pending autocomplete so it does not outlive Insert mode.
ins_compl_clear_autocomplete_delay();
// Always update o_lnum, so that a "CTRL-O ." that adds a line
// still puts the cursor back after the inserted text.
if (ins_at_eol) {
@@ -519,8 +526,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 +547,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;
}
}
}
}
@@ -573,6 +584,10 @@ static int insert_execute(VimState *state, int key)
if (key != K_EVENT) {
did_cursorhold = true;
}
if (key != K_EVENT && key != K_COMPLETE_DELAY) {
// Don't want delayed autocompletion from the previous key either.
ins_compl_clear_autocomplete_delay();
}
// Special handling of keys while the popup menu is visible or wanted
// and the cursor is still in the completed word. Only when there is
@@ -995,6 +1010,25 @@ static int insert_handle_key(InsertState *s)
break;
}
case K_COMPLETE_DELAY: // 'autocompletedelay' expired
// If CTRL-G U was used apply it to the next typed key.
if (dont_sync_undo == kTrue) {
dont_sync_undo = kNone;
}
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 +1305,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) {
@@ -1294,8 +1329,9 @@ static void insert_do_cindent(InsertState *s)
static void insert_handle_key_post(InsertState *s)
{
// If typed something may trigger CursorHoldI again.
if (s->c != K_EVENT
// If typed something may trigger CursorHoldI again; K_COMPLETE_DELAY is
// injected, not typed.
if (s->c != K_EVENT && s->c != K_COMPLETE_DELAY
// but not in CTRL-X mode, a script can't restore the state
&& ctrl_x_mode_normal()) {
did_cursorhold = false;

View File

@@ -1194,6 +1194,10 @@ static bool gotchars_add_byte(gotchars_state_T *state, uint8_t byte)
goto ret_false;
}
c = TO_SPECIAL(state->prev_c, c);
// Drop K_COMPLETE_DELAY, it's not useful in a recording.
if (c == K_COMPLETE_DELAY) {
state->buflen = 0;
}
}
// When receiving a multibyte character, store it until we have all
// the bytes, so that it won't be split between two buffer blocks,

View File

@@ -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
@@ -2336,13 +2338,6 @@ static void ins_compl_new_leader(void)
ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1);
compl_used_match = false;
if (p_acl > 0) {
pum_undisplay(true);
redraw_later(curwin, UPD_VALID);
update_screen(); // Show char (deletion) immediately
ui_flush();
}
int save_w_wrow = curwin->w_wrow;
int save_w_leftcol = curwin->w_leftcol;
if (compl_started) {
@@ -2371,7 +2366,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);
}
@@ -5418,7 +5415,6 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match
bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0
|| (compl_autocomplete && !ins_compl_has_preinsert());
bool compl_preinsert = ins_compl_has_preinsert();
bool has_autocomplete_delay = (compl_autocomplete && p_acl > 0);
// When user complete function return -1 for findstart which is next
// time of 'always', compl_shown_match become NULL.
@@ -5465,9 +5461,6 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match
// Insert the text of the new completion, or the compl_leader.
if (!started && ins_compl_preinsert_longest()) {
ins_compl_insert(true, true);
if (has_autocomplete_delay) {
update_screen(); // Show the inserted text right away
}
} else if (compl_no_insert && !started && !compl_preinsert) {
ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1);
compl_used_match = false;
@@ -5492,10 +5485,8 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match
// redraw to show the user what was inserted
update_screen(); // TODO(bfredl): no!
if (!has_autocomplete_delay) {
// display the updated popup menu
ins_compl_show_pum();
}
// display the updated popup menu
ins_compl_show_pum();
// Delete old text to be replaced, since we're still searching and
// don't want to match ourselves!
@@ -6269,10 +6260,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 +6271,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 +6326,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 +6343,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)
{

View File

@@ -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)

View File

@@ -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,45 @@ 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.
// Gate the injection like trigger_cursorhold() so the deferred key
// cannot fire while outside Insert mode, but do still fire the key
// when recording a register (gotchars_add_byte() will ignore it).
bool delay_pending = ins_compl_autocomplete_pending() && p_acl > 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.
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);

View File

@@ -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 |
@@ -1670,6 +1665,35 @@ describe('completion', function()
feed('<esc>')
end)
local function run_test_autocompletedelay_ctrl_k(delay1, delay2)
source([[
new
call setline(1, 'foo bar baz')
set autocomplete autocompletedelay=200
]])
feed('ob')
vim.uv.sleep(delay1)
feed('<C-K>')
vim.uv.sleep(delay2)
feed('.,<Esc>')
poke_eventloop()
expect('foo bar baz\nb…')
source([[
set autocomplete& autocompletedelay&
bwipe!
]])
end
-- oldtest: Test_autocompletedelay_ctrl_k()
it("'autocompletedelay' doesn't interfere with i_CTRL-K", function()
-- Ctrl-K typed after 'autocompletedelay' expires
run_test_autocompletedelay_ctrl_k(250, 500)
-- Ctrl-K typed before 'autocompletedelay' expires
run_test_autocompletedelay_ctrl_k(150, 500)
end)
-- oldtest: Test_fuzzy_select_item_when_acl()
it([[first item isn't selected with "fuzzy" and 'acl']], function()
screen:try_resize(60, 10)
@@ -1734,7 +1758,7 @@ describe('completion', function()
screen:expect([[
autocomplete |
autocomxxx |
au{102:^tocom} |
au^ |
{1:~ }|*4
{5:-- INSERT --} |
]])
@@ -1742,7 +1766,7 @@ describe('completion', function()
screen:expect([[
autocomplete |
autocomxxx |
autoc{102:^om} |
autoc^ |
{1:~ }|*4
{5:-- INSERT --} |
]])
@@ -1762,7 +1786,7 @@ describe('completion', function()
screen:expect([[
autocomplete |
autocomxxx |
aut{102:^ocom} |
aut^ |
{1:~ }|*4
{5:-- INSERT --} |
]])
@@ -1770,7 +1794,7 @@ describe('completion', function()
screen:expect([[
autocomplete |
autocomxxx |
au{102:^tocom} |
au^ |
{1:~ }|*4
{5:-- INSERT --} |
]])
@@ -1787,13 +1811,11 @@ describe('completion', function()
-- Preinsert
command('set completeopt& completeopt+=preinsert')
-- Show preinserted text right away but display popup later
feed('<Esc>Sau')
screen:expect([[
autocomplete |
autocomxxx |
au{102:^tocomplete} |
au^ |
{1:~ }|*4
{5:-- INSERT --} |
]])

View File

@@ -6039,17 +6039,19 @@ func Test_autocompletedelay()
call term_sendkeys(buf, "\<BS>")
call VerifyScreenDump(buf, 'Test_autocompletedelay_5', {})
sleep 500m
call VerifyScreenDump(buf, 'Test_autocompletedelay_6', {})
call VerifyScreenDump(buf, 'Test_autocompletedelay_5', {})
" During delay wait, user can open menu using CTRL_N completion
call term_sendkeys(buf, "\<Esc>:set completeopt=menuone\<CR>")
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>")
@@ -6066,6 +6068,80 @@ func Test_autocompletedelay()
call StopVimInTerminal(buf)
endfunc
func Run_test_autocompletedelay_ctrl_g_U(delay1, delay2)
new
call setline(1, 'foo bar baz')
inoremap <buffer> ( ()<C-g>U
set autocomplete autocompletedelay=200
call timer_start(a:delay1, { -> feedkeys('(', 't') })
call timer_start(a:delay2, { -> feedkeys("\<Left>a\<Esc>", 't') })
call feedkeys('ob', 'tx!')
call assert_equal(['foo bar baz', 'b(a)'], getline(1, '$'))
undo
call assert_equal(['foo bar baz'], getline(1, '$'))
set autocomplete& autocompletedelay&
bwipe!
endfunc
func Test_autocompletedelay_ctrl_g_U()
" '(' typed after 'autocompletedelay' expires
call Run_test_autocompletedelay_ctrl_g_U(250, 500)
" '(' typed before 'autocompletedelay' expires
call Run_test_autocompletedelay_ctrl_g_U(150, 500)
endfunc
func Run_test_autocompletedelay_ctrl_k(delay1, delay2)
new
call setline(1, 'foo bar baz')
set autocomplete autocompletedelay=200
call timer_start(a:delay1, { -> feedkeys("\<C-K>", 't') })
call timer_start(a:delay2, { -> feedkeys(".,\<Esc>", 't') })
call feedkeys('ob', 'tx!')
call assert_equal(['foo bar baz', 'b…'], getline(1, '$'))
set autocomplete& autocompletedelay&
bwipe!
endfunc
func Test_autocompletedelay_ctrl_k()
throw 'Skipped: use test/functional/editor/completion_spec.lua'
" Ctrl-K typed after 'autocompletedelay' expires
call Run_test_autocompletedelay_ctrl_k(250, 500)
" Ctrl-K typed before 'autocompletedelay' expires
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(300, { -> feedkeys("\<Esc>q", 't') })
call feedkeys("qaSf", 'tx!')
call assert_equal("Sf\<Esc>", @a)
" Delayed autocompletion still works when recording.
if !has('win32') || has('nvim')
call setline(1, 'foobar foofoo')
call timer_start(300, { -> feedkeys("\<Down>\<C-Y>\<Esc>q", 't') })
call feedkeys("qaof", 'tx!')
call assert_equal('foobar', getline('.'))
" XXX: This doesn't produce the same result when replaying.
call assert_equal("of\<Down>\<C-Y>\<Esc>", @a)
endif
set autocomplete& autocompletedelay&
bwipe!
endfunc
" Preinsert longest prefix when autocomplete
func Test_autocomplete_longest()
func GetLine()
@@ -6366,8 +6442,6 @@ func Test_autocompletedelay_longest_preinsert()
" Preinsert
call term_sendkeys(buf, "\<Esc>:set completeopt& completeopt+=preinsert\<CR>")
" Show preinserted text right away but display popup later
call term_sendkeys(buf, "\<Esc>Sau")
sleep 100m
call VerifyScreenDump(buf, 'Test_autocompletedelay_preinsert_1', {})