This commit is contained in:
zeertzjq 2024-09-12 12:07:11 -05:00 committed by GitHub
commit f359062483
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 395 additions and 25 deletions

View File

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

View File

@ -1090,10 +1090,7 @@ vim.bo.cfu = vim.bo.completefunc
--- 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"

View File

@ -288,6 +288,8 @@ static size_t spell_bad_len = 0; // length of located bad word
static int compl_selected_item = -1;
static int *compl_fuzzy_scores;
/// CTRL-X pressed in Insert mode.
void ins_ctrl_x(void)
{
@ -2991,7 +2993,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;
@ -3007,7 +3009,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.
@ -3145,11 +3147,75 @@ static void get_next_tag_completion(void)
p_ic = save_p_ic;
}
/// Compare function for qsort
static int compare_scores(const void *a, const void *b)
{
int idx_a = *(const int *)a;
int idx_b = *(const int *)b;
int score_a = compl_fuzzy_scores[idx_a];
int score_b = compl_fuzzy_scores[idx_b];
return score_a == score_b ? (idx_a == idx_b ? 0 : (idx_a < idx_b ? -1 : 1))
: (score_a > score_b ? -1 : 1);
}
/// 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() & COT_FUZZY) != 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) {
#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
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;
leader_len = strlen(leader);
}
}
if (expand_wildcards(1, &compl_pattern, &num_matches, &matches,
EW_FILE|EW_DIR|EW_ADDSLASH|EW_SILENT) != OK) {
return;
@ -3172,7 +3238,46 @@ static void get_next_filename_completion(void)
}
}
#endif
ins_compl_add_matches(num_matches, matches, p_fic || p_wic);
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;
} else if (leader_len > 0) {
FreeWild(num_matches, matches);
num_matches = 0;
}
xfree(compl_fuzzy_scores);
ga_clear(&fuzzy_indices);
}
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".
@ -3293,6 +3398,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() & COT_FUZZY) != 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);
@ -3307,7 +3417,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;
@ -3318,9 +3428,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,
@ -3366,12 +3481,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) {
@ -3472,6 +3590,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() & COT_FUZZY) != 0;
assert(curbuf != NULL);
@ -3508,7 +3627,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;
}

View File

@ -1467,10 +1467,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',

View File

@ -3561,6 +3561,143 @@ 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;
}
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;
}
// 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, &current_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) {
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,

View File

@ -4943,9 +4943,9 @@ describe('builtin popupmenu', function()
feed('S hello helio hero h<C-X><C-P>')
screen:expect([[
hello helio hero h^ |
{1:~ }{n: }{mn:h}{n:ello }{1: }|
{1:~ }{n: }{mn:h}{n: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} |
]])
@ -4953,13 +4953,20 @@ describe('builtin popupmenu', function()
feed('<Esc>S hello helio hero h<C-X><C-P><C-P>')
screen:expect([[
hello helio hero h^ |
{1:~ }{n: }{mn:h}{n:ello }{1: }|
{1:~ }{s: }{ms:h}{s:elio }{1: }|
{1:~ }{n: }{mn:h}{n:ero }{1: }|
{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>')
end)

View File

@ -2664,6 +2664,74 @@ func Test_complete_fuzzy_match()
call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
call assert_equal('hello help hero h', getline('.'))
set completeopt-=noinsert
call setline(1, ['xyz yxz x'])
call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
call assert_equal('xyz yxz xyz', getline('.'))
" can fuzzy get yxz when use Ctrl-N twice
call setline(1, ['xyz yxz x'])
call feedkeys("A\<C-X>\<C-N>\<C-N>\<Esc>0", 'tx!')
call assert_equal('xyz yxz yxz', getline('.'))
call setline(1, ['你好 你'])
call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
call assert_equal('你好 你好', getline('.'))
call setline(1, ['你的 我的 的'])
call feedkeys("A\<C-X>\<C-N>\<Esc>0", 'tx!')
call assert_equal('你的 我的 你的', getline('.'))
" can fuzzy get multiple-byte word when use Ctrl-N twice
call setline(1, ['你的 我的 的'])
call feedkeys("A\<C-X>\<C-N>\<C-N>\<Esc>0", 'tx!')
call assert_equal('你的 我的 我的', getline('.'))
" 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')
call setline(1, ['fob'])
call cursor(1, 1)
call feedkeys("A\<C-X>\<C-f>\<Esc>0", 'tx!')
call assert_equal('fobar', getline('.'))
call feedkeys("Sfob\<C-X>\<C-f>\<C-N>\<Esc>0", 'tx!')
call assert_equal('foobar', getline('.'))
call feedkeys("S../\<C-X>\<C-f>\<Esc>0", 'tx!')
call assert_match('../*', getline('.'))
call feedkeys("S../td\<C-X>\<C-f>\<Esc>0", 'tx!')
call assert_match('../testdir', getline('.'))
" can get completion from other buffer
set completeopt=fuzzy,menu,menuone
vnew
call setline(1, ["completeness,", "compatibility", "Composite", "Omnipotent"])
wincmd p
call feedkeys("Somp\<C-N>\<Esc>0", 'tx!')
call assert_equal('completeness', getline('.'))
call feedkeys("Somp\<C-N>\<C-N>\<Esc>0", 'tx!')
call assert_equal('compatibility', getline('.'))
call feedkeys("Somp\<C-P>\<Esc>0", 'tx!')
call assert_equal('Omnipotent', getline('.'))
call feedkeys("Somp\<C-P>\<C-P>\<Esc>0", 'tx!')
call assert_equal('Composite', getline('.'))
call feedkeys("S omp\<C-N>\<Esc>0", 'tx!')
call assert_equal(' completeness', getline('.'))
" fuzzy on whole line completion
call setline(1, ["world is on fire", "no one can save me but you", 'user can execute', ''])
call cursor(4, 1)
call feedkeys("Swio\<C-X>\<C-L>\<Esc>0", 'tx!')
call assert_equal('world is on fire', getline('.'))
call feedkeys("Su\<C-X>\<C-L>\<C-P>\<Esc>0", 'tx!')
call assert_equal('no one can save me but you', getline('.'))
" issue #15526
set completeopt=fuzzy,menuone,menu,noselect
call setline(1, ['Text', 'ToText', ''])
@ -2674,6 +2742,7 @@ func Test_complete_fuzzy_match()
" clean up
set omnifunc=
bw!
bw!
set complete& completeopt&
autocmd! AAAAA_Group
augroup! AAAAA_Group
@ -2684,6 +2753,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\<C-X>\<C-F>\<Esc>0", 'tx!')
call assert_equal('.\fobar', getline('.'))
" Test with completeslash=backslash
set completeslash=backslash
call feedkeys("S.\\fob\<C-X>\<C-F>\<Esc>0", 'tx!')
call assert_equal('.\fobar', getline('.'))
" Test with completeslash=slash
set completeslash=slash
call feedkeys("S.\\fob\<C-X>\<C-F>\<Esc>0", 'tx!')
call assert_equal('./fobar', getline('.'))
" Reset and clean up
let &shellslash = orig_shellslash
set completeslash=
%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()
@ -2704,4 +2805,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\<c-p>", 'tx')
call assert_equal('foobaz', getline('.'))
bw!
bw!
endfunc
" vim: shiftwidth=2 sts=2 expandtab nofoldenable

View File

@ -1500,6 +1500,11 @@ func Test_pum_highlights_match()
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 TermWait(buf)