vim-patch:9.1.1178: not possible to generate completion candidates using fuzzy matching

Problem:  not possible to generate completion candidates using fuzzy
          matching
Solution: add the 'completefuzzycollect' option for (some) ins-completion
          modes (glepnir)

fixes vim/vim#15296
fixes vim/vim#15295
fixes vim/vim#15294
closes: vim/vim#16032

f31cfa29bf

Co-authored-by: glepnir <glephunter@gmail.com>
This commit is contained in:
zeertzjq
2025-03-07 08:18:33 +08:00
parent 3029357520
commit 90d59e6c8a
14 changed files with 562 additions and 194 deletions

View File

@@ -131,7 +131,8 @@ LUA
OPTIONS
todo
'completefuzzycollect' enables fuzzy collection of candidates for (some)
|ins-completion| modes.
PERFORMANCE

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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,
&regmatch, buf, &dir);
(cfc_has_mode() ? NULL : &regmatch), 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 (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;
}
}
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;
} 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);

View File

@@ -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

View File

@@ -1459,6 +1459,30 @@ local options = {
type = 'string',
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',
@@ -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

View File

@@ -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);

View File

@@ -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, &current_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, &current_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;

View File

@@ -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;
}

View File

@@ -6932,35 +6932,6 @@ describe('builtin popupmenu', function()
]])
feed('o<BS><C-R>=Comp()<CR>')
screen:expect_unchanged(true)
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: }|
{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: }|
{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>')
command('hi PmenuMatchSel guibg=NONE')
@@ -6981,6 +6952,41 @@ describe('builtin popupmenu', function()
feed('<C-E><Esc>')
end)
it('completefuzzycollect', function()
exec([[
set completefuzzycollect=keyword,files
set completeopt=menu,menuone
]])
feed('S hello helio hero h<C-X><C-P>')
screen:expect([[
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 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)
-- oldtest: Test_pum_highlights_match_with_abbr()
it('can highlight matched text with abbr', function()
exec([[

View File

@@ -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']],

View File

@@ -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)
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!
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& cfc& cpt&
endfunc
func Test_complete_fuzzy_with_completeslash()
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&
" 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 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

View File

@@ -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