Merge pull request #21850 from zeertzjq/vim-8.2.4463

vim-patch:8.2.{4463,4465,4475,4477,4478,4479,4608}: fuzzy cmdline builtin completion
This commit is contained in:
zeertzjq
2023-01-17 14:34:27 +08:00
committed by GitHub
12 changed files with 943 additions and 139 deletions

View File

@@ -3000,6 +3000,10 @@ getcompletion({pat}, {type} [, {filtered}]) *getcompletion()*
is applied to filter the results. Otherwise all the matches is applied to filter the results. Otherwise all the matches
are returned. The 'wildignorecase' option always applies. are returned. The 'wildignorecase' option always applies.
If the 'wildoptions' option contains "fuzzy", then fuzzy
matching is used to get the completion matches. Otherwise
regular expression matching is used.
If {type} is "cmdline", then the |cmdline-completion| result is If {type} is "cmdline", then the |cmdline-completion| result is
returned. For example, to complete the possible values after returned. For example, to complete the possible values after
a ":call" command: > a ":call" command: >

View File

@@ -7126,6 +7126,14 @@ A jump table for the options with a short description can be found at |Q_op|.
global global
A list of words that change how |cmdline-completion| is done. A list of words that change how |cmdline-completion| is done.
The following values are supported: 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 popup menu pum Display the completion matches using the popup menu
in the same style as the |ins-completion-menu|. in the same style as the |ins-completion-menu|.
tagfile When using CTRL-D to list matching tags, the kind of tagfile When using CTRL-D to list matching tags, the kind of

View File

@@ -88,6 +88,7 @@
#include "nvim/regexp.h" #include "nvim/regexp.h"
#include "nvim/runtime.h" #include "nvim/runtime.h"
#include "nvim/screen.h" #include "nvim/screen.h"
#include "nvim/search.h"
#include "nvim/sign.h" #include "nvim/sign.h"
#include "nvim/spell.h" #include "nvim/spell.h"
#include "nvim/statusline.h" #include "nvim/statusline.h"
@@ -2337,7 +2338,6 @@ int ExpandBufnames(char *pat, int *num_file, char ***file, int options)
int round; int round;
char *p; char *p;
int attempt; int attempt;
char *patc;
bufmatch_T *matches = NULL; bufmatch_T *matches = NULL;
*num_file = 0; // return values in case of FAIL *num_file = 0; // return values in case of FAIL
@@ -2347,31 +2347,40 @@ int ExpandBufnames(char *pat, int *num_file, char ***file, int options)
return FAIL; return FAIL;
} }
// Make a copy of "pat" and change "^" to "\(^\|[\/]\)". const bool fuzzy = cmdline_fuzzy_complete(pat);
if (*pat == '^') {
patc = xmalloc(strlen(pat) + 11); char *patc = NULL;
STRCPY(patc, "\\(^\\|[\\/]\\)"); // Make a copy of "pat" and change "^" to "\(^\|[\/]\)" (if doing regular
STRCPY(patc + 11, pat + 1); // expression matching)
} else { if (!fuzzy) {
patc = pat; if (*pat == '^') {
patc = xmalloc(strlen(pat) + 11);
STRCPY(patc, "\\(^\\|[\\/]\\)");
STRCPY(patc + 11, pat + 1);
} else {
patc = pat;
}
} }
fuzmatch_str_T *fuzmatch = NULL;
// attempt == 0: try match with '\<', match at start of word // attempt == 0: try match with '\<', match at start of word
// attempt == 1: try match without '\<', match anywhere // attempt == 1: try match without '\<', match anywhere
for (attempt = 0; attempt <= 1; attempt++) { for (attempt = 0; attempt <= (fuzzy ? 0 : 1); attempt++) {
if (attempt > 0 && patc == pat) {
break; // there was no anchor, no need to try again
}
regmatch_T regmatch; regmatch_T regmatch;
regmatch.regprog = vim_regcomp(patc + attempt * 11, RE_MAGIC); if (!fuzzy) {
if (regmatch.regprog == NULL) { if (attempt > 0 && patc == pat) {
if (patc != pat) { break; // there was no anchor, no need to try again
xfree(patc); }
regmatch.regprog = vim_regcomp(patc + attempt * 11, RE_MAGIC);
if (regmatch.regprog == NULL) {
if (patc != pat) {
xfree(patc);
}
return FAIL;
} }
return FAIL;
} }
int score = 0;
// round == 1: Count the matches. // round == 1: Count the matches.
// round == 2: Build the array to keep the matches. // round == 2: Build the array to keep the matches.
for (round = 1; round <= 2; round++) { for (round = 1; round <= 2; round++) {
@@ -2387,7 +2396,23 @@ int ExpandBufnames(char *pat, int *num_file, char ***file, int options)
continue; continue;
} }
} }
p = buflist_match(&regmatch, buf, p_wic);
if (!fuzzy) {
p = buflist_match(&regmatch, 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 (p != NULL) {
if (round == 1) { if (round == 1) {
count++; count++;
@@ -2397,12 +2422,20 @@ int ExpandBufnames(char *pat, int *num_file, char ***file, int options)
} else { } else {
p = xstrdup(p); p = xstrdup(p);
} }
if (matches != NULL) {
matches[count].buf = buf; if (!fuzzy) {
matches[count].match = p; if (matches != NULL) {
count++; matches[count].buf = buf;
matches[count].match = p;
count++;
} else {
(*file)[count++] = p;
}
} else { } else {
(*file)[count++] = p; fuzmatch[count].idx = count;
fuzmatch[count].str = p;
fuzmatch[count].score = score;
count++;
} }
} }
} }
@@ -2411,40 +2444,50 @@ int ExpandBufnames(char *pat, int *num_file, char ***file, int options)
break; break;
} }
if (round == 1) { if (round == 1) {
*file = xmalloc((size_t)count * sizeof(**file)); if (!fuzzy) {
*file = xmalloc((size_t)count * sizeof(**file));
if (options & WILD_BUFLASTUSED) { if (options & WILD_BUFLASTUSED) {
matches = xmalloc((size_t)count * sizeof(*matches)); matches = xmalloc((size_t)count * sizeof(*matches));
}
} else {
fuzmatch = xmalloc((size_t)count * sizeof(fuzmatch_str_T));
} }
} }
} }
vim_regfree(regmatch.regprog);
if (count) { // match(es) found, break here if (!fuzzy) {
break; vim_regfree(regmatch.regprog);
if (count) { // match(es) found, break here
break;
}
} }
} }
if (patc != pat) { if (!fuzzy && patc != pat) {
xfree(patc); xfree(patc);
} }
if (matches != NULL) { if (!fuzzy) {
if (count > 1) { if (matches != NULL) {
qsort(matches, (size_t)count, sizeof(bufmatch_T), buf_time_compare); if (count > 1) {
} qsort(matches, (size_t)count, sizeof(bufmatch_T), buf_time_compare);
}
// if the current buffer is first in the list, place it at the end // if the current buffer is first in the list, place it at the end
if (matches[0].buf == curbuf) { if (matches[0].buf == curbuf) {
for (int i = 1; i < count; i++) { for (int i = 1; i < count; i++) {
(*file)[i - 1] = matches[i].match; (*file)[i - 1] = matches[i].match;
} }
(*file)[count - 1] = matches[0].match; (*file)[count - 1] = matches[0].match;
} else { } else {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
(*file)[i] = matches[i].match; (*file)[i] = matches[i].match;
}
} }
xfree(matches);
} }
xfree(matches); } else {
fuzzymatches_to_strmatches(fuzmatch, file, count, false);
} }
*num_file = count; *num_file = count;

