From 8159fb18a92e9a9f5e35201bd92bf651f4d5835c Mon Sep 17 00:00:00 2001 From: glepnir Date: Wed, 17 Jul 2024 20:32:54 +0200 Subject: [PATCH] patch 9.1.0598: fuzzy completion does not work with default completion Problem: fuzzy completion does not work with default completion Solution: Make it work (glepnir) closes: #15193 Signed-off-by: glepnir Signed-off-by: Christian Brabandt --- src/insexpand.c | 103 +++++++++-- src/proto/search.pro | 1 + src/search.c | 164 ++++++++++++++++++ src/testdir/dumps/Test_pum_highlights_10.dump | 6 +- src/testdir/dumps/Test_pum_highlights_11.dump | 4 +- src/testdir/test_ins_complete.vim | 63 +++++++ src/version.c | 2 + 7 files changed, 323 insertions(+), 20 deletions(-) diff --git a/src/insexpand.c b/src/insexpand.c index 21b53d1e3e850..fa4ac7dd3f9bd 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -203,6 +203,8 @@ static int compl_opt_suppress_empty = FALSE; static int compl_selected_item = -1; +static int *compl_fuzzy_scores; + static int ins_compl_add(char_u *str, int len, char_u *fname, char_u **cptext, typval_T *user_data, int cdir, int flags, int adup); static void ins_compl_longest_match(compl_T *match); static void ins_compl_del_pum(void); @@ -3322,7 +3324,8 @@ typedef struct process_next_cpt_value( ins_compl_next_state_T *st, int *compl_type_arg, - pos_T *start_match_pos) + pos_T *start_match_pos, + int in_fuzzy) { int compl_type = -1; int status = INS_COMPL_CPT_OK; @@ -3338,7 +3341,7 @@ process_next_cpt_value( 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() && (!in_fuzzy && 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 @@ -3505,6 +3508,18 @@ 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) ? -1 : (score_a < score_b) ? 1 : 0; +} + /* * Get the next set of filename matching "compl_pattern". */ @@ -3513,6 +3528,21 @@ get_next_filename_completion(void) { char_u **matches; int num_matches; + char_u *ptr; + garray_T fuzzy_indices; + int i; + int score; + char_u *leader = ins_compl_leader(); + int leader_len = STRLEN(leader); + int in_fuzzy = ((get_cot_flags() & COT_FUZZY) != 0 && leader_len > 0); + char_u **sorted_matches; + int *fuzzy_indices_data; + + if (in_fuzzy) + { + vim_free(compl_pattern); + compl_pattern = vim_strsave((char_u *)"*"); + } if (expand_wildcards(1, &compl_pattern, &num_matches, &matches, EW_FILE|EW_DIR|EW_ADDSLASH|EW_SILENT) != OK) @@ -3523,12 +3553,9 @@ get_next_filename_completion(void) #ifdef BACKSLASH_IN_FILENAME if (curbuf->b_p_csl[0] != NUL) { - int i; - for (i = 0; i < num_matches; ++i) { - char_u *ptr = matches[i]; - + ptr = matches[i]; while (*ptr != NUL) { if (curbuf->b_p_csl[0] == 's' && *ptr == '\\') @@ -3540,6 +3567,41 @@ get_next_filename_completion(void) } } #endif + + if (in_fuzzy) + { + ga_init2(&fuzzy_indices, sizeof(int), 10); + compl_fuzzy_scores = (int *)alloc(sizeof(int) * num_matches); + + for (i = 0; i < num_matches; i++) + { + ptr = matches[i]; + score = fuzzy_match_str(ptr, leader); + if (score > 0) + { + if (ga_grow(&fuzzy_indices, 1) == OK) + { + ((int *)fuzzy_indices.ga_data)[fuzzy_indices.ga_len] = i; + compl_fuzzy_scores[i] = score; + fuzzy_indices.ga_len++; + } + } + } + + fuzzy_indices_data = (int *)fuzzy_indices.ga_data; + qsort(fuzzy_indices_data, fuzzy_indices.ga_len, sizeof(int), compare_scores); + + sorted_matches = (char_u **)alloc(sizeof(char_u *) * fuzzy_indices.ga_len); + for (i = 0; i < fuzzy_indices.ga_len; ++i) + sorted_matches[i] = vim_strsave(matches[fuzzy_indices_data[i]]); + + FreeWild(num_matches, matches); + matches = sorted_matches; + num_matches = fuzzy_indices.ga_len; + vim_free(compl_fuzzy_scores); + ga_clear(&fuzzy_indices); + } + ins_compl_add_matches(num_matches, matches, p_fic || p_wic); } @@ -3687,8 +3749,10 @@ get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_pos) int save_p_scs; int save_p_ws; int looped_around = FALSE; - char_u *ptr; - int len; + char_u *ptr = NULL; + int len = 0; + int in_fuzzy = (get_cot_flags() & COT_FUZZY) != 0 && compl_length > 0; + char_u *leader = ins_compl_leader(); // If 'infercase' is set, don't use 'smartcase' here save_p_scs = p_scs; @@ -3702,7 +3766,7 @@ get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_pos) save_p_ws = p_ws; if (st->ins_buf != curbuf) p_ws = FALSE; - else if (*st->e_cpt == '.') + else if (*st->e_cpt == '.' && !in_fuzzy) p_ws = TRUE; looped_around = FALSE; for (;;) @@ -3713,9 +3777,13 @@ get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_pos) // 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 ((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); + else if (in_fuzzy) + 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()); else found_new_match = searchit(NULL, st->ins_buf, st->cur_match_pos, NULL, compl_direction, compl_pattern, compl_patternlen, @@ -3764,8 +3832,9 @@ get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_pos) && start_pos->col == st->cur_match_pos->col) continue; - ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos, - &len, &cont_s_ipos); + if (!in_fuzzy) + ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos, + &len, &cont_s_ipos); if (ptr == NULL) continue; @@ -3864,6 +3933,7 @@ ins_compl_get_exp(pos_T *ini) int i; int found_new_match; int type = ctrl_x_mode; + int in_fuzzy = (get_cot_flags() & COT_FUZZY) != 0; if (!compl_started) { @@ -3889,8 +3959,11 @@ ins_compl_get_exp(pos_T *ini) st.ins_buf = curbuf; // In case the buffer was wiped out. compl_old_match = compl_curr_match; // remember the last current match - st.cur_match_pos = (compl_dir_forward()) - ? &st.last_match_pos : &st.first_match_pos; + if (in_fuzzy) + st.cur_match_pos = (compl_dir_forward()) + ? &st.last_match_pos : &st.first_match_pos; + else + st.cur_match_pos = &st.last_match_pos; // For ^N/^P loop over all the flags/windows/buffers in 'complete'. for (;;) @@ -3904,7 +3977,7 @@ ins_compl_get_exp(pos_T *ini) 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, in_fuzzy); if (status == INS_COMPL_CPT_END) break; diff --git a/src/proto/search.pro b/src/proto/search.pro index 08526c80f24ea..866599409cd5c 100644 --- a/src/proto/search.pro +++ b/src/proto/search.pro @@ -41,6 +41,7 @@ void f_matchfuzzy(typval_T *argvars, typval_T *rettv); void f_matchfuzzypos(typval_T *argvars, typval_T *rettv); int fuzzy_match_str(char_u *str, char_u *pat); garray_T *fuzzy_match_str_with_pos(char_u *str, char_u *pat); +int search_for_fuzzy_match(buf_T *buf, pos_T *pos, char_u *pattern, int dir, pos_T *start_pos, int *len, char_u **ptr, int whole_line); void fuzmatch_str_free(fuzmatch_str_T *fuzmatch, int count); int fuzzymatches_to_strmatches(fuzmatch_str_T *fuzmatch, char_u ***matches, int count, int funcsort); /* vim: set ft=c : */ diff --git a/src/search.c b/src/search.c index d1eb5007d2119..f7aab7b5eb635 100644 --- a/src/search.c +++ b/src/search.c @@ -53,6 +53,7 @@ static int fuzzy_match_str_compare(const void *s1, const void *s2); static void fuzzy_match_str_sort(fuzmatch_str_T *fm, int sz); static int fuzzy_match_func_compare(const void *s1, const void *s2); static void fuzzy_match_func_sort(fuzmatch_str_T *fm, int sz); +static int fuzzy_match_str_in_line(char_u **ptr, char_u *pat, int *len, pos_T *current_pos); #define SEARCH_STAT_DEF_TIMEOUT 40L #define SEARCH_STAT_DEF_MAX_COUNT 99 @@ -5139,6 +5140,169 @@ fuzzy_match_str_with_pos(char_u *str UNUSED, char_u *pat UNUSED) #endif } +/* + * 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 int +fuzzy_match_str_in_line(char_u **ptr, char_u *pat, int *len, pos_T *current_pos) +{ + char_u *str = *ptr; + char_u *strBegin = str; + char_u *end = NULL; + char_u *start = NULL; + int found = FALSE; + int result; + char save_end; + + if (str == NULL || pat == NULL) + return found; + + while (*str != NUL) + { + // 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 + save_end = *end; + *end = NUL; + + // Perform fuzzy match + result = fuzzy_match_str(start, pat); + *end = save_end; + + if (result > 0) + { + *len = (int)(end - start); + current_pos->col += (int)(end - strBegin); + found = TRUE; + *ptr = start; + 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); + } + + 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. + */ + int +search_for_fuzzy_match( + buf_T *buf, + pos_T *pos, + char_u *pattern, + int dir, + pos_T *start_pos, + int *len, + char_u **ptr, + int whole_line) +{ + pos_T current_pos = *pos; + pos_T circly_end; + int found_new_match = FAIL; + int looped_around = FALSE; + + if (whole_line) + current_pos.lnum += dir; + + do { + 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; + } + + // Check if looped around and back to start position + if (looped_around && EQUAL_POS(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, FALSE); + // If ptr is end of line is reached, move to next line + // or previous line based on direction + if (**ptr != NUL) + { + if (!whole_line) + { + *ptr += current_pos.col; + // Try to find a fuzzy match in the current line starting from current position + found_new_match = fuzzy_match_str_in_line(ptr, pattern, len, ¤t_pos); + if (found_new_match) + { + *pos = current_pos; + break; + } + } + else + { + if (fuzzy_match_str(*ptr, pattern) > 0) + { + found_new_match = TRUE; + *pos = current_pos; + *len = STRLEN(*ptr); + 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; + } while (TRUE); + + return found_new_match; +} + /* * Free an array of fuzzy string matches "fuzmatch[count]". */ diff --git a/src/testdir/dumps/Test_pum_highlights_10.dump b/src/testdir/dumps/Test_pum_highlights_10.dump index 5db4e59e4d9a1..790b028b9cc57 100644 --- a/src/testdir/dumps/Test_pum_highlights_10.dump +++ b/src/testdir/dumps/Test_pum_highlights_10.dump @@ -1,7 +1,7 @@ -| +0&#ffffff0|h|e|l@1|o| |h|e|l|i|o| |h|e|r|o| |h|e|r|o> @52 -|~+0#4040ff13&| @15| +0#0000001#ffd7ff255|h+0#0000e05&|e+0#0000001&|l@1|o| @9| +0#4040ff13#ffffff0@41 +| +0&#ffffff0|h|e|l@1|o| |h|e|l|i|o| |h|e|r|o| |h|e|l@1|o> @51 +|~+0#4040ff13&| @15| +0#0000001#ffd7ff255|h+0#0000e05&|e+0#0000001&|r|o| @10| +0#4040ff13#ffffff0@41 |~| @15| +0#0000001#ffd7ff255|h+0#0000e05&|e+0#0000001&|l|i|o| @9| +0#4040ff13#ffffff0@41 -|~| @15| +0#0000001#e0e0e08|h+0#00e0e07&|e+0#0000001&|r|o| @10| +0#4040ff13#ffffff0@41 +|~| @15| +0#0000001#e0e0e08|h+0#00e0e07&|e+0#0000001&|l@1|o| @9| +0#4040ff13#ffffff0@41 |~| @73 |~| @73 |~| @73 diff --git a/src/testdir/dumps/Test_pum_highlights_11.dump b/src/testdir/dumps/Test_pum_highlights_11.dump index 720d839fae29f..ef75a89ed37dc 100644 --- a/src/testdir/dumps/Test_pum_highlights_11.dump +++ b/src/testdir/dumps/Test_pum_highlights_11.dump @@ -1,7 +1,7 @@ | +0&#ffffff0|h|e|l@1|o| |h|e|l|i|o| |h|e|r|o| |h|e|l|i|o> @51 -|~+0#4040ff13&| @15| +0#0000001#ffd7ff255|h+0#0000e05&|e+0#0000001&|l@1|o| @9| +0#4040ff13#ffffff0@41 +|~+0#4040ff13&| @15| +0#0000001#ffd7ff255|h+0#0000e05&|e+0#0000001&|r|o| @10| +0#4040ff13#ffffff0@41 |~| @15| +0#0000001#e0e0e08|h+0#00e0e07&|e+0#0000001&|l|i|o| @9| +0#4040ff13#ffffff0@41 -|~| @15| +0#0000001#ffd7ff255|h+0#0000e05&|e+0#0000001&|r|o| @10| +0#4040ff13#ffffff0@41 +|~| @15| +0#0000001#ffd7ff255|h+0#0000e05&|e+0#0000001&|l@1|o| @9| +0#4040ff13#ffffff0@41 |~| @73 |~| @73 |~| @73 diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index 48589ce18887c..e9f9c9e54b932 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -2586,9 +2586,72 @@ func Test_complete_fuzzy_match() call feedkeys("A\\\0", 'tx!') call assert_equal('hello help hero h', getline('.')) + set completeopt-=noinsert + call setline(1, ['xyz yxz x']) + call feedkeys("A\\\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\\\\0", 'tx!') + call assert_equal('xyz yxz yxz', getline('.')) + + call setline(1, ['你好 你']) + call feedkeys("A\\\0", 'tx!') + call assert_equal('你好 你好', getline('.')) + call setline(1, ['你的 我的 的']) + call feedkeys("A\\\0", 'tx!') + call assert_equal('你的 我的 你的', getline('.')) + " can fuzzy get multiple-byte word when use Ctrl-N twice + call setline(1, ['你的 我的 的']) + call feedkeys("A\\\\0", 'tx!') + call assert_equal('你的 我的 我的', getline('.')) + + " respect wrapscan + set nowrapscan + call setline(1, ["xyz", "yxz", ""]) + call cursor(3, 1) + call feedkeys("Sy\\\0", 'tx!') + call assert_equal('y', getline('.')) + set wrapscan + call feedkeys("Sy\\\0", 'tx!') + call assert_equal('xyz', getline('.')) + + " fuzzy on file + call writefile([''], 'fobar', 'D') + call writefile([''], 'foobar', 'D') + call setline(1, ['fob']) + call cursor(1, 1) + call feedkeys("A\\\0", 'tx!') + call assert_equal('fobar', getline('.')) + call feedkeys("Sfob\\\\0", 'tx!') + call assert_equal('foobar', getline('.')) + + " can get completion from other buffer + set completeopt=fuzzy,menu,menuone + vnew + call setline(1, ["completeness,", "compatibility", "Composite", "Omnipotent"]) + wincmd p + call feedkeys("Somp\\0", 'tx!') + call assert_equal('completeness', getline('.')) + call feedkeys("Somp\\\0", 'tx!') + call assert_equal('compatibility', getline('.')) + call feedkeys("Somp\\0", 'tx!') + call assert_equal('Omnipotent', getline('.')) + call feedkeys("Somp\\\0", 'tx!') + call assert_equal('Composite', 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\\\0", 'tx!') + call assert_equal('world is on fire', getline('.')) + call feedkeys("Su\\\\0", 'tx!') + call assert_equal('no one can save me but you', getline('.')) + " clean up set omnifunc= bw! + bw! set complete& completeopt& autocmd! AAAAA_Group augroup! AAAAA_Group diff --git a/src/version.c b/src/version.c index e22fe614fb858..acb4badaef1cd 100644 --- a/src/version.c +++ b/src/version.c @@ -704,6 +704,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 598, /**/ 597, /**/