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, "\")