View File

@@ -91,6 +91,38 @@ static int compl_selected;
#define SHOW_MATCH(m) (showtail ? showmatches_gettail(matches[m], false) : matches[m]) #define SHOW_MATCH(m) (showtail ? showmatches_gettail(matches[m], false) : matches[m])
/// Returns true if fuzzy completion is supported for a given cmdline completion
/// context.
static bool cmdline_fuzzy_completion_supported(const expand_T *const xp)
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE
{
return (wop_flags & WOP_FUZZY)
&& 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_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.
bool cmdline_fuzzy_complete(const char *const fuzzystr)
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE
{
return (wop_flags & WOP_FUZZY) && *fuzzystr != NUL;
}
/// Sort function for the completion matches. /// Sort function for the completion matches.
/// <SNR> functions should be sorted to the end. /// <SNR> functions should be sorted to the end.
static int sort_func_compare(const void *s1, const void *s2) static int sort_func_compare(const void *s1, const void *s2)
@@ -223,8 +255,13 @@ int nextwild(expand_T *xp, int type, int options, bool escape)
// Get next/previous match for a previous expanded pattern. // Get next/previous match for a previous expanded pattern.
p2 = ExpandOne(xp, NULL, NULL, 0, type); p2 = ExpandOne(xp, NULL, NULL, 0, type);
} else { } else {
if (cmdline_fuzzy_completion_supported(xp)) {
// If fuzzy matching, don't modify the search string
p1 = xstrdup(xp->xp_pattern);
} else {
p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context);
}
// Translate string into pattern and expand it. // Translate string into pattern and expand it.
p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context);
const int use_options = (options const int use_options = (options
| WILD_HOME_REPLACE | WILD_HOME_REPLACE
| WILD_ADD_SLASH | WILD_ADD_SLASH
@@ -1335,13 +1372,16 @@ static const char *set_cmd_index(const char *cmd, exarg_T *eap, expand_T *xp, in
{ {
const char *p = NULL; const char *p = NULL;
size_t len = 0; size_t len = 0;
const bool fuzzy = cmdline_fuzzy_complete(cmd);
// Isolate the command and search for it in the command table. // Isolate the command and search for it in the command table.
// Exceptions: // Exceptions:
// - the 'k' command can directly be followed by any character, but // - the 'k' command can directly be followed by any character, but do
// do accept "keepmarks", "keepalt" and "keepjumps". // accept "keepmarks", "keepalt" and "keepjumps". As fuzzy matching can
// find matches anywhere in the command name, do this only for command
// expansion based on regular expression and not for fuzzy matching.
// - the 's' command can be followed directly by 'c', 'g', 'i', 'I' or 'r' // - the 's' command can be followed directly by 'c', 'g', 'i', 'I' or 'r'
if (*cmd == 'k' && cmd[1] != 'e') { if (!fuzzy && (*cmd == 'k' && cmd[1] != 'e')) {
eap->cmdidx = CMD_k; eap->cmdidx = CMD_k;
p = cmd + 1; p = cmd + 1;
} else { } else {
@@ -1375,7 +1415,9 @@ static const char *set_cmd_index(const char *cmd, exarg_T *eap, expand_T *xp, in
eap->cmdidx = excmd_get_cmdidx(cmd, len); eap->cmdidx = excmd_get_cmdidx(cmd, len);
if (cmd[0] >= 'A' && cmd[0] <= 'Z') { // User defined commands support alphanumeric characters.
// Also when doing fuzzy expansion, support alphanumeric characters.
if ((cmd[0] >= 'A' && cmd[0] <= 'Z') || (fuzzy && *p != NUL)) {
while (ASCII_ISALNUM(*p) || *p == '*') { // Allow * wild card while (ASCII_ISALNUM(*p) || *p == '*') { // Allow * wild card
p++; p++;
} }
@@ -2330,7 +2372,12 @@ int expand_cmdline(expand_T *xp, const char *str, int col, int *matchcount, char
// add star to file name, or convert to regexp if not exp. files. // add star to file name, or convert to regexp if not exp. files.
assert((str + col) - xp->xp_pattern >= 0); assert((str + col) - xp->xp_pattern >= 0);
xp->xp_pattern_len = (size_t)((str + col) - xp->xp_pattern); xp->xp_pattern_len = (size_t)((str + col) - xp->xp_pattern);
file_str = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context); if (cmdline_fuzzy_completion_supported(xp)) {
// If fuzzy matching, don't modify the search string
file_str = xstrdup(xp->xp_pattern);
} else {
file_str = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context);
}
if (p_wic) { if (p_wic) {
options += WILD_ICASE; options += WILD_ICASE;
@@ -2490,7 +2537,7 @@ static char *get_healthcheck_names(expand_T *xp FUNC_ATTR_UNUSED, int idx)
} }
/// Do the expansion based on xp->xp_context and "rmp". /// Do the expansion based on xp->xp_context and "rmp".
static int ExpandOther(expand_T *xp, regmatch_T *rmp, char ***matches, int *numMatches) static int ExpandOther(char *pat, expand_T *xp, regmatch_T *rmp, char ***matches, int *numMatches)
{ {
typedef CompleteListItemGetter ExpandFunc; typedef CompleteListItemGetter ExpandFunc;
static struct expgen { static struct expgen {
@@ -2538,10 +2585,16 @@ static int ExpandOther(expand_T *xp, regmatch_T *rmp, char ***matches, int *numM
// right function to do the expansion. // right function to do the expansion.
for (int i = 0; i < (int)ARRAY_SIZE(tab); i++) { for (int i = 0; i < (int)ARRAY_SIZE(tab); i++) {
if (xp->xp_context == tab[i].context) { 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.
const bool fuzzy = cmdline_fuzzy_complete(pat);
if (tab[i].ic) { if (tab[i].ic) {
rmp->rm_ic = true; rmp->rm_ic = true;
} }
ExpandGeneric(xp, rmp, matches, numMatches, tab[i].func, tab[i].escaped); ExpandGeneric(xp, rmp, matches, numMatches, tab[i].func, tab[i].escaped,
fuzzy ? pat : NULL);
ret = OK; ret = OK;
break; break;
} }
@@ -2581,9 +2634,11 @@ static int map_wildopts_to_ewflags(int options)
/// @param options WILD_ flags /// @param options WILD_ flags
static int ExpandFromContext(expand_T *xp, char *pat, char ***matches, int *numMatches, int options) static int ExpandFromContext(expand_T *xp, char *pat, char ***matches, int *numMatches, int options)
{ {
regmatch_T regmatch; regmatch_T regmatch = { .rm_ic = false };
int ret; int ret;
int flags = map_wildopts_to_ewflags(options); int flags = map_wildopts_to_ewflags(options);
const bool fuzzy = cmdline_fuzzy_complete(pat)
&& cmdline_fuzzy_completion_supported(xp);
if (xp->xp_context == EXPAND_FILES if (xp->xp_context == EXPAND_FILES
|| xp->xp_context == EXPAND_DIRECTORIES || xp->xp_context == EXPAND_DIRECTORIES
@@ -2664,26 +2719,30 @@ static int ExpandFromContext(expand_T *xp, char *pat, char ***matches, int *numM
return nlua_expand_pat(xp, pat, numMatches, matches); return nlua_expand_pat(xp, pat, numMatches, matches);
} }
regmatch.regprog = vim_regcomp(pat, magic_isset() ? RE_MAGIC : 0); if (!fuzzy) {
if (regmatch.regprog == NULL) { regmatch.regprog = vim_regcomp(pat, magic_isset() ? RE_MAGIC : 0);
return FAIL; if (regmatch.regprog == NULL) {
} return FAIL;
}
// set ignore-case according to p_ic, p_scs and pat // set ignore-case according to p_ic, p_scs and pat
regmatch.rm_ic = ignorecase(pat); regmatch.rm_ic = ignorecase(pat);
}
if (xp->xp_context == EXPAND_SETTINGS if (xp->xp_context == EXPAND_SETTINGS
|| xp->xp_context == EXPAND_BOOL_SETTINGS) { || xp->xp_context == EXPAND_BOOL_SETTINGS) {
ret = ExpandSettings(xp, &regmatch, numMatches, matches); ret = ExpandSettings(xp, &regmatch, pat, numMatches, matches);
} else if (xp->xp_context == EXPAND_MAPPINGS) { } else if (xp->xp_context == EXPAND_MAPPINGS) {
ret = ExpandMappings(&regmatch, numMatches, matches); ret = ExpandMappings(pat, &regmatch, numMatches, matches);
} else if (xp->xp_context == EXPAND_USER_DEFINED) { } else if (xp->xp_context == EXPAND_USER_DEFINED) {
ret = ExpandUserDefined(xp, &regmatch, matches, numMatches); ret = ExpandUserDefined(xp, &regmatch, matches, numMatches);
} else { } else {
ret = ExpandOther(xp, &regmatch, matches, numMatches); ret = ExpandOther(pat, xp, &regmatch, matches, numMatches);
} }
vim_regfree(regmatch.regprog); if (!fuzzy) {
vim_regfree(regmatch.regprog);
}
xfree(tofree); xfree(tofree);
return ret; return ret;
@@ -2695,13 +2754,17 @@ static int ExpandFromContext(expand_T *xp, char *pat, char ***matches, int *numM
/// obtain strings, one by one. The strings are matched against a regexp /// obtain strings, one by one. The strings are matched against a regexp
/// program. Matching strings are copied into an array, which is returned. /// program. Matching strings are copied into an array, which is returned.
/// ///
/// If "fuzzystr" is not NULL, then fuzzy matching is used. Otherwise,
/// regex matching is used.
///
/// @param func returns a string from the list /// @param func returns a string from the list
static void ExpandGeneric(expand_T *xp, regmatch_T *regmatch, char ***matches, int *numMatches, static void ExpandGeneric(expand_T *xp, regmatch_T *regmatch, char ***matches, int *numMatches,
CompleteListItemGetter func, int escaped) CompleteListItemGetter func, int escaped, const char *const fuzzystr)
{ {
int i; int i;
size_t count = 0; size_t count = 0;
char *str; char *str;
const bool fuzzy = fuzzystr != NULL;
// count the number of matching names // count the number of matching names
for (i = 0;; i++) { for (i = 0;; i++) {
@@ -2712,7 +2775,14 @@ static void ExpandGeneric(expand_T *xp, regmatch_T *regmatch, char ***matches, i
if (*str == NUL) { // skip empty strings if (*str == NUL) { // skip empty strings
continue; continue;
} }
if (vim_regexec(regmatch, str, (colnr_T)0)) {
bool match;
if (!fuzzy) {
match = vim_regexec(regmatch, str, (colnr_T)0);
} else {
match = fuzzy_match_str(str, fuzzystr) != 0;
}
if (match) {
count++; count++;
} }
} }
@@ -2721,7 +2791,12 @@ static void ExpandGeneric(expand_T *xp, regmatch_T *regmatch, char ***matches, i
} }
assert(count < INT_MAX); assert(count < INT_MAX);
*numMatches = (int)count; *numMatches = (int)count;
*matches = xmalloc(count * sizeof(char *)); fuzmatch_str_T *fuzmatch = NULL;
if (fuzzy) {
fuzmatch = xmalloc(count * sizeof(fuzmatch_str_T));
} else {
*matches = xmalloc(count * sizeof(char *));
}
// copy the matching names into allocated memory // copy the matching names into allocated memory
count = 0; count = 0;
@@ -2733,38 +2808,66 @@ static void ExpandGeneric(expand_T *xp, regmatch_T *regmatch, char ***matches, i
if (*str == NUL) { // Skip empty strings. if (*str == NUL) { // Skip empty strings.
continue; continue;
} }
if (vim_regexec(regmatch, str, (colnr_T)0)) {
if (escaped) { bool match;
str = vim_strsave_escaped(str, " \t\\."); int score = 0;
} else { if (!fuzzy) {
str = xstrdup(str); match = vim_regexec(regmatch, str, (colnr_T)0);
} } else {
(*matches)[count++] = str; score = fuzzy_match_str(str, fuzzystr);
if (func == get_menu_names) { match = (score != 0);
// Test for separator added by get_menu_names(). }
str += strlen(str) - 1; if (!match) {
if (*str == '\001') { continue;
*str = '.'; }
}
if (escaped) {
str = vim_strsave_escaped(str, " \t\\.");
} else {
str = xstrdup(str);
}
if (fuzzy) {
fuzmatch[count].idx = (int)count;
fuzmatch[count].str = str;
fuzmatch[count].score = score;
} else {
(*matches)[count] = str;
}
count++;
if (func == get_menu_names) {
// Test for separator added by get_menu_names().
str += strlen(str) - 1;
if (*str == '\001') {
*str = '.';
} }
} }
} }
// Sort the results. Keep menu's in the specified order. // Sort the results. Keep menu's in the specified order.
bool funcsort = false;
if (xp->xp_context != EXPAND_MENUNAMES && xp->xp_context != EXPAND_MENUS) { if (xp->xp_context != EXPAND_MENUNAMES && xp->xp_context != EXPAND_MENUS) {
if (xp->xp_context == EXPAND_EXPRESSION if (xp->xp_context == EXPAND_EXPRESSION
|| xp->xp_context == EXPAND_FUNCTIONS || xp->xp_context == EXPAND_FUNCTIONS
|| xp->xp_context == EXPAND_USER_FUNC) { || xp->xp_context == EXPAND_USER_FUNC) {
// <SNR> functions should be sorted to the end. // <SNR> functions should be sorted to the end.
qsort((void *)(*matches), (size_t)(*numMatches), sizeof(char *), sort_func_compare); funcsort = true;
if (!fuzzy) {
qsort(*matches, (size_t)(*numMatches), sizeof(char *), sort_func_compare);
}
} else { } else {
sort_strings(*matches, *numMatches); if (!fuzzy) {
sort_strings(*matches, *numMatches);
}
} }
} }
// Reset the variables used for special highlight names expansion, so that // Reset the variables used for special highlight names expansion, so that
// they don't show up when getting normal highlight names by ID. // they don't show up when getting normal highlight names by ID.
reset_expand_highlight(); reset_expand_highlight();
if (fuzzy) {
fuzzymatches_to_strmatches(fuzmatch, matches, (int)count, funcsort);
}
} }
/// Expand shell command matches in one directory of $PATH. /// Expand shell command matches in one directory of $PATH.
@@ -3369,7 +3472,13 @@ void f_getcompletion(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
} }
theend: theend:
pat = addstar(xpc.xp_pattern, xpc.xp_pattern_len, xpc.xp_context); if (cmdline_fuzzy_completion_supported(&xpc)) {
// when fuzzy matching, don't modify the search string
pat = xstrdup(xpc.xp_pattern);
} else {
pat = addstar(xpc.xp_pattern, xpc.xp_pattern_len, xpc.xp_context);
}
ExpandOne(&xpc, pat, NULL, options, WILD_ALL_KEEP); ExpandOne(&xpc, pat, NULL, options, WILD_ALL_KEEP);
tv_list_alloc_ret(rettv, xpc.xp_numfiles); tv_list_alloc_ret(rettv, xpc.xp_numfiles);

View File

@@ -18,6 +18,7 @@
#include "nvim/ascii.h" #include "nvim/ascii.h"
#include "nvim/buffer_defs.h" #include "nvim/buffer_defs.h"
#include "nvim/charset.h" #include "nvim/charset.h"
#include "nvim/cmdexpand.h"
#include "nvim/eval.h" #include "nvim/eval.h"
#include "nvim/eval/typval.h" #include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h" #include "nvim/eval/typval_defs.h"
@@ -39,6 +40,7 @@
#include "nvim/pos.h" #include "nvim/pos.h"
#include "nvim/regexp.h" #include "nvim/regexp.h"
#include "nvim/runtime.h" #include "nvim/runtime.h"
#include "nvim/search.h"
#include "nvim/strings.h" #include "nvim/strings.h"
#include "nvim/vim.h" #include "nvim/vim.h"
@@ -1268,7 +1270,7 @@ char_u *set_context_in_map_cmd(expand_T *xp, char *cmd, char *arg, bool forceit,
/// Find all mapping/abbreviation names that match regexp "regmatch". /// Find all mapping/abbreviation names that match regexp "regmatch".
/// For command line expansion of ":[un]map" and ":[un]abbrev" in all modes. /// For command line expansion of ":[un]map" and ":[un]abbrev" in all modes.
/// @return OK if matches found, FAIL otherwise. /// @return OK if matches found, FAIL otherwise.
int ExpandMappings(regmatch_T *regmatch, int *num_file, char ***file) int ExpandMappings(char *pat, regmatch_T *regmatch, int *numMatches, char ***matches)
{ {
mapblock_T *mp; mapblock_T *mp;
int hash; int hash;
@@ -1277,14 +1279,18 @@ int ExpandMappings(regmatch_T *regmatch, int *num_file, char ***file)
char *p; char *p;
int i; int i;
*num_file = 0; // return values in case of FAIL fuzmatch_str_T *fuzmatch = NULL;
*file = NULL; const bool fuzzy = cmdline_fuzzy_complete(pat);
*numMatches = 0; // return values in case of FAIL
*matches = NULL;
// round == 1: Count the matches. // round == 1: Count the matches.
// round == 2: Build the array to keep the matches. // round == 2: Build the array to keep the matches.
for (round = 1; round <= 2; round++) { for (round = 1; round <= 2; round++) {
count = 0; count = 0;
// First search in map modifier arguments
for (i = 0; i < 7; i++) { for (i = 0; i < 7; i++) {
if (i == 0) { if (i == 0) {
p = "<silent>"; p = "<silent>";
@@ -1304,13 +1310,29 @@ int ExpandMappings(regmatch_T *regmatch, int *num_file, char ***file)
continue; continue;
} }
if (vim_regexec(regmatch, p, (colnr_T)0)) { bool match;
if (round == 1) { int score = 0;
count++; if (!fuzzy) {
match = vim_regexec(regmatch, p, (colnr_T)0);
} else {
score = fuzzy_match_str(p, pat);
match = (score != 0);
}
if (!match) {
continue;
}
if (round == 2) {
if (fuzzy) {
fuzmatch[count].idx = count;
fuzmatch[count].str = xstrdup(p);
fuzmatch[count].score = score;
} else { } else {
(*file)[count++] = xstrdup(p); (*matches)[count] = xstrdup(p);
} }
} }
count++;
} }
for (hash = 0; hash < 256; hash++) { for (hash = 0; hash < 256; hash++) {
@@ -1327,12 +1349,28 @@ int ExpandMappings(regmatch_T *regmatch, int *num_file, char ***file)
for (; mp; mp = mp->m_next) { for (; mp; mp = mp->m_next) {
if (mp->m_mode & expand_mapmodes) { if (mp->m_mode & expand_mapmodes) {
p = (char *)translate_mapping((char_u *)mp->m_keys, CPO_TO_CPO_FLAGS); p = (char *)translate_mapping((char_u *)mp->m_keys, CPO_TO_CPO_FLAGS);
if (p != NULL && vim_regexec(regmatch, p, (colnr_T)0)) { if (p != NULL) {
if (round == 1) { bool match;
count++; int score = 0;
if (!fuzzy) {
match = vim_regexec(regmatch, p, (colnr_T)0);
} else { } else {
(*file)[count++] = p; score = fuzzy_match_str(p, pat);
p = NULL; match = (score != 0);
}
if (match) {
if (round == 2) {
if (fuzzy) {
fuzmatch[count].idx = count;
fuzmatch[count].str = p;
fuzmatch[count].score = score;
} else {
(*matches)[count] = p;
}
p = NULL;
}
count++;
} }
} }
xfree(p); xfree(p);
@@ -1345,16 +1383,27 @@ int ExpandMappings(regmatch_T *regmatch, int *num_file, char ***file)
} }
if (round == 1) { if (round == 1) {
*file = xmalloc((size_t)count * sizeof(char *)); if (fuzzy) {
fuzmatch = xmalloc((size_t)count * sizeof(fuzmatch_str_T));
} else {
*matches = xmalloc((size_t)count * sizeof(char *));
}
} }
} // for (round) } // for (round)
if (fuzzy) {
fuzzymatches_to_strmatches(fuzmatch, matches, count, false);
}
if (count > 1) { if (count > 1) {
// Sort the matches // Sort the matches
sort_strings(*file, count); // Fuzzy matching already sorts the matches
if (!fuzzy) {
sort_strings(*matches, count);
}
// Remove multiple entries // Remove multiple entries
char **ptr1 = *file; char **ptr1 = *matches;
char **ptr2 = ptr1 + 1; char **ptr2 = ptr1 + 1;
char **ptr3 = ptr1 + count; char **ptr3 = ptr1 + count;
@@ -1368,7 +1417,7 @@ int ExpandMappings(regmatch_T *regmatch, int *num_file, char ***file)
} }
} }
*num_file = count; *numMatches = count;
return count == 0 ? FAIL : OK; return count == 0 ? FAIL : OK;
} }

View File

@@ -36,6 +36,7 @@
#include "nvim/buffer.h" #include "nvim/buffer.h"
#include "nvim/change.h" #include "nvim/change.h"
#include "nvim/charset.h" #include "nvim/charset.h"
#include "nvim/cmdexpand.h"
#include "nvim/cursor_shape.h" #include "nvim/cursor_shape.h"
#include "nvim/decoration_provider.h" #include "nvim/decoration_provider.h"
#include "nvim/diff.h" #include "nvim/diff.h"
@@ -81,6 +82,7 @@
#include "nvim/regexp.h" #include "nvim/regexp.h"
#include "nvim/runtime.h" #include "nvim/runtime.h"
#include "nvim/screen.h" #include "nvim/screen.h"
#include "nvim/search.h"
#include "nvim/sign_defs.h" #include "nvim/sign_defs.h"
#include "nvim/spell.h" #include "nvim/spell.h"
#include "nvim/spellfile.h" #include "nvim/spellfile.h"
@@ -4698,13 +4700,56 @@ void set_context_in_set_cmd(expand_T *xp, char *arg, int opt_flags)
} }
} }
int ExpandSettings(expand_T *xp, regmatch_T *regmatch, int *num_file, char ***file) /// 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 bool match_str(char *const str, regmatch_T *const regmatch, char **const matches,
const int idx, const bool test_only, const bool fuzzy,
const char *const fuzzystr, fuzmatch_str_T *const fuzmatch)
{
if (!fuzzy) {
if (vim_regexec(regmatch, str, (colnr_T)0)) {
if (!test_only) {
matches[idx] = xstrdup(str);
}
return true;
}
} else {
const int score = fuzzy_match_str(str, fuzzystr);
if (score != 0) {
if (!test_only) {
fuzmatch[idx].idx = idx;
fuzmatch[idx].str = xstrdup(str);
fuzmatch[idx].score = score;
}
return true;
}
}
return false;
}
int ExpandSettings(expand_T *xp, regmatch_T *regmatch, char *fuzzystr, int *numMatches,
char ***matches)
{ {
int num_normal = 0; // Nr of matching non-term-code settings int num_normal = 0; // Nr of matching non-term-code settings
int count = 0; int count = 0;
static char *(names[]) = { "all" }; static char *(names[]) = { "all" };
int ic = regmatch->rm_ic; // remember the ignore-case flag int ic = regmatch->rm_ic; // remember the ignore-case flag
fuzmatch_str_T *fuzmatch = NULL;
const bool fuzzy = cmdline_fuzzy_complete(fuzzystr);
// do this loop twice: // do this loop twice:
// loop == 0: count the number of matching options // loop == 0: count the number of matching options
// loop == 1: copy the matching options into allocated memory // loop == 1: copy the matching options into allocated memory
@@ -4714,11 +4759,12 @@ int ExpandSettings(expand_T *xp, regmatch_T *regmatch, int *num_file, char ***fi
if (xp->xp_context != EXPAND_BOOL_SETTINGS) { if (xp->xp_context != EXPAND_BOOL_SETTINGS) {
for (match = 0; match < (int)ARRAY_SIZE(names); for (match = 0; match < (int)ARRAY_SIZE(names);
match++) { match++) {
if (vim_regexec(regmatch, names[match], (colnr_T)0)) { if (match_str(names[match], regmatch, *matches,
count, (loop == 0), fuzzy, fuzzystr, fuzmatch)) {
if (loop == 0) { if (loop == 0) {
num_normal++; num_normal++;
} else { } else {
(*file)[count++] = xstrdup(names[match]); count++;
} }
} }
} }
@@ -4733,33 +4779,45 @@ int ExpandSettings(expand_T *xp, regmatch_T *regmatch, int *num_file, char ***fi
&& !(options[opt_idx].flags & P_BOOL)) { && !(options[opt_idx].flags & P_BOOL)) {
continue; continue;
} }
match = false;
if (vim_regexec(regmatch, str, (colnr_T)0)
|| (options[opt_idx].shortname != NULL
&& vim_regexec(regmatch,
options[opt_idx].shortname,
(colnr_T)0))) {
match = true;
}
if (match) { if (match_str(str, regmatch, *matches, count, (loop == 0),
fuzzy, fuzzystr, fuzmatch)) {
if (loop == 0) { if (loop == 0) {
num_normal++; num_normal++;
} else { } else {
(*file)[count++] = xstrdup(str); count++;
}
} else if (!fuzzy && options[opt_idx].shortname != NULL
&& vim_regexec(regmatch, 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) {
num_normal++;
} else {
(*matches)[count++] = xstrdup(str);
} }
} }
} }
if (loop == 0) { if (loop == 0) {
if (num_normal > 0) { if (num_normal > 0) {
*num_file = num_normal; *numMatches = num_normal;
} else { } else {
return OK; return OK;
} }
*file = xmalloc((size_t)(*num_file) * sizeof(char *)); if (!fuzzy) {
*matches = xmalloc((size_t)(*numMatches) * sizeof(char *));
} else {
fuzmatch = xmalloc((size_t)(*numMatches) * sizeof(fuzmatch_str_T));
}
} }
} }
if (fuzzy) {
fuzzymatches_to_strmatches(fuzmatch, matches, count, false);
}
return OK; return OK;
} }

View File

@@ -795,6 +795,7 @@ EXTERN char *p_wop; // 'wildoptions'
EXTERN unsigned wop_flags; EXTERN unsigned wop_flags;
#define WOP_TAGFILE 0x01 #define WOP_TAGFILE 0x01
#define WOP_PUM 0x02 #define WOP_PUM 0x02
#define WOP_FUZZY 0x04
EXTERN long p_window; // 'window' EXTERN long p_window; // 'window'
EXTERN char *p_wak; // 'winaltkeys' EXTERN char *p_wak; // 'winaltkeys'
EXTERN char *p_wig; // 'wildignore' EXTERN char *p_wig; // 'wildignore'

View File

@@ -91,7 +91,7 @@ static char *(p_swb_values[]) = { "useopen", "usetab", "split", "newtab", "vspli
static char *(p_spk_values[]) = { "cursor", "screen", "topline", NULL }; static char *(p_spk_values[]) = { "cursor", "screen", "topline", NULL };
static char *(p_tc_values[]) = { "followic", "ignore", "match", "followscs", "smart", NULL }; static char *(p_tc_values[]) = { "followic", "ignore", "match", "followscs", "smart", NULL };
static char *(p_ve_values[]) = { "block", "insert", "all", "onemore", "none", "NONE", NULL }; 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[]) = { "tagfile", "pum", "fuzzy", NULL };
static char *(p_wak_values[]) = { "yes", "menu", "no", NULL }; static char *(p_wak_values[]) = { "yes", "menu", "no", NULL };
static char *(p_mousem_values[]) = { "extend", "popup", "popup_setpos", "mac", NULL }; static char *(p_mousem_values[]) = { "extend", "popup", "popup_setpos", "mac", NULL };
static char *(p_sel_values[]) = { "inclusive", "exclusive", "old", NULL }; static char *(p_sel_values[]) = { "inclusive", "exclusive", "old", NULL };

View File

@@ -5230,8 +5230,7 @@ static bool vgr_match_buflines(qf_list_T *qfl, char *fname, buf_T *buf, char *sp
const size_t sz = sizeof(matches) / sizeof(matches[0]); const size_t sz = sizeof(matches) / sizeof(matches[0]);
// Fuzzy string match // Fuzzy string match
while (fuzzy_match((char_u *)str + col, (char_u *)spat, false, &score, matches, while (fuzzy_match(str + col, spat, false, &score, matches, (int)sz) > 0) {
(int)sz) > 0) {
// Pass the buffer number so that it gets used even for a // Pass the buffer number so that it gets used even for a
// dummy buffer, unless duplicate_name is set, then the // dummy buffer, unless duplicate_name is set, then the
// buffer will be wiped out below. // buffer will be wiped out below.

View File

@@ -2942,7 +2942,7 @@ typedef struct {
#define FUZZY_MATCH_RECURSION_LIMIT 10 #define FUZZY_MATCH_RECURSION_LIMIT 10
/// Compute a score for a fuzzy matched string. The matching character locations /// Compute a score for a fuzzy matched string. The matching character locations
/// are in 'matches'. /// are in "matches".
static int fuzzy_match_compute_score(const char *const str, const int strSz, static int fuzzy_match_compute_score(const char *const str, const int strSz,
const uint32_t *const matches, const int numMatches) const uint32_t *const matches, const int numMatches)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE
@@ -3007,7 +3007,7 @@ static int fuzzy_match_compute_score(const char *const str, const int strSz,
return score; return score;
} }
/// Perform a recursive search for fuzzy matching 'fuzpat' in 'str'. /// Perform a recursive search for fuzzy matching "fuzpat" in "str".
/// @return the number of matching characters. /// @return the number of matching characters.
static int fuzzy_match_recursive(const char *fuzpat, const char *str, uint32_t strIdx, static int fuzzy_match_recursive(const char *fuzpat, const char *str, uint32_t strIdx,
int *const outScore, const char *const strBegin, const int strLen, int *const outScore, const char *const strBegin, const int strLen,
@@ -3107,23 +3107,23 @@ static int fuzzy_match_recursive(const char *fuzpat, const char *str, uint32_t s
/// Uses char_u for match indices. Therefore patterns are limited to /// Uses char_u for match indices. Therefore patterns are limited to
/// MAX_FUZZY_MATCHES characters. /// MAX_FUZZY_MATCHES characters.
/// ///
/// @return true if 'pat_arg' matches 'str'. Also returns the match score in /// @return true if "pat_arg" matches "str". Also returns the match score in
/// 'outScore' and the matching character positions in 'matches'. /// "outScore" and the matching character positions in "matches".
bool fuzzy_match(char_u *const str, const char_u *const pat_arg, const bool matchseq, bool fuzzy_match(char *const str, const char *const pat_arg, const bool matchseq,
int *const outScore, uint32_t *const matches, const int maxMatches) int *const outScore, uint32_t *const matches, const int maxMatches)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL
{ {
const int len = mb_charlen((char *)str); const int len = mb_charlen(str);
bool complete = false; bool complete = false;
int numMatches = 0; int numMatches = 0;
*outScore = 0; *outScore = 0;
char *const save_pat = xstrdup((char *)pat_arg); char *const save_pat = xstrdup(pat_arg);
char *pat = save_pat; char *pat = save_pat;
char *p = pat; char *p = pat;
// Try matching each word in 'pat_arg' in 'str' // Try matching each word in "pat_arg" in "str"
while (true) { while (true) {
if (matchseq) { if (matchseq) {
complete = true; complete = true;
@@ -3146,7 +3146,7 @@ bool fuzzy_match(char_u *const str, const char_u *const pat_arg, const bool matc
int score = 0; int score = 0;
int recursionCount = 0; int recursionCount = 0;
const int matchCount const int matchCount
= fuzzy_match_recursive(pat, (char *)str, 0, &score, (char *)str, len, NULL, = fuzzy_match_recursive(pat, str, 0, &score, str, len, NULL,
matches + numMatches, matches + numMatches,
maxMatches - numMatches, 0, &recursionCount); maxMatches - numMatches, 0, &recursionCount);
if (matchCount == 0) { if (matchCount == 0) {
@@ -3183,14 +3183,14 @@ static int fuzzy_match_item_compare(const void *const s1, const void *const s2)
return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1; return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1;
} }
/// Fuzzy search the string 'str' in a list of 'items' and return the matching /// Fuzzy search the string "str" in a list of "items" and return the matching
/// strings in 'fmatchlist'. /// strings in "fmatchlist".
/// If 'matchseq' is true, then for multi-word search strings, match all the /// If "matchseq" is true, then for multi-word search strings, match all the
/// words in sequence. /// words in sequence.
/// If 'items' is a list of strings, then search for 'str' in the list. /// If "items" is a list of strings, then search for "str" in the list.
/// If 'items' is a list of dicts, then either use 'key' to lookup the string /// If "items" is a list of dicts, then either use "key" to lookup the string
/// for each item or use 'item_cb' Funcref function to get the string. /// for each item or use "item_cb" Funcref function to get the string.
/// If 'retmatchpos' is true, then return a list of positions where 'str' /// If "retmatchpos" is true, then return a list of positions where "str"
/// matches for each item. /// matches for each item.
static void fuzzy_match_in_list(list_T *const l, char *const str, const bool matchseq, static void fuzzy_match_in_list(list_T *const l, char *const str, const bool matchseq,
const char *const key, Callback *const item_cb, const char *const key, Callback *const item_cb,
@@ -3245,14 +3245,14 @@ static void fuzzy_match_in_list(list_T *const l, char *const str, const bool mat
} }
int score; int score;
if (itemstr != NULL && fuzzy_match((char_u *)itemstr, (char_u *)str, matchseq, &score, matches, if (itemstr != NULL && fuzzy_match(itemstr, str, matchseq, &score, matches,
MAX_FUZZY_MATCHES)) { MAX_FUZZY_MATCHES)) {
items[match_count].idx = (int)match_count; items[match_count].idx = (int)match_count;
items[match_count].item = li; items[match_count].item = li;
items[match_count].score = score; items[match_count].score = score;
// Copy the list of matching positions in itemstr to a list, if // Copy the list of matching positions in itemstr to a list, if
// 'retmatchpos' is set. // "retmatchpos" is set.
if (retmatchpos) { if (retmatchpos) {
items[match_count].lmatchpos = tv_list_alloc(kListLenMayKnow); items[match_count].lmatchpos = tv_list_alloc(kListLenMayKnow);
int j = 0; int j = 0;
@@ -3326,8 +3326,8 @@ static void fuzzy_match_in_list(list_T *const l, char *const str, const bool mat
xfree(items); xfree(items);
} }
/// Do fuzzy matching. Returns the list of matched strings in 'rettv'. /// Do fuzzy matching. Returns the list of matched strings in "rettv".
/// If 'retmatchpos' is true, also returns the matching character positions. /// If "retmatchpos" is true, also returns the matching character positions.
static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv, static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv,
const bool retmatchpos) const bool retmatchpos)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
@@ -3411,6 +3411,109 @@ void f_matchfuzzypos(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
do_fuzzymatch(argvars, rettv, true); do_fuzzymatch(argvars, rettv, true);
} }
/// Same as fuzzy_match_item_compare() except for use with a string match
static int fuzzy_match_str_compare(const void *const s1, const void *const s2)
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE
{
const int v1 = ((fuzmatch_str_T *)s1)->score;
const int v2 = ((fuzmatch_str_T *)s2)->score;
const int idx1 = ((fuzmatch_str_T *)s1)->idx;
const 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 *const fm, const int sz)
FUNC_ATTR_NONNULL_ALL
{
// Sort the list by the descending order of the match score
qsort(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. <SNR> functions should be sorted to the end.
static int fuzzy_match_func_compare(const void *const s1, const void *const s2)
FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL FUNC_ATTR_PURE
{
const int v1 = ((fuzmatch_str_T *)s1)->score;
const int v2 = ((fuzmatch_str_T *)s2)->score;
const int idx1 = ((fuzmatch_str_T *)s1)->idx;
const int idx2 = ((fuzmatch_str_T *)s2)->idx;
const char *const str1 = ((fuzmatch_str_T *)s1)->str;
const char *const 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.
/// <SNR> functions should be sorted to the end.
static void fuzzy_match_func_sort(fuzmatch_str_T *const fm, const int sz)
FUNC_ATTR_NONNULL_ALL
{
// Sort the list by the descending order of the match score
qsort(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 *const str, const char *const pat)
FUNC_ATTR_WARN_UNUSED_RESULT
{
if (str == NULL || pat == NULL) {
return 0;
}
int score = 0;
uint32_t matchpos[MAX_FUZZY_MATCHES];
fuzzy_match(str, pat, true, &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".
void fuzzymatches_to_strmatches(fuzmatch_str_T *const fuzmatch, char ***const matches,
const int count, const bool funcsort)
FUNC_ATTR_NONNULL_ARG(2)
{
if (count <= 0) {
return;
}
*matches = xmalloc((size_t)count * sizeof(char *));
// Sort the list by the descending order of the match score
if (funcsort) {
fuzzy_match_func_sort(fuzmatch, count);
} else {
fuzzy_match_str_sort(fuzmatch, count);
}
for (int i = 0; i < count; i++) {
(*matches)[i] = fuzmatch[i].str;
}
xfree(fuzmatch);
}
/// Free a list of fuzzy string matches.
void fuzmatch_str_free(fuzmatch_str_T *const fuzmatch, int count)
{
if (count <= 0 || fuzmatch == NULL) {
return;
}
while (count--) {
xfree(fuzmatch[count].str);
}
xfree(fuzmatch);
}
/// Get line "lnum" and copy it into "buf[LSIZE]". /// Get line "lnum" and copy it into "buf[LSIZE]".
/// The copy is made because the regexp may make the line invalid when using a /// The copy is made because the regexp may make the line invalid when using a
/// mark. /// mark.

View File

@@ -99,6 +99,14 @@ typedef struct searchstat {
int last_maxcount; // the max count of the last search int last_maxcount; // the max count of the last search
} searchstat_T; } searchstat_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 *str;
int score;
} fuzmatch_str_T;
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "search.h.generated.h" # include "search.h.generated.h"
#endif #endif

View File

@@ -650,6 +650,22 @@ func Test_getcompletion()
call assert_fails('call getcompletion("abc", [])', 'E475:') call assert_fails('call getcompletion("abc", [])', 'E475:')
endfunc endfunc
" Test for getcompletion() with "fuzzy" in 'wildoptions'
func Test_getcompletion_wildoptions()
let save_wildoptions = &wildoptions
set wildoptions&
let l = getcompletion('space', 'option')
call assert_equal([], l)
let l = getcompletion('ier', 'command')
call assert_equal([], l)
set wildoptions=fuzzy
let l = getcompletion('space', 'option')
call assert_true(index(l, 'backspace') >= 0)
let l = getcompletion('ier', 'command')
call assert_true(index(l, 'compiler') >= 0)
let &wildoptions = save_wildoptions
endfunc
func Test_fullcommand() func Test_fullcommand()
let tests = { let tests = {
\ '': '', \ '': '',
@@ -2654,6 +2670,412 @@ func Test_cmdline_complete_dlist()
call assert_equal("\"dlist 10 /pat/ | chistory", @:) call assert_equal("\"dlist 10 /pat/ | chistory", @:)
endfunc 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\<C-A>\<C-B>\"\<CR>", 'tx')
call assert_equal('"argdel cge', @:)
set wildoptions=fuzzy
call feedkeys(":argdel cge\<C-A>\<C-B>\"\<CR>", '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\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"augroup mfg', @:)
call feedkeys(":augroup My*p\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"augroup MyFuzzyGroup', @:)
set wildoptions=fuzzy
call feedkeys(":augroup mfg\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"augroup MyFuzzyGroup', @:)
call feedkeys(":augroup My*p\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"augroup My*p', @:)
augroup! MyFuzzyGroup
" buffer name fuzzy completion
set wildoptions&
edit SomeFile.txt
enew
call feedkeys(":b SF\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"b SF', @:)
call feedkeys(":b S*File.txt\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"b SomeFile.txt', @:)
set wildoptions=fuzzy
call feedkeys(":b SF\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"b SomeFile.txt', @:)
call feedkeys(":b S*File.txt\<Tab>\<C-B>\"\<CR>", '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\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"b CmdStateFile', @:)
set wildoptions=fuzzy
call feedkeys(":b CmdStateFile\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_match('Xcmd/Xstate/Xfile.js$', @:)
cd -
call delete('Xcmd', 'rf')
endif
" :behave suboptions fuzzy completion
set wildoptions&
call feedkeys(":behave xm\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"behave xm', @:)
call feedkeys(":behave xt*m\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"behave xterm', @:)
set wildoptions=fuzzy
call feedkeys(":behave xm\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"behave xterm', @:)
call feedkeys(":behave xt*m\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"behave xt*m', @:)
let g:Sline = ''
call feedkeys(":behave win\<C-D>\<F4>\<C-B>\"\<CR>", '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\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"sbwin', @:)
call feedkeys(":sbr*d\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"sbrewind', @:)
set wildoptions=fuzzy
call feedkeys(":sbwin\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"sbrewind', @:)
call feedkeys(":sbr*d\<Tab>\<C-B>\"\<CR>", '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\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"cscope ret', @:)
call feedkeys(":cscope re*t\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"cscope reset', @:)
set wildoptions=fuzzy
call feedkeys(":cscope ret\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"cscope reset', @:)
call feedkeys(":cscope re*t\<Tab>\<C-B>\"\<CR>", '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\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"diffget sbuf', @:)
call feedkeys(":diffput sbuf\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"diffput sbuf', @:)
set wildoptions=fuzzy
call feedkeys(":diffget sbuf\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"diffget SomeBuffer', @:)
call feedkeys(":diffput sbuf\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"diffput SomeBuffer', @:)
%bw!
" directory name fuzzy completion - NOT supported
" environment variable name fuzzy completion
set wildoptions&
call feedkeys(":echo $VUT\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"echo $VUT', @:)
set wildoptions=fuzzy
call feedkeys(":echo $VUT\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"echo $VIMRUNTIME', @:)
" autocmd event fuzzy completion
set wildoptions&
call feedkeys(":autocmd BWout\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"autocmd BWout', @:)
set wildoptions=fuzzy
call feedkeys(":autocmd BWout\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"autocmd BufWipeout', @:)
" vim expression fuzzy completion
let g:PerPlaceCount = 10
set wildoptions&
call feedkeys(":let c = ppc\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"let c = ppc', @:)
set wildoptions=fuzzy
call feedkeys(":let c = ppc\<Tab>\<C-B>\"\<CR>", '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\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"call Test_w_fuz', @:)
set wildoptions=fuzzy
call feedkeys(":call Test_w_fuz\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"call Test_wildoptions_fuzzy()', @:)
" user defined command name completion
set wildoptions&
call feedkeys(":MsFeat\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"MsFeat', @:)
set wildoptions=fuzzy
call feedkeys(":MsFeat\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"MissingFeature', @:)
" :help tag fuzzy completion - NOT supported
" highlight group name fuzzy completion
set wildoptions&
call feedkeys(":highlight SKey\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"highlight SKey', @:)
call feedkeys(":highlight Sp*Key\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"highlight SpecialKey', @:)
set wildoptions=fuzzy
call feedkeys(":highlight SKey\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"highlight SpecialKey', @:)
call feedkeys(":highlight Sp*Key\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"highlight Sp*Key', @:)
" :history suboptions fuzzy completion
set wildoptions&
call feedkeys(":history dg\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"history dg', @:)
call feedkeys(":history se*h\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"history search', @:)
set wildoptions=fuzzy
call feedkeys(":history dg\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"history debug', @:)
call feedkeys(":history se*h\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"history se*h', @:)
" :language locale name fuzzy completion
if has('unix')
set wildoptions&
call feedkeys(":lang psx\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"lang psx', @:)
set wildoptions=fuzzy
call feedkeys(":lang psx\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"lang POSIX', @:)
endif
" :mapclear buffer argument fuzzy completion
set wildoptions&
call feedkeys(":mapclear buf\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"mapclear buf', @:)
set wildoptions=fuzzy
call feedkeys(":mapclear buf\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"mapclear <buffer>', @:)
" map name fuzzy completion
" test regex completion works
set wildoptions=fuzzy
call feedkeys(":cnoremap <ex\<Tab> <esc> \<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"cnoremap <expr> <esc> \<Tab>", @:)
nmap <plug>MyLongMap :p<CR>
call feedkeys(":nmap MLM\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"nmap <Plug>MyLongMap", @:)
call feedkeys(":nmap MLM \<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"nmap MLM \t", @:)
call feedkeys(":nmap <F2> one two \<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"nmap <F2> one two \t", @:)
" duplicate entries should be removed
vmap <plug>MyLongMap :<C-U>#<CR>
call feedkeys(":nmap MLM\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"nmap <Plug>MyLongMap", @:)
nunmap <plug>MyLongMap
vunmap <plug>MyLongMap
call feedkeys(":nmap ABC\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"nmap ABC\t", @:)
" results should be sorted by best match
nmap <Plug>format :
nmap <Plug>goformat :
nmap <Plug>TestFOrmat :
nmap <Plug>fendoff :
nmap <Plug>state :
nmap <Plug>FendingOff :
call feedkeys(":nmap <Plug>fo\<C-A>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"nmap <Plug>format <Plug>TestFOrmat <Plug>FendingOff <Plug>goformat <Plug>fendoff", @:)
nunmap <Plug>format
nunmap <Plug>goformat
nunmap <Plug>TestFOrmat
nunmap <Plug>fendoff
nunmap <Plug>state
nunmap <Plug>FendingOff
" abbreviation fuzzy completion
set wildoptions=fuzzy
call feedkeys(":iabbr wait\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"iabbr <nowait>", @:)
iabbr WaitForCompletion WFC
call feedkeys(":iabbr fcl\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"iabbr WaitForCompletion", @:)
call feedkeys(":iabbr a1z\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"iabbr a1z\t", @:)
iunabbrev WaitForCompletion
" menu name fuzzy completion
if has('gui_running')
set wildoptions&
call feedkeys(":menu pup\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"menu pup', @:)
set wildoptions=fuzzy
call feedkeys(":menu pup\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"menu PopUp.', @:)
endif
" :messages suboptions fuzzy completion
set wildoptions&
call feedkeys(":messages clr\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"messages clr', @:)
set wildoptions=fuzzy
call feedkeys(":messages clr\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"messages clear', @:)
" :set option name fuzzy completion
set wildoptions&
call feedkeys(":set brkopt\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"set brkopt', @:)
set wildoptions=fuzzy
call feedkeys(":set brkopt\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"set breakindentopt', @:)
set wildoptions&
call feedkeys(":set fixeol\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"set fixendofline', @:)
set wildoptions=fuzzy
call feedkeys(":set fixeol\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"set fixendofline', @:)
" :set <term_option>
" Nvim does not support term options
" set wildoptions&
" call feedkeys(":set t_E\<Tab>\<C-B>\"\<CR>", 'tx')
" call assert_equal('"set t_EC', @:)
" call feedkeys(":set <t_E\<Tab>\<C-B>\"\<CR>", 'tx')
" call assert_equal('"set <t_EC>', @:)
" set wildoptions=fuzzy
" call feedkeys(":set t_E\<Tab>\<C-B>\"\<CR>", 'tx')
" call assert_equal('"set t_EC', @:)
" call feedkeys(":set <t_E\<Tab>\<C-B>\"\<CR>", 'tx')
" call assert_equal('"set <t_EC>', @:)
" :packadd directory name fuzzy completion - NOT supported
" shell command name fuzzy completion - NOT supported
" :sign suboptions fuzzy completion
set wildoptions&
call feedkeys(":sign ufe\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"sign ufe', @:)
set wildoptions=fuzzy
call feedkeys(":sign ufe\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"sign undefine', @:)
" :syntax suboptions fuzzy completion
set wildoptions&
call feedkeys(":syntax kwd\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"syntax kwd', @:)
set wildoptions=fuzzy
call feedkeys(":syntax kwd\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"syntax keyword', @:)
" syntax group name fuzzy completion
set wildoptions&
call feedkeys(":syntax list mpar\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"syntax list mpar', @:)
set wildoptions=fuzzy
call feedkeys(":syntax list mpar\<Tab>\<C-B>\"\<CR>", 'tx')
" Fuzzy match favours NvimParenthesis over MatchParen
" call assert_equal('"syntax list MatchParen', @:)
call assert_equal('"syntax list NvimParenthesis', @:)
" :syntime suboptions fuzzy completion
if has('profile')
set wildoptions&
call feedkeys(":syntime clr\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"syntime clr', @:)
set wildoptions=fuzzy
call feedkeys(":syntime clr\<Tab>\<C-B>\"\<CR>", '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\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"let SVar', @:)
set wildoptions=fuzzy
call feedkeys(":let SVar\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"let SomeVariable', @:)
" Test for sorting the results by the best match
%bw!
command T123format :
command T123goformat :
command T123TestFOrmat :
command T123fendoff :
command T123state :
command T123FendingOff :
set wildoptions=fuzzy
call feedkeys(":T123fo\<C-A>\<C-B>\"\<CR>", 'tx')
call assert_equal('"T123format T123TestFOrmat T123FendingOff T123goformat T123fendoff', @:)
delcommand T123format
delcommand T123goformat
delcommand T123TestFOrmat
delcommand T123fendoff
delcommand T123state
delcommand T123FendingOff
%bw
" Test for fuzzy completion of a command with lower case letters and a
" number
command Foo2Bar :
set wildoptions=fuzzy
call feedkeys(":foo2\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"Foo2Bar', @:)
call feedkeys(":foo\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"Foo2Bar', @:)
call feedkeys(":bar\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"Foo2Bar', @:)
delcommand Foo2Bar
" Test for command completion for a command starting with 'k'
command KillKillKill :
set wildoptions&
call feedkeys(":killkill\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal("\"killkill\<Tab>", @:)
set wildoptions=fuzzy
call feedkeys(":killkill\<Tab>\<C-B>\"\<CR>", 'tx')
call assert_equal('"KillKillKill', @:)
delcom KillKillKill
set wildoptions&
%bw!
endfunc
" Test for :breakadd argument completion " Test for :breakadd argument completion
func Test_cmdline_complete_breakadd() func Test_cmdline_complete_breakadd()
call feedkeys(":breakadd \<C-A>\<C-B>\"\<CR>", 'tx') call feedkeys(":breakadd \<C-A>\<C-B>\"\<CR>", 'tx')