diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index 5c0ebcf2e7..f52cf40adb 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -1,4 +1,4 @@ -*insert.txt* For Vim version 9.1. Last change: 2025 Mar 09 +*insert.txt* For Vim version 9.1. Last change: 2025 Apr 14 VIM REFERENCE MANUAL by Bram Moolenaar @@ -1167,6 +1167,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/options.txt b/runtime/doc/options.txt index 83e00e0a2f..99e6d54f89 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1,4 +1,4 @@ -*options.txt* For Vim version 9.1. Last change: 2025 Apr 13 +*options.txt* For Vim version 9.1. Last change: 2025 Apr 14 VIM REFERENCE MANUAL by Bram Moolenaar @@ -2085,6 +2085,28 @@ A jump table for the options with a short description can be found at |Q_op|. |i_CTRL-X_CTRL-D| ] tag completion t same as "]" + 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/doc/version9.txt b/runtime/doc/version9.txt index befd92ffd6..50fb66ff93 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -1,4 +1,4 @@ -*version9.txt* For Vim version 9.1. Last change: 2025 Apr 12 +*version9.txt* For Vim version 9.1. Last change: 2025 Apr 14 VIM REFERENCE MANUAL by Bram Moolenaar @@ -41617,6 +41617,10 @@ Completion: ~ - improved commandline completion for the |:hi| command - New option value for 'wildmode': "noselect" - do not auto select an entry in the wildmenu +- New flags for 'complete': + "f{func}" - complete using given function + "f" - complete using 'completefunc' + "o" - complete using 'omnifunc' Options: ~ - the default for 'commentstring' contains whitespace padding to have diff --git a/src/insexpand.c b/src/insexpand.c index 9df0445392..4afb3d9a81 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -109,6 +109,7 @@ struct compl_S int 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 }; // values for cp_flags @@ -124,7 +125,7 @@ struct compl_S * "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; @@ -171,7 +172,10 @@ static int compl_started = FALSE; static int ctrl_x_mode = CTRL_X_NORMAL; static int compl_matches = 0; // number of completion matches -static string_T compl_pattern = {NULL, 0}; +static string_T compl_pattern = {NULL, 0}; // search pattern for matching items +#ifdef FEAT_COMPL_FUNC +static string_T cpt_compl_pattern = {NULL, 0}; // pattern returned by func in 'cpt' +#endif static int compl_direction = FORWARD; static int compl_shows_dir = FORWARD; static int compl_pending = 0; // > 1 for postponed CTRL-N @@ -208,6 +212,10 @@ static int compl_selected_item = -1; static int *compl_fuzzy_scores; +static int *cpt_func_refresh_always; // array indicating which 'cpt' functions have 'refresh:always' set +static int cpt_value_count; // total number of completion sources specified in the 'cpt' option +static int cpt_value_idx; // index of the current completion source being expanded + // "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; @@ -227,7 +235,14 @@ static void ins_compl_fixRedoBufForLeader(char_u *ptr_arg); # if defined(FEAT_COMPL_FUNC) || defined(FEAT_EVAL) static void ins_compl_add_list(list_T *list); static void ins_compl_add_dict(dict_T *dict); +static int get_userdefined_compl_info(colnr_T curs_col, callback_T *cb, int *startcol); +static callback_T *get_cpt_func_callback(char_u *funcname); +static void get_cpt_func_completion_matches(callback_T *cb); # endif +static int cpt_compl_src_init(char_u *p_cpt); +static int is_cpt_func_refresh_always(void); +static void cpt_compl_src_clear(void); +static void cpt_compl_refresh(void); static int ins_compl_key2dir(int c); static int ins_compl_pum_key(int c); static int ins_compl_key2count(int c); @@ -873,6 +888,7 @@ ins_compl_add( 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) { @@ -1937,6 +1953,26 @@ find_line_end(char_u *ptr) return s; } +/* + * Free a completion item in the list + */ + static void +ins_compl_item_free(compl_T *match) +{ + int i; + + VIM_CLEAR_STRING(match->cp_str); + // several entries may use the same fname, free it just once. + if (match->cp_flags & CP_FREE_FNAME) + vim_free(match->cp_fname); + for (i = 0; i < CPT_COUNT; ++i) + vim_free(match->cp_text[i]); +#ifdef FEAT_EVAL + clear_tv(&match->cp_user_data); +#endif + vim_free(match); +} + /* * Free the list of completions */ @@ -1944,7 +1980,6 @@ find_line_end(char_u *ptr) ins_compl_free(void) { compl_T *match; - int i; VIM_CLEAR_STRING(compl_pattern); VIM_CLEAR_STRING(compl_leader); @@ -1960,16 +1995,7 @@ ins_compl_free(void) { match = compl_curr_match; compl_curr_match = compl_curr_match->cp_next; - VIM_CLEAR_STRING(match->cp_str); - // several entries may use the same fname, free it just once. - if (match->cp_flags & CP_FREE_FNAME) - vim_free(match->cp_fname); - for (i = 0; i < CPT_COUNT; ++i) - vim_free(match->cp_text[i]); -#ifdef FEAT_EVAL - clear_tv(&match->cp_user_data); -#endif - vim_free(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; @@ -1993,6 +2019,7 @@ ins_compl_clear(void) edit_submode_extra = NULL; VIM_CLEAR_STRING(compl_orig_text); compl_enter_selects = FALSE; + cpt_compl_src_clear(); #ifdef FEAT_EVAL // clear v:completed_item set_vim_var_dict(VV_COMPLETED_ITEM, dict_alloc_lock(VAR_FIXED)); @@ -2026,8 +2053,8 @@ ins_compl_win_active(win_T *wp UNUSED) } /* - * 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 either edited or + * using the longest common string. */ int ins_compl_used_match(void) @@ -2185,7 +2212,11 @@ ins_compl_new_leader(void) compl_used_match = FALSE; if (compl_started) + { ins_compl_set_original_text(compl_leader.string, compl_leader.length); + if (is_cpt_func_refresh_always()) + cpt_compl_refresh(); + } else { #ifdef FEAT_SPELL @@ -2298,6 +2329,7 @@ ins_compl_restart(void) compl_matches = 0; compl_cont_status = 0; compl_cont_mode = 0; + cpt_compl_src_clear(); } /* @@ -3051,24 +3083,30 @@ get_insert_callback(int type) /* * Execute user defined complete function 'completefunc', 'omnifunc' or * 'thesaurusfunc', and get matches in "matches". - * "type" is either CTRL_X_OMNI or CTRL_X_FUNCTION or CTRL_X_THESAURUS. + * "type" can be one of CTRL_X_OMNI, CTRL_X_FUNCTION, or CTRL_X_THESAURUS. + * Callback function "cb" is set if triggered by a function in the 'cpt' + * option; otherwise, it is NULL. */ static void -expand_by_function(int type, char_u *base) +expand_by_function(int type, char_u *base, callback_T *cb) { list_T *matchlist = NULL; dict_T *matchdict = NULL; typval_T args[3]; char_u *funcname; pos_T pos; - callback_T *cb; typval_T rettv; int save_State = State; int retval; + int is_cpt_function = (cb != NULL); - funcname = get_complete_funcname(type); - if (*funcname == NUL) - return; + if (!is_cpt_function) + { + funcname = get_complete_funcname(type); + if (*funcname == NUL) + return; + cb = get_insert_callback(type); + } // Call 'completefunc' to obtain the list of matches. args[0].v_type = VAR_NUMBER; @@ -3083,7 +3121,6 @@ expand_by_function(int type, char_u *base) // Insert mode in another buffer. ++textlock; - cb = get_insert_callback(type); retval = call_callback(cb, 0, &rettv, 2, args); // Call a function, which returns a list or dict. @@ -3650,6 +3687,7 @@ typedef struct int found_all; // found all matches of a certain type. char_u *dict; // dictionary file to search int dict_f; // "dict" is an exact file name or not + callback_T *func_cb; // callback of function in 'cpt' option } ins_compl_next_state_T; /* @@ -3763,6 +3801,19 @@ process_next_cpt_value( st->dict_f = DICT_FIRST; } } +#ifdef FEAT_COMPL_FUNC + 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; + } +#endif #ifdef FEAT_FIND_ID else if (*st->e_cpt == 'i') compl_type = CTRL_X_PATH_PATTERNS; @@ -3821,7 +3872,7 @@ get_next_dict_tsr_completion(int compl_type, char_u *dict, int dict_f) { #ifdef FEAT_COMPL_FUNC if (thesaurus_func_complete(compl_type)) - expand_by_function(compl_type, compl_pattern.string); + expand_by_function(compl_type, compl_pattern.string, NULL); else #endif ins_compl_dictionaries( @@ -4412,6 +4463,38 @@ get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_pos) return found_new_match; } +/* + * Return the callback function associated with "funcname". + */ +#ifdef FEAT_COMPL_FUNC + static callback_T * +get_cpt_func_callback(char_u *funcname) +{ + static callback_T cb; + char_u buf[LSIZE]; + int slen; + + 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_T *cb) +{ + // Re-insert the text removed by ins_compl_delete(). + ins_compl_insert_bytes(compl_orig_text.string + get_compl_len(), -1); + // Get matches + get_cpt_func_completion_matches(cb); + // Undo insertion + ins_compl_delete(); +} +#endif + /* * get the next set of completion matches for "type". * Returns TRUE if a new match is found. Otherwise returns FALSE. @@ -4453,8 +4536,13 @@ get_next_completion_match(int type, ins_compl_next_state_T *st, pos_T *ini) #ifdef FEAT_COMPL_FUNC 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.string, NULL); + break; case CTRL_X_OMNI: - expand_by_function(type, compl_pattern.string); + expand_by_function(type, compl_pattern.string, NULL); break; #endif @@ -4513,6 +4601,10 @@ ins_compl_get_exp(pos_T *ini) ? (char_u *)"." : curbuf->b_p_cpt); st.e_cpt = st.e_cpt_copy == NULL ? (char_u *)"" : 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)) + return FAIL; } else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf)) st.ins_buf = curbuf; // In case the buffer was wiped out. @@ -4522,7 +4614,7 @@ ins_compl_get_exp(pos_T *ini) ? &st.last_match_pos : &st.first_match_pos; // For ^N/^P loop over all the flags/windows/buffers in 'complete'. - for (;;) + for (cpt_value_idx = 0;;) { found_new_match = FAIL; st.set_match_pos = FALSE; @@ -4538,7 +4630,10 @@ ins_compl_get_exp(pos_T *ini) if (status == INS_COMPL_CPT_END) break; if (status == INS_COMPL_CPT_CONT) + { + cpt_value_idx++; continue; + } } // If complete() was called then compl_pattern has been reset. The @@ -4549,6 +4644,9 @@ 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 @@ -4575,6 +4673,7 @@ 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()) @@ -5478,9 +5577,13 @@ get_cmdline_compl_info(char_u *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 + * Callback function "cb" is set if triggered by a function in the 'cpt' + * option; otherwise, it is NULL. + * "startcol", when not NULL, contains the column returned by function. */ static int -get_userdefined_compl_info(colnr_T curs_col UNUSED) +get_userdefined_compl_info(colnr_T curs_col UNUSED, callback_T *cb UNUSED, + int *startcol UNUSED) { int ret = FAIL; @@ -5493,16 +5596,22 @@ get_userdefined_compl_info(colnr_T curs_col UNUSED) char_u *funcname; pos_T pos; int save_State = State; - callback_T *cb; + int len; + string_T *compl_pat; + int is_cpt_function = (cb != NULL); - // Call 'completefunc' or 'omnifunc' or 'thesaurusfunc' and get pattern - // length as a string - funcname = get_complete_funcname(ctrl_x_mode); - if (*funcname == NUL) + if (!is_cpt_function) { - semsg(_(e_option_str_is_not_set), ctrl_x_mode_function() - ? "completefunc" : "omnifunc"); - return FAIL; + // Call 'completefunc' or 'omnifunc' or 'thesaurusfunc' and get pattern + // length as a string + funcname = get_complete_funcname(ctrl_x_mode); + if (*funcname == NUL) + { + semsg(_(e_option_str_is_not_set), ctrl_x_mode_function() + ? "completefunc" : "omnifunc"); + return FAIL; + } + cb = get_insert_callback(ctrl_x_mode); } args[0].v_type = VAR_NUMBER; @@ -5512,7 +5621,6 @@ get_userdefined_compl_info(colnr_T curs_col UNUSED) args[2].v_type = VAR_UNKNOWN; pos = curwin->w_cursor; ++textlock; - cb = get_insert_callback(ctrl_x_mode); col = call_callback_retnr(cb, 2, args); --textlock; @@ -5526,6 +5634,9 @@ get_userdefined_compl_info(colnr_T curs_col UNUSED) 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. @@ -5534,6 +5645,8 @@ get_userdefined_compl_info(colnr_T curs_col UNUSED) // 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)) @@ -5546,24 +5659,27 @@ get_userdefined_compl_info(colnr_T curs_col UNUSED) compl_opt_refresh_always = FALSE; compl_opt_suppress_empty = 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. line = ml_get(curwin->w_cursor.lnum); - compl_length = curs_col - compl_col; - compl_pattern.string = vim_strnsave(line + compl_col, (size_t)compl_length); - if (compl_pattern.string == NULL) + len = curs_col - col; + compl_pat = is_cpt_function ? &cpt_compl_pattern : &compl_pattern; + compl_pat->string = vim_strnsave(line + col, (size_t)len); + if (compl_pat->string == NULL) { - compl_pattern.length = 0; + compl_pat->length = 0; return FAIL; } + compl_pat->length = (size_t)compl_length; - compl_pattern.length = (size_t)compl_length; + if (!is_cpt_function) + { + compl_col = col; + compl_length = len; + } ret = OK; #endif @@ -5644,7 +5760,7 @@ compl_get_info(char_u *line, int startcol, colnr_T curs_col, int *line_invalid) 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 } @@ -6130,3 +6246,220 @@ spell_back_to_badword(void) start_arrow(&tpos); } #endif + +/* + * Reset the info associated with completion sources. + */ + static void +cpt_compl_src_clear(void) +{ + VIM_CLEAR(cpt_func_refresh_always); + cpt_value_idx = -1; + cpt_value_count = 0; +} + +/* + * Initialize the info associated with completion sources. + */ + static int +cpt_compl_src_init(char_u *cpt_str) +{ + int count = 0; + char_u *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 = ALLOC_CLEAR_MULT(int, count); + if (cpt_func_refresh_always == NULL) + { + cpt_value_count = 0; + return FAIL; + } + } + return OK; +} + +/* + * Return TRUE if any of the completion sources have 'refresh' set to 'always'. + */ + static int +is_cpt_func_refresh_always(void) +{ +#ifdef FEAT_COMPL_FUNC + int i; + + for (i = 0; i < cpt_value_count; i++) + if (cpt_func_refresh_always[i]) + return TRUE; +#endif + return FALSE; +} + +/* + * Make the completion list non-cyclic. + */ +#ifdef FEAT_COMPL_FUNC + static void +ins_compl_make_linear(void) +{ + compl_T *m; + + if (compl_first_match == NULL || compl_first_match->cp_prev == NULL) + return; + m = compl_first_match->cp_prev; + m->cp_next = NULL; + compl_first_match->cp_prev = NULL; +} +#endif + +/* + * Remove the matches linked to the current completion source (as indicated by + * cpt_value_idx) from the completion list. + */ +#ifdef FEAT_COMPL_FUNC + static compl_T * +remove_old_matches(void) +{ + compl_T *sublist_start = NULL, *sublist_end = NULL, *insert_at = NULL; + compl_T *current, *next; + int 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; +} +#endif + +/* + * Retrieve completion matches using the callback function "cb" and store the + * 'refresh:always' flag. + */ +#ifdef FEAT_COMPL_FUNC + static void +get_cpt_func_completion_matches(callback_T *cb UNUSED) +{ + int ret; + int startcol; + + VIM_CLEAR_STRING(cpt_compl_pattern); + 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.string, cb); + cpt_func_refresh_always[cpt_value_idx] = compl_opt_refresh_always; + compl_opt_refresh_always = FALSE; + } +} +#endif + +/* + * Retrieve completion matches from functions in the 'cpt' option where the + * 'refresh:always' flag is set. + */ + static void +cpt_compl_refresh(void) +{ +#ifdef FEAT_COMPL_FUNC + char_u *cpt; + char_u *p; + callback_T *cb; + + // Make the completion list linear (non-cyclic) + ins_compl_make_linear(); + // Make a copy of 'cpt' in case the buffer gets wiped out + cpt = vim_strsave(curbuf->b_p_cpt); + + cpt_value_idx = 0; + for (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; + + vim_free(cpt); + // Make the list cyclic + compl_matches = ins_compl_make_cyclic(); +#endif +} diff --git a/src/optionstr.c b/src/optionstr.c index 60e28d68b7..47c340577f 100644 --- a/src/optionstr.c +++ b/src/optionstr.c @@ -1552,48 +1552,57 @@ did_set_commentstring(optset_T *args) #endif /* - * The 'complete' option is changed. + * Check if value for 'complete' is valid when 'complete' option is changed. */ char * did_set_complete(optset_T *args) { char_u **varp = (char_u **)args->os_varp; - char_u *s; + char_u *p = NULL; + char_u buffer[LSIZE]; + char_u *buf_ptr; + int escape; - // check if it is a valid value for 'complete' -- Acevedo - for (s = *varp; *s;) + for (p = *varp; *p; ) { - while (*s == ',' || *s == ' ') - s++; - if (!*s) - break; - if (vim_strchr((char_u *)".wbuksid]tU", *s) == NULL) - return illegal_char(args->os_errbuf, args->os_errbuflen, *s); - if (*++s != NUL && *s != ',' && *s != ' ') + vim_memset(buffer, 0, LSIZE); + buf_ptr = buffer; + escape = 0; + + // Extract substring while handling escaped commas + while (*p && (*p != ',' || escape) && buf_ptr < (buffer + LSIZE - 1)) { - if (s[-1] == 'k' || s[-1] == 's') + if (*p == '\\' && *(p + 1) == ',') { - // skip optional filename after 'k' and 's' - while (*s && *s != ',' && *s != ' ') - { - if (*s == '\\' && s[1] != NUL) - ++s; - ++s; - } + escape = 1; // Mark escape mode + p++; // Skip '\' } else { - if (args->os_errbuf != NULL) - { - vim_snprintf((char *)args->os_errbuf, args->os_errbuflen, - _(e_illegal_character_after_chr), *--s); - return args->os_errbuf; - } - return ""; + escape = 0; + *buf_ptr++ = *p; + } + p++; + } + *buf_ptr = NUL; + + if (vim_strchr((char_u *)".wbuksid]tUfo", *buffer) == NULL) + return illegal_char(args->os_errbuf, args->os_errbuflen, *buffer); + + if (!vim_strchr((char_u *)"ksf", *buffer) && *(buffer + 1) != NUL) + { + if (args->os_errbuf) + { + vim_snprintf((char *)args->os_errbuf, args->os_errbuflen, + _(e_illegal_character_after_chr), *buffer); + return args->os_errbuf; } } - } + // Skip comma and spaces + while (*p == ',' || *p == ' ') + p++; + } return NULL; } @@ -1601,7 +1610,7 @@ did_set_complete(optset_T *args) expand_set_complete(optexpand_T *args, int *numMatches, char_u ***matches) { static char *(p_cpt_values[]) = { - ".", "w", "b", "u", "k", "kspell", "s", "i", "d", "]", "t", "U", + ".", "w", "b", "u", "k", "kspell", "s", "i", "d", "]", "t", "U", "f", "o", NULL}; return expand_set_opt_string( args, diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index bc971b5da8..5c67dbf4f2 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -130,10 +130,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() @@ -153,11 +158,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_omni_autoload() @@ -210,6 +225,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 @@ -255,7 +280,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 @@ -272,7 +297,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 @@ -293,6 +326,7 @@ func Test_CompleteDone_vevent_keys() endfunc set omnifunc=CompleteFunc set completefunc=CompleteFunc + set complete=.,fCompleteFunc set completeopt+=menuone new @@ -316,7 +350,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) @@ -374,6 +412,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 @@ -416,6 +469,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 @@ -449,6 +511,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 @@ -492,11 +572,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 @@ -552,24 +672,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() @@ -839,6 +969,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) @@ -851,6 +985,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) @@ -863,9 +1001,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 @@ -884,7 +1028,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 @@ -1557,18 +1709,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 @@ -1608,6 +2105,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 v9.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 v9.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 v9.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) @@ -2484,10 +3118,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/src/testdir/test_options.vim b/src/testdir/test_options.vim index 70570cf276..834ca44df2 100644 --- a/src/testdir/test_options.vim +++ b/src/testdir/test_options.vim @@ -272,6 +272,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 diff --git a/src/version.c b/src/version.c index 0a5b4ecf5d..2758382375 100644 --- a/src/version.c +++ b/src/version.c @@ -704,6 +704,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1301, /**/ 1300, /**/