diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index 122d16428d..0d0f83c9af 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -1112,26 +1112,23 @@ Stop completion *compl-stop* CTRL-X CTRL-Z Stop completion without changing the text. -AUTO-COMPLETION *compl-autocomplete* +AUTOCOMPLETION *ins-autocompletion* -To get LSP-driven auto-completion, see |lsp-completion|. To get basic -auto-completion without installing plugins or LSP, try this: >lua +Vim can display a completion menu as you type, similar to using |i_CTRL-N|, +but triggered automatically. See |'autocomplete'|. The menu items are +collected from the sources listed in the |'complete'| option. + +Unlike manual |i_CTRL-N| completion, this mode uses a decaying timeout to keep +Vim responsive. Sources earlier in the |'complete'| list are given more time +(higher priority), but every source is guaranteed a time slice, however small. + +This mode is fully compatible with other completion modes. You can invoke +any of them at any time by typing |CTRL-X|, which temporarily suspends +autocompletion. To use |i_CTRL-N| specifically, press |CTRL-E| first to +dismiss the popup menu (see |complete_CTRL-E|). + +To get LSP-driven auto-completion, see |lsp-completion|. - local triggers = {'.'} - vim.api.nvim_create_autocmd('InsertCharPre', { - buffer = vim.api.nvim_get_current_buf(), - callback = function() - if vim.fn.pumvisible() == 1 or vim.fn.state('m') == 'm' then - return - end - local char = vim.v.char - if vim.list_contains(triggers, char) then - local key = vim.keycode('') - vim.api.nvim_feedkeys(key, 'm', false) - end - end - }) -< FUNCTIONS FOR FINDING COMPLETIONS *complete-functions* diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index f2af0330a9..e042aa5cfa 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -2059,8 +2059,7 @@ you want to trigger on EVERY keypress you can either: `LspAttach`, before you call `vim.lsp.completion.enable(… {autotrigger=true})`. See the |lsp-attach| example. -• Call `vim.lsp.completion.get()` from the handler described at - |compl-autocomplete|. +• Call `vim.lsp.completion.get()` from an |InsertCharPre| autocommand. *vim.lsp.completion.enable()* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 75a0edb75f..d6a1666426 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -236,6 +236,7 @@ LUA OPTIONS +• 'autocomplete' enables |ins-autocompletion|. • 'autowriteall' writes all buffers upon receiving `SIGHUP`, `SIGQUIT` or `SIGTSTP`. • 'chistory' and 'lhistory' set size of the |quickfix-stack|. • 'completefuzzycollect' enables fuzzy collection of candidates for (some) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 4ca5db5eb6..fa3e538ca6 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -741,6 +741,12 @@ A jump table for the options with a short description can be found at |Q_op|. the current directory won't change when navigating to it. Note: When this option is on some plugins may not work. + *'autocomplete'* *'ac'* *'noautocomplete'* *'noac'* +'autocomplete' 'ac' boolean (default off) + global + When on, Vim shows a completion menu as you type, similar to using + |i_CTRL-N|, but triggered automatically. See |ins-autocompletion|. + *'autoindent'* *'ai'* *'noautoindent'* *'noai'* 'autoindent' 'ai' boolean (default on) local to buffer @@ -1525,9 +1531,9 @@ A jump table for the options with a short description can be found at |Q_op|. If the Dict returned by the {func} includes {"refresh": "always"}, the function will be invoked again whenever the leading text changes. - If generating matches is potentially slow, |complete_check()| - should be used to avoid blocking and preserve editor - responsiveness. + If generating matches is potentially slow, call + |complete_check()| periodically to keep Vim responsive. This + is especially important for |ins-autocompletion|. F equivalent to using "F{func}", where the function is taken from the 'completefunc' option. o equivalent to using "F{func}", where the function is taken from @@ -1655,6 +1661,9 @@ A jump table for the options with a short description can be found at |Q_op|. completion in the preview window. Only works in combination with "menu" or "menuone". + Only "fuzzy", "popup" and "preview" have an effect when 'autocomplete' + is enabled. + This option does not apply to |cmdline-completion|. See 'wildoptions' for that. diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 990c227187..b30a84e4bd 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -111,6 +111,15 @@ vim.o.acd = vim.o.autochdir vim.go.autochdir = vim.o.autochdir vim.go.acd = vim.go.autochdir +--- When on, Vim shows a completion menu as you type, similar to using +--- `i_CTRL-N`, but triggered automatically. See `ins-autocompletion`. +--- +--- @type boolean +vim.o.autocomplete = false +vim.o.ac = vim.o.autocomplete +vim.go.autocomplete = vim.o.autocomplete +vim.go.ac = vim.go.autocomplete + --- Copy indent from current line when starting a new line (typing --- in Insert mode or when using the "o" or "O" command). If you do not --- type anything on the new line except or CTRL-D and then type @@ -1047,9 +1056,9 @@ vim.bo.cms = vim.bo.commentstring --- If the Dict returned by the {func} includes {"refresh": "always"}, --- the function will be invoked again whenever the leading text --- changes. ---- If generating matches is potentially slow, `complete_check()` ---- should be used to avoid blocking and preserve editor ---- responsiveness. +--- If generating matches is potentially slow, call +--- `complete_check()` periodically to keep Vim responsive. This +--- is especially important for `ins-autocompletion`. --- F equivalent to using "F{func}", where the function is taken from --- the 'completefunc' option. --- o equivalent to using "F{func}", where the function is taken from @@ -1189,6 +1198,9 @@ vim.go.cia = vim.go.completeitemalign --- completion in the preview window. Only works in --- combination with "menu" or "menuone". --- +--- Only "fuzzy", "popup" and "preview" have an effect when 'autocomplete' +--- is enabled. +--- --- This option does not apply to `cmdline-completion`. See 'wildoptions' --- for that. --- diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 7d51b5593c..79a0bb950b 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -29,7 +29,7 @@ --- on EVERY keypress you can either: --- - Extend `client.server_capabilities.completionProvider.triggerCharacters` on `LspAttach`, --- before you call `vim.lsp.completion.enable(… {autotrigger=true})`. See the |lsp-attach| example. ---- - Call `vim.lsp.completion.get()` from the handler described at |compl-autocomplete|. +--- - Call `vim.lsp.completion.get()` from an |InsertCharPre| autocommand. local M = {} diff --git a/runtime/optwin.vim b/runtime/optwin.vim index 4dd97571bd..418c782df2 100644 --- a/runtime/optwin.vim +++ b/runtime/optwin.vim @@ -1,7 +1,7 @@ " These commands create the option window. " " Maintainer: The Vim Project -" Last Change: 2025 Jul 16 +" Last Change: 2025 Jul 25 " Former Maintainer: Bram Moolenaar " If there already is an option window, jump to that one. @@ -734,13 +734,19 @@ endif if has("insert_expand") call AddOption("complete", gettext("specifies how Insert mode completion works for CTRL-N and CTRL-P")) call append("$", "\t" .. s:local_to_buffer) - call OptionL("cfc") - call AddOption("completefuzzycollect", gettext("use fuzzy collection for specific completion modes")) call OptionL("cpt") + call AddOption("autocomplete", gettext("automatic completion in insert mode")) + call BinOptionG("ac", &ac) call AddOption("completeopt", gettext("whether to use a popup menu for Insert mode completion")) call OptionL("cot") call AddOption("completeitemalign", gettext("popup menu item align order")) call OptionG("cia", &cia) + call AddOption("completefuzzycollect", gettext("use fuzzy collection for specific completion modes")) + call OptionL("cfc") + if exists("+completepopup") + call AddOption("completepopup", gettext("options for the Insert mode completion info popup")) + call OptionG("cpp", &cpp) + endif call AddOption("pumheight", gettext("maximum height of the popup menu")) call OptionG("ph", &ph) call AddOption("pumwidth", gettext("minimum width of the popup menu")) diff --git a/src/nvim/cursor.c b/src/nvim/cursor.c index fbd3098a8a..bb9f22cb3a 100644 --- a/src/nvim/cursor.c +++ b/src/nvim/cursor.c @@ -480,6 +480,19 @@ int gchar_cursor(void) return utf_ptr2char(get_cursor_pos_ptr()); } +/// Return the character immediately before the cursor. +int char_before_cursor(void) +{ + if (curwin->w_cursor.col == 0) { + return -1; + } + + char *line = get_cursor_line_ptr(); + char *p = line + curwin->w_cursor.col; + int prev_len = utf_head_off(line, p - 1) + 1; + return utf_ptr2char(p - prev_len); +} + /// Write a character at the current cursor position. /// It is directly written into the block. void pchar_cursor(char c) diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 3c189e74c8..32ee040d14 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -849,6 +849,16 @@ static int insert_handle_key(InsertState *s) case Ctrl_H: s->did_backspace = ins_bs(s->c, BACKSPACE_CHAR, &s->inserted_space); auto_format(false, true); + if (s->did_backspace && p_ac && !char_avail() && curwin->w_cursor.col > 0) { + s->c = char_before_cursor(); + if (ins_compl_setup_autocompl(s->c)) { + redraw_later(curwin, UPD_VALID); + update_screen(); // Show char deletion immediately + ui_flush(); + insert_do_complete(s); // Trigger autocompletion + return 1; + } + } break; case Ctrl_W: // delete word before the cursor @@ -1224,6 +1234,14 @@ normalchar: // When inserting a character the cursor line must never be in a // closed fold. foldOpenCursor(); + // Trigger autocompletion + if (p_ac && !char_avail() && ins_compl_setup_autocompl(s->c)) { + redraw_later(curwin, UPD_VALID); + update_screen(); // Show character immediately + ui_flush(); + insert_do_complete(s); + } + break; } // end of switch (s->c) @@ -1978,6 +1996,7 @@ void insertchar(int c, int flags, int second_indent) if (!ISSPECIAL(c) && (utf_char2len(c) == 1) && !has_event(EVENT_INSERTCHARPRE) + && !test_disable_char_avail && vpeekc() != NUL && !(State & REPLACE_FLAG) && !cindent_on() diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index e131816836..89bf34fcc9 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -285,6 +285,25 @@ static expand_T compl_xp; static win_T *compl_curr_win = NULL; ///< win where completion is active static buf_T *compl_curr_buf = NULL; ///< buf where completion is active +#define COMPL_INITIAL_TIMEOUT_MS 80 +// Autocomplete uses a decaying timeout: starting from COMPL_INITIAL_TIMEOUT_MS, +// if the current source exceeds its timeout, it is interrupted and the next +// begins with half the time. A small minimum timeout ensures every source +// gets at least a brief chance. +static bool compl_autocomplete = false; ///< whether autocompletion is active +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 + +// Halve the current completion timeout, simulating exponential decay. +#define COMPL_MIN_TIMEOUT_MS 5 +#define DECAY_COMPL_TIMEOUT() \ + do { \ + if (compl_timeout_ms > COMPL_MIN_TIMEOUT_MS) { \ + compl_timeout_ms /= 2; \ + } \ + } while (0) + // List of flags for method of completion. static int compl_cont_status = 0; #define CONT_ADDING 1 ///< "normal" or "adding" expansion @@ -311,6 +330,7 @@ typedef struct cpt_source_T { bool cs_refresh_always; ///< Whether 'refresh:always' is set for func int cs_startcol; ///< Start column returned by func int cs_max_matches; ///< Max items to display from this source + uint64_t compl_start_tv; ///< Timestamp when match collection starts } cpt_source_T; #define STARTCOL_NONE -9 @@ -331,7 +351,7 @@ void ins_ctrl_x(void) { if (!ctrl_x_mode_cmdline()) { // if the next ^X<> won't ADD nothing, then reset compl_cont_status - if (compl_cont_status & CONT_N_ADDS) { + if ((compl_cont_status & CONT_N_ADDS) && !p_ac) { compl_cont_status |= CONT_INTRPT; } else { compl_cont_status = 0; @@ -643,6 +663,10 @@ static void do_autocmd_completedone(int c, int mode, char *word) bool ins_compl_accept_char(int c) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { + if (compl_autocomplete && compl_from_nonkeyword) { + return false; + } + if (ctrl_x_mode & CTRL_X_WANT_IDENT) { // When expanding an identifier only accept identifier chars. return vim_isIDc(c); @@ -853,7 +877,9 @@ static inline void free_cptext(char *const *const cptext) /// Returns true if matches should be sorted based on proximity to the cursor. static bool is_nearest_active(void) { - return (get_cot_flags() & (kOptCotFlagNearest|kOptCotFlagFuzzy)) == kOptCotFlagNearest; + unsigned flags = get_cot_flags(); + return (compl_autocomplete || (flags & kOptCotFlagNearest)) + && !(flags & kOptCotFlagFuzzy); } /// Add a match to the list of matches @@ -1229,7 +1255,7 @@ bool pum_wanted(void) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { // "completeopt" must contain "menu" or "menuone" - return (get_cot_flags() & (kOptCotFlagMenu | kOptCotFlagMenuone)) != 0; + return (get_cot_flags() & (kOptCotFlagMenu | kOptCotFlagMenuone)) != 0 || compl_autocomplete; } /// Check that there are two or more matches to be shown in the popup menu. @@ -1248,7 +1274,7 @@ static bool pum_enough_matches(void) comp = comp->cp_next; } while (!is_first_match(comp)); - if (get_cot_flags() & kOptCotFlagMenuone) { + if ((get_cot_flags() & kOptCotFlagMenuone) || compl_autocomplete) { return i >= 1; } return i >= 2; @@ -1456,7 +1482,7 @@ static int ins_compl_build_pum(void) } unsigned cur_cot_flags = get_cot_flags(); - bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0; + bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 || compl_autocomplete; bool fuzzy_filter = (cur_cot_flags & kOptCotFlagFuzzy) != 0; compl_T *match_head = NULL, *match_tail = NULL; @@ -1860,9 +1886,9 @@ static void ins_compl_files(int count, char **files, bool thesaurus, int flags, char *leader = in_fuzzy_collect ? ins_compl_leader() : NULL; int leader_len = in_fuzzy_collect ? (int)ins_compl_leader_len() : 0; - for (int i = 0; i < count && !got_int && !compl_interrupted; i++) { + for (int i = 0; i < count && !got_int && !compl_interrupted && !compl_time_slice_expired; i++) { FILE *fp = os_fopen(files[i], "r"); // open dictionary file - if (flags != DICT_EXACT && !shortmess(SHM_COMPLETIONSCAN)) { + if (flags != DICT_EXACT && !shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) { msg_hist_off = true; // reset in msg_trunc() msg_ext_set_kind("completion"); vim_snprintf(IObuff, IOSIZE, @@ -1876,7 +1902,8 @@ static void ins_compl_files(int count, char **files, bool thesaurus, int flags, // Read dictionary file line by line. // Check each line for a match. - while (!got_int && !compl_interrupted && !vim_fgets(buf, LSIZE, fp)) { + while (!got_int && !compl_interrupted && !compl_time_slice_expired + && !vim_fgets(buf, LSIZE, fp)) { char *ptr = buf; if (regmatch != NULL) { while (vim_regexec(regmatch, buf, (colnr_T)(ptr - buf))) { @@ -2025,6 +2052,9 @@ void ins_compl_clear(void) API_CLEAR_STRING(compl_orig_text); compl_enter_selects = false; cpt_sources_clear(); + compl_autocomplete = false; + compl_from_nonkeyword = false; + compl_num_bests = 0; // clear v:completed_item set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc_lock(VAR_FIXED)); } @@ -2085,7 +2115,7 @@ int ins_compl_len(void) static bool ins_compl_has_preinsert(void) { return (get_cot_flags() & (kOptCotFlagFuzzy|kOptCotFlagPreinsert|kOptCotFlagMenuone)) - == (kOptCotFlagPreinsert|kOptCotFlagMenuone); + == (kOptCotFlagPreinsert|kOptCotFlagMenuone) && !compl_autocomplete; } /// Returns true if the pre-insert effect is valid and the cursor is within @@ -2187,6 +2217,9 @@ static void ins_compl_new_leader(void) // Matches were cleared, need to search for them now. // Set "compl_restarting" to avoid that the first match is inserted. compl_restarting = true; + if (p_ac) { + compl_autocomplete = true; + } if (ins_complete(Ctrl_N, true) == FAIL) { compl_cont_status = 0; } @@ -2282,6 +2315,9 @@ static void ins_compl_restart(void) compl_cont_status = 0; compl_cont_mode = 0; cpt_sources_clear(); + compl_autocomplete = false; + compl_from_nonkeyword = false; + compl_num_bests = 0; } /// Set the first match, the original text. @@ -2574,6 +2610,9 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval) edit_submode = NULL; redraw_mode = true; } + compl_autocomplete = false; + compl_from_nonkeyword = false; + compl_best_matches = 0; if (c == Ctrl_C && cmdwin_type != 0) { // Avoid the popup menu remains displayed when leaving the @@ -3215,7 +3254,11 @@ void f_complete_check(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) RedrawingDisabled = 0; ins_compl_check_keys(0, true); - rettv->vval.v_number = ins_compl_interrupted(); + if (compl_autocomplete && compl_time_slice_expired) { + rettv->vval.v_number = true; + } else { + rettv->vval.v_number = ins_compl_interrupted(); + } RedrawingDisabled = saved; } @@ -3554,6 +3597,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar { int compl_type = -1; int status = INS_COMPL_CPT_OK; + bool skip_source = compl_autocomplete && compl_from_nonkeyword; st->found_all = false; *advance_cpt_idx = false; @@ -3562,7 +3606,8 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar st->e_cpt++; } - if (*st->e_cpt == '.' && !curbuf->b_scanned) { + if (*st->e_cpt == '.' && !curbuf->b_scanned && !skip_source + && !compl_time_slice_expired) { st->ins_buf = curbuf; st->first_match_pos = *start_match_pos; // Move the cursor back one character so that ^N can match the @@ -3580,7 +3625,8 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar // Remember the first match so that the loop stops when we // wrap and come back there a second time. st->set_match_pos = true; - } else if (vim_strchr("buwU", (uint8_t)(*st->e_cpt)) != NULL + } else if (!skip_source && !compl_time_slice_expired + && vim_strchr("buwU", (uint8_t)(*st->e_cpt)) != NULL && (st->ins_buf = ins_compl_next_buf(st->ins_buf, *st->e_cpt)) != curbuf) { // Scan a buffer, but not the current one. if (st->ins_buf->b_ml.ml_mfp != NULL) { // loaded buffer @@ -3599,7 +3645,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar st->dict = st->ins_buf->b_fname; st->dict_f = DICT_EXACT; } - if (!shortmess(SHM_COMPLETIONSCAN)) { + if (!shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) { msg_hist_off = true; // reset in msg_trunc() msg_ext_set_kind("completion"); vim_snprintf(IObuff, IOSIZE, _("Scanning: %s"), @@ -3615,35 +3661,37 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar } else { if (ctrl_x_mode_line_or_eval()) { // compl_type = -1; - } else if (*st->e_cpt == 'k' || *st->e_cpt == 's') { - if (*st->e_cpt == 'k') { - compl_type = CTRL_X_DICTIONARY; - } else { - compl_type = CTRL_X_THESAURUS; - } - if (*++st->e_cpt != ',' && *st->e_cpt != NUL) { - st->dict = st->e_cpt; - st->dict_f = DICT_FIRST; - } } else if (*st->e_cpt == 'F' || *st->e_cpt == 'o') { compl_type = CTRL_X_FUNCTION; st->func_cb = get_callback_if_cpt_func(st->e_cpt); if (!st->func_cb) { compl_type = -1; } - } else if (*st->e_cpt == 'i') { - compl_type = CTRL_X_PATH_PATTERNS; - } else if (*st->e_cpt == 'd') { - compl_type = CTRL_X_PATH_DEFINES; - } else if (*st->e_cpt == 'f') { - compl_type = CTRL_X_BUFNAMES; - } else if (*st->e_cpt == ']' || *st->e_cpt == 't') { - compl_type = CTRL_X_TAGS; - if (!shortmess(SHM_COMPLETIONSCAN)) { - msg_ext_set_kind("completion"); - msg_hist_off = true; // reset in msg_trunc() - vim_snprintf(IObuff, IOSIZE, "%s", _("Scanning tags.")); - msg_trunc(IObuff, true, HLF_R); + } else if (!skip_source) { + if (*st->e_cpt == 'k' || *st->e_cpt == 's') { + if (*st->e_cpt == 'k') { + compl_type = CTRL_X_DICTIONARY; + } else { + compl_type = CTRL_X_THESAURUS; + } + if (*++st->e_cpt != ',' && *st->e_cpt != NUL) { + st->dict = st->e_cpt; + st->dict_f = DICT_FIRST; + } + } else if (*st->e_cpt == 'i') { + compl_type = CTRL_X_PATH_PATTERNS; + } else if (*st->e_cpt == 'd') { + compl_type = CTRL_X_PATH_DEFINES; + } else if (*st->e_cpt == 'f') { + compl_type = CTRL_X_BUFNAMES; + } else if (*st->e_cpt == ']' || *st->e_cpt == 't') { + compl_type = CTRL_X_TAGS; + if (!shortmess(SHM_COMPLETIONSCAN) && !compl_autocomplete) { + msg_ext_set_kind("completion"); + msg_hist_off = true; // reset in msg_trunc() + vim_snprintf(IObuff, IOSIZE, "%s", _("Scanning tags.")); + msg_trunc(IObuff, true, HLF_R); + } } } @@ -4066,7 +4114,8 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ { char *ptr = NULL; int len = 0; - bool in_collect = (cfc_has_mode() && compl_length > 0); + bool in_fuzzy_collect = (cfc_has_mode() && compl_length > 0) + || ((get_cot_flags() & kOptCotFlagFuzzy) && compl_autocomplete); char *leader = ins_compl_leader(); int score = 0; const bool in_curbuf = st->ins_buf == curbuf; @@ -4095,7 +4144,7 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ msg_silent++; // Don't want messages for wrapscan. - if (in_collect) { + if (in_fuzzy_collect) { found_new_match = search_for_fuzzy_match(st->ins_buf, st->cur_match_pos, leader, compl_direction, start_pos, &len, &ptr, &score); @@ -4152,7 +4201,7 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ continue; } - if (!in_collect) { + if (!in_fuzzy_collect) { ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos, &len, &cont_s_ipos); } @@ -4172,7 +4221,7 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ if (ins_compl_add_infercase(ptr, len, p_ic, in_curbuf ? NULL : st->ins_buf->b_sfname, 0, cont_s_ipos, score) != NOTDONE) { - if (in_collect && score == compl_first_match->cp_next->cp_score) { + if (in_fuzzy_collect && score == compl_first_match->cp_next->cp_score) { compl_num_bests++; } found_new_match = OK; @@ -4453,6 +4502,15 @@ static void prepare_cpt_compl_funcs(void) xfree(cpt); } +/// Start the timer for the current completion source. +static void compl_source_start_timer(int source_idx) +{ + if (compl_autocomplete && cpt_sources_array != NULL) { + cpt_sources_array[source_idx].compl_start_tv = os_hrtime(); + compl_time_slice_expired = false; + } +} + /// Safely advance the cpt_sources_index by one. static int advance_cpt_sources_index_safe(void) { @@ -4464,6 +4522,8 @@ static int advance_cpt_sources_index_safe(void) return FAIL; } +#define COMPL_FUNC_TIMEOUT_MS 300 +#define COMPL_FUNC_TIMEOUT_NON_KW_MS 1000 /// Get the next expansion(s), using "compl_pattern". /// The search starts at position "ini" in curbuf and in the direction /// compl_direction. @@ -4478,6 +4538,7 @@ static int ins_compl_get_exp(pos_T *ini) int found_new_match; int type = ctrl_x_mode; bool may_advance_cpt_idx = false; + pos_T start_pos = *ini; assert(curbuf != NULL); @@ -4496,7 +4557,14 @@ static int ins_compl_get_exp(pos_T *ini) st.e_cpt_copy = xstrdup((compl_cont_status & CONT_LOCAL) ? "." : curbuf->b_p_cpt); strip_caret_numbers_in_place(st.e_cpt_copy); st.e_cpt = st.e_cpt_copy; - st.last_match_pos = st.first_match_pos = *ini; + + // In large buffers, timeout may miss nearby matches — search above cursor +#define LOOKBACK_LINE_COUNT 1000 + if (compl_autocomplete && is_nearest_active()) { + start_pos.lnum = MAX(1, start_pos.lnum - LOOKBACK_LINE_COUNT); + start_pos.col = 0; + } + st.last_match_pos = st.first_match_pos = start_pos; } else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf)) { st.ins_buf = curbuf; // In case the buffer was wiped out. } @@ -4508,6 +4576,10 @@ static int ins_compl_get_exp(pos_T *ini) if (cpt_sources_array != NULL && ctrl_x_mode_normal() && !ctrl_x_mode_line_or_eval() && !(compl_cont_status & CONT_LOCAL)) { cpt_sources_index = 0; + if (compl_autocomplete) { + compl_source_start_timer(0); + compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS; + } } // For ^N/^P loop over all the flags/windows/buffers in 'complete' @@ -4520,14 +4592,17 @@ static int ins_compl_get_exp(pos_T *ini) // entries from 'complete' that look in loaded buffers. if ((ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval()) && (!compl_started || st.found_all)) { - int status = process_next_cpt_value(&st, &type, ini, + int status = process_next_cpt_value(&st, &type, &start_pos, cfc_has_mode(), &may_advance_cpt_idx); if (status == INS_COMPL_CPT_END) { break; } if (status == INS_COMPL_CPT_CONT) { - if (may_advance_cpt_idx && !advance_cpt_sources_index_safe()) { - break; + if (may_advance_cpt_idx) { + if (!advance_cpt_sources_index_safe()) { + break; + } + compl_source_start_timer(cpt_sources_index); } continue; } @@ -4539,11 +4614,24 @@ static int ins_compl_get_exp(pos_T *ini) break; } - // get the next set of completion matches - found_new_match = get_next_completion_match(type, &st, ini); + if (compl_autocomplete && type == CTRL_X_FUNCTION) { + // LSP servers may sporadically take >1s to respond (e.g., while + // loading modules), but other sources might already have matches. + // To show results quickly use a short timeout for keyword + // completion. Allow longer timeout for non-keyword completion + // where only function based sources (e.g. LSP) are active. + compl_timeout_ms = compl_from_nonkeyword + ? COMPL_FUNC_TIMEOUT_NON_KW_MS : COMPL_FUNC_TIMEOUT_MS; + } - if (may_advance_cpt_idx && !advance_cpt_sources_index_safe()) { - break; + // get the next set of completion matches + found_new_match = get_next_completion_match(type, &st, &start_pos); + + if (may_advance_cpt_idx) { + if (!advance_cpt_sources_index_safe()) { + break; + } + compl_source_start_timer(cpt_sources_index); } // break the loop for specialized modes (use 'complete' just for the @@ -4562,7 +4650,7 @@ static int ins_compl_get_exp(pos_T *ini) || compl_interrupted) { break; } - compl_started = true; + compl_started = !compl_time_slice_expired; } else { // Mark a buffer scanned when it has been scanned completely if (buf_valid(st.ins_buf) && (type == 0 || type == CTRL_X_PATH_PATTERNS)) { @@ -4573,6 +4661,11 @@ static int ins_compl_get_exp(pos_T *ini) compl_started = false; } + // Reset the timeout after collecting matches from function source + if (compl_autocomplete && type == CTRL_X_FUNCTION) { + compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS; + } + // For `^P` completion, reset `compl_curr_match` to the head to avoid // mixing matches from different sources. if (!compl_dir_forward()) { @@ -4858,7 +4951,7 @@ static int find_next_completion_match(bool allow_get_expansion, int todo, bool a bool found_end = false; compl_T *found_compl = NULL; unsigned cur_cot_flags = get_cot_flags(); - bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0; + bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 || compl_autocomplete; bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0; while (--todo >= 0) { @@ -4969,7 +5062,7 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match const bool started = compl_started; buf_T *const orig_curbuf = curbuf; unsigned cur_cot_flags = get_cot_flags(); - bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0; + bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0 || compl_autocomplete; bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0; bool compl_preinsert = ins_compl_has_preinsert(); @@ -5048,7 +5141,7 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match // Enter will select a match when the match wasn't inserted and the popup // menu is visible. - if (compl_no_insert && !started) { + if (compl_no_insert && !started && compl_selected_item != -1) { compl_enter_selects = true; } else { compl_enter_selects = !insert_match && compl_match_array != NULL; @@ -5062,6 +5155,23 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match return num_matches; } +/// Check if the current completion source exceeded its timeout. If so, stop +/// collecting, and halve the timeout. +static void check_elapsed_time(void) +{ + if (cpt_sources_array == NULL) { + return; + } + + uint64_t start_tv = cpt_sources_array[cpt_sources_index].compl_start_tv; + uint64_t elapsed_ms = (os_hrtime() - start_tv) / 1000000; + + if (elapsed_ms > compl_timeout_ms) { + compl_time_slice_expired = true; + DECAY_COMPL_TIMEOUT(); + } +} + /// Call this while finding completions, to check whether the user has hit a key /// that should change the currently displayed completion, or exit completion /// mode. Also, when compl_pending is not zero, show a completion as soon as @@ -5109,8 +5219,14 @@ void ins_compl_check_keys(int frequency, bool in_compl_func) vungetc(c); } } + } else if (compl_autocomplete) { + check_elapsed_time(); } - if (compl_pending != 0 && !got_int && !(cot_flags & kOptCotFlagNoinsert)) { + + if (compl_pending != 0 && !got_int && !(cot_flags & kOptCotFlagNoinsert) + && !compl_autocomplete) { + // Insert the first match immediately and advance compl_shown_match, + // before finding other matches. int todo = compl_pending > 0 ? compl_pending : -compl_pending; compl_pending = 0; @@ -5229,6 +5345,7 @@ static int get_normal_compl_info(char *line, int startcol, colnr_T curs_col) compl_pattern = cbuf_to_string(S_LEN("\\<\\k\\k")); compl_col += curs_col; compl_length = 0; + compl_from_nonkeyword = true; } else { // Search the point of change class of multibyte character // or not a word single byte character backward. @@ -5652,7 +5769,7 @@ static int ins_compl_start(void) compl_startpos.col = compl_col; } - if (!shortmess(SHM_COMPLETIONMENU)) { + if (!shortmess(SHM_COMPLETIONMENU) && !compl_autocomplete) { if (compl_cont_status & CONT_LOCAL) { edit_submode = _(ctrl_x_msgs[CTRL_X_LOCAL_MSG]); } else { @@ -5685,7 +5802,7 @@ static int ins_compl_start(void) // showmode might reset the internal line pointers, so it must // be called before line = ml_get(), or when this address is no // longer needed. -- Acevedo. - if (!shortmess(SHM_COMPLETIONMENU)) { + if (!shortmess(SHM_COMPLETIONMENU) && !compl_autocomplete) { edit_submode_extra = _("-- Searching..."); edit_submode_highl = HLF_COUNT; showmode(); @@ -5824,7 +5941,7 @@ int ins_complete(int c, bool enable_pum) compl_cont_status &= ~CONT_S_IPOS; } - if (!shortmess(SHM_COMPLETIONMENU)) { + if (!shortmess(SHM_COMPLETIONMENU) && !compl_autocomplete) { ins_compl_show_statusmsg(); } @@ -5838,6 +5955,17 @@ int ins_complete(int c, bool enable_pum) return OK; } +/// Returns true if the given character 'c' can be used to trigger +/// autocompletion. +bool ins_compl_setup_autocompl(int c) +{ + if (vim_isprintc(c)) { + compl_autocomplete = true; + return true; + } + return false; +} + /// Remove (if needed) and show the popup menu static void show_pum(int prev_w_wrow, int prev_w_leftcol) { @@ -6095,6 +6223,7 @@ static void get_cpt_func_completion_matches(Callback *cb) set_compl_globals(startcol, curwin->w_cursor.col, true); expand_by_function(0, cpt_compl_pattern.data, cb); + cpt_sources_array[cpt_sources_index].cs_refresh_always = compl_opt_refresh_always; compl_opt_refresh_always = false; } @@ -6133,6 +6262,7 @@ static void cpt_compl_refresh(void) } cpt_sources_array[cpt_sources_index].cs_startcol = startcol; if (ret == OK) { + compl_source_start_timer(cpt_sources_index); get_cpt_func_completion_matches(cb); } } else { diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index 089695fb2c..71d97e49d2 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -302,6 +302,7 @@ EXTERN char *p_cia; ///< 'completeitemalign' EXTERN unsigned cia_flags; ///< order flags of 'completeitemalign' EXTERN char *p_cot; ///< 'completeopt' EXTERN unsigned cot_flags; ///< flags from 'completeopt' +EXTERN int p_ac; ///< 'autocomplete' #ifdef BACKSLASH_IN_FILENAME EXTERN char *p_csl; ///< 'completeslash' #endif diff --git a/src/nvim/options.lua b/src/nvim/options.lua index aa6f8ee966..88f0fa7417 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -228,6 +228,19 @@ local options = { type = 'boolean', varname = 'p_acd', }, + { + abbreviation = 'ac', + defaults = false, + desc = [=[ + When on, Vim shows a completion menu as you type, similar to using + |i_CTRL-N|, but triggered automatically. See |ins-autocompletion|. + ]=], + full_name = 'autocomplete', + scope = { 'global' }, + short_desc = N_('automatic completion in insert mode'), + type = 'boolean', + varname = 'p_ac', + }, { abbreviation = 'ai', defaults = true, @@ -1465,9 +1478,9 @@ local options = { If the Dict returned by the {func} includes {"refresh": "always"}, the function will be invoked again whenever the leading text changes. - If generating matches is potentially slow, |complete_check()| - should be used to avoid blocking and preserve editor - responsiveness. + If generating matches is potentially slow, call + |complete_check()| periodically to keep Vim responsive. This + is especially important for |ins-autocompletion|. F equivalent to using "F{func}", where the function is taken from the 'completefunc' option. o equivalent to using "F{func}", where the function is taken from @@ -1653,6 +1666,9 @@ local options = { completion in the preview window. Only works in combination with "menu" or "menuone". + Only "fuzzy", "popup" and "preview" have an effect when 'autocomplete' + is enabled. + This option does not apply to |cmdline-completion|. See 'wildoptions' for that. ]=], diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 08686f465f..573441e6e3 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -563,30 +563,41 @@ endfunc func Test_cpt_func_cursorcol() func CptColTest(findstart, query) if a:findstart - call assert_equal("foo bar", getline(1)) - call assert_equal(8, col('.')) + call assert_equal(b:info_compl_line, getline(1)) + call assert_equal(b:info_cursor_col, col('.')) return col('.') endif - call assert_equal("foo ", getline(1)) - call assert_equal(5, col('.')) + call assert_equal(b:expn_compl_line, getline(1)) + call assert_equal(b:expn_cursor_col, col('.')) " return v:none + return [] endfunc set complete=FCptColTest new - call feedkeys("ifoo bar\", "tx") - bwipe! - new + + " Replace mode + let b:info_compl_line = "foo barxyz" + let b:expn_compl_line = "foo barbaz" + let b:info_cursor_col = 10 + let b:expn_cursor_col = 5 + call feedkeys("ifoo barbaz\2hRxy\", "tx") + + " Insert mode + let b:info_compl_line = "foo bar" + let b:expn_compl_line = "foo " + let b:info_cursor_col = 8 + let b:expn_cursor_col = 5 + call feedkeys("Sfoo bar\", "tx") + set completeopt=longest - call feedkeys("ifoo bar\", "tx") - bwipe! - new + call feedkeys("Sfoo bar\", "tx") + set completeopt=menuone - call feedkeys("ifoo bar\", "tx") - bwipe! - new + call feedkeys("Sfoo bar\", "tx") + set completeopt=menuone,preinsert - call feedkeys("ifoo bar\", "tx") + call feedkeys("Sfoo bar\", "tx") bwipe! set complete& completeopt& delfunc CptColTest @@ -3725,7 +3736,7 @@ func Test_cfc_with_longest() exe "normal ggdGShello helio heo\\\" call assert_equal("hello helio heo", getline('.')) - " kdcit + " dict call writefile(['help'], 'test_keyword.txt', 'D') set complete=ktest_keyword.txt exe "normal ggdGSh\\" @@ -4943,6 +4954,27 @@ func Test_complete_fuzzy_omnifunc_backspace() unlet g:do_complete endfunc +" Test that option shortmess=c turns off completion messages +func Test_shortmess() + CheckScreendump + + let lines =<< trim END + call setline(1, ['hello', 'hullo', 'heee']) + END + + call writefile(lines, 'Xpumscript', 'D') + let buf = RunVimInTerminal('-S Xpumscript', #{rows: 12}) + call term_sendkeys(buf, "Goh\") + call TermWait(buf, 200) + call VerifyScreenDump(buf, 'Test_shortmess_complmsg_1', {}) + call term_sendkeys(buf, "\:set shm+=c\") + call term_sendkeys(buf, "Sh\") + call TermWait(buf, 200) + call VerifyScreenDump(buf, 'Test_shortmess_complmsg_2', {}) + + call StopVimInTerminal(buf) +endfunc + " Test 'complete' containing F{func} that complete from nonkeyword func Test_nonkeyword_trigger() @@ -5059,25 +5091,321 @@ func Test_nonkeyword_trigger() unlet g:CallCount endfunc -" Test that option shortmess=c turns off completion messages -func Test_shortmess() - CheckScreendump +func Test_autocomplete_trigger() + " Trigger expansion even when another char is waiting in the typehead + call Ntest_override("char_avail", 1) - let lines =<< trim END - call setline(1, ['hello', 'hullo', 'heee']) - END + let g:CallCount = 0 + func! NonKeywordComplete(findstart, base) + let line = getline('.')->strpart(0, col('.') - 1) + let nonkeyword2 = len(line) > 1 && match(line[-2:-2], '\k') != 0 + if a:findstart + return nonkeyword2 ? col('.') - 3 : (col('.') - 2) + else + let g:CallCount += 1 + return [$"{a:base}foo", $"{a:base}bar"] + endif + endfunc - call writefile(lines, 'Xpumscript', 'D') - let buf = RunVimInTerminal('-S Xpumscript', #{rows: 12}) - call term_sendkeys(buf, "Goh\") - call TermWait(buf, 200) - call VerifyScreenDump(buf, 'Test_shortmess_complmsg_1', {}) - call term_sendkeys(buf, "\:set shm+=c\") - call term_sendkeys(buf, "Sh\") - call TermWait(buf, 200) - call VerifyScreenDump(buf, 'Test_shortmess_complmsg_2', {}) + new + inoremap let b:matches = complete_info(["matches"]).matches + inoremap let b:selected = complete_info(["selected"]).selected - call StopVimInTerminal(buf) + call setline(1, ['abc', 'abcd', 'fo', 'b', '']) + set autocomplete + + " Test 1a: Nonkeyword doesn't open menu without F{func} when autocomplete + call feedkeys("GS=\\0", 'tx!') + call assert_equal([], b:matches) + call assert_equal('=', getline('.')) + " ^N opens menu of keywords (of len > 1) + call feedkeys("S=\\\\0", 'tx!') + call assert_equal(['abc', 'abcd', 'fo'], b:matches->mapnew('v:val.word')) + call assert_equal('=abc', getline('.')) + + " Test 1b: With F{func} nonkeyword collects matches + set complete=.,FNonKeywordComplete + let g:CallCount = 0 + call feedkeys("S=\\0", 'tx!') + call assert_equal(['=foo', '=bar'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + call assert_equal('=', getline('.')) + let g:CallCount = 0 + call feedkeys("S->\\0", 'tx!') + call assert_equal(['->foo', '->bar'], b:matches->mapnew('v:val.word')) + call assert_equal(2, g:CallCount) + call assert_equal('->', getline('.')) + + " Test 1c: Keyword after nonkeyword can collect both types of items + let g:CallCount = 0 + call feedkeys("S#a\\0", 'tx!') + call assert_equal(['abcd', 'abc', '#afoo', '#abar'], b:matches->mapnew('v:val.word')) + call assert_equal(2, g:CallCount) + call assert_equal('#a', getline('.')) + let g:CallCount = 0 + call feedkeys("S#a.\\0", 'tx!') + call assert_equal(['.foo', '.bar'], b:matches->mapnew('v:val.word')) + call assert_equal(3, g:CallCount) + call assert_equal('#a.', getline('.')) + let g:CallCount = 0 + call feedkeys("S#a.a\\0", 'tx!') + call assert_equal(['abcd', 'abc', '.afoo', '.abar'], b:matches->mapnew('v:val.word')) + call assert_equal(4, g:CallCount) + call assert_equal('#a.a', getline('.')) + + " Test 1d: Nonkeyword after keyword collects items again + let g:CallCount = 0 + call feedkeys("Sa\\0", 'tx!') + call assert_equal(['abcd', 'abc', 'afoo', 'abar'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + call assert_equal('a', getline('.')) + let g:CallCount = 0 + call feedkeys("Sa#\\0", 'tx!') + call assert_equal(['#foo', '#bar'], b:matches->mapnew('v:val.word')) + call assert_equal(2, g:CallCount) + call assert_equal('a#', getline('.')) + + " Test 2: Filter nonkeyword and keyword matches with differet startpos + for fuzzy in range(2) + if fuzzy + set completeopt+=fuzzy + endif + call feedkeys("S#ab\\\0", 'tx!') + if fuzzy + call assert_equal(['#abar', 'abc', 'abcd'], b:matches->mapnew('v:val.word')) + else " Ordering of items is by 'nearest' to cursor by default + call assert_equal(['abcd', 'abc', '#abar'], b:matches->mapnew('v:val.word')) + endif + call assert_equal(-1, b:selected) + call assert_equal('#ab', getline('.')) + call feedkeys("S#ab" . repeat("\", 3) . "\\0", 'tx!') + call assert_equal(fuzzy ? '#abcd' : '#abar', getline('.')) + call assert_equal(2, b:selected) + + let g:CallCount = 0 + call feedkeys("GS#aba\\0", 'tx!') + call assert_equal(['#abar'], b:matches->mapnew('v:val.word')) + call assert_equal(2, g:CallCount) + call assert_equal('#aba', getline('.')) + + let g:CallCount = 0 + call feedkeys("S#abc\\0", 'tx!') + if fuzzy + call assert_equal(['abc', 'abcd'], b:matches->mapnew('v:val.word')) + else + call assert_equal(['abcd', 'abc'], b:matches->mapnew('v:val.word')) + endif + call assert_equal(2, g:CallCount) + set completeopt& + endfor + + " Test 3: Navigate menu containing nonkeyword and keyword items + call feedkeys("S#a\\0", 'tx!') + call assert_equal(['abcd', 'abc', '#afoo', '#abar'], b:matches->mapnew('v:val.word')) + call feedkeys("S#a" . repeat("\", 3) . "\0", 'tx!') + call assert_equal('#afoo', getline('.')) + call feedkeys("S#a" . repeat("\", 3) . "\\0", 'tx!') + call assert_equal('#abc', getline('.')) + + call feedkeys("S#a.a\\0", 'tx!') + call assert_equal(['abcd', 'abc', '.afoo', '.abar'], b:matches->mapnew('v:val.word')) + call feedkeys("S#a.a" . repeat("\", 2) . "\0", 'tx!') + call assert_equal('#a.abc', getline('.')) + call feedkeys("S#a.a" . repeat("\", 3) . "\0", 'tx!') + call assert_equal('#a.afoo', getline('.')) + call feedkeys("S#a.a" . repeat("\", 3) . "\\0", 'tx!') + call assert_equal('#a.abc', getline('.')) + call feedkeys("S#a.a" . repeat("\", 6) . "\0", 'tx!') + call assert_equal('#a.abar', getline('.')) + + " Test 4a: When autocomplete menu is active, ^X^N completes buffer keywords + let g:CallCount = 0 + call feedkeys("S#a\\\\0", 'tx!') + call assert_equal(['abc', 'abcd'], b:matches->mapnew('v:val.word')) + call assert_equal(2, g:CallCount) + + " Test 4b: When autocomplete menu is active, ^X^O completes omnifunc + let g:CallCount = 0 + set omnifunc=NonKeywordComplete + call feedkeys("S#a\\\\0", 'tx!') + call assert_equal(['#afoo', '#abar'], b:matches->mapnew('v:val.word')) + call assert_equal(3, g:CallCount) + + " Test 4c: When autocomplete menu is active, ^E^N completes keyword + call feedkeys("Sa\\\0", 'tx!') + call assert_equal([], b:matches->mapnew('v:val.word')) + let g:CallCount = 0 + call feedkeys("Sa\\\\0", 'tx!') + call assert_equal(['abc', 'abcd', 'afoo', 'abar'], b:matches->mapnew('v:val.word')) + call assert_equal(2, g:CallCount) + + " Test 4d: When autocomplete menu is active, ^X^L completes lines + %d + let g:CallCount = 0 + call setline(1, ["afoo bar", "barbar foo", "foo bar", "and"]) + call feedkeys("Goa\\\\0", 'tx!') + call assert_equal(['afoo bar', 'and'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + + " Test 5: When invalid prefix stops completion, backspace should restart it + %d + set complete& + call setline(1, ["afoo bar", "barbar foo", "foo bar", "and"]) + call feedkeys("Goabc\\0", 'tx!') + call assert_equal([], b:matches->mapnew('v:val.word')) + call feedkeys("Sabc\\\\0", 'tx!') + call assert_equal(['and', 'afoo'], b:matches->mapnew('v:val.word')) + call feedkeys("Szx\\\0", 'tx!') + call assert_equal([], b:matches->mapnew('v:val.word')) + call feedkeys("Sazx\\\\0", 'tx!') + call assert_equal(['and', 'afoo'], b:matches->mapnew('v:val.word')) + + bw! + call Ntest_override("char_avail", 0) + delfunc NonKeywordComplete + set autocomplete& + unlet g:CallCount +endfunc + +" Test autocomplete timing +func Test_autocomplete_timer() + + let g:CallCount = 0 + func! TestComplete(delay, check, refresh, findstart, base) + if a:findstart + return col('.') - 1 + else + let g:CallCount += 1 + if a:delay + sleep 310m " Exceed timeout + endif + if a:check + while !complete_check() + sleep 2m + endwhile + " return v:none " This should trigger after interrupted by timeout + return [] + endif + let res = [["ab", "ac", "ad"], ["abb", "abc", "abd"], ["acb", "cc", "cd"]] + if a:refresh + return #{words: res[g:CallCount - 1], refresh: 'always'} + endif + return res[g:CallCount - 1] + endif + endfunc + + " Trigger expansion even when another char is waiting in the typehead + call Ntest_override("char_avail", 1) + + new + inoremap let b:matches = complete_info(["matches"]).matches + inoremap let b:selected = complete_info(["selected"]).selected + set autocomplete + + call setline(1, ['abc', 'bcd', 'cde']) + + " Test 1: When matches are found before timeout expires, it exits + " 'collection' mode and transitions to 'filter' mode. + set complete=.,Ffunction('TestComplete'\\,\ [0\\,\ 0\\,\ 0]) + let g:CallCount = 0 + call feedkeys("Goa\\0", 'tx!') + call assert_equal(['abc', 'ab', 'ac', 'ad'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + + let g:CallCount = 0 + call feedkeys("Sab\\0", 'tx!') + call assert_equal(['abc', 'ab'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + + " Test 2: When timeout expires before all matches are found, it returns + " with partial list but still transitions to 'filter' mode. + set complete=.,Ffunction('TestComplete'\\,\ [1\\,\ 0\\,\ 0]) + let g:CallCount = 0 + call feedkeys("Sab\\0", 'tx!') + call assert_equal(['abc', 'ab'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + + " Test 3: When interrupted by ^N before timeout expires, it remains in + " 'collection' mode without transitioning. + set complete=.,Ffunction('TestComplete'\\,\ [0\\,\ 1\\,\ 0]) + let g:CallCount = 0 + call feedkeys("Sa\b\\0", 'tx!') + call assert_equal(2, g:CallCount) + + let g:CallCount = 0 + call feedkeys("Sa\b\c\\0", 'tx!') + call assert_equal(3, g:CallCount) + + " Test 4: Simulate long running func that is stuck in complete_check() + let g:CallCount = 0 + set complete=.,Ffunction('TestComplete'\\,\ [0\\,\ 1\\,\ 0]) + call feedkeys("Sa\\0", 'tx!') + call assert_equal(['abc'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + + let g:CallCount = 0 + call feedkeys("Sab\\0", 'tx!') + call assert_equal(['abc'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + + " Test 5: refresh:always stays in 'collection' mode + set complete=.,Ffunction('TestComplete'\\,\ [0\\,\ 0\\,\ 1]) + let g:CallCount = 0 + call feedkeys("Sa\\0", 'tx!') + call assert_equal(['abc', 'ab', 'ac', 'ad'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + + let g:CallCount = 0 + call feedkeys("Sab\\0", 'tx!') + call assert_equal(['abc', 'abb', 'abd'], b:matches->mapnew('v:val.word')) + call assert_equal(2, g:CallCount) + + " Test 6: and navigate menu + set complete=.,Ffunction('TestComplete'\\,\ [0\\,\ 0\\,\ 0]) + let g:CallCount = 0 + call feedkeys("Sab\\\\0", 'tx!') + call assert_equal(['abc', 'ab'], b:matches->mapnew('v:val.word')) + call assert_equal(0, b:selected) + call assert_equal(1, g:CallCount) + call feedkeys("Sab\\\\\0", 'tx!') + call assert_equal(1, b:selected) + call feedkeys("Sab\\\\\0", 'tx!') + call assert_equal(-1, b:selected) + + " Test 7: Following 'cot' option values have no effect + set completeopt=menu,menuone,noselect,noinsert,longest,preinsert + set complete=.,Ffunction('TestComplete'\\,\ [0\\,\ 0\\,\ 0]) + let g:CallCount = 0 + call feedkeys("Sab\\\\0", 'tx!') + call assert_equal(['abc', 'ab'], b:matches->mapnew('v:val.word')) + call assert_equal(0, b:selected) + call assert_equal(1, g:CallCount) + call assert_equal('abc', getline(4)) + set completeopt& + + " Test 8: {func} completes after space, but not '.' + set complete=.,Ffunction('TestComplete'\\,\ [0\\,\ 0\\,\ 0]) + let g:CallCount = 0 + call feedkeys("S \\\0", 'tx!') + call assert_equal(['ab', 'ac', 'ad'], b:matches->mapnew('v:val.word')) + call assert_equal(1, g:CallCount) + set complete=. + call feedkeys("S \\\0", 'tx!') + call assert_equal([], b:matches->mapnew('v:val.word')) + + " Test 9: Matches nearest to the cursor are prioritized (by default) + %d + let g:CallCount = 0 + set complete=. + call setline(1, ["fo", "foo", "foobar", "foobarbaz"]) + call feedkeys("jof\\0", 'tx!') + call assert_equal(['foo', 'foobar', 'fo', 'foobarbaz'], b:matches->mapnew('v:val.word')) + + bw! + call Ntest_override("char_avail", 0) + delfunc TestComplete + set autocomplete& complete& + unlet g:CallCount endfunc " vim: shiftwidth=2 sts=2 expandtab nofoldenable