# HG changeset patch # User Bram Moolenaar # Date 1645709405 -3600 # Node ID ae38d2e81fca95923e2853d2585c9090583f0065 # Parent f4a227222e7acf8c2f04842d3cb7ab89883ac7e0 patch 8.2.4463: completion only uses strict matching Commit: https://github.com/vim/vim/commit/38b85cb4d7216705058708bacbc25ab90cd61595 Author: Yegappan Lakshmanan Date: Thu Feb 24 13:28:41 2022 +0000 patch 8.2.4463: completion only uses strict matching Problem: Completion only uses strict matching. Solution: Add the "fuzzy" item for 'wildoptions'. (Yegappan Lakshmanan, closes #9803) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -9087,6 +9087,14 @@ A jump table for the options with a shor feature} A list of words that change how |cmdline-completion| is done. The following values are supported: + fuzzy Use fuzzy matching to find completion matches. When + this value is specified, wildcard expansion will not + be used for completion. The matches will be sorted by + the "best match" rather than alphabetically sorted. + This will find more matches than the wildcard + expansion. Currently fuzzy matching based completion + is not supported for file and directory names and + instead wildcard expansion is used. pum Display the completion matches using the popupmenu in the same style as the |ins-completion-menu|. tagfile When using CTRL-D to list matching tags, the kind of diff --git a/src/buffer.c b/src/buffer.c --- a/src/buffer.c +++ b/src/buffer.c @@ -2728,10 +2728,12 @@ ExpandBufnames( int round; char_u *p; int attempt; - char_u *patc; + char_u *patc = NULL; #ifdef FEAT_VIMINFO bufmatch_T *matches = NULL; #endif + int fuzzy; + fuzmatch_str_T *fuzmatch = NULL; *num_file = 0; // return values in case of FAIL *file = NULL; @@ -2741,32 +2743,42 @@ ExpandBufnames( return FAIL; #endif - // Make a copy of "pat" and change "^" to "\(^\|[\/]\)". - if (*pat == '^') + fuzzy = cmdline_fuzzy_complete(pat); + + // Make a copy of "pat" and change "^" to "\(^\|[\/]\)" (if doing regular + // expression matching) + if (!fuzzy) { - patc = alloc(STRLEN(pat) + 11); - if (patc == NULL) - return FAIL; - STRCPY(patc, "\\(^\\|[\\/]\\)"); - STRCPY(patc + 11, pat + 1); + if (*pat == '^') + { + patc = alloc(STRLEN(pat) + 11); + if (patc == NULL) + return FAIL; + STRCPY(patc, "\\(^\\|[\\/]\\)"); + STRCPY(patc + 11, pat + 1); + } + else + patc = pat; } - else - patc = pat; // attempt == 0: try match with '\<', match at start of word // attempt == 1: try match without '\<', match anywhere - for (attempt = 0; attempt <= 1; ++attempt) + for (attempt = 0; attempt <= (fuzzy ? 0 : 1); ++attempt) { regmatch_T regmatch; - - if (attempt > 0 && patc == pat) - break; // there was no anchor, no need to try again - regmatch.regprog = vim_regcomp(patc + attempt * 11, RE_MAGIC); - if (regmatch.regprog == NULL) + int score = 0; + + if (!fuzzy) { - if (patc != pat) - vim_free(patc); - return FAIL; + if (attempt > 0 && patc == pat) + break; // there was no anchor, no need to try again + regmatch.regprog = vim_regcomp(patc + attempt * 11, RE_MAGIC); + if (regmatch.regprog == NULL) + { + if (patc != pat) + vim_free(patc); + return FAIL; + } } // round == 1: Count the matches. @@ -2786,7 +2798,22 @@ ExpandBufnames( continue; #endif - p = buflist_match(®match, buf, p_wic); + if (!fuzzy) + p = buflist_match(®match, buf, p_wic); + else + { + p = NULL; + // first try matching with the short file name + if ((score = fuzzy_match_str(buf->b_sfname, pat)) != 0) + p = buf->b_sfname; + if (p == NULL) + { + // next try matching with the full path file name + if ((score = fuzzy_match_str(buf->b_ffname, pat)) != 0) + p = buf->b_ffname; + } + } + if (p != NULL) { if (round == 1) @@ -2797,16 +2824,27 @@ ExpandBufnames( p = home_replace_save(buf, p); else p = vim_strsave(p); -#ifdef FEAT_VIMINFO - if (matches != NULL) + + if (!fuzzy) { - matches[count].buf = buf; - matches[count].match = p; +#ifdef FEAT_VIMINFO + if (matches != NULL) + { + matches[count].buf = buf; + matches[count].match = p; + count++; + } + else +#endif + (*file)[count++] = p; + } + else + { + fuzmatch[count].idx = count; + fuzmatch[count].str = p; + fuzmatch[count].score = score; count++; } - else -#endif - (*file)[count++] = p; } } } @@ -2814,47 +2852,72 @@ ExpandBufnames( break; if (round == 1) { - *file = ALLOC_MULT(char_u *, count); - if (*file == NULL) + if (!fuzzy) { - vim_regfree(regmatch.regprog); - if (patc != pat) - vim_free(patc); - return FAIL; + *file = ALLOC_MULT(char_u *, count); + if (*file == NULL) + { + vim_regfree(regmatch.regprog); + if (patc != pat) + vim_free(patc); + return FAIL; + } +#ifdef FEAT_VIMINFO + if (options & WILD_BUFLASTUSED) + matches = ALLOC_MULT(bufmatch_T, count); +#endif } -#ifdef FEAT_VIMINFO - if (options & WILD_BUFLASTUSED) - matches = ALLOC_MULT(bufmatch_T, count); -#endif + else + { + fuzmatch = ALLOC_MULT(fuzmatch_str_T, count); + if (fuzmatch == NULL) + { + *num_file = 0; + *file = NULL; + return FAIL; + } + } } } - vim_regfree(regmatch.regprog); - if (count) // match(es) found, break here - break; + + if (!fuzzy) + { + vim_regfree(regmatch.regprog); + if (count) // match(es) found, break here + break; + } } - if (patc != pat) + if (!fuzzy && patc != pat) vim_free(patc); #ifdef FEAT_VIMINFO - if (matches != NULL) + if (!fuzzy) { - int i; - if (count > 1) - qsort(matches, count, sizeof(bufmatch_T), buf_compare); - // if the current buffer is first in the list, place it at the end - if (matches[0].buf == curbuf) + if (matches != NULL) { - for (i = 1; i < count; i++) - (*file)[i-1] = matches[i].match; - (*file)[count-1] = matches[0].match; + int i; + if (count > 1) + qsort(matches, count, sizeof(bufmatch_T), buf_compare); + // if the current buffer is first in the list, place it at the end + if (matches[0].buf == curbuf) + { + for (i = 1; i < count; i++) + (*file)[i-1] = matches[i].match; + (*file)[count-1] = matches[0].match; + } + else + { + for (i = 0; i < count; i++) + (*file)[i] = matches[i].match; + } + vim_free(matches); } - else - { - for (i = 0; i < count; i++) - (*file)[i] = matches[i].match; - } - vim_free(matches); + } + else + { + if (fuzzymatches_to_strmatches(fuzmatch, file, count, FALSE) == FAIL) + return FAIL; } #endif diff --git a/src/cmdexpand.c b/src/cmdexpand.c --- a/src/cmdexpand.c +++ b/src/cmdexpand.c @@ -18,7 +18,8 @@ static int cmd_showtail; // Only show pa static void set_expand_context(expand_T *xp); static int ExpandGeneric(expand_T *xp, regmatch_T *regmatch, char_u ***matches, int *numMatches, - char_u *((*func)(expand_T *, int)), int escaped); + char_u *((*func)(expand_T *, int)), int escaped, + char_u *fuzzystr); static int ExpandFromContext(expand_T *xp, char_u *, char_u ***, int *, int); static int expand_showtail(expand_T *xp); static int expand_shellcmd(char_u *filepat, char_u ***matches, int *numMatches, int flagsarg); @@ -40,6 +41,43 @@ static int compl_selected; #define SHOW_FILE_TEXT(m) (showtail ? sm_gettail(matches[m]) : matches[m]) /* + * Returns TRUE if fuzzy completion is supported for a given cmdline completion + * context. + */ + static int +cmdline_fuzzy_completion_supported(expand_T *xp) +{ + return (vim_strchr(p_wop, WOP_FUZZY) != NULL + && xp->xp_context != EXPAND_BOOL_SETTINGS + && xp->xp_context != EXPAND_COLORS + && xp->xp_context != EXPAND_COMPILER + && xp->xp_context != EXPAND_DIRECTORIES + && xp->xp_context != EXPAND_FILES + && xp->xp_context != EXPAND_FILES_IN_PATH + && xp->xp_context != EXPAND_FILETYPE + && xp->xp_context != EXPAND_HELP + && xp->xp_context != EXPAND_MAPPINGS + && xp->xp_context != EXPAND_OLD_SETTING + && xp->xp_context != EXPAND_OWNSYNTAX + && xp->xp_context != EXPAND_PACKADD + && xp->xp_context != EXPAND_SHELLCMD + && xp->xp_context != EXPAND_TAGS + && xp->xp_context != EXPAND_TAGS_LISTFILES + && xp->xp_context != EXPAND_USER_DEFINED + && xp->xp_context != EXPAND_USER_LIST); +} + +/* + * Returns TRUE if fuzzy completion for cmdline completion is enabled and + * 'fuzzystr' is not empty. + */ + int +cmdline_fuzzy_complete(char_u *fuzzystr) +{ + return vim_strchr(p_wop, WOP_FUZZY) != NULL && *fuzzystr != NUL; +} + +/* * sort function for the completion matches. * functions should be sorted to the end. */ @@ -195,9 +233,14 @@ nextwild( } else { + if (cmdline_fuzzy_completion_supported(xp)) + // If fuzzy matching, don't modify the search string + p1 = vim_strsave(xp->xp_pattern); + else + p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context); + // Translate string into pattern and expand it. - if ((p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, - xp->xp_context)) == NULL) + if (p1 == NULL) p2 = NULL; else { @@ -2188,9 +2231,15 @@ expand_cmdline( // add star to file name, or convert to regexp if not exp. files. xp->xp_pattern_len = (int)(str + col - xp->xp_pattern); - file_str = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context); - if (file_str == NULL) - return EXPAND_UNSUCCESSFUL; + if (cmdline_fuzzy_completion_supported(xp)) + // If fuzzy matching, don't modify the search string + file_str = vim_strsave(xp->xp_pattern); + else + { + file_str = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context); + if (file_str == NULL) + return EXPAND_UNSUCCESSFUL; + } if (p_wic) options += WILD_ICASE; @@ -2317,6 +2366,7 @@ get_mapclear_arg(expand_T *xp UNUSED, in */ static int ExpandOther( + char_u *pat, expand_T *xp, regmatch_T *rmp, char_u ***matches, @@ -2386,10 +2436,16 @@ ExpandOther( { if (xp->xp_context == tab[i].context) { + // Use fuzzy matching if 'wildoptions' has 'fuzzy'. + // If no search pattern is supplied, then don't use fuzzy + // matching and return all the found items. + int fuzzy = cmdline_fuzzy_complete(pat); + if (tab[i].ic) rmp->rm_ic = TRUE; ret = ExpandGeneric(xp, rmp, matches, numMatches, - tab[i].func, tab[i].escaped); + tab[i].func, tab[i].escaped, + fuzzy ? pat : NULL); break; } } @@ -2530,7 +2586,7 @@ ExpandFromContext( if (xp->xp_context == EXPAND_SETTINGS || xp->xp_context == EXPAND_BOOL_SETTINGS) - ret = ExpandSettings(xp, ®match, numMatches, matches); + ret = ExpandSettings(xp, ®match, pat, numMatches, matches); else if (xp->xp_context == EXPAND_MAPPINGS) ret = ExpandMappings(®match, numMatches, matches); # if defined(FEAT_EVAL) @@ -2538,7 +2594,7 @@ ExpandFromContext( ret = ExpandUserDefined(xp, ®match, matches, numMatches); # endif else - ret = ExpandOther(xp, ®match, matches, numMatches); + ret = ExpandOther(pat, xp, ®match, matches, numMatches); vim_regfree(regmatch.regprog); vim_free(tofree); @@ -2553,6 +2609,9 @@ ExpandFromContext( * obtain strings, one by one. The strings are matched against a regexp * program. Matching strings are copied into an array, which is returned. * + * If 'fuzzy' is TRUE, then fuzzy matching is used. Otherwise, regex matching + * is used. + * * Returns OK when no problems encountered, FAIL for error (out of memory). */ static int @@ -2563,12 +2622,17 @@ ExpandGeneric( int *numMatches, char_u *((*func)(expand_T *, int)), // returns a string from the list - int escaped) + int escaped, + char_u *fuzzystr) { int i; int count = 0; int round; char_u *str; + fuzmatch_str_T *fuzmatch = NULL; + int score = 0; + int fuzzy = (fuzzystr != NULL); + int funcsort = FALSE; // do this loop twice: // round == 0: count the number of matching names @@ -2583,7 +2647,8 @@ ExpandGeneric( if (*str == NUL) // skip empty strings continue; - if (vim_regexec(regmatch, str, (colnr_T)0)) + if (vim_regexec(regmatch, str, (colnr_T)0) || + (fuzzy && ((score = fuzzy_match_str(str, fuzzystr)) != 0))) { if (round) { @@ -2594,11 +2659,20 @@ ExpandGeneric( if (str == NULL) { FreeWild(count, *matches); + if (fuzzy) + fuzmatch_str_free(fuzmatch, count); *numMatches = 0; *matches = NULL; return FAIL; } - (*matches)[count] = str; + if (fuzzy) + { + fuzmatch[count].idx = count; + fuzmatch[count].str = str; + fuzmatch[count].score = score; + } + else + (*matches)[count] = str; # ifdef FEAT_MENU if (func == get_menu_names && str != NULL) { @@ -2616,8 +2690,11 @@ ExpandGeneric( { if (count == 0) return OK; - *matches = ALLOC_MULT(char_u *, count); - if (*matches == NULL) + if (fuzzy) + fuzmatch = ALLOC_MULT(fuzmatch_str_T, count); + else + *matches = ALLOC_MULT(char_u *, count); + if ((fuzzy && (fuzmatch == NULL)) || (*matches == NULL)) { *numMatches = 0; *matches = NULL; @@ -2635,11 +2712,18 @@ ExpandGeneric( || xp->xp_context == EXPAND_FUNCTIONS || xp->xp_context == EXPAND_USER_FUNC || xp->xp_context == EXPAND_DISASSEMBLE) + { // functions should be sorted to the end. - qsort((void *)*matches, (size_t)*numMatches, sizeof(char_u *), + funcsort = TRUE; + if (!fuzzy) + qsort((void *)*matches, (size_t)*numMatches, sizeof(char_u *), sort_func_compare); + } else - sort_strings(*matches, *numMatches); + { + if (!fuzzy) + sort_strings(*matches, *numMatches); + } } #if defined(FEAT_SYN_HL) @@ -2647,6 +2731,11 @@ ExpandGeneric( // they don't show up when getting normal highlight names by ID. reset_expand_highlight(); #endif + + if (fuzzy && fuzzymatches_to_strmatches(fuzmatch, matches, count, + funcsort) == FAIL) + return FAIL; + return OK; } diff --git a/src/option.c b/src/option.c --- a/src/option.c +++ b/src/option.c @@ -6447,12 +6447,70 @@ set_context_in_set_cmd( } } +/* + * Returns TRUE if 'str' either matches 'regmatch' or fuzzy matches 'pat'. + * + * If 'test_only' is TRUE and 'fuzzy' is FALSE and if 'str' matches the regular + * expression 'regmatch', then returns TRUE. Otherwise returns FALSE. + * + * If 'test_only' is FALSE and 'fuzzy' is FALSE and if 'str' matches the + * regular expression 'regmatch', then stores the match in matches[idx] and + * returns TRUE. + * + * If 'test_only' is TRUE and 'fuzzy' is TRUE and if 'str' fuzzy matches + * 'fuzzystr', then returns TRUE. Otherwise returns FALSE. + * + * If 'test_only' is FALSE and 'fuzzy' is TRUE and if 'str' fuzzy matches + * 'fuzzystr', then stores the match details in fuzmatch[idx] and returns TRUE. + */ + static int +match_str( + char_u *str, + regmatch_T *regmatch, + char_u **matches, + int idx, + int test_only, + int fuzzy, + char_u *fuzzystr, + fuzmatch_str_T *fuzmatch) +{ + if (!fuzzy) + { + if (vim_regexec(regmatch, str, (colnr_T)0)) + { + if (!test_only) + matches[idx] = vim_strsave(str); + return TRUE; + } + } + else + { + int score; + + score = fuzzy_match_str(str, fuzzystr); + if (score != 0) + { + if (!test_only) + { + fuzmatch[idx].idx = idx; + fuzmatch[idx].str = vim_strsave(str); + fuzmatch[idx].score = score; + } + + return TRUE; + } + } + + return FALSE; +} + int ExpandSettings( expand_T *xp, regmatch_T *regmatch, - int *num_file, - char_u ***file) + char_u *fuzzystr, + int *numMatches, + char_u ***matches) { int num_normal = 0; // Nr of matching non-term-code settings int num_term = 0; // Nr of matching terminal code settings @@ -6465,6 +6523,10 @@ ExpandSettings( char_u name_buf[MAX_KEY_NAME_LEN]; static char *(names[]) = {"all", "termcap"}; int ic = regmatch->rm_ic; // remember the ignore-case flag + int fuzzy; + fuzmatch_str_T *fuzmatch = NULL; + + fuzzy = cmdline_fuzzy_complete(fuzzystr); // do this loop twice: // loop == 0: count the number of matching options @@ -6475,13 +6537,16 @@ ExpandSettings( if (xp->xp_context != EXPAND_BOOL_SETTINGS) { for (match = 0; match < (int)ARRAY_LENGTH(names); ++match) - if (vim_regexec(regmatch, (char_u *)names[match], (colnr_T)0)) + { + if (match_str((char_u *)names[match], regmatch, *matches, + count, (loop == 0), fuzzy, fuzzystr, fuzmatch)) { if (loop == 0) num_normal++; else - (*file)[count++] = vim_strsave((char_u *)names[match]); + count++; } + } } for (opt_idx = 0; (str = (char_u *)options[opt_idx].fullname) != NULL; opt_idx++) @@ -6494,12 +6559,37 @@ ExpandSettings( is_term_opt = istermoption_idx(opt_idx); if (is_term_opt && num_normal > 0) continue; - match = FALSE; - if (vim_regexec(regmatch, str, (colnr_T)0) - || (options[opt_idx].shortname != NULL + + if (match_str(str, regmatch, *matches, count, (loop == 0), + fuzzy, fuzzystr, fuzmatch)) + { + if (loop == 0) + { + if (is_term_opt) + num_term++; + else + num_normal++; + } + else + count++; + } + else if (!fuzzy && options[opt_idx].shortname != NULL && vim_regexec(regmatch, - (char_u *)options[opt_idx].shortname, (colnr_T)0))) - match = TRUE; + (char_u *)options[opt_idx].shortname, (colnr_T)0)) + { + // Compare against the abbreviated option name (for regular + // expression match). Fuzzy matching (previous if) already + // matches against both the expanded and abbreviated names. + if (loop == 0) + { + if (is_term_opt) + num_term++; + else + num_normal++; + } + else + (*matches)[count++] = vim_strsave(str); + } else if (is_term_opt) { name_buf[0] = '<'; @@ -6509,25 +6599,18 @@ ExpandSettings( name_buf[4] = str[3]; name_buf[5] = '>'; name_buf[6] = NUL; - if (vim_regexec(regmatch, name_buf, (colnr_T)0)) + + if (match_str(name_buf, regmatch, *matches, count, (loop == 0), + fuzzy, fuzzystr, fuzmatch)) { - match = TRUE; - str = name_buf; + if (loop == 0) + num_term++; + else + count++; } } - if (match) - { - if (loop == 0) - { - if (is_term_opt) - num_term++; - else - num_normal++; - } - else - (*file)[count++] = vim_strsave(str); - } } + /* * Check terminal key codes, these are not in the option table */ @@ -6544,9 +6627,14 @@ ExpandSettings( name_buf[3] = str[1]; name_buf[4] = NUL; - match = FALSE; - if (vim_regexec(regmatch, name_buf, (colnr_T)0)) - match = TRUE; + if (match_str(name_buf, regmatch, *matches, count, + (loop == 0), fuzzy, fuzzystr, fuzmatch)) + { + if (loop == 0) + num_term++; + else + count++; + } else { name_buf[0] = '<'; @@ -6557,15 +6645,15 @@ ExpandSettings( name_buf[5] = '>'; name_buf[6] = NUL; - if (vim_regexec(regmatch, name_buf, (colnr_T)0)) - match = TRUE; - } - if (match) - { - if (loop == 0) - num_term++; - else - (*file)[count++] = vim_strsave(name_buf); + if (match_str(name_buf, regmatch, *matches, count, + (loop == 0), fuzzy, fuzzystr, + fuzmatch)) + { + if (loop == 0) + num_term++; + else + count++; + } } } @@ -6579,31 +6667,49 @@ ExpandSettings( STRCPY(name_buf + 1, str); STRCAT(name_buf, ">"); - if (vim_regexec(regmatch, name_buf, (colnr_T)0)) + if (match_str(name_buf, regmatch, *matches, count, (loop == 0), + fuzzy, fuzzystr, fuzmatch)) { if (loop == 0) num_term++; else - (*file)[count++] = vim_strsave(name_buf); + count++; } } } if (loop == 0) { if (num_normal > 0) - *num_file = num_normal; + *numMatches = num_normal; else if (num_term > 0) - *num_file = num_term; + *numMatches = num_term; else return OK; - *file = ALLOC_MULT(char_u *, *num_file); - if (*file == NULL) + if (!fuzzy) { - *file = (char_u **)""; - return FAIL; + *matches = ALLOC_MULT(char_u *, *numMatches); + if (*matches == NULL) + { + *matches = (char_u **)""; + return FAIL; + } + } + else + { + fuzmatch = ALLOC_MULT(fuzmatch_str_T, *numMatches); + if (fuzmatch == NULL) + { + *matches = (char_u **)""; + return FAIL; + } } } } + + if (fuzzy && + fuzzymatches_to_strmatches(fuzmatch, matches, count, FALSE) == FAIL) + return FAIL; + return OK; } diff --git a/src/option.h b/src/option.h --- a/src/option.h +++ b/src/option.h @@ -358,6 +358,7 @@ typedef enum { // flags for the 'wildoptions' option // each defined char should be unique over all values. +#define WOP_FUZZY 'z' #define WOP_TAGFILE 't' #define WOP_PUM 'p' diff --git a/src/optionstr.c b/src/optionstr.c --- a/src/optionstr.c +++ b/src/optionstr.c @@ -57,7 +57,7 @@ static char *(p_tbis_values[]) = {"tiny" static char *(p_ttym_values[]) = {"xterm", "xterm2", "dec", "netterm", "jsbterm", "pterm", "urxvt", "sgr", NULL}; #endif static char *(p_ve_values[]) = {"block", "insert", "all", "onemore", "none", "NONE", NULL}; -static char *(p_wop_values[]) = {"tagfile", "pum", NULL}; +static char *(p_wop_values[]) = {"fuzzy", "tagfile", "pum", NULL}; #ifdef FEAT_WAK static char *(p_wak_values[]) = {"yes", "menu", "no", NULL}; #endif diff --git a/src/proto/cmdexpand.pro b/src/proto/cmdexpand.pro --- a/src/proto/cmdexpand.pro +++ b/src/proto/cmdexpand.pro @@ -1,4 +1,5 @@ /* cmdexpand.c */ +int cmdline_fuzzy_complete(char_u *fuzzystr); int nextwild(expand_T *xp, int type, int options, int escape); char_u *ExpandOne(expand_T *xp, char_u *str, char_u *orig, int options, int mode); void ExpandInit(expand_T *xp); diff --git a/src/proto/option.pro b/src/proto/option.pro --- a/src/proto/option.pro +++ b/src/proto/option.pro @@ -63,7 +63,7 @@ void reset_modifiable(void); void set_iminsert_global(void); void set_imsearch_global(void); void set_context_in_set_cmd(expand_T *xp, char_u *arg, int opt_flags); -int ExpandSettings(expand_T *xp, regmatch_T *regmatch, int *num_file, char_u ***file); +int ExpandSettings(expand_T *xp, regmatch_T *regmatch, char_u *pat, int *numMatches, char_u ***matches); int ExpandOldSetting(int *num_file, char_u ***file); int shortmess(int x); void vimrc_found(char_u *fname, char_u *envname); diff --git a/src/proto/search.pro b/src/proto/search.pro --- a/src/proto/search.pro +++ b/src/proto/search.pro @@ -40,4 +40,7 @@ void f_searchcount(typval_T *argvars, ty int fuzzy_match(char_u *str, char_u *pat_arg, int matchseq, int *outScore, int_u *matches, int maxMatches); 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); +int fuzzymatches_to_strmatches(fuzmatch_str_T *fuzmatch, char_u ***matches, int count, int funcsort); +void fuzmatch_str_free(fuzmatch_str_T *fuzmatch, int count); /* vim: set ft=c : */ diff --git a/src/search.c b/src/search.c --- a/src/search.c +++ b/src/search.c @@ -1166,7 +1166,7 @@ searchit( return submatch + 1; } -#ifdef FEAT_EVAL +#if defined(FEAT_EVAL) || defined(FEAT_PROTO) void set_search_direction(int cdir) { @@ -4107,7 +4107,7 @@ get_spat_last_idx(void) } #endif -#ifdef FEAT_EVAL +#if defined(FEAT_EVAL) || defined(FEAT_PROTO) /* * "searchcount()" function */ @@ -4230,6 +4230,7 @@ the_end: restore_incsearch_state(); #endif } +#endif /* * Fuzzy string matching @@ -4611,6 +4612,7 @@ fuzzy_match( return numMatches != 0; } +#if defined(FEAT_EVAL) || defined(FEAT_PROTO) /* * Sort the fuzzy matches in the descending order of the match score. * For items with same score, retain the order using the index (stable sort) @@ -4933,5 +4935,131 @@ f_matchfuzzypos(typval_T *argvars, typva { do_fuzzymatch(argvars, rettv, TRUE); } - #endif + +/* + * Same as fuzzy_match_item_compare() except for use with a string match + */ + static int +fuzzy_match_str_compare(const void *s1, const void *s2) +{ + int v1 = ((fuzmatch_str_T *)s1)->score; + int v2 = ((fuzmatch_str_T *)s2)->score; + int idx1 = ((fuzmatch_str_T *)s1)->idx; + int idx2 = ((fuzmatch_str_T *)s2)->idx; + + return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1; +} + +/* + * Sort fuzzy matches by score + */ + static void +fuzzy_match_str_sort(fuzmatch_str_T *fm, int sz) +{ + // Sort the list by the descending order of the match score + qsort((void *)fm, (size_t)sz, sizeof(fuzmatch_str_T), + fuzzy_match_str_compare); +} + +/* + * Same as fuzzy_match_item_compare() except for use with a function name + * string match. functions should be sorted to the end. + */ + static int +fuzzy_match_func_compare(const void *s1, const void *s2) +{ + int v1 = ((fuzmatch_str_T *)s1)->score; + int v2 = ((fuzmatch_str_T *)s2)->score; + int idx1 = ((fuzmatch_str_T *)s1)->idx; + int idx2 = ((fuzmatch_str_T *)s2)->idx; + char_u *str1 = ((fuzmatch_str_T *)s1)->str; + char_u *str2 = ((fuzmatch_str_T *)s2)->str; + + if (*str1 != '<' && *str2 == '<') return -1; + if (*str1 == '<' && *str2 != '<') return 1; + return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1; +} + +/* + * Sort fuzzy matches of function names by score. + * functions should be sorted to the end. + */ + static void +fuzzy_match_func_sort(fuzmatch_str_T *fm, int sz) +{ + // Sort the list by the descending order of the match score + qsort((void *)fm, (size_t)sz, sizeof(fuzmatch_str_T), + fuzzy_match_func_compare); +} + +/* + * Fuzzy match 'pat' in 'str'. Returns 0 if there is no match. Otherwise, + * returns the match score. + */ + int +fuzzy_match_str(char_u *str, char_u *pat) +{ + int score = 0; + int_u matchpos[256]; + + if (str == NULL || pat == NULL) + return 0; + + fuzzy_match(str, pat, FALSE, &score, matchpos, + sizeof(matchpos) / sizeof(matchpos[0])); + + return score; +} + +/* + * Copy a list of fuzzy matches into a string list after sorting the matches by + * the fuzzy score. Frees the memory allocated for 'fuzmatch'. + * Returns OK on success and FAIL on memory allocation failure. + */ + int +fuzzymatches_to_strmatches( + fuzmatch_str_T *fuzmatch, + char_u ***matches, + int count, + int funcsort) +{ + int i; + + if (count <= 0) + return OK; + + *matches = ALLOC_MULT(char_u *, count); + if (*matches == NULL) + { + for (i = 0; i < count; i++) + vim_free(fuzmatch[i].str); + vim_free(fuzmatch); + return FAIL; + } + + // Sort the list by the descending order of the match score + if (funcsort) + fuzzy_match_func_sort((void *)fuzmatch, (size_t)count); + else + fuzzy_match_str_sort((void *)fuzmatch, (size_t)count); + + for (i = 0; i < count; i++) + (*matches)[i] = fuzmatch[i].str; + vim_free(fuzmatch); + + return OK; +} + +/* + * Free a list of fuzzy string matches. + */ + void +fuzmatch_str_free(fuzmatch_str_T *fuzmatch, int count) +{ + if (count <= 0 || fuzmatch == NULL) + return; + while (count--) + vim_free(fuzmatch[count].str); + vim_free(fuzmatch); +} diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -4516,3 +4516,11 @@ typedef struct { int sw_same_win; // VIsual_active was not reset int sw_visual_active; } switchwin_T; + +// Fuzzy matched string list item. Used for fuzzy match completion. Items are +// usually sorted by 'score'. The 'idx' member is used for stable-sort. +typedef struct { + int idx; + char_u *str; + int score; +} fuzmatch_str_T; diff --git a/src/testdir/gen_opt_test.vim b/src/testdir/gen_opt_test.vim --- a/src/testdir/gen_opt_test.vim +++ b/src/testdir/gen_opt_test.vim @@ -11,6 +11,7 @@ set nomore " Clear out t_WS, we don't want to resize the actual terminal. let script = [ \ '" DO NOT EDIT: Generated with gen_opt_test.vim', + \ '" Used by test_options.vim.', \ '', \ 'let save_columns = &columns', \ 'let save_lines = &lines', @@ -152,7 +153,7 @@ let test_values = { \ 'virtualedit': [['', 'all', 'all,block'], ['xxx']], \ 'whichwrap': [['', 'b,s', 'bs'], ['xxx']], \ 'wildmode': [['', 'full', 'list:full', 'full,longest'], ['xxx', 'a4', 'full,full,full,full,full']], - \ 'wildoptions': [['', 'tagfile'], ['xxx']], + \ 'wildoptions': [['', 'tagfile', 'pum', 'fuzzy'], ['xxx']], \ 'winaltkeys': [['menu', 'no'], ['', 'xxx']], \ \ 'luadll': [[], []], diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim --- a/src/testdir/test_cmdline.vim +++ b/src/testdir/test_cmdline.vim @@ -1574,6 +1574,12 @@ func Test_cmdwin_jump_to_win() call assert_equal(1, winnr('$')) endfunc +func Test_cmdwin_tabpage() + tabedit + call assert_fails("silent norm q/g :I\", 'E11:') + tabclose! +endfunc + func Test_cmdwin_interrupted() CheckFeature cmdwin CheckScreendump @@ -2438,4 +2444,321 @@ func Test_cmdline_complete_dlist() call assert_equal("\"dlist 10 /pat/ | chistory", @:) endfunc +" Test for 'fuzzy' in 'wildoptions' (fuzzy completion) +func Test_wildoptions_fuzzy() + " argument list (only for :argdel) + argadd change.py count.py charge.py + set wildoptions& + call feedkeys(":argdel cge\\\"\", 'tx') + call assert_equal('"argdel cge', @:) + set wildoptions=fuzzy + call feedkeys(":argdel cge\\\"\", 'tx') + call assert_equal('"argdel change.py charge.py', @:) + %argdelete + + " autocmd group name fuzzy completion + set wildoptions& + augroup MyFuzzyGroup + augroup END + call feedkeys(":augroup mfg\\\"\", 'tx') + call assert_equal('"augroup mfg', @:) + call feedkeys(":augroup My*p\\\"\", 'tx') + call assert_equal('"augroup MyFuzzyGroup', @:) + set wildoptions=fuzzy + call feedkeys(":augroup mfg\\\"\", 'tx') + call assert_equal('"augroup MyFuzzyGroup', @:) + call feedkeys(":augroup My*p\\\"\", 'tx') + call assert_equal('"augroup My*p', @:) + augroup! MyFuzzyGroup + + " buffer name fuzzy completion + set wildoptions& + edit SomeFile.txt + enew + call feedkeys(":b SF\\\"\", 'tx') + call assert_equal('"b SF', @:) + call feedkeys(":b S*File.txt\\\"\", 'tx') + call assert_equal('"b SomeFile.txt', @:) + set wildoptions=fuzzy + call feedkeys(":b SF\\\"\", 'tx') + call assert_equal('"b SomeFile.txt', @:) + call feedkeys(":b S*File.txt\\\"\", 'tx') + call assert_equal('"b S*File.txt', @:) + %bw! + + " buffer name (full path) fuzzy completion + if has('unix') + set wildoptions& + call mkdir('Xcmd/Xstate/Xfile.js', 'p') + edit Xcmd/Xstate/Xfile.js + cd Xcmd/Xstate + enew + call feedkeys(":b CmdStateFile\\\"\", 'tx') + call assert_equal('"b CmdStateFile', @:) + set wildoptions=fuzzy + call feedkeys(":b CmdStateFile\\\"\", 'tx') + call assert_match('Xcmd/Xstate/Xfile.js$', @:) + cd - + call delete('Xcmd', 'rf') + endif + + " :behave suboptions fuzzy completion + set wildoptions& + call feedkeys(":behave xm\\\"\", 'tx') + call assert_equal('"behave xm', @:) + call feedkeys(":behave xt*m\\\"\", 'tx') + call assert_equal('"behave xterm', @:) + set wildoptions=fuzzy + call feedkeys(":behave xm\\\"\", 'tx') + call assert_equal('"behave xterm', @:) + call feedkeys(":behave xt*m\\\"\", 'tx') + call assert_equal('"behave xt*m', @:) + let g:Sline = '' + call feedkeys(":behave win\\\\"\", 'tx') + call assert_equal('mswin', g:Sline) + call assert_equal('"behave win', @:) + + " colorscheme name fuzzy completion - NOT supported + + " built-in command name fuzzy completion + set wildoptions& + call feedkeys(":sbwin\\\"\", 'tx') + call assert_equal('"sbwin', @:) + call feedkeys(":sbr*d\\\"\", 'tx') + call assert_equal('"sbrewind', @:) + set wildoptions=fuzzy + call feedkeys(":sbwin\\\"\", 'tx') + call assert_equal('"sbrewind', @:) + call feedkeys(":sbr*d\\\"\", 'tx') + call assert_equal('"sbr*d', @:) + + " compiler name fuzzy completion - NOT supported + + " :cscope suboptions fuzzy completion + if has('cscope') + set wildoptions& + call feedkeys(":cscope ret\\\"\", 'tx') + call assert_equal('"cscope ret', @:) + call feedkeys(":cscope re*t\\\"\", 'tx') + call assert_equal('"cscope reset', @:) + set wildoptions=fuzzy + call feedkeys(":cscope ret\\\"\", 'tx') + call assert_equal('"cscope reset', @:) + call feedkeys(":cscope re*t\\\"\", 'tx') + call assert_equal('"cscope re*t', @:) + endif + + " :diffget/:diffput buffer name fuzzy completion + new SomeBuffer + diffthis + new OtherBuffer + diffthis + set wildoptions& + call feedkeys(":diffget sbuf\\\"\", 'tx') + call assert_equal('"diffget sbuf', @:) + call feedkeys(":diffput sbuf\\\"\", 'tx') + call assert_equal('"diffput sbuf', @:) + set wildoptions=fuzzy + call feedkeys(":diffget sbuf\\\"\", 'tx') + call assert_equal('"diffget SomeBuffer', @:) + call feedkeys(":diffput sbuf\\\"\", 'tx') + call assert_equal('"diffput SomeBuffer', @:) + %bw! + + " directory name fuzzy completion - NOT supported + + " environment variable name fuzzy completion + set wildoptions& + call feedkeys(":echo $VUT\\\"\", 'tx') + call assert_equal('"echo $VUT', @:) + set wildoptions=fuzzy + call feedkeys(":echo $VUT\\\"\", 'tx') + call assert_equal('"echo $VIMRUNTIME', @:) + + " autocmd event fuzzy completion + set wildoptions& + call feedkeys(":autocmd BWout\\\"\", 'tx') + call assert_equal('"autocmd BWout', @:) + set wildoptions=fuzzy + call feedkeys(":autocmd BWout\\\"\", 'tx') + call assert_equal('"autocmd BufWipeout', @:) + + " vim expression fuzzy completion + let g:PerPlaceCount = 10 + set wildoptions& + call feedkeys(":let c = ppc\\\"\", 'tx') + call assert_equal('"let c = ppc', @:) + set wildoptions=fuzzy + call feedkeys(":let c = ppc\\\"\", 'tx') + call assert_equal('"let c = PerPlaceCount', @:) + + " file name fuzzy completion - NOT supported + + " files in path fuzzy completion - NOT supported + + " filetype name fuzzy completion - NOT supported + + " user defined function name completion + set wildoptions& + call feedkeys(":call Test_w_fuz\\\"\", 'tx') + call assert_equal('"call Test_w_fuz', @:) + set wildoptions=fuzzy + call feedkeys(":call Test_w_fuz\\\"\", 'tx') + call assert_equal('"call Test_wildoptions_fuzzy()', @:) + + " user defined command name completion + set wildoptions& + call feedkeys(":MsFeat\\\"\", 'tx') + call assert_equal('"MsFeat', @:) + set wildoptions=fuzzy + call feedkeys(":MsFeat\\\"\", 'tx') + call assert_equal('"MissingFeature', @:) + + " :help tag fuzzy completion - NOT supported + + " highlight group name fuzzy completion + set wildoptions& + call feedkeys(":highlight SKey\\\"\", 'tx') + call assert_equal('"highlight SKey', @:) + call feedkeys(":highlight Sp*Key\\\"\", 'tx') + call assert_equal('"highlight SpecialKey', @:) + set wildoptions=fuzzy + call feedkeys(":highlight SKey\\\"\", 'tx') + call assert_equal('"highlight SpecialKey', @:) + call feedkeys(":highlight Sp*Key\\\"\", 'tx') + call assert_equal('"highlight Sp*Key', @:) + + " :history suboptions fuzzy completion + set wildoptions& + call feedkeys(":history dg\\\"\", 'tx') + call assert_equal('"history dg', @:) + call feedkeys(":history se*h\\\"\", 'tx') + call assert_equal('"history search', @:) + set wildoptions=fuzzy + call feedkeys(":history dg\\\"\", 'tx') + call assert_equal('"history debug', @:) + call feedkeys(":history se*h\\\"\", 'tx') + call assert_equal('"history se*h', @:) + + " :language locale name fuzzy completion + if has('unix') + set wildoptions& + call feedkeys(":lang psx\\\"\", 'tx') + call assert_equal('"lang psx', @:) + set wildoptions=fuzzy + call feedkeys(":lang psx\\\"\", 'tx') + call assert_equal('"lang POSIX', @:) + endif + + " :mapclear buffer argument fuzzy completion + set wildoptions& + call feedkeys(":mapclear buf\\\"\", 'tx') + call assert_equal('"mapclear buf', @:) + set wildoptions=fuzzy + call feedkeys(":mapclear buf\\\"\", 'tx') + call assert_equal('"mapclear ', @:) + + " map name fuzzy completion - NOT supported + + " menu name fuzzy completion + if has('gui_running') + set wildoptions& + call feedkeys(":menu pup\\\"\", 'tx') + call assert_equal('"menu pup', @:) + set wildoptions=fuzzy + call feedkeys(":menu pup\\\"\", 'tx') + call assert_equal('"menu PopUp.', @:) + endif + + " :messages suboptions fuzzy completion + set wildoptions& + call feedkeys(":messages clr\\\"\", 'tx') + call assert_equal('"messages clr', @:) + set wildoptions=fuzzy + call feedkeys(":messages clr\\\"\", 'tx') + call assert_equal('"messages clear', @:) + + " :set option name fuzzy completion + set wildoptions& + call feedkeys(":set brkopt\\\"\", 'tx') + call assert_equal('"set brkopt', @:) + set wildoptions=fuzzy + call feedkeys(":set brkopt\\\"\", 'tx') + call assert_equal('"set breakindentopt', @:) + set wildoptions& + call feedkeys(":set fixeol\\\"\", 'tx') + call assert_equal('"set fixendofline', @:) + set wildoptions=fuzzy + call feedkeys(":set fixeol\\\"\", 'tx') + call assert_equal('"set fixendofline', @:) + + " :set + set wildoptions& + call feedkeys(":set t_E\\\"\", 'tx') + call assert_equal('"set t_EC', @:) + call feedkeys(":set \\"\", 'tx') + call assert_equal('"set ', @:) + set wildoptions=fuzzy + call feedkeys(":set t_E\\\"\", 'tx') + call assert_equal('"set t_EC', @:) + call feedkeys(":set \\"\", 'tx') + call assert_equal('"set ', @:) + + " :packadd directory name fuzzy completion - NOT supported + + " shell command name fuzzy completion - NOT supported + + " :sign suboptions fuzzy completion + set wildoptions& + call feedkeys(":sign ufe\\\"\", 'tx') + call assert_equal('"sign ufe', @:) + set wildoptions=fuzzy + call feedkeys(":sign ufe\\\"\", 'tx') + call assert_equal('"sign undefine', @:) + + " :syntax suboptions fuzzy completion + set wildoptions& + call feedkeys(":syntax kwd\\\"\", 'tx') + call assert_equal('"syntax kwd', @:) + set wildoptions=fuzzy + call feedkeys(":syntax kwd\\\"\", 'tx') + call assert_equal('"syntax keyword', @:) + + " syntax group name fuzzy completion + set wildoptions& + call feedkeys(":syntax list mpar\\\"\", 'tx') + call assert_equal('"syntax list mpar', @:) + set wildoptions=fuzzy + call feedkeys(":syntax list mpar\\\"\", 'tx') + call assert_equal('"syntax list MatchParen', @:) + + " :syntime suboptions fuzzy completion + if has('profile') + set wildoptions& + call feedkeys(":syntime clr\\\"\", 'tx') + call assert_equal('"syntime clr', @:) + set wildoptions=fuzzy + call feedkeys(":syntime clr\\\"\", 'tx') + call assert_equal('"syntime clear', @:) + endif + + " tag name fuzzy completion - NOT supported + + " tag name and file fuzzy completion - NOT supported + + " user names fuzzy completion - how to test this functionality? + + " user defined variable name fuzzy completion + let g:SomeVariable=10 + set wildoptions& + call feedkeys(":let SVar\\\"\", 'tx') + call assert_equal('"let SVar', @:) + set wildoptions=fuzzy + call feedkeys(":let SVar\\\"\", 'tx') + call assert_equal('"let SomeVariable', @:) + + set wildoptions& + %bw! +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -755,6 +755,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 4463, +/**/ 4462, /**/ 4461,