neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit 9c9c7ae30f50685e62984084767905c7d456cdd9
parent 33b0a004eb20fd5a0013b832414596190c059828
Author: zeertzjq <zeertzjq@outlook.com>
Date:   Tue,  3 Jun 2025 07:00:25 +0800

Merge pull request #34247 from zeertzjq/vim-9.1.1301

vim-patch: various 'complete' features
Diffstat:
Mruntime/doc/insert.txt | 3+++
Mruntime/doc/news.txt | 5+++++
Mruntime/doc/options.txt | 29+++++++++++++++++++++++++++++
Mruntime/lua/vim/_meta/options.lua | 29+++++++++++++++++++++++++++++
Msrc/nvim/insexpand.c | 643++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/nvim/options.lua | 31++++++++++++++++++++++++++++++-
Msrc/nvim/optionstr.c | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/nvim/popupmenu.h | 1+
Mtest/old/testdir/test_ins_complete.vim | 958+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtest/old/testdir/test_options.vim | 17+++++++++++++++++
10 files changed, 1702 insertions(+), 99 deletions(-)

diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt @@ -1178,6 +1178,9 @@ For example, the function can contain this: > let matches = ... list of words ... return {'words': matches, 'refresh': 'always'} < +If looking for matches is time-consuming, |complete_check()| may be used to +maintain responsiveness. + *complete-items* Each list item can either be a string or a Dictionary. When it is a string it is used as the completion. When it is a Dictionary it can contain these diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt @@ -178,6 +178,11 @@ OPTIONS • 'chistory' and 'lhistory' set size of the |quickfix-stack|. • 'completefuzzycollect' enables fuzzy collection of candidates for (some) |ins-completion| modes. +• 'complete' new flags: + • "F{func}" complete using given function + • "F" complete using 'completefunc' + • "o" complete using 'omnifunc' +• 'complete' allows limiting matches for sources using "{flag}^<limit>". • 'completeopt' flag "nearset" sorts completion results by distance to cursor. • 'diffopt' `inline:` configures diff highlighting for changes within a line. • 'grepformat' is now a |global-local| option. diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt @@ -1506,6 +1506,28 @@ A jump table for the options with a short description can be found at |Q_op|. ] tag completion t same as "]" f scan the buffer names (as opposed to buffer contents) + F{func} call the function {func}. Multiple "F" flags may be specified. + Refer to |complete-functions| for details on how the function + is invoked and what it should return. The value can be the + name of a function or a |Funcref|. For |Funcref| values, + spaces must be escaped with a backslash ('\'), and commas with + double backslashes ('\\') (see |option-backslash|). + If the Dict returned by the {func} includes {"refresh": "always"}, + the function will be invoked again whenever the leading text + changes. + Completion matches are always inserted at the keyword + boundary, regardless of the column returned by {func} when + a:findstart is 1. This ensures compatibility with other + completion sources. + To make further modifications to the inserted text, {func} + can make use of |CompleteDonePre|. + If generating matches is potentially slow, |complete_check()| + should be used to avoid blocking and preserve editor + responsiveness. + 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 + the 'omnifunc' option. Unloaded buffers are not loaded, thus their autocmds |:autocmd| are not executed, this may lead to unexpected completions from some files @@ -1516,6 +1538,13 @@ A jump table for the options with a short description can be found at |Q_op|. based expansion (e.g., dictionary |i_CTRL-X_CTRL-K|, included patterns |i_CTRL-X_CTRL-I|, tags |i_CTRL-X_CTRL-]| and normal expansions). + An optional match limit can be specified for a completion source by + appending a caret ("^") followed by a {count} to the source flag. + For example: ".^9,w,u,t^5" limits matches from the current buffer + to 9 and from tags to 5. Other sources remain unlimited. + Note: The match limit takes effect only during forward completion + (CTRL-N) and is ignored during backward completion (CTRL-P). + *'completefunc'* *'cfu'* 'completefunc' 'cfu' string (default "") local to buffer diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua @@ -1027,6 +1027,28 @@ vim.bo.cms = vim.bo.commentstring --- ] tag completion --- t same as "]" --- f scan the buffer names (as opposed to buffer contents) +--- F{func} call the function {func}. Multiple "F" flags may be specified. +--- Refer to `complete-functions` for details on how the function +--- is invoked and what it should return. The value can be the +--- name of a function or a `Funcref`. For `Funcref` values, +--- spaces must be escaped with a backslash ('\'), and commas with +--- double backslashes ('\\') (see `option-backslash`). +--- If the Dict returned by the {func} includes {"refresh": "always"}, +--- the function will be invoked again whenever the leading text +--- changes. +--- Completion matches are always inserted at the keyword +--- boundary, regardless of the column returned by {func} when +--- a:findstart is 1. This ensures compatibility with other +--- completion sources. +--- To make further modifications to the inserted text, {func} +--- can make use of `CompleteDonePre`. +--- If generating matches is potentially slow, `complete_check()` +--- should be used to avoid blocking and preserve editor +--- responsiveness. +--- 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 +--- the 'omnifunc' option. --- --- Unloaded buffers are not loaded, thus their autocmds `:autocmd` are --- not executed, this may lead to unexpected completions from some files @@ -1037,6 +1059,13 @@ vim.bo.cms = vim.bo.commentstring --- based expansion (e.g., dictionary `i_CTRL-X_CTRL-K`, included patterns --- `i_CTRL-X_CTRL-I`, tags `i_CTRL-X_CTRL-]` and normal expansions). --- +--- An optional match limit can be specified for a completion source by +--- appending a caret ("^") followed by a {count} to the source flag. +--- For example: ".^9,w,u,t^5" limits matches from the current buffer +--- to 9 and from tags to 5. Other sources remain unlimited. +--- Note: The match limit takes effect only during forward completion +--- (CTRL-N) and is ignored during backward completion (CTRL-P). +--- --- @type string vim.o.complete = ".,w,b,u,t" vim.o.cpt = vim.o.complete diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c @@ -25,6 +25,7 @@ #include "nvim/edit.h" #include "nvim/errors.h" #include "nvim/eval.h" +#include "nvim/eval/executor.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" #include "nvim/eval/userfunc.h" @@ -174,6 +175,7 @@ struct compl_S { bool cp_in_match_array; ///< collected by compl_match_array int cp_user_abbr_hlattr; ///< highlight attribute for abbr int cp_user_kind_hlattr; ///< highlight attribute for kind + int cp_cpt_source_idx; ///< index of this match's source in 'cpt' option }; /// state information used for getting the next set of insert completion @@ -190,6 +192,7 @@ typedef struct { bool found_all; ///< found all matches of a certain type. char *dict; ///< dictionary file to search int dict_f; ///< "dict" is an exact file name or not + Callback *func_cb; ///< callback of function in 'cpt' option } ins_compl_next_state_T; #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -213,7 +216,7 @@ static const char e_compldel[] = N_("E840: Completion function deleted text"); // "compl_first_match" points to the start of the list. // "compl_curr_match" points to the currently selected entry. // "compl_shown_match" is different from compl_curr_match during -// ins_compl_get_exp(). +// ins_compl_get_exp(), when new matches are added to the list. // "compl_old_match" points to previous "compl_curr_match". static compl_T *compl_first_match = NULL; @@ -259,7 +262,8 @@ static bool compl_started = false; static int ctrl_x_mode = CTRL_X_NORMAL; static int compl_matches = 0; ///< number of completion matches -static String compl_pattern = STRING_INIT; +static String compl_pattern = STRING_INIT; ///< search pattern for matching items +static String cpt_compl_pattern = STRING_INIT; ///< pattern returned by func in 'cpt' static Direction compl_direction = FORWARD; static Direction compl_shows_dir = FORWARD; static int compl_pending = 0; ///< > 1 for postponed CTRL-N @@ -302,6 +306,20 @@ static int compl_selected_item = -1; static int *compl_fuzzy_scores; +/// Define the structure for completion source (in 'cpt' option) information +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 +} cpt_source_T; + +/// Pointer to the array of completion sources +static cpt_source_T *cpt_sources_array; +/// Total number of completion sources specified in the 'cpt' option +static int cpt_sources_count; +/// Index of the current completion source being expanded +static int cpt_sources_index = -1; + // "compl_match_array" points the currently displayed list of entries in the // popup menu. It is NULL when there is no popup menu. static pumitem_T *compl_match_array = NULL; @@ -975,7 +993,6 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons match = xcalloc(1, sizeof(compl_T)); match->cp_number = flags & CP_ORIGINAL_TEXT ? 0 : -1; match->cp_str = cbuf_to_string(str, (size_t)len); - match->cp_score = score; // match-fname is: // - compl_curr_match->cp_fname if it is a string equal to fname. @@ -995,6 +1012,8 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons match->cp_flags = flags; match->cp_user_abbr_hlattr = user_hl ? user_hl[0] : -1; match->cp_user_kind_hlattr = user_hl ? user_hl[1] : -1; + match->cp_score = score; + match->cp_cpt_source_idx = cpt_sources_index; if (cptext != NULL) { for (int i = 0; i < CPT_COUNT; i++) { @@ -1344,6 +1363,65 @@ static void trigger_complete_changed_event(int cur) restore_v_event(v_event, &save_v_event); } +/// Trim compl_match_array to enforce max_matches per completion source. +/// +/// Note: This special-case trimming is a workaround because compl_match_array +/// becomes inconsistent with compl_first_match (list) after former is sorted by +/// fuzzy score. The two structures end up in different orders. +/// Ideally, compl_first_match list should have been sorted instead. +/// +/// Returns recalculated index of shown match. +static int trim_compl_match_array(int shown_match_idx) +{ + int remove_count = 0; + + // Count current matches per source. + int *match_counts = xcalloc((size_t)cpt_sources_count, sizeof(int)); + for (int i = 0; i < compl_match_arraysize; i++) { + int src_idx = compl_match_array[i].pum_cpt_source_idx; + if (src_idx != -1) { + match_counts[src_idx]++; + } + } + + // Calculate size of trimmed array, respecting max_matches per source. + int new_size = 0; + for (int i = 0; i < cpt_sources_count; i++) { + int limit = cpt_sources_array[i].cs_max_matches; + new_size += (limit > 0 && match_counts[i] > limit) ? limit : match_counts[i]; + } + + if (new_size == compl_match_arraysize) { + goto theend; + } + + // Create trimmed array while enforcing per-source limits + pumitem_T *trimmed = xcalloc((size_t)new_size, sizeof(pumitem_T)); + memset(match_counts, 0, sizeof(int) * (size_t)cpt_sources_count); + int trimmed_idx = 0; + for (int i = 0; i < compl_match_arraysize; i++) { + int src_idx = compl_match_array[i].pum_cpt_source_idx; + if (src_idx != -1) { + int limit = cpt_sources_array[src_idx].cs_max_matches; + if (limit <= 0 || match_counts[src_idx] < limit) { + trimmed[trimmed_idx++] = compl_match_array[i]; + match_counts[src_idx]++; + } else if (i < shown_match_idx) { + remove_count++; + } + } else { + trimmed[trimmed_idx++] = compl_match_array[i]; + } + } + xfree(compl_match_array); + compl_match_array = trimmed; + compl_match_arraysize = new_size; + +theend: + xfree(match_counts); + return shown_match_idx - remove_count; +} + /// pumitem qsort compare func static int ins_compl_fuzzy_cmp(const void *a, const void *b) { @@ -1377,6 +1455,10 @@ static int ins_compl_build_pum(void) bool fuzzy_sort = fuzzy_filter && !(cur_cot_flags & kOptCotFlagNosort); compl_T *match_head = NULL, *match_tail = NULL; + int match_count = 0; + int cur_source = -1; + bool max_matches_found = false; + bool is_forward = compl_shows_dir_forward(); // If the current match is the original text don't find the first // match after it, don't highlight anything. @@ -1411,7 +1493,21 @@ static int ins_compl_build_pum(void) comp->cp_flags &= ~CP_ICASE; } + if (is_forward && !fuzzy_sort && comp->cp_cpt_source_idx != -1) { + if (cur_source != comp->cp_cpt_source_idx) { + cur_source = comp->cp_cpt_source_idx; + match_count = 1; + max_matches_found = false; + } else if (cpt_sources_array && !max_matches_found) { + int max_matches = cpt_sources_array[cur_source].cs_max_matches; + if (max_matches > 0 && match_count > max_matches) { + max_matches_found = true; + } + } + } + if (!match_at_original_text(comp) + && !max_matches_found && (compl_leader.data == NULL || ins_compl_equal(comp, compl_leader.data, compl_leader.size) || (fuzzy_filter && comp->cp_score > 0))) { @@ -1454,6 +1550,9 @@ static int ins_compl_build_pum(void) shown_match_ok = true; } } + if (is_forward && !fuzzy_sort && comp->cp_cpt_source_idx != -1) { + match_count++; + } i++; } @@ -1495,6 +1594,7 @@ static int ins_compl_build_pum(void) compl_match_array[i].pum_kind = comp->cp_text[CPT_KIND]; compl_match_array[i].pum_info = comp->cp_text[CPT_INFO]; compl_match_array[i].pum_score = comp->cp_score; + compl_match_array[i].pum_cpt_source_idx = comp->cp_cpt_source_idx; compl_match_array[i].pum_user_abbr_hlattr = comp->cp_user_abbr_hlattr; compl_match_array[i].pum_user_kind_hlattr = comp->cp_user_kind_hlattr; compl_match_array[i++].pum_extra = comp->cp_text[CPT_MENU] != NULL @@ -1514,6 +1614,9 @@ static int ins_compl_build_pum(void) shown_match_ok = true; } + if (fuzzy_sort && cpt_sources_array != NULL) { + cur = trim_compl_match_array(cur); // Truncate by max_matches in 'cpt' + } if (!shown_match_ok) { // no displayed match at all cur = -1; } @@ -1887,6 +1990,19 @@ char *find_line_end(char *ptr) return s; } +/// Free a completion item in the list +static void ins_compl_item_free(compl_T *match) +{ + API_CLEAR_STRING(match->cp_str); + // several entries may use the same fname, free it just once. + if (match->cp_flags & CP_FREE_FNAME) { + xfree(match->cp_fname); + } + free_cptext(match->cp_text); + tv_clear(&match->cp_user_data); + xfree(match); +} + /// Free the list of completions static void ins_compl_free(void) { @@ -1904,14 +2020,7 @@ static void ins_compl_free(void) do { compl_T *match = compl_curr_match; compl_curr_match = compl_curr_match->cp_next; - API_CLEAR_STRING(match->cp_str); - // several entries may use the same fname, free it just once. - if (match->cp_flags & CP_FREE_FNAME) { - xfree(match->cp_fname); - } - free_cptext(match->cp_text); - tv_clear(&match->cp_user_data); - xfree(match); + ins_compl_item_free(match); } while (compl_curr_match != NULL && !is_first_match(compl_curr_match)); compl_first_match = compl_curr_match = NULL; compl_shown_match = NULL; @@ -1935,6 +2044,7 @@ void ins_compl_clear(void) kv_destroy(compl_orig_extmarks); API_CLEAR_STRING(compl_orig_text); compl_enter_selects = false; + cpt_sources_clear(); // clear v:completed_item set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc_lock(VAR_FIXED)); } @@ -1952,8 +2062,8 @@ bool ins_compl_win_active(win_T *wp) return ins_compl_active() && wp == compl_curr_win && wp->w_buffer == compl_curr_buf; } -/// Selected one of the matches. When false the match was edited or using the -/// longest common string. +/// Selected one of the matches. When false, the match was edited or +/// using the longest common string. bool ins_compl_used_match(void) { return compl_used_match; @@ -2087,6 +2197,9 @@ static void ins_compl_new_leader(void) if (compl_started) { ins_compl_set_original_text(compl_leader.data, compl_leader.size); + if (is_cpt_func_refresh_always()) { + cpt_compl_refresh(); + } } else { spell_bad_len = 0; // need to redetect bad word // Matches were cleared, need to search for them now. @@ -2170,6 +2283,7 @@ static void ins_compl_restart(void) compl_matches = 0; compl_cont_status = 0; compl_cont_mode = 0; + cpt_sources_clear(); } /// Set the first match, the original text. @@ -2791,8 +2905,9 @@ static Callback *get_insert_callback(int type) /// Execute user defined complete function 'completefunc', 'omnifunc' or /// 'thesaurusfunc', and get matches in "matches". /// -/// @param type either CTRL_X_OMNI or CTRL_X_FUNCTION or CTRL_X_THESAURUS -static void expand_by_function(int type, char *base) +/// @param type one of CTRL_X_OMNI or CTRL_X_FUNCTION or CTRL_X_THESAURUS +/// @param cb set if triggered by a function in 'cpt' option, otherwise NULL +static void expand_by_function(int type, char *base, Callback *cb) { list_T *matchlist = NULL; dict_T *matchdict = NULL; @@ -2800,9 +2915,14 @@ static void expand_by_function(int type, char *base) const int save_State = State; assert(curbuf != NULL); - char *funcname = get_complete_funcname(type); - if (*funcname == NUL) { - return; + + const bool is_cpt_function = (cb != NULL); + if (!is_cpt_function) { + char *funcname = get_complete_funcname(type); + if (*funcname == NUL) { + return; + } + cb = get_insert_callback(type); } // Call 'completefunc' to obtain the list of matches. @@ -2819,8 +2939,6 @@ static void expand_by_function(int type, char *base) // Insert mode in another buffer. textlock++; - Callback *cb = get_insert_callback(type); - // Call a function, which returns a list or dict. if (callback_call(cb, 2, args, &rettv)) { switch (rettv.v_type) { @@ -3279,6 +3397,7 @@ static void get_complete_info(list_T *what_list, dict_T *retdict) #define CI_WHAT_MATCHES 0x20 #define CI_WHAT_ALL 0xff int what_flag; + const bool compl_fuzzy_match = (get_cot_flags() & kOptCotFlagFuzzy) != 0; if (what_list == NULL) { what_flag = CI_WHAT_ALL & ~(CI_WHAT_MATCHES|CI_WHAT_COMPLETED); @@ -3345,7 +3464,9 @@ static void get_complete_info(list_T *what_list, dict_T *retdict) && compl_curr_match->cp_number == match->cp_number) { selected_idx = list_idx; } - list_idx += 1; + if (compl_fuzzy_match || match->cp_in_match_array) { + list_idx += 1; + } } match = match->cp_next; } while (match != NULL && !is_first_match(match)); @@ -3392,6 +3513,20 @@ static bool thesaurus_func_complete(int type) && (*curbuf->b_p_tsrfu != NUL || *p_tsrfu != NUL); } +/// Check if 'cpt' list index can be advanced to the next completion source. +static bool may_advance_cpt_index(const char *cpt) +{ + const char *p = cpt; + + if (cpt_sources_index == -1) { + return false; + } + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + return (*p != NUL); +} + /// Return value of process_next_cpt_value() enum { INS_COMPL_CPT_OK = 1, @@ -3418,12 +3553,13 @@ enum { /// the "st->e_cpt" option value and process the next matching source. /// INS_COMPL_CPT_END if all the values in "st->e_cpt" are processed. static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_arg, - pos_T *start_match_pos, bool fuzzy_collect) + pos_T *start_match_pos, bool fuzzy_collect, bool *advance_cpt_idx) { int compl_type = -1; int status = INS_COMPL_CPT_OK; st->found_all = false; + *advance_cpt_idx = false; while (*st->e_cpt == ',' || *st->e_cpt == ' ') { st->e_cpt++; @@ -3492,6 +3628,12 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar 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') { @@ -3510,6 +3652,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar // in any case e_cpt is advanced to the next entry copy_option_part(&st->e_cpt, IObuff, IOSIZE, ","); + *advance_cpt_idx = may_advance_cpt_index(st->e_cpt); st->found_all = true; if (compl_type == -1) { @@ -3539,7 +3682,7 @@ static void get_next_include_file_completion(int compl_type) static void get_next_dict_tsr_completion(int compl_type, char *dict, int dict_f) { if (thesaurus_func_complete(compl_type)) { - expand_by_function(compl_type, compl_pattern.data); + expand_by_function(compl_type, compl_pattern.data, NULL); } else { ins_compl_dictionaries(dict != NULL ? dict @@ -4102,6 +4245,42 @@ static void get_register_completion(void) } } +/// Return the callback function associated with "p" if it points to a +/// userfunc. +static Callback *get_callback_if_cpt_func(char *p) +{ + static Callback cb; + char buf[LSIZE]; + + if (*p == 'o') { + return &curbuf->b_ofu_cb; + } + if (*p == 'F') { + if (*++p != ',' && *p != NUL) { + callback_free(&cb); + size_t slen = copy_option_part(&p, buf, LSIZE, ","); + if (slen > 0 && option_set_callback_func(buf, &cb)) { + return &cb; + } + return NULL; + } else { + return &curbuf->b_cfu_cb; + } + } + return NULL; +} + +/// Retrieve new completion matches by invoking callback "cb". +static void expand_cpt_function(Callback *cb) +{ + // Re-insert the text removed by ins_compl_delete(). + ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1); + // Get matches + get_cpt_func_completion_matches(cb); + // Undo insertion + ins_compl_delete(false); +} + /// get the next set of completion matches for "type". /// @return true if a new match is found, otherwise false. static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_T *ini) @@ -4136,8 +4315,14 @@ static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_ break; case CTRL_X_FUNCTION: + if (ctrl_x_mode_normal()) { // Invoked by a func in 'cpt' option + expand_cpt_function(st->func_cb); + } else { + expand_by_function(type, compl_pattern.data, NULL); + } + break; case CTRL_X_OMNI: - expand_by_function(type, compl_pattern.data); + expand_by_function(type, compl_pattern.data, NULL); break; case CTRL_X_SPELL: @@ -4179,6 +4364,84 @@ static void get_next_bufname_token(void) } } +/// Strips carets followed by numbers. This suffix typically represents the +/// max_matches setting. +static void strip_caret_numbers_in_place(char *str) +{ + char *read = str, *write = str, *p; + + if (str == NULL) { + return; + } + + while (*read) { + if (*read == '^') { + p = read + 1; + while (ascii_isdigit(*p)) { + p++; + } + if ((*p == ',' || *p == '\0') && p != read + 1) { + read = p; + continue; + } else { + *write++ = *read++; + } + } else { + *write++ = *read++; + } + } + *write = '\0'; +} + +/// Call functions specified in the 'cpt' option with findstart=1, +/// and retrieve the startcol. +static void prepare_cpt_compl_funcs(void) +{ + // Make a copy of 'cpt' in case the buffer gets wiped out + char *cpt = xstrdup(curbuf->b_p_cpt); + strip_caret_numbers_in_place(cpt); + + // Re-insert the text removed by ins_compl_delete(). + ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1); + + int idx = 0; + for (char *p = cpt; *p;) { + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + Callback *cb = get_callback_if_cpt_func(p); + if (cb) { + int startcol; + if (get_userdefined_compl_info(curwin->w_cursor.col, cb, &startcol) == FAIL) { + if (startcol == -3) { + cpt_sources_array[idx].cs_refresh_always = false; + } else { + startcol = -2; + } + } + cpt_sources_array[idx].cs_startcol = startcol; + } + (void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p + idx++; + } + + // Undo insertion + ins_compl_delete(false); + + xfree(cpt); +} + +/// Safely advance the cpt_sources_index by one. +static int advance_cpt_sources_index_safe(void) +{ + if (cpt_sources_index < cpt_sources_count - 1) { + cpt_sources_index++; + return OK; + } + semsg(_(e_list_index_out_of_range_nr), cpt_sources_index + 1); + return FAIL; +} + /// Get the next expansion(s), using "compl_pattern". /// The search starts at position "ini" in curbuf and in the direction /// compl_direction. @@ -4192,6 +4455,7 @@ static int ins_compl_get_exp(pos_T *ini) static bool st_cleared = false; int found_new_match; int type = ctrl_x_mode; + bool may_advance_cpt_idx = false; assert(curbuf != NULL); @@ -4208,6 +4472,7 @@ static int ins_compl_get_exp(pos_T *ini) xfree(st.e_cpt_copy); // Make a copy of 'complete', in case the buffer is wiped out. 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; } else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf)) { @@ -4218,6 +4483,16 @@ static int ins_compl_get_exp(pos_T *ini) compl_old_match = compl_curr_match; // remember the last current match st.cur_match_pos = compl_dir_forward() ? &st.last_match_pos : &st.first_match_pos; + if (ctrl_x_mode_normal() && !ctrl_x_mode_line_or_eval() + && !(compl_cont_status & CONT_LOCAL)) { + // ^N completion, not ^X^L or complete() or ^X^N + if (!compl_started) { // Before showing menu the first time + setup_cpt_sources(); + } + prepare_cpt_compl_funcs(); + cpt_sources_index = 0; + } + // For ^N/^P loop over all the flags/windows/buffers in 'complete' while (true) { found_new_match = FAIL; @@ -4228,11 +4503,15 @@ 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, cfc_has_mode()); + int status = process_next_cpt_value(&st, &type, ini, + 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; + } continue; } } @@ -4246,6 +4525,10 @@ static int ins_compl_get_exp(pos_T *ini) // get the next set of completion matches found_new_match = get_next_completion_match(type, &st, ini); + if (may_advance_cpt_idx && !advance_cpt_sources_index_safe()) { + break; + } + // break the loop for specialized modes (use 'complete' just for the // generic ctrl_x_mode == CTRL_X_NORMAL) or when we've found a new match if ((ctrl_x_mode_not_default() && !ctrl_x_mode_line_or_eval()) @@ -4273,6 +4556,7 @@ static int ins_compl_get_exp(pos_T *ini) compl_started = false; } } + cpt_sources_index = -1; compl_started = true; if ((ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval()) @@ -4531,6 +4815,22 @@ static compl_T *find_comp_when_fuzzy(void) return NULL; } +/// Find the appropriate completion item when 'complete' ('cpt') includes +/// a 'max_matches' postfix. In this case, we search for a match where +/// 'cp_in_match_array' is set, indicating that the match is also present +/// in 'compl_match_array'. +static compl_T *find_comp_when_cpt_sources(void) +{ + bool is_forward = compl_shows_dir_forward(); + compl_T *match = compl_shown_match; + + do { + match = is_forward ? match->cp_next : match->cp_prev; + } while (match->cp_next && !match->cp_in_match_array + && !match_at_original_text(match)); + return match; +} + /// Find the next set of matches for completion. Repeat the completion "todo" /// times. The number of matches found is returned in 'num_matches'. /// @@ -4551,19 +4851,30 @@ static int find_next_completion_match(bool allow_get_expansion, int todo, bool a unsigned cur_cot_flags = get_cot_flags(); bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0; bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0; + bool cpt_sources_active = compl_match_array && cpt_sources_array; while (--todo >= 0) { if (compl_shows_dir_forward() && compl_shown_match->cp_next != NULL) { - compl_shown_match = compl_fuzzy_match && compl_match_array != NULL - ? find_comp_when_fuzzy() : compl_shown_match->cp_next; + if (compl_match_array != NULL && compl_fuzzy_match) { + compl_shown_match = find_comp_when_fuzzy(); + } else if (cpt_sources_active) { + compl_shown_match = find_comp_when_cpt_sources(); + } else { + compl_shown_match = compl_shown_match->cp_next; + } found_end = (compl_first_match != NULL && (is_first_match(compl_shown_match->cp_next) || is_first_match(compl_shown_match))); } else if (compl_shows_dir_backward() && compl_shown_match->cp_prev != NULL) { found_end = is_first_match(compl_shown_match); - compl_shown_match = compl_fuzzy_match && compl_match_array != NULL - ? find_comp_when_fuzzy() : compl_shown_match->cp_prev; + if (compl_match_array != NULL && compl_fuzzy_match) { + compl_shown_match = find_comp_when_fuzzy(); + } else if (cpt_sources_active) { + compl_shown_match = find_comp_when_cpt_sources(); + } else { + compl_shown_match = compl_shown_match->cp_prev; + } found_end |= is_first_match(compl_shown_match); } else { if (!allow_get_expansion) { @@ -5021,21 +5332,49 @@ static int get_cmdline_compl_info(char *line, colnr_T curs_col) return OK; } +/// Set global variables related to completion: +/// compl_col, compl_length, compl_pattern, and cpt_compl_pattern. +static void set_compl_globals(int startcol, colnr_T curs_col, bool is_cpt_compl) +{ + if (startcol < 0 || startcol > curs_col) { + startcol = curs_col; + } + int len = curs_col - startcol; + + // Re-obtain line in case it has changed + char *line = ml_get(curwin->w_cursor.lnum); + + String *pattern = is_cpt_compl ? &cpt_compl_pattern : &compl_pattern; + pattern->data = xstrnsave(line + startcol, (size_t)len); + pattern->size = (size_t)len; + if (!is_cpt_compl) { + compl_col = startcol; + compl_length = len; + } +} + /// Get the pattern, column and length for user defined completion ('omnifunc', /// 'completefunc' and 'thesaurusfunc') -/// Sets the global variables: compl_col, compl_length and compl_pattern. /// Uses the global variable: spell_bad_len -static int get_userdefined_compl_info(colnr_T curs_col) +/// +/// @param cb set if triggered by a function in the 'cpt' option, otherwise NULL +/// @param startcol when not NULL, contains the column returned by function. +static int get_userdefined_compl_info(colnr_T curs_col, Callback *cb, int *startcol) { // Call user defined function 'completefunc' with "a:findstart" // set to 1 to obtain the length of text to use for completion. const int save_State = State; - // Call 'completefunc' or 'omnifunc' and get pattern length as a string - char *funcname = get_complete_funcname(ctrl_x_mode); - if (*funcname == NUL) { - semsg(_(e_notset), ctrl_x_mode_function() ? "completefunc" : "omnifunc"); - return FAIL; + const bool is_cpt_function = (cb != NULL); + if (!is_cpt_function) { + // Call 'completefunc' or 'omnifunc' or 'thesaurusfunc' and get pattern + // length as a string + char *funcname = get_complete_funcname(ctrl_x_mode); + if (*funcname == NUL) { + semsg(_(e_notset), ctrl_x_mode_function() ? "completefunc" : "omnifunc"); + return FAIL; + } + cb = get_insert_callback(ctrl_x_mode); } typval_T args[3]; @@ -5047,7 +5386,6 @@ static int get_userdefined_compl_info(colnr_T curs_col) pos_T pos = curwin->w_cursor; textlock++; - Callback *cb = get_insert_callback(ctrl_x_mode); colnr_T col = (colnr_T)callback_call_retnr(cb, 2, args); textlock--; @@ -5060,6 +5398,10 @@ static int get_userdefined_compl_info(colnr_T curs_col) return FAIL; } + if (startcol != NULL) { + *startcol = col; + } + // Return value -2 means the user complete function wants to cancel the // complete without an error, do the same if the function did not execute // successfully. @@ -5069,6 +5411,9 @@ static int get_userdefined_compl_info(colnr_T curs_col) // Return value -3 does the same as -2 and leaves CTRL-X mode. if (col == -3) { + if (is_cpt_function) { + return FAIL; + } ctrl_x_mode = CTRL_X_NORMAL; edit_submode = NULL; if (!shortmess(SHM_COMPLETIONMENU)) { @@ -5081,20 +5426,9 @@ static int get_userdefined_compl_info(colnr_T curs_col) // completion. compl_opt_refresh_always = false; - if (col < 0) { - col = curs_col; + if (!is_cpt_function) { + set_compl_globals(col, curs_col, false); } - compl_col = col; - if (compl_col > curs_col) { - compl_col = curs_col; - } - - // Setup variables for completion. Need to obtain "line" again, - // it may have become invalid. - char *line = ml_get(curwin->w_cursor.lnum); - compl_length = curs_col - compl_col; - compl_pattern = cbuf_to_string(line + compl_col, (size_t)compl_length); - return OK; } @@ -5146,7 +5480,7 @@ static int compl_get_info(char *line, int startcol, colnr_T curs_col, bool *line return get_cmdline_compl_info(line, curs_col); } else if (ctrl_x_mode_function() || ctrl_x_mode_omni() || thesaurus_func_complete(ctrl_x_mode)) { - if (get_userdefined_compl_info(curs_col) == FAIL) { + if (get_userdefined_compl_info(curs_col, NULL, NULL) != OK) { return FAIL; } *line_invalid = true; // "line" may have become invalid @@ -5576,3 +5910,210 @@ static void spell_back_to_badword(void) start_arrow(&tpos); } } + +/// Reset the info associated with completion sources. +static void cpt_sources_clear(void) +{ + XFREE_CLEAR(cpt_sources_array); + cpt_sources_index = -1; + cpt_sources_count = 0; +} + +/// Setup completion sources. +static void setup_cpt_sources(void) +{ + char buf[LSIZE]; + + int count = 0; + for (char *p = curbuf->b_p_cpt; *p;) { + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + if (*p) { // If not end of string, count this segment + (void)copy_option_part(&p, buf, LSIZE, ","); // Advance p + count++; + } + } + if (count == 0) { + return; + } + + cpt_sources_clear(); + cpt_sources_count = count; + cpt_sources_array = xcalloc((size_t)count, sizeof(cpt_source_T)); + + int idx = 0; + for (char *p = curbuf->b_p_cpt; *p;) { + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + if (*p) { // If not end of string, count this segment + memset(buf, 0, LSIZE); + size_t slen = copy_option_part(&p, buf, LSIZE, ","); // Advance p + char *t; + if (slen > 0 && (t = vim_strchr(buf, '^')) != NULL) { + cpt_sources_array[idx].cs_max_matches = atoi(t + 1); + } + idx++; + } + } +} + +/// Return true if any of the completion sources have 'refresh' set to 'always'. +static bool is_cpt_func_refresh_always(void) +{ + for (int i = 0; i < cpt_sources_count; i++) { + if (cpt_sources_array[i].cs_refresh_always) { + return true; + } + } + return false; +} + +/// Make the completion list non-cyclic. +static void ins_compl_make_linear(void) +{ + if (compl_first_match == NULL || compl_first_match->cp_prev == NULL) { + return; + } + compl_T *m = compl_first_match->cp_prev; + m->cp_next = NULL; + compl_first_match->cp_prev = NULL; +} + +/// Remove the matches linked to the current completion source (as indicated by +/// cpt_sources_index) from the completion list. +static compl_T *remove_old_matches(void) +{ + compl_T *sublist_start = NULL, *sublist_end = NULL, *insert_at = NULL; + compl_T *current, *next; + bool compl_shown_removed = false; + bool forward = (compl_first_match->cp_cpt_source_idx < 0); + + compl_direction = forward ? FORWARD : BACKWARD; + compl_shows_dir = compl_direction; + + // Identify the sublist of old matches that needs removal + for (current = compl_first_match; current != NULL; current = current->cp_next) { + if (current->cp_cpt_source_idx < cpt_sources_index + && (forward || (!forward && !insert_at))) { + insert_at = current; + } + + if (current->cp_cpt_source_idx == cpt_sources_index) { + if (!sublist_start) { + sublist_start = current; + } + sublist_end = current; + if (!compl_shown_removed && compl_shown_match == current) { + compl_shown_removed = true; + } + } + + if ((forward && current->cp_cpt_source_idx > cpt_sources_index) + || (!forward && insert_at)) { + break; + } + } + + // Re-assign compl_shown_match if necessary + if (compl_shown_removed) { + if (forward) { + compl_shown_match = compl_first_match; + } else { // Last node will have the prefix that is being completed + for (current = compl_first_match; current->cp_next != NULL; + current = current->cp_next) {} + compl_shown_match = current; + } + } + + if (!sublist_start) { // No nodes to remove + return insert_at; + } + + // Update links to remove sublist + if (sublist_start->cp_prev) { + sublist_start->cp_prev->cp_next = sublist_end->cp_next; + } else { + compl_first_match = sublist_end->cp_next; + } + + if (sublist_end->cp_next) { + sublist_end->cp_next->cp_prev = sublist_start->cp_prev; + } + + // Free all nodes in the sublist + sublist_end->cp_next = NULL; + for (current = sublist_start; current != NULL; current = next) { + next = current->cp_next; + ins_compl_item_free(current); + } + + return insert_at; +} + +/// Retrieve completion matches using the callback function "cb" and store the +/// 'refresh:always' flag. +static void get_cpt_func_completion_matches(Callback *cb) +{ + int startcol = cpt_sources_array[cpt_sources_index].cs_startcol; + + API_CLEAR_STRING(cpt_compl_pattern); + + if (startcol == -2 || startcol == -3) { + return; + } + + 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; +} + +/// Retrieve completion matches from functions in the 'cpt' option where the +/// 'refresh:always' flag is set. +static void cpt_compl_refresh(void) +{ + // Make the completion list linear (non-cyclic) + ins_compl_make_linear(); + // Make a copy of 'cpt' in case the buffer gets wiped out + char *cpt = xstrdup(curbuf->b_p_cpt); + strip_caret_numbers_in_place(cpt); + + cpt_sources_index = 0; + for (char *p = cpt; *p;) { + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + + if (cpt_sources_array[cpt_sources_index].cs_refresh_always) { + Callback *cb = get_callback_if_cpt_func(p); + if (cb) { + compl_curr_match = remove_old_matches(); + int startcol; + int ret = get_userdefined_compl_info(curwin->w_cursor.col, cb, &startcol); + if (ret == FAIL) { + if (startcol == -3) { + cpt_sources_array[cpt_sources_index].cs_refresh_always = false; + } else { + startcol = -2; + } + } + cpt_sources_array[cpt_sources_index].cs_startcol = startcol; + if (ret == OK) { + get_cpt_func_completion_matches(cb); + } + } + } + + (void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p + if (may_advance_cpt_index(p)) { + (void)advance_cpt_sources_index_safe(); + } + } + cpt_sources_index = -1; + + xfree(cpt); + // Make the list cyclic + compl_matches = ins_compl_make_cyclic(); +} diff --git a/src/nvim/options.lua b/src/nvim/options.lua @@ -1412,7 +1412,7 @@ local options = { abbreviation = 'cpt', cb = 'did_set_complete', defaults = '.,w,b,u,t', - values = { '.', 'w', 'b', 'u', 'k', 'kspell', 's', 'i', 'd', ']', 't', 'U', 'f' }, + values = { '.', 'w', 'b', 'u', 'k', 'kspell', 's', 'i', 'd', ']', 't', 'U', 'f', 'F', 'o' }, deny_duplicates = true, desc = [=[ This option specifies how keyword completion |ins-completion| works @@ -1438,6 +1438,28 @@ local options = { ] tag completion t same as "]" f scan the buffer names (as opposed to buffer contents) + F{func} call the function {func}. Multiple "F" flags may be specified. + Refer to |complete-functions| for details on how the function + is invoked and what it should return. The value can be the + name of a function or a |Funcref|. For |Funcref| values, + spaces must be escaped with a backslash ('\'), and commas with + double backslashes ('\\') (see |option-backslash|). + If the Dict returned by the {func} includes {"refresh": "always"}, + the function will be invoked again whenever the leading text + changes. + Completion matches are always inserted at the keyword + boundary, regardless of the column returned by {func} when + a:findstart is 1. This ensures compatibility with other + completion sources. + To make further modifications to the inserted text, {func} + can make use of |CompleteDonePre|. + If generating matches is potentially slow, |complete_check()| + should be used to avoid blocking and preserve editor + responsiveness. + 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 + the 'omnifunc' option. Unloaded buffers are not loaded, thus their autocmds |:autocmd| are not executed, this may lead to unexpected completions from some files @@ -1447,6 +1469,13 @@ local options = { As you can see, CTRL-N and CTRL-P can be used to do any 'iskeyword'- based expansion (e.g., dictionary |i_CTRL-X_CTRL-K|, included patterns |i_CTRL-X_CTRL-I|, tags |i_CTRL-X_CTRL-]| and normal expansions). + + An optional match limit can be specified for a completion source by + appending a caret ("^") followed by a {count} to the source flag. + For example: ".^9,w,u,t^5" limits matches from the current buffer + to 9 and from tags to 5. Other sources remain unlimited. + Note: The match limit takes effect only during forward completion + (CTRL-N) and is ignored during backward completion (CTRL-P). ]=], full_name = 'complete', list = 'onecomma', diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c @@ -45,6 +45,7 @@ #include "nvim/spellfile.h" #include "nvim/spellsuggest.h" #include "nvim/strings.h" +#include "nvim/tag.h" #include "nvim/terminal.h" #include "nvim/types_defs.h" #include "nvim/vim_defs.h" @@ -54,10 +55,12 @@ # include "optionstr.c.generated.h" #endif -static const char e_unclosed_expression_sequence[] - = N_("E540: Unclosed expression sequence"); +static const char e_illegal_character_after_chr[] + = N_("E535: Illegal character after <%c>"); static const char e_comma_required[] = N_("E536: Comma required"); +static const char e_unclosed_expression_sequence[] + = N_("E540: Unclosed expression sequence"); static const char e_unbalanced_groups[] = N_("E542: Unbalanced groups"); static const char e_backupext_and_patchmode_are_equal[] @@ -836,41 +839,67 @@ const char *did_set_commentstring(optset_T *args) return NULL; } -/// The 'complete' option is changed. +/// Check if value for 'complete' is valid when 'complete' option is changed. const char *did_set_complete(optset_T *args) { char **varp = (char **)args->os_varp; - - // check if it is a valid value for 'complete' -- Acevedo - for (char *s = *varp; *s;) { - while (*s == ',' || *s == ' ') { - s++; - } - if (!*s) { - break; + char buffer[LSIZE]; + uint8_t char_before = NUL; + + for (char *p = *varp; *p;) { + memset(buffer, 0, LSIZE); + char *buf_ptr = buffer; + int escape = 0; + + // Extract substring while handling escaped commas + while (*p && (*p != ',' || escape) && buf_ptr < (buffer + LSIZE - 1)) { + if (*p == '\\' && *(p + 1) == ',') { + escape = 1; // Mark escape mode + p++; // Skip '\' + } else { + escape = 0; + *buf_ptr++ = *p; + } + p++; } - if (vim_strchr(".wbuksid]tUf", (uint8_t)(*s)) == NULL) { - return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*s)); + *buf_ptr = NUL; + + if (vim_strchr(".wbuksid]tUfFo", (uint8_t)(*buffer)) == NULL) { + return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*buffer)); } - if (*++s != NUL && *s != ',' && *s != ' ') { - if (s[-1] == 'k' || s[-1] == 's') { - // skip optional filename after 'k' and 's' - while (*s && *s != ',' && *s != ' ') { - if (*s == '\\' && s[1] != NUL) { - s++; + + if (vim_strchr("ksF", (uint8_t)(*buffer)) == NULL && *(buffer + 1) != NUL + && *(buffer + 1) != '^') { + char_before = (uint8_t)(*buffer); + } else { + char *t; + // Test for a number after '^' + if ((t = vim_strchr(buffer, '^')) != NULL) { + *t++ = NUL; + if (!*t) { + char_before = '^'; + } else { + for (; *t; t++) { + if (!ascii_isdigit(*t)) { + char_before = '^'; + break; + } } - s++; - } - } else { - if (args->os_errbuf != NULL) { - vim_snprintf(args->os_errbuf, args->os_errbuflen, - _("E535: Illegal character after <%c>"), - *--s); - return args->os_errbuf; } - return ""; } } + if (char_before != NUL) { + if (args->os_errbuf != NULL) { + vim_snprintf(args->os_errbuf, args->os_errbuflen, + _(e_illegal_character_after_chr), char_before); + return args->os_errbuf; + } + return NULL; + } + // Skip comma and spaces + while (*p == ',' || *p == ' ') { + p++; + } } return NULL; } diff --git a/src/nvim/popupmenu.h b/src/nvim/popupmenu.h @@ -16,6 +16,7 @@ typedef struct { char *pum_info; ///< extra info int pum_score; ///< fuzzy match score int pum_idx; ///< index of item before sorting by score + int pum_cpt_source_idx; ///< index of completion source in 'cpt' int pum_user_abbr_hlattr; ///< highlight attribute for abbr int pum_user_kind_hlattr; ///< highlight attribute for kind } pumitem_T; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim @@ -134,10 +134,15 @@ func Test_omni_dash() new exe "normal Gofind -\<C-x>\<C-o>" call assert_equal("find -help", getline('$')) + %d + set complete=o + exe "normal Gofind -\<C-n>" + " 'complete' inserts at 'iskeyword' boundary (so you get --help) + call assert_equal("find --help", getline('$')) bwipe! delfunc Omni - set omnifunc= + set omnifunc= complete& endfunc func Test_omni_throw() @@ -157,11 +162,21 @@ func Test_omni_throw() call assert_exception('he he he') call assert_equal(1, g:CallCount) endtry + %d + set complete=o + let g:CallCount = 0 + try + exe "normal ifoo\<C-n>" + call assert_false(v:true, 'command should have failed') + catch + call assert_exception('he he he') + call assert_equal(1, g:CallCount) + endtry bwipe! delfunc Omni unlet g:CallCount - set omnifunc= + set omnifunc= complete& endfunc func Test_completefunc_args() @@ -184,6 +199,16 @@ func Test_completefunc_args() call assert_equal(0, s:args[1][0]) set omnifunc= + set complete=FCompleteFunc + call feedkeys("i\<C-N>\<Esc>", 'x') + call assert_equal([1, 1], s:args[0]) + call assert_equal(0, s:args[1][0]) + set complete=o + call feedkeys("i\<C-N>\<Esc>", 'x') + call assert_equal([1, 1], s:args[0]) + call assert_equal(0, s:args[1][0]) + set complete& + bwipe! unlet s:args delfunc CompleteFunc @@ -230,7 +255,7 @@ func s:CompleteDone_CheckCompletedItemDict(pre) call assert_equal( ['one', 'two'], v:completed_item[ 'user_data' ] ) if a:pre - call assert_equal('function', complete_info().mode) + call assert_equal(a:pre == 1 ? 'function' : 'keyword', complete_info().mode) endif let s:called_completedone = 1 @@ -248,7 +273,15 @@ func Test_CompleteDoneNone() call assert_true(s:called_completedone) call assert_equal(oldline, newline) + let s:called_completedone = 0 + + set complete=F<SID>CompleteDone_CompleteFuncNone + execute "normal a\<C-N>\<C-Y>" + set complete& + let newline = join(map(range(&columns), 'nr2char(screenchar(&lines-1, v:val+1))'), '') + call assert_true(s:called_completedone) + call assert_equal(oldline, newline) let s:called_completedone = 0 au! CompleteDone endfunc @@ -269,6 +302,7 @@ func Test_CompleteDone_vevent_keys() endfunc set omnifunc=CompleteFunc set completefunc=CompleteFunc + set complete=.,FCompleteFunc set completeopt+=menuone new @@ -292,7 +326,11 @@ func Test_CompleteDone_vevent_keys() call assert_equal('vim', g:complete_word) call assert_equal('keyword', g:complete_type) - call feedkeys("Shello vim visual v\<C-X>\<C-N>\<C-Y>", 'tx') + call feedkeys("Shello vim visual v\<C-N>\<ESC>", 'tx') + call assert_equal('', g:complete_word) + call assert_equal('keyword', g:complete_type) + + call feedkeys("Shello vim visual v\<C-N>\<C-Y>", 'tx') call assert_equal('vim', g:complete_word) call assert_equal('keyword', g:complete_type) @@ -350,6 +388,21 @@ func Test_CompleteDoneDict() call assert_true(s:called_completedone) let s:called_completedone = 0 + au! CompleteDonePre + au! CompleteDone + + au CompleteDonePre * :call <SID>CompleteDone_CheckCompletedItemDict(2) + au CompleteDone * :call <SID>CompleteDone_CheckCompletedItemDict(0) + + set complete=.,F<SID>CompleteDone_CompleteFuncDict + execute "normal a\<C-N>\<C-Y>" + set complete& + + call assert_equal(['one', 'two'], v:completed_item[ 'user_data' ]) + call assert_true(s:called_completedone) + + let s:called_completedone = 0 + au! CompleteDonePre au! CompleteDone endfunc @@ -393,6 +446,15 @@ func Test_CompleteDoneDictNoUserData() call assert_true(s:called_completedone) let s:called_completedone = 0 + + set complete=.,F<SID>CompleteDone_CompleteFuncDictNoUserData + execute "normal a\<C-N>\<C-Y>" + set complete& + + call assert_equal('', v:completed_item[ 'user_data' ]) + call assert_true(s:called_completedone) + + let s:called_completedone = 0 au! CompleteDone endfunc @@ -426,6 +488,24 @@ func Test_CompleteDoneList() call assert_true(s:called_completedone) let s:called_completedone = 0 + + set complete=.,F<SID>CompleteDone_CompleteFuncList + execute "normal a\<C-N>\<C-Y>" + set complete& + + call assert_equal('', v:completed_item[ 'user_data' ]) + call assert_true(s:called_completedone) + + let s:called_completedone = 0 + + set complete=.,F + execute "normal a\<C-N>\<C-Y>" + set complete& + + call assert_equal('', v:completed_item[ 'user_data' ]) + call assert_true(s:called_completedone) + + let s:called_completedone = 0 au! CompleteDone endfunc @@ -468,11 +548,98 @@ func Test_completefunc_info() set completefunc=CompleteTest call feedkeys("i\<C-X>\<C-U>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx") call assert_equal("matched{'pum_visible': 1, 'mode': 'function', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1)) + %d + set complete=.,FCompleteTest + call feedkeys("i\<C-N>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx") + call assert_equal("matched{'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1)) + %d + set complete=.,F + call feedkeys("i\<C-N>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx") + call assert_equal("matched{'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1)) + set completeopt& + set complete& + set completefunc& +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('.')) + return col('.') + endif + call assert_equal("foo bar", getline(1)) + call assert_equal(8, col('.')) + " return v:none + endfunc + + set complete=FCptColTest + new + call feedkeys("ifoo bar\<C-N>", "tx") + bwipe! + new + set completeopt=longest + call feedkeys("ifoo bar\<C-N>", "tx") + bwipe! + new + set completeopt=menuone + call feedkeys("ifoo bar\<C-N>", "tx") + bwipe! + new + set completeopt=menuone,preinsert + call feedkeys("ifoo bar\<C-N>", "tx") + bwipe! + set complete& completeopt& + delfunc CptColTest +endfunc + +func ScrollInfoWindowUserDefinedFn(findstart, query) + " User defined function (i_CTRL-X_CTRL-U) + if a:findstart + return col('.') + endif + let infostr = range(20)->mapnew({_, v -> string(v)})->join("\n") + return [{'word': 'foo', 'info': infostr}, {'word': 'bar'}] +endfunc + +func ScrollInfoWindowPageDown() + call win_execute(popup_findinfo(), "normal! \<PageDown>") + return '' +endfunc + +func ScrollInfoWindowPageUp() + call win_execute(popup_findinfo(), "normal! \<PageUp>") + return '' +endfunc + +func ScrollInfoWindowTest(mvmt, count, fline) + new + set completeopt=menuone,popup,noinsert,noselect + set completepopup=height:5 + set completefunc=ScrollInfoWindowUserDefinedFn + let keyseq = "i\<C-X>\<C-U>\<C-N>" + for _ in range(a:count) + let keyseq .= (a:mvmt == "pageup" ? "\<C-R>\<C-R>=ScrollInfoWindowPageUp()\<CR>" : + \ "\<C-R>\<C-R>=ScrollInfoWindowPageDown()\<CR>") + endfor + let keyseq .= "\<C-R>\<C-R>=string(popup_getpos(popup_findinfo()))\<CR>\<ESC>" + call feedkeys(keyseq, "tx") + call assert_match('''firstline'': ' . a:fline, getline(1)) bwipe! set completeopt& + set completepopup& set completefunc& endfunc +func Test_scroll_info_window() + throw 'Skipped: popup_findinfo() is N/A' + call ScrollInfoWindowTest("", 0, 1) + call ScrollInfoWindowTest("pagedown", 1, 4) + call ScrollInfoWindowTest("pagedown", 2, 7) + call ScrollInfoWindowTest("pagedown", 3, 11) + call ScrollInfoWindowTest("pageup", 3, 1) +endfunc + func CompleteInfoUserDefinedFn(findstart, query) " User defined function (i_CTRL-X_CTRL-U) if a:findstart @@ -482,24 +649,34 @@ func CompleteInfoUserDefinedFn(findstart, query) endfunc func CompleteInfoTestUserDefinedFn(mvmt, idx, noselect) - new if a:noselect set completeopt=menuone,popup,noinsert,noselect else set completeopt=menu,preview endif - set completefunc=CompleteInfoUserDefinedFn - call feedkeys("i\<C-X>\<C-U>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx") - let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : '' - call assert_equal(completed. "{'pum_visible': 1, 'mode': 'function', 'selected': " . a:idx . ", 'items': [" . + let items = "[" . \ "{'word': 'foo', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " . \ "{'word': 'bar', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " . \ "{'word': 'baz', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " . \ "{'word': 'qux', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}" . - \ "]}", getline(1)) + \ "]" + new + set completefunc=CompleteInfoUserDefinedFn + call feedkeys("i\<C-X>\<C-U>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx") + let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : '' + call assert_equal(completed. "{'pum_visible': 1, 'mode': 'function', 'selected': " . a:idx . ", 'items': " . items . "}", getline(1)) + %d + set complete=.,FCompleteInfoUserDefinedFn + call feedkeys("i\<C-N>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx") + let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : '' + call assert_equal(completed. "{'pum_visible': 1, 'mode': 'keyword', 'selected': " . a:idx . ", 'items': " . items . "}", getline(1)) + %d + set complete=.,F + call feedkeys("i\<C-N>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx") + let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : '' + call assert_equal(completed. "{'pum_visible': 1, 'mode': 'keyword', 'selected': " . a:idx . ", 'items': " . items . "}", getline(1)) bwipe! - set completeopt& - set completefunc& + set completeopt& completefunc& complete& endfunc func Test_complete_info_user_defined_fn() @@ -867,6 +1044,10 @@ func Test_completefunc_error() set completefunc=CompleteFunc call setline(1, ['', 'abcd', '']) call assert_fails('exe "normal 2G$a\<C-X>\<C-U>"', 'E565:') + set complete=FCompleteFunc + call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:') + set complete=F + call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:') " delete text when called for the second time func CompleteFunc2(findstart, base) @@ -879,6 +1060,10 @@ func Test_completefunc_error() set completefunc=CompleteFunc2 call setline(1, ['', 'abcd', '']) call assert_fails('exe "normal 2G$a\<C-X>\<C-U>"', 'E565:') + set complete=FCompleteFunc2 + call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:') + set complete=F + call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:') " Jump to a different window from the complete function func CompleteFunc3(findstart, base) @@ -891,9 +1076,15 @@ func Test_completefunc_error() set completefunc=CompleteFunc3 new call assert_fails('exe "normal a\<C-X>\<C-U>"', 'E565:') + %d + set complete=FCompleteFunc3 + call assert_fails('exe "normal a\<C-N>"', 'E565:') + %d + set complete=F + call assert_fails('exe "normal a\<C-N>"', 'E565:') close! - set completefunc& + set completefunc& complete& delfunc CompleteFunc delfunc CompleteFunc2 delfunc CompleteFunc3 @@ -912,8 +1103,16 @@ func Test_completefunc_invalid_data() set completefunc=CompleteFunc exe "normal i\<C-X>\<C-U>" call assert_equal('moon', getline(1)) - set completefunc& - close! + %d + set complete=FCompleteFunc + exe "normal i\<C-N>" + call assert_equal('moon', getline(1)) + %d + set complete=F + exe "normal i\<C-N>" + call assert_equal('moon', getline(1)) + set completefunc& complete& + bw! endfunc " Test for errors in using complete() function @@ -1589,18 +1788,363 @@ func Test_complete_item_refresh_always() return #{words: res, refresh: 'always'} endif endfunc - new set completeopt=menu,longest set completefunc=Tcomplete + new exe "normal! iup\<C-X>\<C-U>\<BS>\<BS>\<BS>\<BS>\<BS>" call assert_equal('up', getline(1)) call assert_equal(6, g:CallCount) + %d + let g:CallCount = 0 + set complete=FTcomplete + exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>" + call assert_equal('up', getline(1)) + call assert_equal(6, g:CallCount) + %d + let g:CallCount = 0 + set complete=F + exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>" + call assert_equal('up', getline(1)) + call assert_equal(6, g:CallCount) + %d + let g:CallCount = 0 + set omnifunc=Tcomplete + set complete=o + exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>" + call assert_equal('up', getline(1)) + call assert_equal(6, g:CallCount) + bw! set completeopt& + set complete& set completefunc& - bw! delfunc Tcomplete endfunc +" Test for 'cpt' user func that fails (return -2/-3) when refresh:always +func Test_cpt_func_refresh_always_fail() + func! CompleteFail(retval, findstart, base) + if a:findstart + return a:retval + endif + call assert_equal(-999, a:findstart) " Should not reach here + endfunc + new + set complete=Ffunction('CompleteFail'\\,\ [-2]) + exe "normal! ia\<C-N>" + %d + set complete=Ffunction('CompleteFail'\\,\ [-3]) + exe "normal! ia\<C-N>" + bw! + + func! CompleteFailIntermittent(retval, findstart, base) + if a:findstart + if g:CallCount == 2 + let g:CallCount += 1 + return a:retval + endif + return col('.') - 1 + endif + let g:CallCount += 1 + let res = [[], ['foo', 'fbar'], ['foo1', 'foo2'], ['foofail'], ['fooo3']] + return #{words: res[g:CallCount], refresh: 'always'} + endfunc + new + set completeopt=menuone,noselect + set complete=Ffunction('CompleteFailIntermittent'\\,\ [-2]) + let g:CallCount = 0 + exe "normal! if\<C-N>\<c-r>=complete_info([\"items\"])\<cr>" + call assert_match('''word'': ''foo''.*''word'': ''fbar''', getline(1)) + call assert_equal(1, g:CallCount) + %d + let g:CallCount = 0 + exe "normal! if\<C-N>o\<c-r>=complete_info([\"items\", \"selected\"])\<cr>" + call assert_match('''selected'': -1.*''word'': ''foo1''.*''word'': ''foo2''', getline(1)) + call assert_equal(2, g:CallCount) + %d + set complete=Ffunction('CompleteFailIntermittent'\\,\ [-3]) + let g:CallCount = 0 + exe "normal! if\<C-N>o\<c-r>=complete_info([\"items\", \"selected\"])\<cr>" + call assert_match('''selected'': -1.*''word'': ''foo1''.*''word'': ''foo2''', getline(1)) + call assert_equal(2, g:CallCount) + %d + set complete=Ffunction('CompleteFailIntermittent'\\,\ [-2]) + " completion mode is dismissed when there are no matches in list + let g:CallCount = 0 + exe "normal! if\<C-N>oo\<c-r>=complete_info([\"items\"])\<cr>" + call assert_equal('foo{''items'': []}', getline(1)) + call assert_equal(3, g:CallCount) + %d + let g:CallCount = 0 + exe "normal! if\<C-N>oo\<bs>\<c-r>=complete_info([\"items\"])\<cr>" + call assert_equal('fo{''items'': []}', getline(1)) + call assert_equal(3, g:CallCount) + %d + " completion mode continues when matches from other sources present + set complete=.,Ffunction('CompleteFailIntermittent'\\,\ [-2]) + call setline(1, 'fooo1') + let g:CallCount = 0 + exe "normal! Gof\<C-N>oo\<c-r>=complete_info([\"items\", \"selected\"])\<cr>" + call assert_equal('foo{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', ' + \ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}', + \ getline(2)) + call assert_equal(3, g:CallCount) + %d + call setline(1, 'fooo1') + let g:CallCount = 0 + exe "normal! Gof\<C-N>oo\<bs>\<c-r>=complete_info([\"items\"])\<cr>" + call assert_match('''word'': ''fooo1''.*''word'': ''fooo3''', getline(2)) + call assert_equal(4, g:CallCount) + %d + " refresh will stop when -3 is returned + set complete=.,,\ Ffunction('CompleteFailIntermittent'\\,\ [-3]) + call setline(1, 'fooo1') + let g:CallCount = 0 + exe "normal! Gof\<C-N>o\<bs>\<c-r>=complete_info([\"items\", \"selected\"])\<cr>" + call assert_equal('f{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', ' + \ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}', + \ getline(2)) + call assert_equal(3, g:CallCount) + %d + call setline(1, 'fooo1') + let g:CallCount = 0 + exe "normal! Gof\<C-N>oo\<bs>\<c-r>=complete_info([\"items\", \"selected\"])\<cr>" + call assert_equal('fo{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', ' + \ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}', + \ getline(2)) + call assert_equal(3, g:CallCount) + bw! + + set complete& completeopt& + delfunc CompleteFail + delfunc CompleteFailIntermittent +endfunc + +" Select items before they are removed by refresh:always +func Test_cpt_select_item_refresh_always() + + func CompleteMenuWords() + let info = complete_info(["items", "selected"]) + call map(info.items, {_, v -> v.word}) + return info + endfunc + + func! CompleteItemsSelect(compl, findstart, base) + if a:findstart + return col('.') - 1 + endif + let g:CallCount += 1 + if g:CallCount == 2 + return #{words: a:compl, refresh: 'always'} + endif + let res = [[], ['fo', 'foobar'], [], ['foo1', 'foo2']] + return #{words: res[g:CallCount], refresh: 'always'} + endfunc + + new + set complete=.,Ffunction('CompleteItemsSelect'\\,\ [[]]) + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-n>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('fo{''selected'': 1, ''items'': [''foobarbar'', ''fo'', ''foobar'']}', getline(2)) + call assert_equal(1, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-p>\<c-p>\<c-p>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('fo{''selected'': 0, ''items'': [''fo'', ''foobar'', ''foobarbar'']}', getline(2)) + call assert_equal(1, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-n>o\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('foo{''selected'': -1, ''items'': []}' , getline(2)) + call assert_equal(1, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'']}', getline(2)) + call assert_equal(2, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-p>\<c-p>\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'']}', getline(2)) + call assert_equal(2, g:CallCount) + + %d + set complete=.,Ffunction('CompleteItemsSelect'\\,\ [['foonext']]) + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'', ''foonext'']}', getline(2)) + call assert_equal(2, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-p>\<c-p>\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('f{''selected'': -1, ''items'': [''foonext'', ''foobarbar'']}', getline(2)) + call assert_equal(2, g:CallCount) + + %d + call setline(1, "foob") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('foo{''selected'': 0, ''items'': [''foob'', ''foonext'']}', getline(2)) + call assert_equal(2, g:CallCount) + %d + call setline(1, "foob") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<bs>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('fo{''selected'': 0, ''items'': [''foob'', ''foo1'', ''foo2'']}', getline(2)) + call assert_equal(3, g:CallCount) + + %d + call setline(1, "foob") + let g:CallCount = 0 + exe "normal! Gof\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('foo{''selected'': 1, ''items'': [''foonext'', ''foob'']}', getline(2)) + call assert_equal(2, g:CallCount) + %d + call setline(1, "foob") + let g:CallCount = 0 + exe "normal! Gof\<c-p>\<bs>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('fo{''selected'': 2, ''items'': [''foo1'', ''foo2'', ''foob'']}', getline(2)) + call assert_equal(3, g:CallCount) + + %d + set complete=.,Ffunction('CompleteItemsSelect'\\,\ [['fo'\\,\ 'foonext']]) + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'', ''fo'', ''foonext'']}', getline(2)) + call assert_equal(2, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-p>\<c-p>\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>" + call assert_equal('f{''selected'': -1, ''items'': [''fo'', ''foonext'', ''foobarbar'']}', getline(2)) + call assert_equal(2, g:CallCount) + bw! + + set complete& + delfunc CompleteMenuWords + delfunc CompleteItemsSelect +endfunc + +" Test two functions together, each returning refresh:always +func Test_cpt_multi_func_refresh_always() + + func CompleteMenuMatches() + let info = complete_info(["matches", "selected"]) + call map(info.matches, {_, v -> v.word}) + return info + endfunc + + func! CompleteItems1(findstart, base) + if a:findstart + return col('.') - 1 + endif + let g:CallCount1 += 1 + let res = [[], [], ['foo1', 'foobar1'], [], ['foo11', 'foo12'], [], ['foo13', 'foo14']] + return #{words: res[g:CallCount1], refresh: 'always'} + endfunc + + func! CompleteItems2(findstart, base) + if a:findstart + return col('.') - 1 + endif + let g:CallCount2 += 1 + let res = [[], [], [], ['foo2', 'foobar2'], ['foo21', 'foo22'], ['foo23'], []] + return #{words: res[g:CallCount2], refresh: 'always'} + endfunc + + set complete= + exe "normal! if\<C-N>\<c-r>=CompleteMenuMatches()\<cr>" + " \x0e is <c-n> + call assert_equal("f\x0e" . '{''matches'': [], ''selected'': -1}', getline(1)) + + set completeopt=menuone,noselect + set complete=FCompleteItems1,FCompleteItems2 + + new + let g:CallCount1 = 0 + let g:CallCount2 = 0 + exe "normal! if\<c-n>o\<c-n>o\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('foo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1)) + call assert_equal(3, g:CallCount1) + call assert_equal(3, g:CallCount2) + %d + let g:CallCount1 = 0 + let g:CallCount2 = 0 + exe "normal! if\<c-p>o\<c-p>o\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('foo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1)) + call assert_equal(3, g:CallCount1) + call assert_equal(3, g:CallCount2) + %d + let g:CallCount1 = 0 + let g:CallCount2 = 0 + exe "normal! if\<c-p>\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('f{''matches'': [], ''selected'': -1}', getline(1)) + call assert_equal(1, g:CallCount1) + call assert_equal(1, g:CallCount2) + %d + let g:CallCount1 = 1 + let g:CallCount2 = 1 + exe "normal! if\<c-n>\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('f{''matches'': [''foo1'', ''foobar1''], ''selected'': -1}', getline(1)) + call assert_equal(2, g:CallCount2) + call assert_equal(2, g:CallCount2) + %d + let g:CallCount1 = 1 + let g:CallCount2 = 1 + exe "normal! if\<c-n>o\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('fo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1)) + call assert_equal(3, g:CallCount2) + call assert_equal(3, g:CallCount2) + %d + let g:CallCount1 = 1 + let g:CallCount2 = 1 + exe "normal! if\<c-p>o\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('fo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1)) + call assert_equal(3, g:CallCount2) + call assert_equal(3, g:CallCount2) + %d + let g:CallCount1 = 1 + let g:CallCount2 = 1 + exe "normal! if\<c-n>oo\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('foo{''matches'': [''foo11'', ''foo12'', ''foo21'', ''foo22''], ''selected'': -1}', getline(1)) + call assert_equal(4, g:CallCount2) + call assert_equal(4, g:CallCount2) + %d + let g:CallCount1 = 1 + let g:CallCount2 = 1 + exe "normal! if\<c-n>oo\<bs>\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('fo{''matches'': [''foo23''], ''selected'': -1}', getline(1)) + call assert_equal(5, g:CallCount2) + call assert_equal(5, g:CallCount2) + %d + let g:CallCount1 = 1 + let g:CallCount2 = 1 + exe "normal! if\<c-p>oo\<bs>\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('fo{''matches'': [''foo23''], ''selected'': -1}', getline(1)) + call assert_equal(5, g:CallCount2) + call assert_equal(5, g:CallCount2) + %d + let g:CallCount1 = 1 + let g:CallCount2 = 1 + exe "normal! if\<c-n>oo\<bs>o\<c-r>=CompleteMenuMatches()\<cr>" + call assert_equal('foo{''matches'': [''foo13'', ''foo14''], ''selected'': -1}', getline(1)) + call assert_equal(6, g:CallCount2) + call assert_equal(6, g:CallCount2) + bw! + + set complete& completeopt& + delfunc CompleteMenuMatches + delfunc CompleteItems1 + delfunc CompleteItems2 +endfunc + " Test for completing from a thesaurus file without read permission func Test_complete_unreadable_thesaurus_file() CheckUnix @@ -1640,6 +2184,143 @@ func Test_no_mapping_for_ctrl_x_key() bwipe! endfunc +" Test for different ways of setting a function in 'complete' option +func Test_cpt_func_callback() + func CompleteFunc1(callnr, findstart, base) + call add(g:CompleteFunc1Args, [a:callnr, a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + func CompleteFunc2(findstart, base) + call add(g:CompleteFunc2Args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + + let lines =<< trim END + #" Test for using a global function name + set complete=Fg:CompleteFunc2 + new + call setline(1, 'global') + LET g:CompleteFunc2Args = [] + call feedkeys("A\<C-N>\<Esc>", 'x') + call assert_equal([[1, ''], [0, 'global']], g:CompleteFunc2Args) + set complete& + bw! + + #" Test for using a function() + set complete=Ffunction('g:CompleteFunc1'\\,\ [10]) + new + call setline(1, 'one') + LET g:CompleteFunc1Args = [] + call feedkeys("A\<C-N>\<Esc>", 'x') + call assert_equal([[10, 1, ''], [10, 0, 'one']], g:CompleteFunc1Args) + set complete& + bw! + + #" Using a funcref variable + set complete=Ffuncref('g:CompleteFunc1'\\,\ [11]) + new + call setline(1, 'two') + LET g:CompleteFunc1Args = [] + call feedkeys("A\<C-N>\<Esc>", 'x') + call assert_equal([[11, 1, ''], [11, 0, 'two']], g:CompleteFunc1Args) + set complete& + bw! + + END + call CheckLegacyAndVim9Success(lines) + + " Test for using a script-local function name + func s:CompleteFunc3(findstart, base) + call add(g:CompleteFunc3Args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set complete=Fs:CompleteFunc3 + new + call setline(1, 'script1') + let g:CompleteFunc3Args = [] + call feedkeys("A\<C-N>\<Esc>", 'x') + call assert_equal([[1, ''], [0, 'script1']], g:CompleteFunc3Args) + set complete& + bw! + + let &complete = 'Fs:CompleteFunc3' + new + call setline(1, 'script2') + let g:CompleteFunc3Args = [] + call feedkeys("A\<C-N>\<Esc>", 'x') + call assert_equal([[1, ''], [0, 'script2']], g:CompleteFunc3Args) + bw! + delfunc s:CompleteFunc3 + set complete& + + " In Vim9 script s: can be omitted + let lines =<< trim END + vim9script + var CompleteFunc4Args = [] + def CompleteFunc4(findstart: bool, base: string): any + add(CompleteFunc4Args, [findstart, base]) + return findstart ? 0 : [] + enddef + set complete=FCompleteFunc4 + new + setline(1, 'script1') + feedkeys("A\<C-N>\<Esc>", 'x') + assert_equal([[1, ''], [0, 'script1']], CompleteFunc4Args) + set complete& + bw! + END + call CheckScriptSuccess(lines) + + " Vim9 tests + let lines =<< trim END + vim9script + + def Vim9CompleteFunc(callnr: number, findstart: number, base: string): any + add(g:Vim9completeFuncArgs, [callnr, findstart, base]) + return findstart ? 0 : [] + enddef + + # Test for using a def function with completefunc + set complete=Ffunction('Vim9CompleteFunc'\\,\ [60]) + new | only + setline(1, 'one') + g:Vim9completeFuncArgs = [] + feedkeys("A\<C-N>\<Esc>", 'x') + assert_equal([[60, 1, ''], [60, 0, 'one']], g:Vim9completeFuncArgs) + bw! + + # Test for using a global function name + &complete = 'Fg:CompleteFunc2' + new | only + setline(1, 'two') + g:CompleteFunc2Args = [] + feedkeys("A\<C-N>\<Esc>", 'x') + assert_equal([[1, ''], [0, 'two']], g:CompleteFunc2Args) + bw! + + # Test for using a script-local function name + def LocalCompleteFunc(findstart: number, base: string): any + add(g:LocalCompleteFuncArgs, [findstart, base]) + return findstart ? 0 : [] + enddef + &complete = 'FLocalCompleteFunc' + new | only + setline(1, 'three') + g:LocalCompleteFuncArgs = [] + feedkeys("A\<C-N>\<Esc>", 'x') + assert_equal([[1, ''], [0, 'three']], g:LocalCompleteFuncArgs) + bw! + END + call CheckScriptSuccess(lines) + + " cleanup + set completefunc& complete& + delfunc CompleteFunc1 + delfunc CompleteFunc2 + unlet g:CompleteFunc1Args g:CompleteFunc2Args + %bw! +endfunc + " Test for different ways of setting the 'completefunc' option func Test_completefunc_callback() func CompleteFunc1(callnr, findstart, base) @@ -2517,10 +3198,19 @@ endfunc func Test_complete_smartindent() new setlocal smartindent completefunc=FooBarComplete - exe "norm! o{\<cr>\<c-x>\<c-u>\<c-p>}\<cr>\<esc>" let result = getline(1,'$') call assert_equal(['', '{','}',''], result) + %d + setlocal complete=FFooBarComplete + exe "norm! o{\<cr>\<c-n>\<c-p>}\<cr>\<esc>" + let result = getline(1,'$') + call assert_equal(['', '{','}',''], result) + %d + setlocal complete=F + exe "norm! o{\<cr>\<c-n>\<c-p>}\<cr>\<esc>" + let result = getline(1,'$') + call assert_equal(['', '{','}',''], result) bw! delfunction! FooBarComplete endfunc @@ -3468,6 +4158,199 @@ func Test_complete_multiline_marks() delfunc Omni_test endfunc +func Test_complete_match_count() + func! PrintMenuWords() + let info = complete_info(["selected", "matches"]) + call map(info.matches, {_, v -> v.word}) + return info + endfunc + + new + set cpt=.^0,w + call setline(1, ["fo", "foo", "foobar", "fobarbaz"]) + exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''fo'', ''foo'', ''foobar'', ''fobarbaz''], ''selected'': 0}', getline(5)) + 5d + set cpt=.^0,w + exe "normal! Gof\<c-p>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fobarbaz{''matches'': [''fo'', ''foo'', ''foobar'', ''fobarbaz''], ''selected'': 3}', getline(5)) + 5d + set cpt=.^1,w + exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''fo''], ''selected'': 0}', getline(5)) + 5d + " max_matches is ignored for backward search + exe "normal! Gof\<c-p>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fobarbaz{''matches'': [''fo'', ''foo'', ''foobar'', ''fobarbaz''], ''selected'': 3}', getline(5)) + 5d + set cpt=.^2,w + exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''fo'', ''foo''], ''selected'': 0}', getline(5)) + 5d + set cot=menuone,noselect + set cpt=.^1,w + exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('f{''matches'': [''fo''], ''selected'': -1}', getline(5)) + set cot& + + func ComplFunc(findstart, base) + if a:findstart + return col(".") + endif + return ["foo1", "foo2", "foo3", "foo4"] + endfunc + + %d + set completefunc=ComplFunc + set cpt=.^1,F^2 + call setline(1, ["fo", "foo", "foobar", "fobarbaz"]) + exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5)) + 5d + set cpt=.^1,,,F^2,,, + call setline(1, ["fo", "foo", "foobar", "fobarbaz"]) + exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('foo1{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 1}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('foo2{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 2}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-n>\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('f{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': -1}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5)) + + 5d + exe "normal! Gof\<c-n>\<c-p>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('f{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': -1}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('foo2{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 2}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('foo1{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 1}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5)) + 5d + exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-p>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('f{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': -1}', getline(5)) + + %d + call setline(1, ["foo"]) + set cpt=FComplFunc^2,. + exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('foo1{''matches'': [''foo1'', ''foo2'', ''foo''], ''selected'': 0}', getline(2)) + bw! + + " Test refresh:always with max_items + let g:CallCount = 0 + func! CompleteItemsSelect(findstart, base) + if a:findstart + return col('.') - 1 + endif + let g:CallCount += 1 + let res = [[], ['foobar'], ['foo1', 'foo2', 'foo3'], ['foo4', 'foo5', 'foo6']] + return #{words: res[g:CallCount], refresh: 'always'} + endfunc + + new + set complete=.,Ffunction('CompleteItemsSelect')^2 + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('foobar{''matches'': [''foobarbar'', ''foobar''], ''selected'': 1}', getline(2)) + call assert_equal(1, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-p>o\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('fo{''matches'': [''foobarbar'', ''foo1'', ''foo2''], ''selected'': -1}', getline(2)) + call assert_equal(2, g:CallCount) + %d + call setline(1, "foobarbar") + let g:CallCount = 0 + exe "normal! Gof\<c-n>\<c-p>o\<bs>\<c-r>=PrintMenuWords()\<cr>" + call assert_equal('f{''matches'': [''foobarbar'', ''foo4'', ''foo5''], ''selected'': -1}', getline(2)) + call assert_equal(3, g:CallCount) + bw! + + " Test 'fuzzy' with max_items + " XXX: Cannot use complete_info() since it is broken for 'fuzzy' + new + set completeopt=menu,noselect,fuzzy + set complete=. + call setline(1, ["abcd", "abac", "abdc"]) + execute "normal Goa\<c-n>c\<c-n>" + call assert_equal('abac', getline(4)) + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^1 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^2 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^3 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^4 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + + func! ComplFunc(findstart, base) + if a:findstart + return col(".") + endif + return ["abcde", "abacr"] + endfunc + + set complete=.,FComplFunc^1 + execute "normal Sa\<c-n>c\<c-n>\<c-n>" + call assert_equal('abacr', getline(4)) + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + set complete=.^1,FComplFunc^1 + execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('abac', getline(4)) + bw! + + " Items with '\n' that cause menu to shift, with no leader (issue #17394) + func! ComplFunc(findstart, base) + if a:findstart == 1 + return col('.') - 1 + endif + return ["one\ntwo\nthree", "four five six", "hello\nworld\nhere"] + endfunc + set completeopt=menuone,popup,noselect,fuzzy infercase + set complete=.^1,FComplFunc^5 + new + call setline(1, ["foo", "bar", "baz"]) + execute "normal Go\<c-n>\<c-n>\<c-n>" + call assert_equal(['one', 'two', 'three'], getline(4, 6)) + %d + call setline(1, ["foo", "bar", "baz"]) + execute "normal Go\<c-n>\<c-n>\<c-n>\<c-p>" + call assert_equal('foo', getline(4)) + execute "normal S\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('foo', getline(4)) + set complete=.^1,FComplFunc^2 + execute "normal S\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>" + call assert_equal('foo', getline(4)) + execute "normal S\<c-n>\<c-p>\<c-p>\<c-p>\<c-n>\<c-n>" + call assert_equal('four five six', getline(4)) + bw! + + set completeopt& complete& infercase& + delfunc PrintMenuWords + delfunc ComplFunc + delfunc CompleteItemsSelect +endfunc + func Test_complete_append_selected_match_default() " when typing a normal character during completion, " completion is ended, see @@ -3587,7 +4470,7 @@ endfunc " Test 'nearest' flag of 'completeopt' func Test_nearest_cpt_option() - func PrintMenuWords() + func! PrintMenuWords() let info = complete_info(["selected", "matches"]) call map(info.matches, {_, v -> v.word}) return info @@ -3876,4 +4759,41 @@ func Test_register_completion() set ignorecase& endfunc +" Test refresh:always with unloaded buffers (issue #17363) +func Test_complete_unloaded_buf_refresh_always() + func TestComplete(findstart, base) + if a:findstart + let line = getline('.') + let start = col('.') - 1 + while start > 0 && line[start - 1] =~ '\a' + let start -= 1 + endwhile + return start + else + let g:CallCount += 1 + let res = ["update1", "update12", "update123"] + return #{words: res, refresh: 'always'} + endif + endfunc + + let g:CallCount = 0 + set completeopt=menu,longest + set completefunc=TestComplete + set complete=b,u,t,i,F + badd foo1 + badd foo2 + new + exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>" + call assert_equal('up', getline(1)) + call assert_equal(6, g:CallCount) + + bd! foo1 + bd! foo2 + bw! + set completeopt& + set complete& + set completefunc& + delfunc TestComplete +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim @@ -277,6 +277,23 @@ func Test_complete() call feedkeys("i\<C-N>\<Esc>", 'xt') bwipe! call assert_fails('set complete=ix', 'E535:') + call assert_fails('set complete=x', 'E539:') + call assert_fails('set complete=..', 'E535:') + set complete=.,w,b,u,k,\ s,i,d,],t,U,F,o + call assert_fails('set complete=i^-10', 'E535:') + call assert_fails('set complete=i^x', 'E535:') + call assert_fails('set complete=k^2,t^-1,s^', 'E535:') + call assert_fails('set complete=t^-1', 'E535:') + call assert_fails('set complete=kfoo^foo2', 'E535:') + call assert_fails('set complete=kfoo^', 'E535:') + call assert_fails('set complete=.^', 'E535:') + set complete=.,w,b,u,k,s,i,d,],t,U,F,o + set complete=. + set complete=.^10,t^0 + set complete+=Ffuncref('foo'\\,\ [10]) + set complete=Ffuncref('foo'\\,\ [10])^10 + set complete& + set complete+=Ffunction('g:foo'\\,\ [10\\,\ 20]) set complete& endfun