aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/news.txt3
-rw-r--r--runtime/doc/options.txt32
-rw-r--r--runtime/lua/vim/_meta/options.lua35
-rw-r--r--runtime/optwin.vim4
-rw-r--r--src/nvim/insexpand.c401
-rw-r--r--src/nvim/option_vars.h4
-rw-r--r--src/nvim/options.lua44
-rw-r--r--src/nvim/optionstr.c1
-rw-r--r--src/nvim/search.c175
-rw-r--r--src/nvim/spell.c2
-rw-r--r--test/functional/ui/popupmenu_spec.lua49
-rw-r--r--test/old/testdir/gen_opt_test.vim3
-rw-r--r--test/old/testdir/test_ins_complete.vim287
-rw-r--r--test/old/testdir/test_options.vim1
-rw-r--r--test/old/testdir/test_popup.vim36
15 files changed, 973 insertions, 104 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 ba1d353f40..dd2460722e 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1517,6 +1517,28 @@ 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
+ A comma-separated list of strings to enable fuzzy collection for
+ specific |ins-completion| modes, affecting how matches are gathered
+ during completion. For specified modes, fuzzy matching is used to
+ find completion candidates instead of the standard prefix-based
+ matching. This option can contain the following values:
+
+ keyword keywords in the current file |i_CTRL-X_CTRL-N|
+ keywords with flags ".", "w", |i_CTRL-N| |i_CTRL-P|
+ "b", "u", "U" and "k{dict}" in 'complete'
+ keywords in 'dictionary' |i_CTRL-X_CTRL-K|
+
+ files file names |i_CTRL-X_CTRL-F|
+
+ whole_line whole lines |i_CTRL-X_CTRL-L|
+
+ When using the 'completeopt' "longest" option value, fuzzy collection
+ can identify the longest common string among the best fuzzy matches
+ and insert it automatically.
+
*'completeitemalign'* *'cia'*
'completeitemalign' 'cia' string (default "abbr,kind,menu")
global
@@ -1536,10 +1558,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. Only makes a
- difference how completion candidates are reduced from the
- list of alternatives, but not how the candidates are
- collected (using different completion types).
+ 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 46366d585b..20d8ac3058 100644
--- a/runtime/lua/vim/_meta/options.lua
+++ b/runtime/lua/vim/_meta/options.lua
@@ -1044,6 +1044,31 @@ vim.o.cfu = vim.o.completefunc
vim.bo.completefunc = vim.o.completefunc
vim.bo.cfu = vim.bo.completefunc
+--- A comma-separated list of strings to enable fuzzy collection for
+--- specific `ins-completion` modes, affecting how matches are gathered
+--- during completion. For specified modes, fuzzy matching is used to
+--- find completion candidates instead of the standard prefix-based
+--- matching. This option can contain the following values:
+---
+--- keyword keywords in the current file `i_CTRL-X_CTRL-N`
+--- keywords with flags ".", "w", `i_CTRL-N` `i_CTRL-P`
+--- "b", "u", "U" and "k{dict}" in 'complete'
+--- keywords in 'dictionary' `i_CTRL-X_CTRL-K`
+---
+--- files file names `i_CTRL-X_CTRL-F`
+---
+--- whole_line whole lines `i_CTRL-X_CTRL-L`
+---
+--- When using the 'completeopt' "longest" option value, fuzzy collection
+--- can identify the longest common string among the best fuzzy matches
+--- and insert it automatically.
+---
+--- @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,10 +1088,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. Only makes a
---- difference how completion candidates are reduced from the
---- list of alternatives, but not how the candidates are
---- collected (using different completion types).
+--- 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..56fbb954a7 100644
--- a/runtime/optwin.vim
+++ b/runtime/optwin.vim
@@ -1,7 +1,7 @@
" These commands create the option window.
"
" Maintainer: The Vim Project <https://github.com/vim/vim>
-" Last Change: 2025 Feb 08
+" Last Change: 2025 Mar 07
" Former Maintainer: Bram Moolenaar <Bram@vim.org>
" If there already is an option window, jump to that one.
@@ -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("use fuzzy collection for specific completion modes"))
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 1be4f601bc..422eb409fe 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;
@@ -284,6 +291,8 @@ static size_t spell_bad_len = 0; // length of located bad word
static int compl_selected_item = -1;
+static int *compl_fuzzy_scores;
+
// "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;
@@ -730,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;
@@ -775,11 +784,25 @@ 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)
+{
+ if (ctrl_x_mode_normal() || ctrl_x_mode_dictionary()) {
+ return (cfc_flags & kOptCfcFlagKeyword) != 0;
+ } else if (ctrl_x_mode_files()) {
+ return (cfc_flags & kOptCfcFlagFiles) != 0;
+ } else if (ctrl_x_mode_whole_line()) {
+ return (cfc_flags & kOptCfcFlagWholeLine) != 0;
+ } else {
+ return false;
+ }
+}
+
/// free cptext
static inline void free_cptext(char *const *const cptext)
{
@@ -816,12 +839,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();
@@ -862,6 +886,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.
@@ -905,6 +930,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;
@@ -923,7 +975,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);
}
@@ -1011,9 +1063,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.
@@ -1047,9 +1097,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.
@@ -1070,7 +1118,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;
@@ -1236,6 +1284,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
@@ -1552,7 +1601,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,
- &regmatch, buf, &dir);
+ (cfc_has_mode() ? NULL : &regmatch), buf, &dir);
if (flags != DICT_EXACT) {
FreeWild(count, files);
}
@@ -1603,7 +1652,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;
}
@@ -1620,6 +1669,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();
+
+ 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)) {
@@ -1638,27 +1692,49 @@ 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++;
+ }
+ }
}
}
line_breakcheck();
@@ -1699,7 +1775,7 @@ char *find_word_end(char *ptr)
/// Find the end of the line, omitting CR and NL at the end.
///
/// @return a pointer to just after the line.
-static char *find_line_end(char *ptr)
+char *find_line_end(char *ptr)
{
char *s = ptr + strlen(ptr);
while (s > ptr && (s[-1] == CAR || s[-1] == NL)) {
@@ -1744,6 +1820,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;
@@ -2742,7 +2819,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);
}
@@ -2838,7 +2915,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;
}
@@ -3141,7 +3218,7 @@ enum {
/// the "st->e_cpt" option value and process the next matching source.
/// INS_COMPL_CPT_END if all the values in "st->e_cpt" are processed.
static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_arg,
- pos_T *start_match_pos)
+ pos_T *start_match_pos, bool fuzzy_collect)
{
int compl_type = -1;
int status = INS_COMPL_CPT_OK;
@@ -3157,7 +3234,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar
st->first_match_pos = *start_match_pos;
// Move the cursor back one character so that ^N can match the
// word immediately after the cursor.
- if (ctrl_x_mode_normal() && dec(&st->first_match_pos) < 0) {
+ if (ctrl_x_mode_normal() && (!fuzzy_collect && dec(&st->first_match_pos) < 0)) {
// Move the cursor to after the last character in the
// buffer, so that word at start of buffer is found
// correctly.
@@ -3297,11 +3374,157 @@ static void get_next_tag_completion(void)
p_ic = save_p_ic;
}
+/// Compare function for qsort
+static int compare_scores(const void *a, const void *b)
+{
+ int idx_a = *(const int *)a;
+ int idx_b = *(const int *)b;
+ int score_a = compl_fuzzy_scores[idx_a];
+ int score_b = compl_fuzzy_scores[idx_b];
+ return score_a == score_b ? (idx_a == idx_b ? 0 : (idx_a < idx_b ? -1 : 1))
+ : (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)compl_best_matches[0]->cp_str.size;
+
+ 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 = ins_compl_leader_len();
+
+ // skip non-consecutive prefixes
+ if (leader_len > 0 && strncmp(prefix, leader, leader_len) != 0) {
+ goto end;
+ }
+
+ prefix = xmemdupz(prefix, (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)
{
char **matches;
int num_matches;
+ char *leader = ins_compl_leader();
+ size_t leader_len = ins_compl_leader_len();
+ 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')
+ ? '/' : (curbuf->b_p_csl[0] == 'b') ? '\\' : PATHSEP;
+#else
+ char pathsep = PATHSEP;
+#endif
+
+ if (in_fuzzy_collect) {
+#ifdef BACKSLASH_IN_FILENAME
+ if (curbuf->b_p_csl[0] == 's') {
+ for (size_t i = 0; i < leader_len; i++) {
+ if (leader[i] == '\\') {
+ leader[i] = '/';
+ }
+ }
+ } else if (curbuf->b_p_csl[0] == 'b') {
+ for (size_t i = 0; i < leader_len; i++) {
+ if (leader[i] == '/') {
+ leader[i] = '\\';
+ }
+ }
+ }
+#endif
+ char *last_sep = strrchr(leader, pathsep);
+ if (last_sep == NULL) {
+ // No path separator or separator is the last character,
+ // fuzzy match the whole leader
+ API_CLEAR_STRING(compl_pattern);
+ compl_pattern = cbuf_to_string("*", 1);
+ } else if (*(last_sep + 1) == NUL) {
+ in_fuzzy_collect = false;
+ } else {
+ // Split leader into path and file parts
+ size_t path_len = (size_t)(last_sep - leader) + 1;
+ char *path_with_wildcard = xmalloc(path_len + 2);
+ vim_snprintf(path_with_wildcard, path_len + 2, "%*.*s*",
+ (int)path_len, (int)path_len, leader);
+ API_CLEAR_STRING(compl_pattern);
+ compl_pattern.data = path_with_wildcard;
+ compl_pattern.size = path_len + 1;
+
+ // Move leader to the file part
+ leader = last_sep + 1;
+ leader_len -= path_len;
+ }
+ }
+
if (expand_wildcards(1, &compl_pattern.data, &num_matches, &matches,
EW_FILE|EW_DIR|EW_ADDSLASH|EW_SILENT) != OK) {
return;
@@ -3324,7 +3547,61 @@ static void get_next_filename_completion(void)
}
}
#endif
- ins_compl_add_matches(num_matches, matches, p_fic || p_wic);
+
+ 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);
+
+ for (int i = 0; i < num_matches; i++) {
+ char *ptr = matches[i];
+ int score = fuzzy_match_str(ptr, leader);
+ if (score > 0) {
+ GA_APPEND(int, &fuzzy_indices, i);
+ compl_fuzzy_scores[i] = score;
+ }
+ }
+
+ // prevent qsort from deref NULL pointer
+ if (fuzzy_indices.ga_len > 0) {
+ int *fuzzy_indices_data = (int *)fuzzy_indices.ga_data;
+ qsort(fuzzy_indices_data, (size_t)fuzzy_indices.ga_len, sizeof(int), compare_scores);
+
+ for (int i = 0; i < fuzzy_indices.ga_len; 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);
+ } else if (leader_len > 0) {
+ FreeWild(num_matches, matches);
+ num_matches = 0;
+ }
+
+ xfree(compl_fuzzy_scores);
+ ga_clear(&fuzzy_indices);
+
+ if (compl_num_bests > 0 && compl_get_longest) {
+ fuzzy_longest_match();
+ }
+ return;
+ }
+
+ if (num_matches > 0) {
+ ins_compl_add_matches(num_matches, matches, p_fic || p_wic);
+ }
}
/// Get the next set of command-line completions matching "compl_pattern".
@@ -3447,6 +3724,12 @@ static char *ins_compl_get_next_word_or_line(buf_T *ins_buf, pos_T *cur_match_po
/// @return OK if a new next match is found, otherwise FAIL.
static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_pos)
{
+ char *ptr = NULL;
+ int len = 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;
assert(st->ins_buf);
@@ -3470,9 +3753,15 @@ 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_line_or_eval() || (compl_cont_status & CONT_SOL)) {
+
+ if (in_collect) {
+ found_new_match = search_for_fuzzy_match(st->ins_buf,
+ st->cur_match_pos, leader, compl_direction,
+ 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 {
@@ -3521,16 +3810,22 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_
&& start_pos->col == st->cur_match_pos->col) {
continue;
}
- int len;
- char *ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos,
- &len, &cont_s_ipos);
+
+ if (!in_collect) {
+ ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos,
+ &len, &cont_s_ipos);
+ }
if (ptr == NULL
|| (ins_compl_has_preinsert() && strcmp(ptr, compl_pattern.data) == 0)) {
continue;
}
+
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;
}
@@ -3609,7 +3904,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);
}
}
}
@@ -3664,7 +3959,7 @@ static int ins_compl_get_exp(pos_T *ini)
// entries from 'complete' that look in loaded buffers.
if ((ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval())
&& (!compl_started || st.found_all)) {
- int status = process_next_cpt_value(&st, &type, ini);
+ int status = process_next_cpt_value(&st, &type, ini, cfc_has_mode());
if (status == INS_COMPL_CPT_END) {
break;
}
@@ -3722,6 +4017,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
@@ -4705,6 +5004,7 @@ static int ins_compl_start(void)
line = ml_get(curwin->w_cursor.lnum);
}
+ bool in_fuzzy = get_cot_flags() & kOptCotFlagFuzzy;
if (compl_status_adding()) {
edit_submode_pre = _(" Adding");
if (ctrl_x_mode_line_or_eval()) {
@@ -4719,6 +5019,9 @@ static int ins_compl_start(void)
compl_length = 0;
compl_col = curwin->w_cursor.col;
compl_lnum = curwin->w_cursor.lnum;
+ } else if (ctrl_x_mode_normal() && in_fuzzy) {
+ compl_startpos = curwin->w_cursor;
+ compl_cont_status &= CONT_S_IPOS;
}
} else {
edit_submode_pre = NULL;
@@ -4746,7 +5049,7 @@ static int ins_compl_start(void)
}
if (ins_compl_add(compl_orig_text.data, (int)compl_orig_text.size,
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 bb63ff638b..f566582c0c 100644
--- a/src/nvim/options.lua
+++ b/src/nvim/options.lua
@@ -1460,6 +1460,40 @@ local options = {
varname = 'p_cfu',
},
{
+ abbreviation = 'cfc',
+ defaults = '',
+ values = { 'keyword', 'files', 'whole_line' },
+ flags = true,
+ deny_duplicates = true,
+ desc = [=[
+ A comma-separated list of strings to enable fuzzy collection for
+ specific |ins-completion| modes, affecting how matches are gathered
+ during completion. For specified modes, fuzzy matching is used to
+ find completion candidates instead of the standard prefix-based
+ matching. This option can contain the following values:
+
+ keyword keywords in the current file |i_CTRL-X_CTRL-N|
+ keywords with flags ".", "w", |i_CTRL-N| |i_CTRL-P|
+ "b", "u", "U" and "k{dict}" in 'complete'
+ keywords in 'dictionary' |i_CTRL-X_CTRL-K|
+
+ files file names |i_CTRL-X_CTRL-F|
+
+ whole_line whole lines |i_CTRL-X_CTRL-L|
+
+ When using the 'completeopt' "longest" option value, fuzzy collection
+ can identify the longest common string among the best fuzzy matches
+ and insert it automatically.
+ ]=],
+ 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,10 +1539,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. Only makes a
- difference how completion candidates are reduced from the
- list of alternatives, but not how the candidates are
- collected (using different completion types).
+ 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 04f33b9445..b38296ac5c 100644
--- a/src/nvim/search.c
+++ b/src/nvim/search.c
@@ -3620,6 +3620,179 @@ garray_T *fuzzy_match_str_with_pos(char *const str, const char *const pat)
return match_positions;
}
+/// 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 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;
+ char *end = NULL;
+ char *start = NULL;
+ bool found = false;
+
+ if (str == NULL || pat == NULL) {
+ return found;
+ }
+ char *line_end = find_line_end(str);
+
+ while (str < line_end) {
+ // Skip non-word characters
+ start = find_word_start(str);
+ if (*start == NUL) {
+ break;
+ }
+ end = find_word_end(start);
+
+ // Extract the word from start to end
+ char save_end = *end;
+ *end = NUL;
+
+ // Perform fuzzy match
+ *score = fuzzy_match_str(start, pat);
+ *end = save_end;
+
+ if (*score > 0) {
+ *len = (int)(end - start);
+ found = true;
+ *ptr = start;
+ if (current_pos) {
+ current_pos->col += (int)(end - strBegin);
+ }
+ break;
+ }
+
+ // Move to the end of the current word for the next iteration
+ str = end;
+ // Ensure we continue searching after the current word
+ while (*str != NUL && !vim_iswordp(str)) {
+ MB_PTR_ADV(str);
+ }
+ }
+
+ if (!found) {
+ *ptr = line_end;
+ }
+
+ return found;
+}
+
+/// Search for the next fuzzy match in the specified buffer.
+/// This function attempts to find the next occurrence of the given pattern
+/// in the buffer, starting from the current position. It handles line wrapping
+/// and direction of search.
+///
+/// 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, 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 (buf == curbuf) {
+ circly_end = *start_pos;
+ } else {
+ circly_end.lnum = buf->b_ml.ml_line_count;
+ circly_end.col = 0;
+ circly_end.coladd = 0;
+ }
+
+ if (whole_line && start_pos->lnum != pos->lnum) {
+ current_pos.lnum += dir;
+ }
+
+ while (true) {
+ // Check if looped around and back to start position
+ if (looped_around && equalpos(current_pos, circly_end)) {
+ break;
+ }
+
+ // Ensure current_pos is valid
+ if (current_pos.lnum >= 1 && current_pos.lnum <= buf->b_ml.ml_line_count) {
+ // Get the current line buffer
+ *ptr = ml_get_buf(buf, current_pos.lnum);
+ if (!whole_line) {
+ *ptr += current_pos.col;
+ }
+
+ // If ptr is end of line is reached, move to next line
+ // or previous line based on direction
+ if (*ptr != NULL && **ptr != NUL) {
+ if (!whole_line) {
+ // 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, &current_pos, score);
+ if (found_new_match) {
+ if (ctrl_x_mode_normal()) {
+ if (strncmp(*ptr, pattern, (size_t)(*len)) == 0 && pattern[*len] == NUL) {
+ char *next_word_end = find_word_start(*ptr + *len);
+ if (*next_word_end != NUL && *next_word_end != NL) {
+ // Find end of the word.
+ while (*next_word_end != NUL) {
+ int l = utfc_ptr2len(next_word_end);
+ if (l < 2 && !vim_iswordc(*next_word_end)) {
+ break;
+ }
+ next_word_end += l;
+ }
+ }
+ *len = (int)(next_word_end - *ptr);
+ }
+ }
+ *pos = current_pos;
+ break;
+ } else if (looped_around && current_pos.lnum == circly_end.lnum) {
+ break;
+ }
+ } else {
+ if (fuzzy_match_str(*ptr, pattern) > 0) {
+ found_new_match = true;
+ *pos = current_pos;
+ *len = ml_get_buf_len(buf, current_pos.lnum);
+ break;
+ }
+ }
+ }
+ }
+
+ // Move to the next line or previous line based on direction
+ if (dir == FORWARD) {
+ if (++current_pos.lnum > buf->b_ml.ml_line_count) {
+ if (p_ws) {
+ current_pos.lnum = 1;
+ looped_around = true;
+ } else {
+ break;
+ }
+ }
+ } else {
+ if (--current_pos.lnum < 1) {
+ if (p_ws) {
+ current_pos.lnum = buf->b_ml.ml_line_count;
+ looped_around = true;
+ } else {
+ break;
+ }
+ }
+ }
+ current_pos.col = 0;
+ }
+
+ return found_new_match;
+}
+
/// Copy a list of fuzzy matches into a string list after sorting the matches by
/// the fuzzy score. Frees the memory allocated for "fuzmatch".
void fuzzymatches_to_strmatches(fuzmatch_str_T *const fuzmatch, char ***const matches,
@@ -4071,7 +4244,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 728f8ed3d0..9403f39ba1 100644
--- a/test/functional/ui/popupmenu_spec.lua
+++ b/test/functional/ui/popupmenu_spec.lua
@@ -6932,29 +6932,58 @@ 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:ello }{1: }|
- {1:~ }{n: }{mn:h}{n:elio }{1: }|
- {1:~ }{s: }{ms:h}{s:ero }{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:ello }{1: }|
- {1:~ }{s: }{ms:h}{s:elio }{1: }|
- {1:~ }{n: }{mn:h}{n:ero }{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} |
]])
+ feed('<Esc>S/non_existing_folder<C-X><C-F>')
+ screen:expect([[
+ /non_existing_folder^ |
+ {1:~ }|*18
+ {2:-- }{6:Pattern not found} |
+ ]])
feed('<C-E><Esc>')
end)
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 a08d0bd252..4bb537c081 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,26 +2819,6 @@ 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
- 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.
- " Add a "0" after the <Esc> to avoid waiting for an escape sequence.
- call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
- call assert_equal('hello help hero hello', getline('.'))
- set completeopt+=noinsert
- call setline(1, ['hello help hero h'])
- call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
- call assert_equal('hello help hero h', getline('.'))
-
- " issue #15526
- set completeopt=fuzzy,menuone,menu,noselect
- call setline(1, ['Text', 'ToText', ''])
- call cursor(2, 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".
@@ -2890,13 +2870,264 @@ func Test_complete_fuzzy_match()
delfunc OnPumChange
delfunc Omni_test
delfunc Comp
- delfunc CompAnother
unlet g:item
unlet g:word
- unlet g:selected
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.
+ " Add a "0" after the <Esc> to avoid waiting for an escape sequence.
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('hello help hero hello', getline('.'))
+ set completeopt+=noinsert
+ call setline(1, ['hello help hero h'])
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('hello help hero h', getline('.'))
+
+ set completeopt-=noinsert
+ call setline(1, ['xyz yxz x'])
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('xyz yxz xyz', getline('.'))
+ " can fuzzy get yxz when use Ctrl-N twice
+ call setline(1, ['xyz yxz x'])
+ call feedkeys("A\<C-X>\<C-N>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('xyz yxz yxz', getline('.'))
+
+ call setline(1, ['你好 你'])
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('你好 你好', getline('.'))
+ call setline(1, ['你的 我的 的'])
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('你的 我的 你的', getline('.'))
+ " can fuzzy get multiple-byte word when use Ctrl-N twice
+ call setline(1, ['你的 我的 的'])
+ call feedkeys("A\<C-X>\<C-N>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('你的 我的 我的', getline('.'))
+
+ " fuzzy on file
+ call writefile([''], 'fobar', 'D')
+ call writefile([''], 'foobar', 'D')
+ call setline(1, ['fob'])
+ call cursor(1, 1)
+ call feedkeys("A\<C-X>\<C-f>\<Esc>0", 'tx!')
+ call assert_equal('fobar', getline('.'))
+ call feedkeys("Sfob\<C-X>\<C-f>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('foobar', getline('.'))
+ call feedkeys("S../\<C-X>\<C-f>\<Esc>0", 'tx!')
+ call assert_match('../*', getline('.'))
+ call feedkeys("S../td\<C-X>\<C-f>\<Esc>0", 'tx!')
+ call assert_match('../testdir', getline('.'))
+
+ " can get completion from other buffer
+ vnew
+ call setline(1, ["completeness,", "compatibility", "Composite", "Omnipotent"])
+ wincmd p
+ call feedkeys("Somp\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('completeness', getline('.'))
+ call feedkeys("Somp\<C-N>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('compatibility', getline('.'))
+ call feedkeys("Somp\<C-P>\<Esc>0", 'tx!')
+ call assert_equal('Omnipotent', getline('.'))
+ call feedkeys("Somp\<C-P>\<C-P>\<Esc>0", 'tx!')
+ call assert_equal('Composite', getline('.'))
+ call feedkeys("S omp\<C-N>\<Esc>0", 'tx!')
+ call assert_equal(' completeness', getline('.'))
+
+ " fuzzy on whole line completion
+ call setline(1, ["world is on fire", "no one can save me but you", 'user can execute', ''])
+ call cursor(4, 1)
+ call feedkeys("Swio\<C-X>\<C-L>\<Esc>0", 'tx!')
+ call assert_equal('world is on fire', getline('.'))
+ call feedkeys("Su\<C-X>\<C-L>\<C-P>\<Esc>0", 'tx!')
+ call assert_equal('no one can save me but you', getline('.'))
+
+ " issue #15412
+ call setline(1, ['alpha bravio charlie'])
+ call feedkeys("Salpha\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha bravio', getline('.'))
+ call feedkeys("Salp\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha', getline('.'))
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha bravio', getline('.'))
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha bravio charlie', getline('.'))
+
+ set complete-=i
+ call feedkeys("Salp\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha', getline('.'))
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha bravio', getline('.'))
+ call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha bravio charlie', getline('.'))
+
+ call setline(1, ['alpha bravio charlie', 'alpha another'])
+ call feedkeys("Salpha\<C-X>\<C-N>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('alpha another', getline('.'))
+ call setline(1, ['你好 我好', '你好 他好'])
+ call feedkeys("S你好\<C-X>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('你好 我好', getline('.'))
+ call feedkeys("S你好\<C-X>\<C-N>\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('你好 他好', getline('.'))
+
+ " issue #15526
+ 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))
+
+ call setline(1, ['fuzzy', 'fuzzycollect', 'completefuzzycollect'])
+ call feedkeys("Gofuzzy\<C-X>\<C-N>\<C-N>\<C-N>\<CR>\<Esc>0", 'tx!')
+ call assert_equal('fuzzycollect', getline(line('.') - 1))
+ call feedkeys("Gofuzzy\<C-X>\<C-N>\<C-N>\<C-N>\<C-N>\<CR>\<Esc>0", 'tx!')
+ call assert_equal('completefuzzycollect', getline(line('.') - 1))
+
+ " keywords in 'dictonary'
+ call writefile(['hello', 'think'], 'test_dict.txt', 'D')
+ set dict=test_dict.txt
+ call feedkeys("Sh\<C-X>\<C-K>\<C-N>\<CR>\<Esc>0", 'tx!')
+ call assert_equal('hello', getline(line('.') - 1))
+ call feedkeys("Sh\<C-X>\<C-K>\<C-N>\<C-N>\<CR>\<Esc>0", 'tx!')
+ call assert_equal('think', getline(line('.') - 1))
+
+ call setline(1, ['foo bar fuzzy', 'completefuzzycollect'])
+ call feedkeys("Gofuzzy\<C-X>\<C-N>\<C-N>\<C-N>\<C-Y>\<Esc>0", 'tx!')
+ call assert_equal('completefuzzycollect', getline('.'))
+
+ bw!
+ bw!
+ set dict&
+ 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('.'))
+
+ " 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=ktest_case5.txt
+ call writefile(['hello friends', 'go', 'hero'], 'test_case5.txt', 'D')
+ exe "normal ggdGSh\<C-N>\<C-N>\<ESC>"
+ call assert_equal("hero", getline('.'))
+ set complete&
+
+ " file
+ call writefile([''], 'hello', 'D')
+ call writefile([''], 'helio', 'D')
+ exe "normal ggdGS./h\<C-X>\<C-f>\<ESC>"
+ call assert_equal('./hel', getline('.'))
+
+ " 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('.'))
+
+ 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('.'))
+
+ 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('.'))
+
+ bw!
+ set completeopt&
+ set completefuzzycollect&
+endfunc
+
+func Test_completefuzzycollect_with_completeslash()
+ CheckMSWindows
+
+ call writefile([''], 'fobar', 'D')
+ let orig_shellslash = &shellslash
+ set cpt&
+ new
+ set completefuzzycollect=files
+ set noshellslash
+
+ " Test with completeslash unset
+ set completeslash=
+ call setline(1, ['.\fob'])
+ call feedkeys("A\<C-X>\<C-F>\<Esc>0", 'tx!')
+ call assert_equal('.\fobar', getline('.'))
+
+ " Test with completeslash=backslash
+ set completeslash=backslash
+ call feedkeys("S.\\fob\<C-X>\<C-F>\<Esc>0", 'tx!')
+ call assert_equal('.\fobar', getline('.'))
+
+ " Test with completeslash=slash
+ set completeslash=slash
+ call feedkeys("S.\\fob\<C-X>\<C-F>\<Esc>0", 'tx!')
+ call assert_equal('./fobar', getline('.'))
+
+ " Reset and clean up
+ let &shellslash = orig_shellslash
+ set completeslash=
+ set completefuzzycollect&
+ %bw!
+endfunc
+
" Check that tie breaking is stable for completeopt+=fuzzy (which should
" behave the same on different platforms).
func Test_complete_fuzzy_match_tie()
@@ -2917,6 +3148,16 @@ func Test_complete_fuzzy_match_tie()
set completeopt&
endfunc
+func Test_complete_backwards_default()
+ new
+ call append(1, ['foobar', 'foobaz'])
+ new
+ call feedkeys("i\<c-p>", 'tx')
+ call assert_equal('foobaz', getline('.'))
+ bw!
+ bw!
+endfunc
+
func Test_complete_info_matches()
let g:what = ['matches']
func ShownInfo()
diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim
index ce2d9ba05a..9104098baa 100644
--- a/test/old/testdir/test_options.vim
+++ b/test/old/testdir/test_options.vim
@@ -513,6 +513,7 @@ func Test_set_completion_string_values()
endif
call assert_equal('.', getcompletion('set complete=', 'cmdline')[1])
call assert_equal('menu', getcompletion('set completeopt=', 'cmdline')[1])
+ call assert_equal('keyword', getcompletion('set completefuzzycollect=', 'cmdline')[0])
if exists('+completeslash')
call assert_equal('backslash', getcompletion('set completeslash=', 'cmdline')[1])
endif
diff --git a/test/old/testdir/test_popup.vim b/test/old/testdir/test_popup.vim
index 988d60916c..8f81db6213 100644
--- a/test/old/testdir/test_popup.vim
+++ b/test/old/testdir/test_popup.vim
@@ -1501,21 +1501,47 @@ 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', {})
+ call term_sendkeys(buf, "\<C-E>\<Esc>")
- " issue #15095 wrong select
- call term_sendkeys(buf, "\<ESC>:set completeopt=fuzzy,menu\<CR>")
+ call term_sendkeys(buf, ":hi PmenuMatchSel ctermfg=14 ctermbg=NONE\<CR>")
+ call TermWait(buf, 50)
+ call term_sendkeys(buf, ":hi PmenuMatch ctermfg=12 ctermbg=NONE\<CR>")
+ call term_sendkeys(buf, ":set cot=menu,noinsert,fuzzy\<CR>")
+ call term_sendkeys(buf, "S\<C-X>\<C-O>")
+ call TermWait(buf, 50)
+ call term_sendkeys(buf, "fb")
+ call VerifyScreenDump(buf, 'Test_pum_highlights_18', {})
+
+ 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_highlights_10', {})
+ 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_highlights_11', {})
+ 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 TermWait(buf)
call StopVimInTerminal(buf)
endfunc