diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index f6c74027f6..a9e74b3c2a 100644 --- 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 index 38a4c20d1c..1c3e0d6a6f 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -178,6 +178,10 @@ 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' • '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 index 80ac6364cf..7eaad29b98 100644 --- 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 diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 7c853571dd..af1265af15 100644 --- 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 diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 0f42f79295..3ae4d2f81b 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -174,6 +174,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_value_idx; ///< index of this match's source in 'cpt' option }; /// state information used for getting the next set of insert completion @@ -190,6 +191,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 +215,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 +261,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 +305,13 @@ static int compl_selected_item = -1; static int *compl_fuzzy_scores; +/// array indicating which 'cpt' functions have 'refresh:always' set +static bool *cpt_func_refresh_always; +/// total number of completion sources specified in the 'cpt' option +static int cpt_value_count; +/// index of the current completion source being expanded +static int cpt_value_idx; + // "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 +985,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 +1004,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_value_idx = cpt_value_idx; if (cptext != NULL) { for (int i = 0; i < CPT_COUNT; i++) { @@ -1887,6 +1898,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 +1928,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 +1952,7 @@ void ins_compl_clear(void) kv_destroy(compl_orig_extmarks); API_CLEAR_STRING(compl_orig_text); compl_enter_selects = false; + cpt_compl_src_clear(); // clear v:completed_item set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc_lock(VAR_FIXED)); } @@ -1952,8 +1970,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 +2105,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 +2191,7 @@ static void ins_compl_restart(void) compl_matches = 0; compl_cont_status = 0; compl_cont_mode = 0; + cpt_compl_src_clear(); } /// Set the first match, the original text. @@ -2791,8 +2813,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 +2823,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 +2847,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) { @@ -3492,12 +3518,25 @@ 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; + if (*st->e_cpt == 'o') { + st->func_cb = &curbuf->b_ofu_cb; + } else { + st->func_cb = (*++st->e_cpt != ',' && *st->e_cpt != NUL) + ? get_cpt_func_callback(st->e_cpt) : &curbuf->b_cfu_cb; + } + 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; +#if 0 } else if (*st->e_cpt == 'f') { compl_type = CTRL_X_BUFNAMES; +#endif } else if (*st->e_cpt == ']' || *st->e_cpt == 't') { compl_type = CTRL_X_TAGS; if (!shortmess(SHM_COMPLETIONSCAN)) { @@ -3539,7 +3578,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 +4141,30 @@ static void get_register_completion(void) } } +/// Return the callback function associated with "funcname". +static Callback *get_cpt_func_callback(char *funcname) +{ + static Callback cb; + char buf[LSIZE]; + + size_t slen = copy_option_part(&funcname, buf, LSIZE, ","); + if (slen > 0 && option_set_callback_func(buf, &cb)) { + return &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 +4199,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: @@ -4210,6 +4279,10 @@ static int ins_compl_get_exp(pos_T *ini) st.e_cpt_copy = xstrdup((compl_cont_status & CONT_LOCAL) ? "." : curbuf->b_p_cpt); st.e_cpt = st.e_cpt_copy; st.last_match_pos = st.first_match_pos = *ini; + + if (ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval()) { + cpt_compl_src_init(st.e_cpt); + } } else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf)) { st.ins_buf = curbuf; // In case the buffer was wiped out. } @@ -4219,7 +4292,7 @@ static int ins_compl_get_exp(pos_T *ini) st.cur_match_pos = compl_dir_forward() ? &st.last_match_pos : &st.first_match_pos; // For ^N/^P loop over all the flags/windows/buffers in 'complete' - while (true) { + for (cpt_value_idx = 0;;) { found_new_match = FAIL; st.set_match_pos = false; @@ -4233,6 +4306,7 @@ static int ins_compl_get_exp(pos_T *ini) break; } if (status == INS_COMPL_CPT_CONT) { + cpt_value_idx++; continue; } } @@ -4246,6 +4320,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 (type > 0) { + cpt_value_idx++; + } + // 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 +4351,7 @@ static int ins_compl_get_exp(pos_T *ini) compl_started = false; } } + cpt_value_idx = -1; compl_started = true; if ((ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval()) @@ -5025,17 +5104,25 @@ static int get_cmdline_compl_info(char *line, colnr_T curs_col) /// '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 +5134,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 +5146,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 +5159,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 +5174,22 @@ static int get_userdefined_compl_info(colnr_T curs_col) // completion. compl_opt_refresh_always = false; - if (col < 0) { + if (col < 0 || col > curs_col) { col = curs_col; } - 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); + int len = curs_col - col; + String *compl_pat = is_cpt_function ? &cpt_compl_pattern : &compl_pattern; + compl_pat->data = xstrnsave(line + col, (size_t)len); + compl_pat->size = (size_t)compl_length; + if (!is_cpt_function) { + compl_col = col; + compl_length = len; + } return OK; } @@ -5146,7 +5241,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 +5671,175 @@ static void spell_back_to_badword(void) start_arrow(&tpos); } } + +/// Reset the info associated with completion sources. +static void cpt_compl_src_clear(void) +{ + XFREE_CLEAR(cpt_func_refresh_always); + cpt_value_idx = -1; + cpt_value_count = 0; +} + +/// Initialize the info associated with completion sources. +static void cpt_compl_src_init(char *cpt_str) +{ + int count = 0; + char *p = cpt_str; + + while (*p) { + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + if (*p) { // If not end of string, count this segment + count++; + copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p + } + } + cpt_compl_src_clear(); + cpt_value_count = count; + if (count > 0) { + cpt_func_refresh_always = xcalloc((size_t)count, sizeof(bool)); + } +} + +/// 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_value_count; i++) { + if (cpt_func_refresh_always[i]) { + 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_value_idx) 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; + int forward = compl_dir_forward(); + + // Identify the sublist of old matches that needs removal + for (current = compl_first_match; current != NULL; current = current->cp_next) { + if (current->cp_cpt_value_idx < cpt_value_idx && (forward || (!forward && !insert_at))) { + insert_at = current; + } + + if (current->cp_cpt_value_idx == cpt_value_idx) { + 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_value_idx > cpt_value_idx) || (!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) +{ + API_CLEAR_STRING(cpt_compl_pattern); + int startcol; + int ret = get_userdefined_compl_info(curwin->w_cursor.col, cb, &startcol); + if (ret == FAIL && startcol == -3) { + cpt_func_refresh_always[cpt_value_idx] = false; + } else if (ret == OK) { + expand_by_function(0, cpt_compl_pattern.data, cb); + cpt_func_refresh_always[cpt_value_idx] = 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) +{ + Callback *cb; + + // 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); + + cpt_value_idx = 0; + for (char *p = cpt; *p; cpt_value_idx++) { + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + + if (cpt_func_refresh_always[cpt_value_idx]) { + if (*p == 'o') { + cb = &curbuf->b_ofu_cb; + } else if (*p == 'f') { + cb = (*(p + 1) != ',' && *(p + 1) != NUL) + ? get_cpt_func_callback(p + 1) : &curbuf->b_cfu_cb; + } + if (cb) { + compl_curr_match = remove_old_matches(); + get_cpt_func_completion_matches(cb); + } + } + + copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p + } + cpt_value_idx = -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 index 6ba29361b9..4b20ac8883 100644 --- 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', '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 diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index 5c4ae8a696..c2e0aab57d 100644 --- 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,40 +839,45 @@ 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; + char buffer[LSIZE]; - // check if it is a valid value for 'complete' -- Acevedo - for (char *s = *varp; *s;) { - while (*s == ',' || *s == ' ') { - s++; - } - if (!*s) { - break; - } - if (vim_strchr(".wbuksid]tUf", (uint8_t)(*s)) == NULL) { - return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*s)); - } - 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++; - } - s++; - } + 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 { - if (args->os_errbuf != NULL) { - vim_snprintf(args->os_errbuf, args->os_errbuflen, - _("E535: Illegal character after <%c>"), - *--s); - return args->os_errbuf; - } - return ""; + escape = 0; + *buf_ptr++ = *p; } + p++; + } + *buf_ptr = NUL; + + if (vim_strchr(".wbuksid]tUfo", (uint8_t)(*buffer)) == NULL) { + return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*buffer)); + } + + if (!vim_strchr("ksf", (uint8_t)(*buffer)) && *(buffer + 1) != NUL) { + if (args->os_errbuf != NULL) { + vim_snprintf(args->os_errbuf, args->os_errbuflen, + _(e_illegal_character_after_chr), (uint8_t)(*buffer)); + return args->os_errbuf; + } + } + + // Skip comma and spaces + while (*p == ',' || *p == ' ') { + p++; } } return NULL; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 0d434386ea..702c1cc3e7 100644 --- 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 -\\" call assert_equal("find -help", getline('$')) + %d + set complete=o + exe "normal Gofind -\" + " '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\" + 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\\", 'x') + call assert_equal([1, 1], s:args[0]) + call assert_equal(0, s:args[1][0]) + set complete=o + call feedkeys("i\\", '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=fCompleteDone_CompleteFuncNone + execute "normal a\\" + 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\\\", 'tx') + call feedkeys("Shello vim visual v\\", 'tx') + call assert_equal('', g:complete_word) + call assert_equal('keyword', g:complete_type) + + call feedkeys("Shello vim visual v\\", '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 CompleteDone_CheckCompletedItemDict(2) + au CompleteDone * :call CompleteDone_CheckCompletedItemDict(0) + + set complete=.,fCompleteDone_CompleteFuncDict + execute "normal a\\" + 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 @@ -392,6 +445,15 @@ func Test_CompleteDoneDictNoUserData() call assert_equal('', v:completed_item[ 'user_data' ]) call assert_true(s:called_completedone) + let s:called_completedone = 0 + + set complete=.,fCompleteDone_CompleteFuncDictNoUserData + execute "normal a\\" + set complete& + + call assert_equal('', v:completed_item[ 'user_data' ]) + call assert_true(s:called_completedone) + let s:called_completedone = 0 au! CompleteDone endfunc @@ -425,6 +487,24 @@ func Test_CompleteDoneList() call assert_equal('', v:completed_item[ 'user_data' ]) call assert_true(s:called_completedone) + let s:called_completedone = 0 + + set complete=.,fCompleteDone_CompleteFuncList + execute "normal a\\" + 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\\" + 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,51 @@ func Test_completefunc_info() set completefunc=CompleteTest call feedkeys("i\\\\=string(complete_info())\\", "tx") call assert_equal("matched{'pum_visible': 1, 'mode': 'function', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1)) - bwipe! + %d + set complete=.,fCompleteTest + call feedkeys("i\\\=string(complete_info())\\", "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\\\=string(complete_info())\\", "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\", "tx") + bwipe! + new + set completeopt=longest + call feedkeys("ifoo bar\", "tx") + bwipe! + new + set completeopt=menuone + call feedkeys("ifoo bar\", "tx") + bwipe! + new + set completeopt=menuone,preinsert + call feedkeys("ifoo bar\", "tx") + bwipe! + set complete& completeopt& + delfunc CptColTest +endfunc + func ScrollInfoWindowUserDefinedFn(findstart, query) " User defined function (i_CTRL-X_CTRL-U) if a:findstart @@ -529,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\\" . a:mvmt . "\\=string(complete_info())\\", "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\\" . a:mvmt . "\\=string(complete_info())\\", "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\" . a:mvmt . "\\=string(complete_info())\\", "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\" . a:mvmt . "\\=string(complete_info())\\", "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() @@ -914,6 +1044,10 @@ func Test_completefunc_error() set completefunc=CompleteFunc call setline(1, ['', 'abcd', '']) call assert_fails('exe "normal 2G$a\\"', 'E565:') + set complete=fCompleteFunc + call assert_fails('exe "normal 2G$a\"', 'E565:') + set complete=f + call assert_fails('exe "normal 2G$a\"', 'E565:') " delete text when called for the second time func CompleteFunc2(findstart, base) @@ -926,6 +1060,10 @@ func Test_completefunc_error() set completefunc=CompleteFunc2 call setline(1, ['', 'abcd', '']) call assert_fails('exe "normal 2G$a\\"', 'E565:') + set complete=fCompleteFunc2 + call assert_fails('exe "normal 2G$a\"', 'E565:') + set complete=f + call assert_fails('exe "normal 2G$a\"', 'E565:') " Jump to a different window from the complete function func CompleteFunc3(findstart, base) @@ -938,9 +1076,15 @@ func Test_completefunc_error() set completefunc=CompleteFunc3 new call assert_fails('exe "normal a\\"', 'E565:') + %d + set complete=fCompleteFunc3 + call assert_fails('exe "normal a\"', 'E565:') + %d + set complete=f + call assert_fails('exe "normal a\"', 'E565:') close! - set completefunc& + set completefunc& complete& delfunc CompleteFunc delfunc CompleteFunc2 delfunc CompleteFunc3 @@ -959,7 +1103,15 @@ func Test_completefunc_invalid_data() set completefunc=CompleteFunc exe "normal i\\" call assert_equal('moon', getline(1)) - set completefunc& + %d + set complete=fCompleteFunc + exe "normal i\" + call assert_equal('moon', getline(1)) + %d + set complete=f + exe "normal i\" + call assert_equal('moon', getline(1)) + set completefunc& complete& close! endfunc @@ -1636,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\\\\\\\" call assert_equal('up', getline(1)) call assert_equal(6, g:CallCount) - set completeopt& - set completefunc& + %d + let g:CallCount = 0 + set complete=fTcomplete + exe "normal! iup\\\\\\" + call assert_equal('up', getline(1)) + call assert_equal(6, g:CallCount) + %d + let g:CallCount = 0 + set complete=f + exe "normal! iup\\\\\\" + 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\\\\\\" + call assert_equal('up', getline(1)) + call assert_equal(6, g:CallCount) bw! + set completeopt& + set complete& + set completefunc& 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\" + %d + set complete=ffunction('CompleteFail'\\,\ [-3]) + exe "normal! ia\" + 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\\=complete_info([\"items\"])\" + call assert_match('''word'': ''foo''.*''word'': ''fbar''', getline(1)) + call assert_equal(1, g:CallCount) + %d + let g:CallCount = 0 + exe "normal! if\o\=complete_info([\"items\", \"selected\"])\" + 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\o\=complete_info([\"items\", \"selected\"])\" + 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\oo\=complete_info([\"items\"])\" + call assert_equal('foo{''items'': []}', getline(1)) + call assert_equal(3, g:CallCount) + %d + let g:CallCount = 0 + exe "normal! if\oo\\=complete_info([\"items\"])\" + 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\oo\=complete_info([\"items\", \"selected\"])\" + 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\oo\\=complete_info([\"items\"])\" + 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\o\\=complete_info([\"items\", \"selected\"])\" + 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\oo\\=complete_info([\"items\", \"selected\"])\" + 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\\\=CompleteMenuWords()\" + 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\\\\=CompleteMenuWords()\" + 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\\o\=CompleteMenuWords()\" + 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\\\\=CompleteMenuWords()\" + 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\\\\\=CompleteMenuWords()\" + 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\\\\=CompleteMenuWords()\" + 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\\\\\=CompleteMenuWords()\" + 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\\\=CompleteMenuWords()\" + 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\\\\=CompleteMenuWords()\" + 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\\\=CompleteMenuWords()\" + 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\\\\=CompleteMenuWords()\" + 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\\\\=CompleteMenuWords()\" + 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\\\\\=CompleteMenuWords()\" + 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\\=CompleteMenuMatches()\" + " \x0e is + 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\o\o\=CompleteMenuMatches()\" + 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\o\o\=CompleteMenuMatches()\" + 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\\=CompleteMenuMatches()\" + 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\\=CompleteMenuMatches()\" + 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\o\=CompleteMenuMatches()\" + 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\o\=CompleteMenuMatches()\" + 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\oo\=CompleteMenuMatches()\" + 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\oo\\=CompleteMenuMatches()\" + 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\oo\\=CompleteMenuMatches()\" + 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\oo\o\=CompleteMenuMatches()\" + 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 @@ -1687,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\\", '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\\", '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\\", '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\\", '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\\", '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\\", '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\\", '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\\", '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\\", '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) @@ -2564,10 +3198,19 @@ endfunc func Test_complete_smartindent() new setlocal smartindent completefunc=FooBarComplete - exe "norm! o{\\\\}\\" let result = getline(1,'$') call assert_equal(['', '{','}',''], result) + %d + setlocal complete=fFooBarComplete + exe "norm! o{\\\}\\" + let result = getline(1,'$') + call assert_equal(['', '{','}',''], result) + %d + setlocal complete=f + exe "norm! o{\\\}\\" + let result = getline(1,'$') + call assert_equal(['', '{','}',''], result) bw! delfunction! FooBarComplete endfunc diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim index 3fb12b4016..2f889e3fa7 100644 --- a/test/old/testdir/test_options.vim +++ b/test/old/testdir/test_options.vim @@ -277,6 +277,14 @@ func Test_complete() call feedkeys("i\\", '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 + set complete=. + set complete+=ffuncref('foo'\\,\ [10]) + set complete=ffuncref('foo'\\,\ [10]) + set complete& + set complete+=ffunction('foo'\\,\ [10\\,\ 20]) set complete& endfun