From 46d89ff9f8fdbae3a922af6f30357bf24573a0da Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 30 Aug 2024 15:37:24 +0800 Subject: [PATCH 1/8] vim-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: vim/vim#15193 https://github.com/vim/vim/commit/8159fb18a92e9a9f5e35201bd92bf651f4d5835c Cherry-pick insexpand.c changes from patch 9.1.0608. N/A patch: vim-patch:9.1.0632: MS-Windows: Compiler Warnings Co-authored-by: glepnir --- src/nvim/insexpand.c | 91 +++++++++++++++-- src/nvim/search.c | 135 +++++++++++++++++++++++++ test/functional/ui/popupmenu_spec.lua | 8 +- test/old/testdir/test_ins_complete.vim | 63 ++++++++++++ 4 files changed, 284 insertions(+), 13 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 5036ec5e45..e9d2df1a70 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -283,6 +283,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; @@ -3008,7 +3010,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 in_fuzzy) { int compl_type = -1; int status = INS_COMPL_CPT_OK; @@ -3024,7 +3026,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() && (!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 // correctly. @@ -3162,11 +3164,30 @@ 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) ? -1 : (score_a < score_b) ? 1 : 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 = strlen(leader); + bool in_fuzzy = ((get_cot_flags() & kOptCotFlagFuzzy) != 0 && leader_len > 0); + + if (in_fuzzy) { + xfree(compl_pattern); + compl_pattern = xstrdup("*"); + } + if (expand_wildcards(1, &compl_pattern, &num_matches, &matches, EW_FILE|EW_DIR|EW_ADDSLASH|EW_SILENT) != OK) { return; @@ -3189,6 +3210,40 @@ static void get_next_filename_completion(void) } } #endif + + if (in_fuzzy) { + 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); + + 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]]); + } + + FreeWild(num_matches, matches); + matches = sorted_matches; + num_matches = fuzzy_indices.ga_len; + } + + xfree(compl_fuzzy_scores); + ga_clear(&fuzzy_indices); + } + ins_compl_add_matches(num_matches, matches, p_fic || p_wic); } @@ -3310,6 +3365,11 @@ 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_fuzzy = (get_cot_flags() & kOptCotFlagFuzzy) != 0 && compl_length > 0; + char *leader = ins_compl_leader(); + // If 'infercase' is set, don't use 'smartcase' here const int save_p_scs = p_scs; assert(st->ins_buf); @@ -3324,7 +3384,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 == '.') { + } else if (*st->e_cpt == '.' && !in_fuzzy) { p_ws = true; } bool looped_around = false; @@ -3335,9 +3395,14 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ 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 ((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, @@ -3383,12 +3448,15 @@ 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_fuzzy) { + ptr = ins_compl_get_next_word_or_line(st->ins_buf, st->cur_match_pos, + &len, &cont_s_ipos); + } if (ptr == NULL) { 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) { @@ -3489,6 +3557,7 @@ static int ins_compl_get_exp(pos_T *ini) static bool st_cleared = false; int found_new_match; int type = ctrl_x_mode; + bool in_fuzzy = (get_cot_flags() & kOptCotFlagFuzzy) != 0; assert(curbuf != NULL); @@ -3513,7 +3582,11 @@ static int ins_compl_get_exp(pos_T *ini) assert(st.ins_buf != NULL); 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' while (true) { @@ -3525,7 +3598,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, in_fuzzy); if (status == INS_COMPL_CPT_END) { break; } diff --git a/src/nvim/search.c b/src/nvim/search.c index f06b679b0d..7ecf7e9e84 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -3564,6 +3564,141 @@ 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) +{ + char *str = *ptr; + char *strBegin = str; + char *end = NULL; + char *start = NULL; + bool found = false; + + 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 + char save_end = *end; + *end = NUL; + + // Perform fuzzy match + int 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. +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) +{ + pos_T current_pos = *pos; + pos_T circly_end; + bool found_new_match = false; + bool looped_around = false; + + if (whole_line) { + current_pos.lnum += dir; + } + + while (true) { + 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 && 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 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 = (int)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; + } + + 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, diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index a64fc99f26..0950bdb110 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -5294,9 +5294,9 @@ describe('builtin popupmenu', function() feed('S hello helio hero h') screen:expect([[ hello helio hero h^ | - {1:~ }{n: }{mn:h}{n:ello }{1: }| + {1:~ }{n: }{mn:h}{n:ero }{1: }| {1:~ }{n: }{mn:h}{n:elio }{1: }| - {1:~ }{s: }{ms:h}{s:ero }{1: }| + {1:~ }{s: }{ms:h}{s:ello }{1: }| {1:~ }|*15 {2:-- }{5:match 1 of 3} | ]]) @@ -5304,9 +5304,9 @@ describe('builtin popupmenu', function() feed('S hello helio hero h') 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: }| + {1:~ }{s: }{ms:h}{s:elio }{1: }| + {1:~ }{n: }{mn:h}{n:ello }{1: }| {1:~ }|*15 {2:-- }{5:match 2 of 3} | ]]) diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index bf7477f088..4181648497 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -2830,6 +2830,68 @@ 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('.')) + " issue #15526 set completeopt=fuzzy,menuone,menu,noselect call setline(1, ['Text', 'ToText', '']) @@ -2840,6 +2902,7 @@ func Test_complete_fuzzy_match() " clean up set omnifunc= bw! + bw! set complete& completeopt& autocmd! AAAAA_Group augroup! AAAAA_Group From 2a79de0a6f2680a26129e96d97ccdef203cb7ebe Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 30 Aug 2024 16:22:08 +0800 Subject: [PATCH 2/8] vim-patch:9.1.0605: internal error with fuzzy completion Problem: internal error with fuzzy completion (techntools) Solution: only fuzzy complete the pattern after directory separator (glepnir) fixes: vim/vim#15287 closes: vim/vim#15291 https://github.com/vim/vim/commit/0be03e14b9a2899f5e96720e3b21935bd9d2d34e Co-authored-by: glepnir --- src/nvim/insexpand.c | 25 +++++++++++++++++++++++-- src/nvim/search.c | 18 ++++++++++-------- test/old/testdir/test_ins_complete.vim | 6 ++++++ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index e9d2df1a70..21e980e646 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -3184,8 +3184,29 @@ static void get_next_filename_completion(void) bool in_fuzzy = ((get_cot_flags() & kOptCotFlagFuzzy) != 0 && leader_len > 0); if (in_fuzzy) { - xfree(compl_pattern); - compl_pattern = xstrdup("*"); + char *last_sep = strrchr(leader, PATHSEP); + if (last_sep == NULL) { + // No path separator or separator is the last character, + // fuzzy match the whole leader + xfree(compl_pattern); + compl_pattern = xstrdup("*"); + compl_patternlen = strlen(compl_pattern); + } else if (*(last_sep + 1) == NUL) { + in_fuzzy = false; + } else { + // Split leader into path and file parts + size_t path_len = (size_t)(last_sep - leader) + 1; + size_t path_with_wildcard_len = path_len + 2; + char *path_with_wildcard = xmalloc(path_with_wildcard_len); + xmemcpyz(path_with_wildcard, leader, path_len); + xstrlcat(path_with_wildcard, "*", path_with_wildcard_len); + xfree(compl_pattern); + compl_pattern = path_with_wildcard; + compl_patternlen = strlen(compl_pattern); + + // Move leader to the file part + leader = last_sep + 1; + } } if (expand_wildcards(1, &compl_pattern, &num_matches, &matches, diff --git a/src/nvim/search.c b/src/nvim/search.c index 7ecf7e9e84..827047d8c7 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -3633,15 +3633,15 @@ bool search_for_fuzzy_match(buf_T *buf, pos_T *pos, char *pattern, int dir, pos_ current_pos.lnum += dir; } - while (true) { - 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 (buf == curbuf) { + circly_end = *start_pos; + } else { + circly_end.lnum = buf->b_ml.ml_line_count; + circly_end.col = 0; + circly_end.coladd = 0; + } + while (true) { // Check if looped around and back to start position if (looped_around && equalpos(current_pos, circly_end)) { break; @@ -3661,6 +3661,8 @@ bool search_for_fuzzy_match(buf_T *buf, pos_T *pos, char *pattern, int dir, pos_ if (found_new_match) { *pos = current_pos; break; + } else if (looped_around && current_pos.lnum == circly_end.lnum) { + break; } } else { if (fuzzy_match_str(*ptr, pattern) > 0) { diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 4181648497..8ee0c16589 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -2869,6 +2869,10 @@ func Test_complete_fuzzy_match() call assert_equal('fobar', getline('.')) call feedkeys("Sfob\\\\0", 'tx!') call assert_equal('foobar', getline('.')) + call feedkeys("S../\\\0", 'tx!') + call assert_match('../*', getline('.')) + call feedkeys("S../td\\\0", 'tx!') + call assert_match('../testdir', getline('.')) " can get completion from other buffer set completeopt=fuzzy,menu,menuone @@ -2883,6 +2887,8 @@ func Test_complete_fuzzy_match() call assert_equal('Omnipotent', getline('.')) call feedkeys("Somp\\\0", 'tx!') call assert_equal('Composite', getline('.')) + call feedkeys("S omp\\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', '']) From e340109bef32342792a536b5ffdc1f2a5ae38023 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 30 Aug 2024 16:34:01 +0800 Subject: [PATCH 3/8] vim-patch:9.1.0631: wrong completion list displayed with non-existing dir + fuzzy completion Problem: wrong completion list displayed with non-existing dir + fuzzy completion (kawarimidoll) Solution: clear list of matches, if leader did not use fuzzy match (glepnir) fixes: vim/vim#15357 closes: vim/vim#15365 https://github.com/vim/vim/commit/6b6280c4a270547f84f01c0e0d9be1b7d6bb9e20 Co-authored-by: glepnir --- src/nvim/insexpand.c | 8 +++++++- test/functional/ui/popupmenu_spec.lua | 7 +++++++ test/old/testdir/test_popup.vim | 5 +++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 21e980e646..413dc5dcc8 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -3206,6 +3206,7 @@ static void get_next_filename_completion(void) // Move leader to the file part leader = last_sep + 1; + leader_len = strlen(leader); } } @@ -3259,13 +3260,18 @@ static void get_next_filename_completion(void) 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; } xfree(compl_fuzzy_scores); ga_clear(&fuzzy_indices); } - ins_compl_add_matches(num_matches, matches, p_fic || p_wic); + 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". diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index 0950bdb110..eb9b0f1cc1 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -5311,6 +5311,13 @@ describe('builtin popupmenu', function() {2:-- }{5:match 2 of 3} | ]]) + feed('S/non_exit_folder') + screen:expect([[ + /non_exit_folder^ | + {1:~ }|*18 + {2:-- }{6:Pattern not found} | + ]]) + feed('') end) diff --git a/test/old/testdir/test_popup.vim b/test/old/testdir/test_popup.vim index 28104bdff5..58b7c4147f 100644 --- a/test/old/testdir/test_popup.vim +++ b/test/old/testdir/test_popup.vim @@ -1513,6 +1513,11 @@ func Test_pum_highlights_match() call TermWait(buf, 50) call VerifyScreenDump(buf, 'Test_pum_highlights_11', {}) + " issue #15357 + call term_sendkeys(buf, "\S/non_exit_folder\\") + call TermWait(buf, 50) + call VerifyScreenDump(buf, 'Test_pum_highlights_15', {}) + call term_sendkeys(buf, "\\") call TermWait(buf) From d75a36bc522b5f0dbac203ebbcd97fe6ed6bc6ce Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 30 Aug 2024 16:42:38 +0800 Subject: [PATCH 4/8] vim-patch:9.1.0634: Ctrl-P not working by default Problem: Ctrl-P not working by default (Jesse Pavel, after v9.1.0598) Solution: Revert part of v9.1.0598 and set cur_match_pos correctly according to compl_dir_forward() fixes: vim/vim#15370 closes: vim/vim#15379 https://github.com/vim/vim/commit/13032a49b7d2a45e7c774cf23ee8f58f56b03781 Co-authored-by: Christian Brabandt --- src/nvim/insexpand.c | 6 +----- test/old/testdir/test_ins_complete.vim | 10 ++++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 413dc5dcc8..7885a2236d 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -3609,11 +3609,7 @@ static int ins_compl_get_exp(pos_T *ini) assert(st.ins_buf != NULL); compl_old_match = compl_curr_match; // remember the last current match - 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; - } + st.cur_match_pos = compl_dir_forward() ? &st.last_match_pos : &st.first_match_pos; // For ^N/^P loop over all the flags/windows/buffers in 'complete' while (true) { diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 8ee0c16589..36fbece990 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -2939,4 +2939,14 @@ func Test_complete_fuzzy_match_tie() set completeopt& endfunc +func Test_complete_backwards_default() + new + call append(1, ['foobar', 'foobaz']) + new + call feedkeys("i\", 'tx') + call assert_equal('foobaz', getline('.')) + bw! + bw! +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable From af199f9f021ef4e6126b0d13f1de010f4b0294f9 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 30 Aug 2024 16:43:31 +0800 Subject: [PATCH 5/8] vim-patch:9.1.0654: completion does not respect completeslash with fuzzy Problem: completion does not respect completeslash with fuzzy (egesip) Solution: Change path separator on Windows, depending on 'completeslash' option value (glepnir) fixes: vim/vim#15392 closes: vim/vim#15418 https://github.com/vim/vim/commit/b9de1a057f9a0b6de6f64a9c1b2078c7069cdd7d Co-authored-by: glepnir --- src/nvim/insexpand.c | 24 ++++++++++++++++++- test/old/testdir/test_ins_complete.vim | 32 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 7885a2236d..15953fc93f 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -3183,8 +3183,30 @@ static void get_next_filename_completion(void) size_t leader_len = strlen(leader); bool in_fuzzy = ((get_cot_flags() & kOptCotFlagFuzzy) != 0 && leader_len > 0); +#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) { - char *last_sep = strrchr(leader, PATHSEP); +#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 diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index 36fbece990..d139135012 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -2919,6 +2919,38 @@ func Test_complete_fuzzy_match() unlet g:word endfunc +func Test_complete_fuzzy_with_completeslash() + CheckMSWindows + + call writefile([''], 'fobar', 'D') + let orig_shellslash = &shellslash + set cpt& + new + set completeopt+=fuzzy + set noshellslash + + " Test with completeslash unset + set completeslash= + call setline(1, ['.\fob']) + call feedkeys("A\\\0", 'tx!') + call assert_equal('.\fobar', getline('.')) + + " Test with completeslash=backslash + set completeslash=backslash + call feedkeys("S.\\fob\\\0", 'tx!') + call assert_equal('.\fobar', getline('.')) + + " Test with completeslash=slash + set completeslash=slash + call feedkeys("S.\\fob\\\0", 'tx!') + call assert_equal('./fobar', getline('.')) + + " Reset and clean up + let &shellslash = orig_shellslash + set completeslash= + %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() From b53ba9d9a7af93f34aee92b32a9ccd478c4092d6 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sun, 1 Sep 2024 06:22:37 +0800 Subject: [PATCH 6/8] vim-patch:9.1.0705: Sorting of fuzzy filename completion is not stable Problem: Sorting of fuzzy filename completion is not stable Solution: Compare indexes when scores are equal. Fix some typos. (zeertzjq) closes: vim/vim#15593 https://github.com/vim/vim/commit/58d705238c0794ee3baa4173831ab157e709a48a --- src/nvim/insexpand.c | 3 ++- test/functional/ui/popupmenu_spec.lua | 4 ++-- test/old/testdir/test_popup.vim | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 15953fc93f..b375d11385 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -3171,7 +3171,8 @@ static int compare_scores(const void *a, const void *b) 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; + return score_a == score_b ? (idx_a == idx_b ? 0 : (idx_a < idx_b ? -1 : 1)) + : (score_a > score_b ? -1 : 1); } /// Get the next set of filename matching "compl_pattern". diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index eb9b0f1cc1..c5c75fa010 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -5311,9 +5311,9 @@ describe('builtin popupmenu', function() {2:-- }{5:match 2 of 3} | ]]) - feed('S/non_exit_folder') + feed('S/non_existing_folder') screen:expect([[ - /non_exit_folder^ | + /non_existing_folder^ | {1:~ }|*18 {2:-- }{6:Pattern not found} | ]]) diff --git a/test/old/testdir/test_popup.vim b/test/old/testdir/test_popup.vim index 58b7c4147f..95b5b5d595 100644 --- a/test/old/testdir/test_popup.vim +++ b/test/old/testdir/test_popup.vim @@ -1514,7 +1514,7 @@ func Test_pum_highlights_match() call VerifyScreenDump(buf, 'Test_pum_highlights_11', {}) " issue #15357 - call term_sendkeys(buf, "\S/non_exit_folder\\") + call term_sendkeys(buf, "\S/non_existing_folder\\") call TermWait(buf, 50) call VerifyScreenDump(buf, 'Test_pum_highlights_15', {}) From 36fcc1d9ae5482a48916157718f1512d65034c37 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 11 Sep 2024 06:04:57 +0800 Subject: [PATCH 7/8] vim-patch:26e4b00: runtime(doc): Revert outdated comment in completeopt's fuzzy documentation Originally, `:set completeopt+=fuzzy` did not affect how the candidate list is collected in default keyword completion. A comment was added to documentation as part of vim/vim#14912 to clarify it. vim/vim#15193 later changed the fuzzy behavior to now change the candidate collection behavior as well so the clarification in docs is now wrong. Remove them here. closes: vim/vim#15656 https://github.com/vim/vim/commit/26e4b000025ea0e25ea7877314d9095737431bae Co-authored-by: Yee Cheng Chin --- runtime/doc/options.txt | 5 +---- runtime/lua/vim/_meta/options.lua | 5 +---- src/nvim/options.lua | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index c2ed19f34f..d930d66e84 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1566,10 +1566,7 @@ 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. *'completeslash'* *'csl'* 'completeslash' 'csl' string (default "") diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index e5cea884c5..40ca803aec 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -1093,10 +1093,7 @@ 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. --- --- @type string vim.o.completeopt = "menu,preview" diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 3142c30080..80ccf0bdf2 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -1537,10 +1537,7 @@ return { 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. ]=], expand_cb = 'expand_set_completeopt', full_name = 'completeopt', From 28fa71d74312f9e2282aef4f6bf21930bd423c85 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Mon, 16 Sep 2024 09:10:51 +0800 Subject: [PATCH 8/8] vim-patch:9.1.0733: keyword completion does not work with fuzzy Problem: keyword completion does not work with fuzzy (egesip) Solution: handle ctrl_x_mode_normal() specifically (glepnir) fixes: vim/vim#15412 closes: vim/vim#15424 https://github.com/vim/vim/commit/7cfe693f9bfa74690867e4d96c25f2205d0d13e4 Co-authored-by: glepnir --- src/nvim/insexpand.c | 4 ++++ src/nvim/search.c | 23 ++++++++++++++++++++ test/old/testdir/test_ins_complete.vim | 30 +++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index b375d11385..7333150722 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -4602,6 +4602,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()) { @@ -4615,6 +4616,9 @@ static int ins_compl_start(void) curbuf->b_p_com = old; compl_length = 0; compl_col = curwin->w_cursor.col; + } else if (ctrl_x_mode_normal() && in_fuzzy) { + compl_startpos = curwin->w_cursor; + compl_cont_status &= CONT_S_IPOS; } } else { edit_submode_pre = NULL; diff --git a/src/nvim/search.c b/src/nvim/search.c index 827047d8c7..f5df4bddc0 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -3628,6 +3628,8 @@ bool search_for_fuzzy_match(buf_T *buf, pos_T *pos, char *pattern, int dir, pos_ pos_T circly_end; bool found_new_match = false; bool looped_around = false; + char *next_word_end = NULL; + char *match_word = NULL; if (whole_line) { current_pos.lnum += dir; @@ -3659,6 +3661,27 @@ bool search_for_fuzzy_match(buf_T *buf, pos_T *pos, char *pattern, int dir, pos_ // Try to find a fuzzy match in the current line starting from current position found_new_match = fuzzy_match_str_in_line(ptr, pattern, len, ¤t_pos); if (found_new_match) { + if (ctrl_x_mode_normal()) { + match_word = xstrnsave(*ptr, (size_t)(*len)); + if (strcmp(match_word, pattern) == 0) { + 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; + } + } else if (looped_around) { + found_new_match = false; + } + *len = (int)(next_word_end - *ptr); + current_pos.col = *len; + } + xfree(match_word); + } *pos = current_pos; break; } else if (looped_around && current_pos.lnum == circly_end.lnum) { diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index d139135012..815e18b97a 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -2898,10 +2898,38 @@ func Test_complete_fuzzy_match() call feedkeys("Su\\\\0", 'tx!') call assert_equal('no one can save me but you', getline('.')) + " issue #15412 + call setline(1, ['alpha bravio charlie']) + call feedkeys("Salpha\\\0", 'tx!') + call assert_equal('alpha bravio', getline('.')) + call feedkeys("Salp\\\0", 'tx!') + call assert_equal('alpha', getline('.')) + call feedkeys("A\\\0", 'tx!') + call assert_equal('alpha bravio', getline('.')) + call feedkeys("A\\\0", 'tx!') + call assert_equal('alpha bravio charlie', getline('.')) + + set complete-=i + call feedkeys("Salp\\\0", 'tx!') + call assert_equal('alpha', getline('.')) + call feedkeys("A\\\0", 'tx!') + call assert_equal('alpha bravio', getline('.')) + call feedkeys("A\\\0", 'tx!') + call assert_equal('alpha bravio charlie', getline('.')) + + call setline(1, ['alpha bravio charlie', 'alpha another']) + call feedkeys("Salpha\\\\0", 'tx!') + call assert_equal('alpha another', getline('.')) + call setline(1, ['你好 我好', '你好 他好']) + call feedkeys("S你好\\\0", 'tx!') + call assert_equal('你好 我好', getline('.')) + call feedkeys("S你好\\\\0", 'tx!') + call assert_equal('你好 他好', getline('.')) + " issue #15526 set completeopt=fuzzy,menuone,menu,noselect call setline(1, ['Text', 'ToText', '']) - call cursor(2, 1) + call cursor(3, 1) call feedkeys("STe\\x\\0", 'tx!') call assert_equal('Tex', getline('.'))