diff options
-rw-r--r-- | runtime/doc/news.txt | 3 | ||||
-rw-r--r-- | runtime/doc/options.txt | 19 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/options.lua | 22 | ||||
-rw-r--r-- | runtime/optwin.vim | 2 | ||||
-rw-r--r-- | src/nvim/insexpand.c | 302 | ||||
-rw-r--r-- | src/nvim/option_vars.h | 4 | ||||
-rw-r--r-- | src/nvim/options.lua | 31 | ||||
-rw-r--r-- | src/nvim/optionstr.c | 1 | ||||
-rw-r--r-- | src/nvim/search.c | 38 | ||||
-rw-r--r-- | src/nvim/spell.c | 2 | ||||
-rw-r--r-- | test/functional/ui/popupmenu_spec.lua | 60 | ||||
-rw-r--r-- | test/old/testdir/gen_opt_test.vim | 3 | ||||
-rw-r--r-- | test/old/testdir/test_ins_complete.vim | 212 | ||||
-rw-r--r-- | test/old/testdir/test_popup.vim | 43 |
14 files changed, 555 insertions, 187 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index c0f3b31cf7..7bdacd73bf 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -131,7 +131,8 @@ LUA OPTIONS -• todo +• 'completefuzzycollect' enables fuzzy collection of candidates for (some) + |ins-completion| modes. PERFORMANCE diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index d477130a29..f44ea926b2 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1517,6 +1517,18 @@ A jump table for the options with a short description can be found at |Q_op|. This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. + *'completefuzzycollect'* *'cfc'* +'completefuzzycollect' 'cfc' string (default "") + global + This option enables fuzzy collection for (only some) specific + |ins-completion| modes, adjusting how items are gathered for fuzzy + matching based on input. + The option can contain the following values (separated by commas), + each enabling fuzzy collection for a specific completion mode: + files file names + keyword keyword completion in 'complete' and current file + whole_line whole lines + *'completeitemalign'* *'cia'* 'completeitemalign' 'cia' string (default "abbr,kind,menu") global @@ -1536,7 +1548,12 @@ A jump table for the options with a short description can be found at |Q_op|. fuzzy Enable |fuzzy-matching| for completion candidates. This allows for more flexible and intuitive matching, where characters can be skipped and matches can be found even - if the exact sequence is not typed. + if the exact sequence is not typed. Note: This option + does not affect the collection of candidate list, it only + controls how completion candidates are reduced from the + list of alternatives. If you want to use |fuzzy-matching| + to gather more alternatives for your candidate list, + see |'completefuzzycollect'|. longest Only insert the longest common text of the matches. If the menu is displayed you can use CTRL-L to add more diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 58a8da4f26..cb199231c7 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -1044,6 +1044,21 @@ vim.o.cfu = vim.o.completefunc vim.bo.completefunc = vim.o.completefunc vim.bo.cfu = vim.bo.completefunc +--- This option enables fuzzy collection for (only some) specific +--- `ins-completion` modes, adjusting how items are gathered for fuzzy +--- matching based on input. +--- The option can contain the following values (separated by commas), +--- each enabling fuzzy collection for a specific completion mode: +--- files file names +--- keyword keyword completion in 'complete' and current file +--- whole_line whole lines +--- +--- @type string +vim.o.completefuzzycollect = "" +vim.o.cfc = vim.o.completefuzzycollect +vim.go.completefuzzycollect = vim.o.completefuzzycollect +vim.go.cfc = vim.go.completefuzzycollect + --- A comma-separated list of strings that controls the alignment and --- display order of items in the popup menu during Insert mode --- completion. The supported values are "abbr", "kind", and "menu". @@ -1063,7 +1078,12 @@ vim.go.cia = vim.go.completeitemalign --- fuzzy Enable `fuzzy-matching` for completion candidates. This --- allows for more flexible and intuitive matching, where --- characters can be skipped and matches can be found even ---- if the exact sequence is not typed. +--- if the exact sequence is not typed. Note: This option +--- does not affect the collection of candidate list, it only +--- controls how completion candidates are reduced from the +--- list of alternatives. If you want to use `fuzzy-matching` +--- to gather more alternatives for your candidate list, +--- see `'completefuzzycollect'`. --- --- longest Only insert the longest common text of the matches. If --- the menu is displayed you can use CTRL-L to add more diff --git a/runtime/optwin.vim b/runtime/optwin.vim index dff3ce39a4..bfeb3367a7 100644 --- a/runtime/optwin.vim +++ b/runtime/optwin.vim @@ -725,6 +725,8 @@ endif if has("insert_expand") call <SID>AddOption("complete", gettext("specifies how Insert mode completion works for CTRL-N and CTRL-P")) call append("$", "\t" .. s:local_to_buffer) + call <SID>OptionL("cfc") + call <SID>AddOption("completefuzzycollect", gettext("using fuzzy collect for defaule completion mode")) call <SID>OptionL("cpt") call <SID>AddOption("completeopt", gettext("whether to use a popup menu for Insert mode completion")) call <SID>OptionL("cot") diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 226cfeb322..fc4c9c9807 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -215,6 +215,13 @@ static compl_T *compl_curr_match = NULL; static compl_T *compl_shown_match = NULL; static compl_T *compl_old_match = NULL; +/// list used to store the compl_T which have the max score +/// used for completefuzzycollect +static compl_T **compl_best_matches = NULL; +static int compl_num_bests = 0; +/// inserted a longest when completefuzzycollect enabled +static bool compl_cfc_longest_ins = false; + /// After using a cursor key <Enter> selects a match in the popup menu, /// otherwise it inserts a line break. static bool compl_enter_selects = false; @@ -732,7 +739,7 @@ static char *ins_compl_infercase_gettext(const char *str, int char_len, int comp /// /// @param[in] cont_s_ipos next ^X<> will set initial_pos int ins_compl_add_infercase(char *str_arg, int len, bool icase, char *fname, Direction dir, - bool cont_s_ipos) + bool cont_s_ipos, int score) FUNC_ATTR_NONNULL_ARG(1) { char *str = str_arg; @@ -777,11 +784,26 @@ int ins_compl_add_infercase(char *str_arg, int len, bool icase, char *fname, Dir flags |= CP_ICASE; } - int res = ins_compl_add(str, len, fname, NULL, false, NULL, dir, flags, false, NULL); + int res = ins_compl_add(str, len, fname, NULL, false, NULL, dir, flags, false, NULL, score); xfree(tofree); return res; } +/// Check if ctrl_x_mode has been configured in 'completefuzzycollect' +static bool cfc_has_mode(void) +{ + switch (ctrl_x_mode) { + case CTRL_X_NORMAL: + return (cfc_flags & kOptCfcFlagKeyword) != 0; + case CTRL_X_FILES: + return (cfc_flags & kOptCfcFlagFiles) != 0; + case CTRL_X_WHOLE_LINE: + return (cfc_flags & kOptCfcFlagWholeLine) != 0; + default: + return false; + } +} + /// free cptext static inline void free_cptext(char *const *const cptext) { @@ -818,12 +840,13 @@ static inline void free_cptext(char *const *const cptext) /// returned in case of error. static int ins_compl_add(char *const str, int len, char *const fname, char *const *const cptext, const bool cptext_allocated, typval_T *user_data, const Direction cdir, - int flags_arg, const bool adup, const int *user_hl) + int flags_arg, const bool adup, const int *user_hl, const int score) FUNC_ATTR_NONNULL_ARG(1) { compl_T *match; const Direction dir = (cdir == kDirectionNotSet ? compl_direction : cdir); int flags = flags_arg; + bool inserted = false; if (flags & CP_FAST) { fast_breakcheck(); @@ -864,6 +887,7 @@ 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. @@ -907,6 +931,33 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons // current match in the list of matches . if (compl_first_match == NULL) { match->cp_next = match->cp_prev = NULL; + } else if (cfc_has_mode() && score > 0 && compl_get_longest) { + compl_T *current = compl_first_match->cp_next; + compl_T *prev = compl_first_match; + inserted = false; + // The direction is ignored when using longest and + // completefuzzycollect, because matches are inserted + // and sorted by score. + while (current != NULL && current != compl_first_match) { + if (current->cp_score < score) { + match->cp_next = current; + match->cp_prev = current->cp_prev; + if (current->cp_prev) { + current->cp_prev->cp_next = match; + } + current->cp_prev = match; + inserted = true; + break; + } + prev = current; + current = current->cp_next; + } + if (!inserted) { + prev->cp_next = match; + match->cp_prev = prev; + match->cp_next = compl_first_match; + compl_first_match->cp_prev = match; + } } else if (dir == FORWARD) { match->cp_next = compl_curr_match->cp_next; match->cp_prev = compl_curr_match; @@ -925,7 +976,7 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons compl_curr_match = match; // Find the longest common string if still doing that. - if (compl_get_longest && (flags & CP_ORIGINAL_TEXT) == 0) { + if (compl_get_longest && (flags & CP_ORIGINAL_TEXT) == 0 && !cfc_has_mode()) { ins_compl_longest_match(match); } @@ -1013,9 +1064,7 @@ static void ins_compl_longest_match(compl_T *match) compl_leader = copy_string(match->cp_str, NULL); bool had_match = (curwin->w_cursor.col > compl_col); - ins_compl_delete(false); - ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); - ins_redraw(false); + ins_compl_longest_insert(compl_leader.data); // When the match isn't there (to avoid matching itself) remove it // again after redrawing. @@ -1049,9 +1098,7 @@ static void ins_compl_longest_match(compl_T *match) compl_leader.size = (size_t)(p - compl_leader.data); bool had_match = (curwin->w_cursor.col > compl_col); - ins_compl_delete(false); - ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); - ins_redraw(false); + ins_compl_longest_insert(compl_leader.data); // When the match isn't there (to avoid matching itself) remove it // again after redrawing. @@ -1072,7 +1119,7 @@ static void ins_compl_add_matches(int num_matches, char **matches, int icase) for (int i = 0; i < num_matches && add_r != FAIL; i++) { add_r = ins_compl_add(matches[i], -1, NULL, NULL, false, NULL, dir, - CP_FAST | (icase ? CP_ICASE : 0), false, NULL); + CP_FAST | (icase ? CP_ICASE : 0), false, NULL, 0); if (add_r == OK) { // If dir was BACKWARD then honor it just once. dir = FORWARD; @@ -1238,6 +1285,7 @@ static int ins_compl_build_pum(void) bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0; bool fuzzy_filter = (cur_cot_flags & kOptCotFlagFuzzy) != 0; bool fuzzy_sort = fuzzy_filter && !(cur_cot_flags & kOptCotFlagNosort); + compl_T *match_head = NULL, *match_tail = NULL; // If the current match is the original text don't find the first @@ -1554,7 +1602,7 @@ static void ins_compl_dictionaries(char *dict_start, char *pat, int flags, bool spell_dump_compl(ptr, regmatch.rm_ic, &dir, 0); } else if (count > 0) { // avoid warning for using "files" uninit ins_compl_files(count, files, thesaurus, flags, - ®match, buf, &dir); + (cfc_has_mode() ? NULL : ®match), buf, &dir); if (flags != DICT_EXACT) { FreeWild(count, files); } @@ -1605,7 +1653,7 @@ static int thesaurus_add_words_in_line(char *fname, char **buf_arg, int dir, con // Add the word. Skip the regexp match. if (wstart != skip_word) { status = ins_compl_add_infercase(wstart, (int)(ptr - wstart), p_ic, - fname, dir, false); + fname, dir, false, 0); if (status == FAIL) { break; } @@ -1622,6 +1670,11 @@ static void ins_compl_files(int count, char **files, bool thesaurus, int flags, regmatch_T *regmatch, char *buf, Direction *dir) FUNC_ATTR_NONNULL_ARG(2, 7) { + bool in_fuzzy_collect = cfc_has_mode() && ctrl_x_mode_normal(); + + char *leader = in_fuzzy_collect ? ins_compl_leader() : NULL; + int leader_len = in_fuzzy_collect ? (int)ins_compl_leader_len() : 0; + for (int i = 0; i < count && !got_int && !compl_interrupted; i++) { FILE *fp = os_fopen(files[i], "r"); // open dictionary file if (flags != DICT_EXACT && !shortmess(SHM_COMPLETIONSCAN)) { @@ -1640,27 +1693,51 @@ static void ins_compl_files(int count, char **files, bool thesaurus, int flags, // Check each line for a match. while (!got_int && !compl_interrupted && !vim_fgets(buf, LSIZE, fp)) { char *ptr = buf; - while (vim_regexec(regmatch, buf, (colnr_T)(ptr - buf))) { - ptr = regmatch->startp[0]; - ptr = ctrl_x_mode_line_or_eval() ? find_line_end(ptr) : find_word_end(ptr); - int add_r = ins_compl_add_infercase(regmatch->startp[0], - (int)(ptr - regmatch->startp[0]), - p_ic, files[i], *dir, false); - if (thesaurus) { - // For a thesaurus, add all the words in the line - ptr = buf; - add_r = thesaurus_add_words_in_line(files[i], &ptr, *dir, regmatch->startp[0]); - } - if (add_r == OK) { - // if dir was BACKWARD then honor it just once - *dir = FORWARD; - } else if (add_r == FAIL) { - break; + if (regmatch != NULL) { + while (vim_regexec(regmatch, buf, (colnr_T)(ptr - buf))) { + ptr = regmatch->startp[0]; + ptr = ctrl_x_mode_line_or_eval() ? find_line_end(ptr) : find_word_end(ptr); + int add_r = ins_compl_add_infercase(regmatch->startp[0], + (int)(ptr - regmatch->startp[0]), + p_ic, files[i], *dir, false, 0); + if (thesaurus) { + // For a thesaurus, add all the words in the line + ptr = buf; + add_r = thesaurus_add_words_in_line(files[i], &ptr, *dir, regmatch->startp[0]); + } + if (add_r == OK) { + // if dir was BACKWARD then honor it just once + *dir = FORWARD; + } else if (add_r == FAIL) { + break; + } + // avoid expensive call to vim_regexec() when at end + // of line + if (*ptr == '\n' || got_int) { + break; + } } - // avoid expensive call to vim_regexec() when at end - // of line - if (*ptr == '\n' || got_int) { - break; + } else if (in_fuzzy_collect && leader_len > 0) { + char *line_end = find_line_end(ptr); + while (ptr < line_end) { + int score = 0; + int len = 0; + if (fuzzy_match_str_in_line(&ptr, leader, &len, NULL, &score)) { + char *end_ptr = ctrl_x_mode_line_or_eval() ? find_line_end(ptr) : find_word_end(ptr); + int add_r = ins_compl_add_infercase(ptr, (int)(end_ptr - ptr), + p_ic, files[i], *dir, false, score); + if (add_r == FAIL) { + break; + } + ptr = end_ptr; // start from next word + if (compl_get_longest && ctrl_x_mode_normal() + && compl_first_match->cp_next + && score == compl_first_match->cp_next->cp_score) { + compl_num_bests++; + } + } else if (find_word_end(ptr) == line_end) { + break; + } } } line_breakcheck(); @@ -1746,6 +1823,7 @@ void ins_compl_clear(void) { compl_cont_status = 0; compl_started = false; + compl_cfc_longest_ins = false; compl_matches = 0; compl_selected_item = -1; compl_ins_end_col = 0; @@ -2744,7 +2822,7 @@ static int ins_compl_add_tv(typval_T *const tv, const Direction dir, bool fast) return FAIL; } int status = ins_compl_add((char *)word, -1, NULL, cptext, true, - &user_data, dir, flags, dup, user_hl); + &user_data, dir, flags, dup, user_hl, 0); if (status != OK) { tv_clear(&user_data); } @@ -2840,7 +2918,7 @@ static void set_completion(colnr_T startcol, list_T *list) } if (ins_compl_add(compl_orig_text.data, (int)compl_orig_text.size, NULL, NULL, false, NULL, 0, - flags | CP_FAST, false, NULL) != OK) { + flags | CP_FAST, false, NULL, 0) != OK) { return; } @@ -3310,6 +3388,87 @@ static int compare_scores(const void *a, const void *b) : (score_a > score_b ? -1 : 1); } +/// insert prefix with redraw +static void ins_compl_longest_insert(char *prefix) +{ + ins_compl_delete(false); + ins_compl_insert_bytes(prefix + get_compl_len(), -1); + ins_redraw(false); +} + +/// Calculate the longest common prefix among the best fuzzy matches +/// stored in compl_best_matches, and insert it as the longest. +static void fuzzy_longest_match(void) +{ + if (compl_num_bests == 0) { + return; + } + + compl_T *nn_compl = compl_first_match->cp_next->cp_next; + bool more_candidates = nn_compl && nn_compl != compl_first_match; + + compl_T *compl = ctrl_x_mode_whole_line() ? compl_first_match + : compl_first_match->cp_next; + if (compl_num_bests == 1) { + // no more candidates insert the match str + if (!more_candidates) { + ins_compl_longest_insert(compl->cp_str.data); + compl_num_bests = 0; + } + compl_num_bests = 0; + return; + } + + compl_best_matches = (compl_T **)xmalloc((size_t)compl_num_bests * sizeof(compl_T *)); + + for (int i = 0; compl != NULL && i < compl_num_bests; i++) { + compl_best_matches[i] = compl; + compl = compl->cp_next; + } + + char *prefix = compl_best_matches[0]->cp_str.data; + int prefix_len = (int)strlen(prefix); + + for (int i = 1; i < compl_num_bests; i++) { + char *match_str = compl_best_matches[i]->cp_str.data; + char *prefix_ptr = prefix; + char *match_ptr = match_str; + int j = 0; + + while (j < prefix_len && *match_ptr != NUL && *prefix_ptr != NUL) { + if (strncmp(prefix_ptr, match_ptr, (size_t)utfc_ptr2len(prefix_ptr)) != 0) { + break; + } + + MB_PTR_ADV(prefix_ptr); + MB_PTR_ADV(match_ptr); + j++; + } + + if (j > 0) { + prefix_len = j; + } + } + + char *leader = ins_compl_leader(); + size_t leader_len = leader != NULL ? strlen(leader) : 0; + + // skip non-consecutive prefixes + if (strncmp(prefix, leader, leader_len) != 0) { + goto end; + } + + prefix = xmemdupz(compl_best_matches[0]->cp_str.data, (size_t)prefix_len); + ins_compl_longest_insert(prefix); + compl_cfc_longest_ins = true; + xfree(prefix); + +end: + xfree(compl_best_matches); + compl_best_matches = NULL; + compl_num_bests = 0; +} + /// Get the next set of filename matching "compl_pattern". static void get_next_filename_completion(void) { @@ -3317,7 +3476,10 @@ static void get_next_filename_completion(void) int num_matches; char *leader = ins_compl_leader(); size_t leader_len = ins_compl_leader_len(); - bool in_fuzzy = ((get_cot_flags() & kOptCotFlagFuzzy) != 0 && leader_len > 0); + bool in_fuzzy_collect = (cfc_has_mode() && leader_len > 0); + bool need_collect_bests = in_fuzzy_collect && compl_get_longest; + int max_score = 0; + Direction dir = compl_direction; #ifdef BACKSLASH_IN_FILENAME char pathsep = (curbuf->b_p_csl[0] == 's') @@ -3326,7 +3488,7 @@ static void get_next_filename_completion(void) char pathsep = PATHSEP; #endif - if (in_fuzzy) { + if (in_fuzzy_collect) { #ifdef BACKSLASH_IN_FILENAME if (curbuf->b_p_csl[0] == 's') { for (size_t i = 0; i < leader_len; i++) { @@ -3349,7 +3511,7 @@ static void get_next_filename_completion(void) API_CLEAR_STRING(compl_pattern); compl_pattern = cbuf_to_string("*", 1); } else if (*(last_sep + 1) == NUL) { - in_fuzzy = false; + in_fuzzy_collect = false; } else { // Split leader into path and file parts size_t path_len = (size_t)(last_sep - leader) + 1; @@ -3389,7 +3551,7 @@ static void get_next_filename_completion(void) } #endif - if (in_fuzzy) { + if (in_fuzzy_collect) { garray_T fuzzy_indices; ga_init(&fuzzy_indices, sizeof(int), 10); compl_fuzzy_scores = (int *)xmalloc(sizeof(int) * (size_t)num_matches); @@ -3408,14 +3570,24 @@ static void get_next_filename_completion(void) int *fuzzy_indices_data = (int *)fuzzy_indices.ga_data; qsort(fuzzy_indices_data, (size_t)fuzzy_indices.ga_len, sizeof(int), compare_scores); - char **sorted_matches = (char **)xmalloc(sizeof(char *) * (size_t)fuzzy_indices.ga_len); for (int i = 0; i < fuzzy_indices.ga_len; i++) { - sorted_matches[i] = xstrdup(matches[fuzzy_indices_data[i]]); + char *match = matches[fuzzy_indices_data[i]]; + int current_score = compl_fuzzy_scores[fuzzy_indices_data[i]]; + if (ins_compl_add(match, -1, NULL, NULL, false, NULL, dir, + CP_FAST | ((p_fic || p_wic) ? CP_ICASE : 0), + false, NULL, current_score) == OK) { + dir = FORWARD; + } + + if (need_collect_bests) { + if (i == 0 || current_score == max_score) { + compl_num_bests++; + max_score = current_score; + } + } } FreeWild(num_matches, matches); - matches = sorted_matches; - num_matches = fuzzy_indices.ga_len; } else if (leader_len > 0) { FreeWild(num_matches, matches); num_matches = 0; @@ -3423,6 +3595,11 @@ static void get_next_filename_completion(void) xfree(compl_fuzzy_scores); ga_clear(&fuzzy_indices); + + if (compl_num_bests > 0 && compl_get_longest) { + fuzzy_longest_match(); + } + return; } if (num_matches > 0) { @@ -3552,8 +3729,9 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ { char *ptr = NULL; int len = 0; - bool in_fuzzy = (get_cot_flags() & kOptCotFlagFuzzy) != 0 && compl_length > 0; + bool in_collect = (cfc_has_mode() && compl_length > 0); char *leader = ins_compl_leader(); + int score = 0; // If 'infercase' is set, don't use 'smartcase' here const int save_p_scs = p_scs; @@ -3569,7 +3747,7 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ const int save_p_ws = p_ws; if (st->ins_buf != curbuf) { p_ws = false; - } else if (*st->e_cpt == '.' && !in_fuzzy) { + } else if (*st->e_cpt == '.') { p_ws = true; } bool looped_around = false; @@ -3578,16 +3756,17 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ bool cont_s_ipos = false; msg_silent++; // Don't want messages for wrapscan. - // ctrl_x_mode_line_or_eval() || word-wise search that - // has added a word that was at the beginning of the line. - if ((ctrl_x_mode_whole_line() && !in_fuzzy) || ctrl_x_mode_eval() - || (compl_cont_status & CONT_SOL)) { - found_new_match = search_for_exact_line(st->ins_buf, st->cur_match_pos, - compl_direction, compl_pattern.data); - } else if (in_fuzzy) { + + if (in_collect) { found_new_match = search_for_fuzzy_match(st->ins_buf, st->cur_match_pos, leader, compl_direction, - start_pos, &len, &ptr, ctrl_x_mode_whole_line()); + start_pos, &len, &ptr, &score); + // ctrl_x_mode_line_or_eval() || word-wise search that + // has added a word that was at the beginning of the line. + } else if (ctrl_x_mode_whole_line() || ctrl_x_mode_eval() + || (compl_cont_status & CONT_SOL)) { + found_new_match = search_for_exact_line(st->ins_buf, st->cur_match_pos, + compl_direction, compl_pattern.data); } else { found_new_match = searchit(NULL, st->ins_buf, st->cur_match_pos, NULL, compl_direction, compl_pattern.data, @@ -3635,7 +3814,7 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ continue; } - if (!in_fuzzy) { + if (!in_collect) { ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos, &len, &cont_s_ipos); } @@ -3646,7 +3825,10 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ if (ins_compl_add_infercase(ptr, len, p_ic, st->ins_buf == curbuf ? NULL : st->ins_buf->b_sfname, - 0, cont_s_ipos) != NOTDONE) { + 0, cont_s_ipos, score) != NOTDONE) { + if (in_collect && score == compl_first_match->cp_next->cp_score) { + compl_num_bests++; + } found_new_match = OK; break; } @@ -3725,7 +3907,7 @@ static void get_next_bufname_token(void) char *tail = path_tail(b->b_sfname); if (strncmp(tail, compl_orig_text.data, compl_orig_text.size) == 0) { ins_compl_add(tail, (int)strlen(tail), NULL, NULL, false, NULL, 0, - p_ic ? CP_ICASE : 0, false, NULL); + p_ic ? CP_ICASE : 0, false, NULL, 0); } } } @@ -3839,6 +4021,10 @@ static int ins_compl_get_exp(pos_T *ini) i = ins_compl_make_cyclic(); } + if (cfc_has_mode() && compl_get_longest && compl_num_bests > 0) { + fuzzy_longest_match(); + } + if (compl_old_match != NULL) { // If several matches were added (FORWARD) or the search failed and has // just been made cyclic then we have to move compl_curr_match to the @@ -4865,9 +5051,9 @@ static int ins_compl_start(void) if (p_ic) { flags |= CP_ICASE; } - if (ins_compl_add(compl_orig_text.data, (int)compl_orig_text.size, + if (ins_compl_add(compl_orig_text.data, -1, NULL, NULL, false, NULL, 0, - flags, false, NULL) != OK) { + flags, false, NULL, 0) != OK) { API_CLEAR_STRING(compl_pattern); API_CLEAR_STRING(compl_orig_text); kv_destroy(compl_orig_extmarks); diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index 3f0445675d..2e5698870f 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -294,8 +294,10 @@ EXTERN char *p_cms; ///< 'commentstring' EXTERN char *p_cpt; ///< 'complete' EXTERN OptInt p_columns; ///< 'columns' EXTERN int p_confirm; ///< 'confirm' +EXTERN char *p_cfc; ///< 'completefuzzycollect' +EXTERN unsigned cfc_flags; ///< flags from 'completefuzzycollect' EXTERN char *p_cia; ///< 'completeitemalign' -EXTERN unsigned cia_flags; ///< order flags of 'completeitemalign' +EXTERN unsigned cia_flags; ///< order flags of 'completeitemalign' EXTERN char *p_cot; ///< 'completeopt' EXTERN unsigned cot_flags; ///< flags from 'completeopt' #ifdef BACKSLASH_IN_FILENAME diff --git a/src/nvim/options.lua b/src/nvim/options.lua index c028999d61..8ee9434a60 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1460,6 +1460,30 @@ local options = { varname = 'p_cfu', }, { + abbreviation = 'cfc', + defaults = '', + values = { 'keyword', 'files', 'whole_line' }, + flags = true, + deny_duplicates = true, + desc = [=[ + This option enables fuzzy collection for (only some) specific + |ins-completion| modes, adjusting how items are gathered for fuzzy + matching based on input. + The option can contain the following values (separated by commas), + each enabling fuzzy collection for a specific completion mode: + files file names + keyword keyword completion in 'complete' and current file + whole_line whole lines + ]=], + full_name = 'completefuzzycollect', + list = 'onecomma', + scope = { 'global' }, + short_desc = N_('use fuzzy collection for specific completion modes'), + type = 'string', + varname = 'p_cfc', + flags_varname = 'cfc_flags', + }, + { abbreviation = 'cia', cb = 'did_set_completeitemalign', defaults = 'abbr,kind,menu', @@ -1505,7 +1529,12 @@ local options = { fuzzy Enable |fuzzy-matching| for completion candidates. This allows for more flexible and intuitive matching, where characters can be skipped and matches can be found even - if the exact sequence is not typed. + if the exact sequence is not typed. Note: This option + does not affect the collection of candidate list, it only + controls how completion candidates are reduced from the + list of alternatives. If you want to use |fuzzy-matching| + to gather more alternatives for your candidate list, + see |'completefuzzycollect'|. longest Only insert the longest common text of the matches. If the menu is displayed you can use CTRL-L to add more diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index 3a6b4c9936..c6cc7af8cd 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -84,6 +84,7 @@ void didset_string_options(void) check_str_opt(kOptCasemap, NULL); check_str_opt(kOptBackupcopy, NULL); check_str_opt(kOptBelloff, NULL); + check_str_opt(kOptCompletefuzzycollect, NULL); check_str_opt(kOptCompleteopt, NULL); check_str_opt(kOptSessionoptions, NULL); check_str_opt(kOptViewoptions, NULL); diff --git a/src/nvim/search.c b/src/nvim/search.c index 07bc84ba84..b31263e127 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -3620,11 +3620,18 @@ garray_T *fuzzy_match_str_with_pos(char *const str, const char *const pat) return match_positions; } -/// This function searches for a fuzzy match of the pattern `pat` within the -/// line pointed to by `*ptr`. It splits the line into words, performs fuzzy -/// matching on each word, and returns the length and position of the first -/// matched word. -static bool fuzzy_match_str_in_line(char **ptr, char *pat, int *len, pos_T *current_pos) +/// This function splits the line pointed to by `*ptr` into words and performs +/// a fuzzy match for the pattern `pat` on each word. It iterates through the +/// line, moving `*ptr` to the start of each word during the process. +/// +/// If a match is found: +/// - `*ptr` points to the start of the matched word. +/// - `*len` is set to the length of the matched word. +/// - `*score` contains the match score. +/// +/// If no match is found, `*ptr` is updated to point beyond the last word +/// or to the end of the line. +bool fuzzy_match_str_in_line(char **ptr, char *pat, int *len, pos_T *current_pos, int *score) { char *str = *ptr; char *strBegin = str; @@ -3649,14 +3656,16 @@ static bool fuzzy_match_str_in_line(char **ptr, char *pat, int *len, pos_T *curr *end = NUL; // Perform fuzzy match - int result = fuzzy_match_str(start, pat); + *score = fuzzy_match_str(start, pat); *end = save_end; - if (result > 0) { + if (*score > 0) { *len = (int)(end - start); - current_pos->col += (int)(end - strBegin); found = true; *ptr = start; + if (current_pos) { + current_pos->col += (int)(end - strBegin); + } break; } @@ -3678,13 +3687,14 @@ static bool fuzzy_match_str_in_line(char **ptr, char *pat, int *len, pos_T *curr /// /// Return true if a match is found, otherwise false. bool search_for_fuzzy_match(buf_T *buf, pos_T *pos, char *pattern, int dir, pos_T *start_pos, - int *len, char **ptr, bool whole_line) + int *len, char **ptr, int *score) { pos_T current_pos = *pos; pos_T circly_end; bool found_new_match = false; bool looped_around = false; + bool whole_line = ctrl_x_mode_whole_line(); if (whole_line) { current_pos.lnum += dir; } @@ -3709,11 +3719,13 @@ bool search_for_fuzzy_match(buf_T *buf, pos_T *pos, char *pattern, int dir, pos_ *ptr = ml_get_buf(buf, current_pos.lnum); // If ptr is end of line is reached, move to next line // or previous line based on direction - if (**ptr != NUL) { + if (*ptr != NULL && **ptr != NUL) { if (!whole_line) { *ptr += current_pos.col; - // Try to find a fuzzy match in the current line starting from current position - found_new_match = fuzzy_match_str_in_line(ptr, pattern, len, ¤t_pos); + // Try to find a fuzzy match in the current line starting + // from current position + found_new_match = fuzzy_match_str_in_line(ptr, pattern, + len, ¤t_pos, score); if (found_new_match) { if (ctrl_x_mode_normal()) { if (strncmp(*ptr, pattern, (size_t)(*len)) == 0 && pattern[*len] == NUL) { @@ -4227,7 +4239,7 @@ search_line: const int add_r = ins_compl_add_infercase(aux, i, p_ic, curr_fname == curbuf->b_fname ? NULL : curr_fname, - dir, cont_s_ipos); + dir, cont_s_ipos, 0); if (add_r == OK) { // if dir was BACKWARD then honor it just once dir = FORWARD; diff --git a/src/nvim/spell.c b/src/nvim/spell.c index 7437a7a32f..c8c3622347 100644 --- a/src/nvim/spell.c +++ b/src/nvim/spell.c @@ -3483,7 +3483,7 @@ static void dump_word(slang_T *slang, char *word, char *pat, Direction *dir, int ? mb_strnicmp(p, pat, strlen(pat)) == 0 : strncmp(p, pat, strlen(pat)) == 0) && ins_compl_add_infercase(p, (int)strlen(p), - p_ic, NULL, *dir, false) == OK) { + p_ic, NULL, *dir, false, 0) == OK) { // if dir was BACKWARD then honor it just once *dir = FORWARD; } diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index 76673c84b8..9403f39ba1 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -6932,25 +6932,48 @@ describe('builtin popupmenu', function() ]]) feed('o<BS><C-R>=Comp()<CR>') screen:expect_unchanged(true) + feed('<C-E><Esc>') + + command('hi PmenuMatchSel guibg=NONE') + command('hi PmenuMatch guibg=NONE') + command('set cot=menu,noinsert,fuzzy') + feed('S<C-X><C-O>') + screen:expect(pum_start) + feed('fb') + screen:expect([[ + fb^ | + {ms:f}{s:oo}{ms:B}{s:az fookind }{1: }| + {mn:f}{n:oo}{mn:b}{n:ar fookind }{1: }| + {mn:f}{n:oo}{mn:b}{n:ala fookind }{1: }| + {1:~ }|*15 + {2:-- }{5:match 1 of 9} | + ]]) + + feed('<C-E><Esc>') + end) + + it('completefuzzycollect', function() + exec([[ + set completefuzzycollect=keyword,files + set completeopt=menu,menuone + ]]) - feed('<Esc>') - command('set completeopt+=fuzzy,menu') feed('S hello helio hero h<C-X><C-P>') screen:expect([[ - hello helio hero h^ | - {1:~ }{n: }{mn:h}{n:ero }{1: }| - {1:~ }{n: }{mn:h}{n:elio }{1: }| - {1:~ }{s: }{ms:h}{s:ello }{1: }| + hello helio hero hello^ | + {1:~ }{n: hero }{1: }| + {1:~ }{n: helio }{1: }| + {1:~ }{s: hello }{1: }| {1:~ }|*15 {2:-- }{5:match 1 of 3} | ]]) feed('<Esc>S hello helio hero h<C-X><C-P><C-P>') screen:expect([[ - hello helio hero h^ | - {1:~ }{n: }{mn:h}{n:ero }{1: }| - {1:~ }{s: }{ms:h}{s:elio }{1: }| - {1:~ }{n: }{mn:h}{n:ello }{1: }| + hello helio hero helio^ | + {1:~ }{n: hero }{1: }| + {1:~ }{s: helio }{1: }| + {1:~ }{n: hello }{1: }| {1:~ }|*15 {2:-- }{5:match 2 of 3} | ]]) @@ -6962,23 +6985,6 @@ describe('builtin popupmenu', function() {2:-- }{6:Pattern not found} | ]]) feed('<C-E><Esc>') - - command('hi PmenuMatchSel guibg=NONE') - command('hi PmenuMatch guibg=NONE') - command('set cot=menu,noinsert,fuzzy') - feed('S<C-X><C-O>') - screen:expect(pum_start) - feed('fb') - screen:expect([[ - fb^ | - {ms:f}{s:oo}{ms:B}{s:az fookind }{1: }| - {mn:f}{n:oo}{mn:b}{n:ar fookind }{1: }| - {mn:f}{n:oo}{mn:b}{n:ala fookind }{1: }| - {1:~ }|*15 - {2:-- }{5:match 1 of 9} | - ]]) - - feed('<C-E><Esc>') end) -- oldtest: Test_pum_highlights_match_with_abbr() diff --git a/test/old/testdir/gen_opt_test.vim b/test/old/testdir/gen_opt_test.vim index aa4c5ec6e0..2127945624 100644 --- a/test/old/testdir/gen_opt_test.vim +++ b/test/old/testdir/gen_opt_test.vim @@ -189,6 +189,9 @@ let test_values = { "\ 'completeopt': [['', 'menu', 'menuone', 'longest', 'preview', 'popup', "\ " 'popuphidden', 'noinsert', 'noselect', 'fuzzy', 'preinsert', 'menu,longest'], "\ " ['xxx', 'menu,,,longest,']], + \ 'completefuzzycollect': [['', 'keyword', 'files', 'whole_line', + \ 'keyword,whole_line', 'files,whole_line', 'keyword,files,whole_line'], + \ ['xxx', 'keyword,,,whole_line,']], \ 'completeitemalign': [['abbr,kind,menu', 'menu,abbr,kind'], \ ['', 'xxx', 'abbr', 'abbr,menu', 'abbr,menu,kind,abbr', \ 'abbr1234,kind,menu']], diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 97a02adf0d..cd2f5d9307 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -2763,7 +2763,7 @@ func Test_completefunc_first_call_complete_add() bwipe! endfunc -func Test_complete_fuzzy_match() +func Test_complete_opt_fuzzy() func OnPumChange() let g:item = get(v:event, 'completed_item', {}) let g:word = get(g:item, 'word', v:null) @@ -2819,8 +2819,66 @@ func Test_complete_fuzzy_match() call feedkeys("S\<C-x>\<C-o>fb\<C-n>", 'tx') call assert_equal('fooBaz', g:word) - " avoid breaking default completion behavior - set completeopt=fuzzy,menu + " test case for nosort option + set cot=menuone,menu,noinsert,fuzzy,nosort + " "fooBaz" should have a higher score when the leader is "fb". + " With "nosort", "foobar" should still be shown first in the popup menu. + call feedkeys("S\<C-x>\<C-o>fb", 'tx') + call assert_equal('foobar', g:word) + call feedkeys("S\<C-x>\<C-o>好", 'tx') + call assert_equal("你好吗", g:word) + + set cot+=noselect + call feedkeys("S\<C-x>\<C-o>好", 'tx') + call assert_equal(v:null, g:word) + call feedkeys("S\<C-x>\<C-o>好\<C-N>", 'tx') + call assert_equal('你好吗', g:word) + + " "nosort" shouldn't enable fuzzy filtering when "fuzzy" isn't present. + set cot=menuone,noinsert,nosort + call feedkeys("S\<C-x>\<C-o>fooB\<C-Y>", 'tx') + call assert_equal('fooBaz', getline('.')) + + set cot=menuone,fuzzy,nosort + func CompAnother() + call complete(col('.'), [#{word: "do" }, #{word: "echo"}, #{word: "for (${1:expr1}, ${2:expr2}, ${3:expr3}) {\n\t$0\n}", abbr: "for" }, #{word: "foo"}]) + return '' + endfunc + call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-N>\<C-N>", 'tx') + call assert_equal("for", g:abbr) + call assert_equal(2, g:selected) + + set cot+=noinsert + call feedkeys("i\<C-R>=CompAnother()\<CR>f", 'tx') + call assert_equal("for", g:abbr) + call assert_equal(2, g:selected) + + set cot=menu,menuone,noselect,fuzzy + call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-N>\<C-N>\<C-N>\<C-N>", 'tx') + call assert_equal("foo", g:word) + call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-P>", 'tx') + call assert_equal("foo", g:word) + call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-P>\<C-P>", 'tx') + call assert_equal("for", g:abbr) + + " clean up + set omnifunc= + bw! + set complete& completeopt& + autocmd! AAAAA_Group + augroup! AAAAA_Group + delfunc OnPumChange + delfunc Omni_test + delfunc Comp + unlet g:item + unlet g:word + unlet g:abbr +endfunc + +func Test_complete_fuzzy_collect() + new + redraw " need this to prevent NULL dereference in Nvim + set completefuzzycollect=keyword,files,whole_line call setline(1, ['hello help hero h']) " Use "!" flag of feedkeys() so that ex_normal_busy is not set and " ins_compl_check_keys() is not skipped. @@ -2852,16 +2910,6 @@ func Test_complete_fuzzy_match() call feedkeys("A\<C-X>\<C-N>\<C-N>\<Esc>0", 'tx!') call assert_equal('你的 我的 我的', getline('.')) - " respect wrapscan - set nowrapscan - call setline(1, ["xyz", "yxz", ""]) - call cursor(3, 1) - call feedkeys("Sy\<C-X>\<C-N>\<Esc>0", 'tx!') - call assert_equal('y', getline('.')) - set wrapscan - call feedkeys("Sy\<C-X>\<C-N>\<Esc>0", 'tx!') - call assert_equal('xyz', getline('.')) - " fuzzy on file call writefile([''], 'fobar', 'D') call writefile([''], 'foobar', 'D') @@ -2877,7 +2925,6 @@ func Test_complete_fuzzy_match() call assert_match('../testdir', getline('.')) " can get completion from other buffer - set completeopt=fuzzy,menu,menuone vnew call setline(1, ["completeness,", "compatibility", "Composite", "Omnipotent"]) wincmd p @@ -2929,79 +2976,109 @@ func Test_complete_fuzzy_match() call assert_equal('你好 他好', getline('.')) " issue #15526 - set completeopt=fuzzy,menuone,menu,noselect + set completeopt=menuone,menu,noselect call setline(1, ['Text', 'ToText', '']) call cursor(3, 1) call feedkeys("STe\<C-X>\<C-N>x\<CR>\<Esc>0", 'tx!') call assert_equal('Tex', getline(line('.') - 1)) - " test case for nosort option - set cot=menuone,menu,noinsert,fuzzy,nosort - " "fooBaz" should have a higher score when the leader is "fb". - " With "nosort", "foobar" should still be shown first in the popup menu. - call feedkeys("S\<C-x>\<C-o>fb", 'tx') - call assert_equal('foobar', g:word) - call feedkeys("S\<C-x>\<C-o>好", 'tx') - call assert_equal("你好吗", g:word) + bw! + bw! + set completeopt& cfc& cpt& +endfunc + +func Test_cfc_with_longest() + new + set completefuzzycollect=keyword,files,whole_line + set completeopt=menu,menuone,longest,fuzzy + + " keyword + exe "normal ggdGShello helio think h\<C-X>\<C-N>\<ESC>" + call assert_equal("hello helio think hel", getline('.')) + exe "normal hello helio think h\<C-X>\<C-P>\<ESC>" + call assert_equal("hello helio think hel", getline('.')) + + " skip non-consecutive prefixes + exe "normal ggdGShello helio heo\<C-X>\<C-N>\<ESC>" + call assert_equal("hello helio heo", getline('.')) + + " kdcit + call writefile(['help'], 'test_keyword.txt', 'D') + set complete=ktest_keyword.txt + exe "normal ggdGSh\<C-N>\<ESC>" + " auto insert help when only have one match + call assert_equal("help", getline('.')) + call writefile(['hello', 'help', 'think'], 'xtest_keyword.txt', 'D') + set complete=kxtest_keyword.txt + " auto insert hel + exe "normal ggdGSh\<C-N>\<ESC>" + call assert_equal("hel", getline('.')) + + " line start with a space + call writefile([' hello'], 'test_case1.txt', 'D') + set complete=ktest_case1.txt + exe "normal ggdGSh\<C-N>\<ESC>" + call assert_equal("hello", getline('.')) - set cot+=noselect - call feedkeys("S\<C-x>\<C-o>好", 'tx') - call assert_equal(v:null, g:word) - call feedkeys("S\<C-x>\<C-o>好\<C-N>", 'tx') - call assert_equal('你好吗', g:word) + " multiple matches + set complete=ktest_case2.txt + call writefile([' hello help what'], 'test_case2.txt', 'D') + exe "normal ggdGSh\<C-N>\<C-N>\<C-N>\<C-N>\<ESC>" + call assert_equal("what", getline('.')) + + " multiple lines of matches + set complete=ktest_case3.txt + call writefile([' hello help what', 'hola', ' hey'], 'test_case3.txt', 'D') + exe "normal ggdGSh\<C-N>\<C-N>\<ESC>" + call assert_equal("hey", getline('.')) + exe "normal ggdGSh\<C-N>\<C-N>\<C-N>\<C-N>\<ESC>" + call assert_equal("hola", getline('.')) + + set complete=ktest_case4.txt + call writefile([' auto int enum register', 'why'], 'test_case4.txt', 'D') + exe "normal ggdGSe\<C-N>\<C-N>\<ESC>" + call assert_equal("enum", getline('.')) + set complete& - " "nosort" shouldn't enable fuzzy filtering when "fuzzy" isn't present. - set cot=menuone,noinsert,nosort - call feedkeys("S\<C-x>\<C-o>fooB\<C-Y>", 'tx') - call assert_equal('fooBaz', getline('.')) + " file + call writefile([''], 'hello', 'D') + call writefile([''], 'helio', 'D') + exe "normal ggdGS./h\<C-X>\<C-f>\<ESC>" + call assert_equal('./hel', getline('.')) - set cot=menuone,fuzzy,nosort - func CompAnother() - call complete(col('.'), [#{word: "do" }, #{word: "echo"}, #{word: "for (${1:expr1}, ${2:expr2}, ${3:expr3}) {\n\t$0\n}", abbr: "for" }, #{word: "foo"}]) - return '' - endfunc - call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-N>\<C-N>", 'tx') - call assert_equal("for", g:abbr) - call assert_equal(2, g:selected) + " word + call setline(1, ['what do you think', 'why i have that', '']) + call cursor(3,1) + call feedkeys("Sw\<C-X>\<C-l>\<C-N>\<Esc>0", 'tx!') + call assert_equal('wh', getline('.')) - set cot+=noinsert - call feedkeys("i\<C-R>=CompAnother()\<CR>f", 'tx') - call assert_equal("for", g:abbr) - call assert_equal(2, g:selected) + exe "normal ggdG" + " auto complete when only one match + exe "normal Shello\<CR>h\<C-X>\<C-N>\<esc>" + call assert_equal('hello', getline('.')) + exe "normal Sh\<C-N>\<C-P>\<esc>" + call assert_equal('hello', getline('.')) - set cot=menu,menuone,noselect,fuzzy - call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-N>\<C-N>\<C-N>\<C-N>", 'tx') - call assert_equal("foo", g:word) - call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-P>", 'tx') - call assert_equal("foo", g:word) - call feedkeys("i\<C-R>=CompAnother()\<CR>\<C-P>\<C-P>", 'tx') - call assert_equal("for", g:abbr) + exe "normal Shello\<CR>h\<C-X>\<C-N>\<Esc>cch\<C-X>\<C-N>\<Esc>" + call assert_equal('hello', getline('.')) + + " continue search for new leader after insert common prefix + exe "normal ohellokate\<CR>h\<C-X>\<C-N>k\<C-y>\<esc>" + call assert_equal('hellokate', getline('.')) - " clean up - set omnifunc= - bw! bw! - set complete& completeopt& - autocmd! AAAAA_Group - augroup! AAAAA_Group - delfunc OnPumChange - delfunc Omni_test - delfunc Comp - delfunc CompAnother - unlet g:item - unlet g:word - unlet g:selected - unlet g:abbr + set completeopt& + set completefuzzycollect& endfunc -func Test_complete_fuzzy_with_completeslash() +func Test_completefuzzycollect_with_completeslash() CheckMSWindows call writefile([''], 'fobar', 'D') let orig_shellslash = &shellslash set cpt& new - set completeopt+=fuzzy + set completefuzzycollect=files set noshellslash " Test with completeslash unset @@ -3023,6 +3100,7 @@ func Test_complete_fuzzy_with_completeslash() " Reset and clean up let &shellslash = orig_shellslash set completeslash= + set completefuzzycollect& %bw! endfunc diff --git a/test/old/testdir/test_popup.vim b/test/old/testdir/test_popup.vim index 51b6861e11..8f81db6213 100644 --- a/test/old/testdir/test_popup.vim +++ b/test/old/testdir/test_popup.vim @@ -1501,22 +1501,6 @@ func Test_pum_highlights_match() call VerifyScreenDump(buf, 'Test_pum_highlights_09', {}) call term_sendkeys(buf, "o\<BS>\<C-R>=Comp()\<CR>") call VerifyScreenDump(buf, 'Test_pum_highlights_09', {}) - - " issue #15095 wrong select - call term_sendkeys(buf, "\<ESC>:set completeopt=fuzzy,menu\<CR>") - call TermWait(buf) - call term_sendkeys(buf, "S hello helio hero h\<C-X>\<C-P>") - call TermWait(buf, 50) - call VerifyScreenDump(buf, 'Test_pum_highlights_10', {}) - - call term_sendkeys(buf, "\<ESC>S hello helio hero h\<C-X>\<C-P>\<C-P>") - call TermWait(buf, 50) - call VerifyScreenDump(buf, 'Test_pum_highlights_11', {}) - - " issue #15357 - call term_sendkeys(buf, "\<ESC>S/non_existing_folder\<C-X>\<C-F>") - call TermWait(buf, 50) - call VerifyScreenDump(buf, 'Test_pum_highlights_15', {}) call term_sendkeys(buf, "\<C-E>\<Esc>") call term_sendkeys(buf, ":hi PmenuMatchSel ctermfg=14 ctermbg=NONE\<CR>") @@ -1530,7 +1514,34 @@ func Test_pum_highlights_match() call term_sendkeys(buf, "\<C-E>\<Esc>") call TermWait(buf) + call StopVimInTerminal(buf) +endfunc +func Test_pum_completefuzzycollect() + CheckScreendump + let lines =<< trim END + set completefuzzycollect=keyword,files + set completeopt=menu,menuone + END + call writefile(lines, 'Xscript', 'D') + let buf = RunVimInTerminal('-S Xscript', {}) + + " issue #15095 wrong select + call term_sendkeys(buf, "S hello helio hero h\<C-X>\<C-P>") + call TermWait(buf, 50) + call VerifyScreenDump(buf, 'Test_pum_completefuzzycollect_01', {}) + + call term_sendkeys(buf, "\<ESC>S hello helio hero h\<C-X>\<C-P>\<C-P>") + call TermWait(buf, 50) + call VerifyScreenDump(buf, 'Test_pum_completefuzzycollect_02', {}) + + " issue #15357 + call term_sendkeys(buf, "\<ESC>S/non_existing_folder\<C-X>\<C-F>") + call TermWait(buf, 50) + call VerifyScreenDump(buf, 'Test_pum_completefuzzycollect_03', {}) + call term_sendkeys(buf, "\<C-E>\<Esc>") + + call TermWait(buf) call StopVimInTerminal(buf) endfunc |