vim-patch:8.2.1893: fuzzy matching does not support multiple words

Problem:    Fuzzy matching does not support multiple words.
Solution:   Add support for matching white space separated words. (Yegappan
            Lakshmanan, closes vim/vim#7163)
8ded5b647a
This commit is contained in:
Sean Dewar 2022-01-01 21:25:41 +00:00
parent 8313d31e4a
commit 30deb14f39
No known key found for this signature in database
GPG Key ID: 08CC2C83AD41B581
3 changed files with 184 additions and 68 deletions

View File

@ -4866,8 +4866,15 @@ matchfuzzy({list}, {str} [, {dict}]) *matchfuzzy()*
the strings in {list} that fuzzy match {str}. The strings in
the returned list are sorted based on the matching score.
The optional {dict} argument always supports the following
items:
matchseq When this item is present and {str} contains
multiple words separated by white space, then
returns only matches that contain the words in
the given sequence.
If {list} is a list of dictionaries, then the optional {dict}
argument supports the following items:
argument supports the following additional items:
key key of the item which is fuzzy matched against
{str}. The value of this item should be a
string.
@ -4881,6 +4888,9 @@ matchfuzzy({list}, {str} [, {dict}]) *matchfuzzy()*
matching is NOT supported. The maximum supported {str} length
is 256.
When {str} has multiple words each separated by white space,
then the list of strings that have all the words is returned.
If there are no matching strings or there is an error, then an
empty list is returned. If length of {str} is greater than
256, then returns an empty list.
@ -4900,7 +4910,12 @@ matchfuzzy({list}, {str} [, {dict}]) *matchfuzzy()*
:echo v:oldfiles->matchfuzzy("test")
< results in a list of file names fuzzy matching "test". >
:let l = readfile("buffer.c")->matchfuzzy("str")
< results in a list of lines in "buffer.c" fuzzy matching "str".
< results in a list of lines in "buffer.c" fuzzy matching "str". >
:echo ['one two', 'two one']->matchfuzzy('two one')
< results in ['two one', 'one two']. >
:echo ['one two', 'two one']->matchfuzzy('two one',
\ {'matchseq': 1})
< results in ['two one'].
matchfuzzypos({list}, {str} [, {dict}]) *matchfuzzypos()*
Same as |matchfuzzy()|, but returns the list of matched

View File

@ -4771,16 +4771,16 @@ the_end:
/// Ported from the lib_fts library authored by Forrest Smith.
/// https://github.com/forrestthewoods/lib_fts/tree/master/code
///
/// Blog describing the algorithm:
/// The following blog describes the fuzzy matching algorithm:
/// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/
///
/// Each matching string is assigned a score. The following factors are checked:
/// Matched letter
/// Unmatched letter
/// Consecutively matched letters
/// Proximity to start
/// Letter following a separator (space, underscore)
/// Uppercase letter following lowercase (aka CamelCase)
/// - Matched letter
/// - Unmatched letter
/// - Consecutively matched letters
/// - Proximity to start
/// - Letter following a separator (space, underscore)
/// - Uppercase letter following lowercase (aka CamelCase)
///
/// Matched letters are good. Unmatched letters are bad. Matching near the start
/// is good. Matching the first letter in the middle of a phrase is good.
@ -4790,16 +4790,17 @@ the_end:
/// File paths are different from file names. File extensions may be ignorable.
/// Single words care about consecutive matches but not separators or camel
/// case.
/// Score starts at 0
/// Score starts at 100
/// Matched letter: +0 points
/// Unmatched letter: -1 point
/// Consecutive match bonus: +5 points
/// Separator bonus: +10 points
/// Camel case bonus: +10 points
/// Unmatched leading letter: -3 points (max: -9)
/// Consecutive match bonus: +15 points
/// First letter bonus: +15 points
/// Separator bonus: +30 points
/// Camel case bonus: +30 points
/// Unmatched leading letter: -5 points (max: -15)
///
/// There is some nuance to this. Scores dont have an intrinsic meaning. The
/// score range isnt 0 to 100. Its roughly [-50, 50]. Longer words have a
/// score range isnt 0 to 100. Its roughly [50, 150]. Longer words have a
/// lower minimum score due to unmatched letter penalty. Longer search patterns
/// have a higher maximum score due to match bonuses.
///
@ -4813,6 +4814,7 @@ the_end:
/// There is not an explicit bonus for an exact match. Unmatched letters receive
/// a penalty. So shorter strings and closer matches are worth more.
typedef struct {
int idx; ///< used for stable sort
listitem_T *item;
int score;
list_T *lmatchpos;
@ -4833,6 +4835,8 @@ typedef struct {
#define MAX_LEADING_LETTER_PENALTY -15
/// penalty for every letter that doesn't match
#define UNMATCHED_LETTER_PENALTY -1
/// penalty for gap in matching positions (-2 * k)
#define GAP_PENALTY -2
/// Score for a string that doesn't fuzzy match the pattern
#define SCORE_NONE -9999
@ -4870,6 +4874,8 @@ static int fuzzy_match_compute_score(const char_u *const str, const int strSz,
// Sequential
if (currIdx == prevIdx + 1) {
score += SEQUENTIAL_BONUS;
} else {
score += GAP_PENALTY * (currIdx - prevIdx);
}
}
@ -4881,7 +4887,7 @@ static int fuzzy_match_compute_score(const char_u *const str, const int strSz,
for (matchidx_T sidx = 0; sidx < currIdx; sidx++) {
neighbor = utf_ptr2char(p);
mb_ptr2char_adv(&p);
MB_PTR_ADV(p);
}
const int curr = utf_ptr2char(p);
@ -4902,7 +4908,9 @@ static int fuzzy_match_compute_score(const char_u *const str, const int strSz,
return score;
}
static bool fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, matchidx_T strIdx,
/// Perform a recursive search for fuzzy matching 'fuzpat' in 'str'.
/// @return the number of matching characters.
static int fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, matchidx_T strIdx,
int *const outScore, const char_u *const strBegin,
const int strLen, const matchidx_T *const srcMatches,
matchidx_T *const matches, const int maxMatches, int nextMatch,
@ -4917,12 +4925,12 @@ static bool fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, match
// Count recursions
(*recursionCount)++;
if (*recursionCount >= FUZZY_MATCH_RECURSION_LIMIT) {
return false;
return 0;
}
// Detect end of strings
if (*fuzpat == '\0' || *str == '\0') {
return false;
return 0;
}
// Loop through fuzpat and str looking for a match
@ -4935,7 +4943,7 @@ static bool fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, match
if (mb_tolower(c1) == mb_tolower(c2)) {
// Supplied matches buffer was too short
if (nextMatch >= maxMatches) {
return false;
return 0;
}
// "Copy-on-Write" srcMatches into matches
@ -4962,9 +4970,9 @@ static bool fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, match
// Advance
matches[nextMatch++] = strIdx;
mb_ptr2char_adv(&fuzpat);
MB_PTR_ADV(fuzpat);
}
mb_ptr2char_adv(&str);
MB_PTR_ADV(str);
strIdx++;
}
@ -4981,12 +4989,12 @@ static bool fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, match
// Recursive score is better than "this"
memcpy(matches, bestRecursiveMatches, maxMatches * sizeof(matches[0]));
*outScore = bestRecursiveScore;
return true;
return nextMatch;
} else if (matched) {
return true; // "this" score is better than recursive
return nextMatch; // "this" score is better than recursive
}
return false; // no match
return 0; // no match
}
/// fuzzy_match()
@ -4996,45 +5004,98 @@ static bool fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, match
/// Scores values have no intrinsic meaning. Possible score range is not
/// normalized and varies with pattern.
/// Recursion is limited internally (default=10) to prevent degenerate cases
/// (fuzpat="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
/// (pat_arg="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
/// Uses char_u for match indices. Therefore patterns are limited to MAXMATCHES
/// characters.
///
/// @return true if 'fuzpat' 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'.
static bool fuzzy_match(char_u *const str, const char_u *const fuzpat, int *const outScore,
matchidx_T *const matches, const int maxMatches)
static bool fuzzy_match(char_u *const str, const char_u *const pat_arg, const bool matchseq,
int *const outScore, matchidx_T *const matches, const int maxMatches)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT
{
int recursionCount = 0;
const int len = mb_charlen(str);
bool complete = false;
int numMatches = 0;
*outScore = 0;
return fuzzy_match_recursive(fuzpat, str, 0, outScore, str, len, NULL, matches, maxMatches, 0,
&recursionCount);
char_u *const save_pat = vim_strsave(pat_arg);
char_u *pat = save_pat;
char_u *p = pat;
// Try matching each word in 'pat_arg' in 'str'
while (true) {
if (matchseq) {
complete = true;
} else {
// Extract one word from the pattern (separated by space)
p = skipwhite(p);
if (*p == NUL) {
break;
}
pat = p;
while (*p != NUL && !ascii_iswhite(utf_ptr2char(p))) {
MB_PTR_ADV(p);
}
if (*p == NUL) { // processed all the words
complete = true;
}
*p = NUL;
}
int score = 0;
int recursionCount = 0;
const int matchCount
= fuzzy_match_recursive(pat, str, 0, &score, str, len, NULL, matches + numMatches,
maxMatches - numMatches, 0, &recursionCount);
if (matchCount == 0) {
numMatches = 0;
break;
}
// Accumulate the match score and the number of matches
*outScore += score;
numMatches += matchCount;
if (complete) {
break;
}
// try matching the next word
p++;
}
xfree(save_pat);
return numMatches != 0;
}
/// Sort the fuzzy matches in the descending order of the match score.
static int fuzzy_item_compare(const void *const s1, const void *const s2)
/// For items with same score, retain the order using the index (stable sort)
static int fuzzy_match_item_compare(const void *const s1, const void *const s2)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE
{
const int v1 = ((const fuzzyItem_T *)s1)->score;
const int v2 = ((const fuzzyItem_T *)s2)->score;
const int idx1 = ((const fuzzyItem_T *)s1)->idx;
const int idx2 = ((const fuzzyItem_T *)s2)->idx;
return v1 == v2 ? 0 : 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
/// strings in 'fmatchlist'.
/// If 'matchseq' is true, then for multi-word search strings, match all the
/// words in sequence.
/// 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
/// 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'
/// matches for each item.
static void match_fuzzy(list_T *const items, char_u *const str, const char_u *const key,
Callback *const item_cb, const bool retmatchpos, list_T *const fmatchlist)
FUNC_ATTR_NONNULL_ARG(2, 4, 6)
static void fuzzy_match_in_list(list_T *const items, char_u *const str, const bool matchseq,
const char_u *const key, Callback *const item_cb,
const bool retmatchpos, list_T *const fmatchlist)
FUNC_ATTR_NONNULL_ARG(2, 5, 7)
{
const long len = tv_list_len(items);
if (len == 0) {
@ -5048,6 +5109,7 @@ static void match_fuzzy(list_T *const items, char_u *const str, const char_u *co
// For all the string items in items, get the fuzzy matching score
TV_LIST_ITER(items, li, {
ptrs[i].idx = i;
ptrs[i].item = li;
ptrs[i].score = SCORE_NONE;
char_u *itemstr = NULL;
@ -5079,15 +5141,20 @@ static void match_fuzzy(list_T *const items, char_u *const str, const char_u *co
}
int score;
if (itemstr != NULL
&& fuzzy_match(itemstr, str, &score, matches, sizeof(matches) / sizeof(matches[0]))) {
if (itemstr != NULL && fuzzy_match(itemstr, str, matchseq, &score, matches,
sizeof(matches) / sizeof(matches[0]))) {
// Copy the list of matching positions in itemstr to a list, if
// 'retmatchpos' is set.
if (retmatchpos) {
const int strsz = mb_charlen(str);
ptrs[i].lmatchpos = tv_list_alloc(strsz);
for (int j = 0; j < strsz; j++) {
ptrs[i].lmatchpos = tv_list_alloc(kListLenMayKnow);
int j = 0;
const char_u *p = str;
while (*p != NUL) {
if (!ascii_iswhite(utf_ptr2char(p))) {
tv_list_append_number(ptrs[i].lmatchpos, matches[j]);
j++;
}
MB_PTR_ADV(p);
}
}
ptrs[i].score = score;
@ -5099,7 +5166,7 @@ static void match_fuzzy(list_T *const items, char_u *const str, const char_u *co
if (found_match) {
// Sort the list by the descending order of the match score
qsort(ptrs, len, sizeof(fuzzyItem_T), fuzzy_item_compare);
qsort(ptrs, len, sizeof(fuzzyItem_T), fuzzy_match_item_compare);
// For matchfuzzy(), return a list of matched strings.
// ['str1', 'str2', 'str3']
@ -5159,6 +5226,7 @@ static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv,
Callback cb = CALLBACK_NONE;
const char_u *key = NULL;
bool matchseq = false;
if (argvars[2].v_type != VAR_UNKNOWN) {
if (argvars[2].v_type != VAR_DICT || argvars[2].vval.v_dict == NULL) {
emsg(_(e_dictreq));
@ -5168,8 +5236,8 @@ static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv,
// To search a dict, either a callback function or a key can be
// specified.
dict_T *const d = argvars[2].vval.v_dict;
const dictitem_T *const di = tv_dict_find(d, "key", -1);
if (di != NULL) {
const dictitem_T *di;
if ((di = tv_dict_find(d, "key", -1)) != NULL) {
if (di->di_tv.v_type != VAR_STRING || di->di_tv.vval.v_string == NULL
|| *di->di_tv.vval.v_string == NUL) {
semsg(_(e_invarg2), tv_get_string(&di->di_tv));
@ -5180,6 +5248,9 @@ static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv,
semsg(_(e_invargval), "text_cb");
return;
}
if ((di = tv_dict_find(d, "matchseq", -1)) != NULL) {
matchseq = true;
}
}
// get the fuzzy matches
@ -5192,8 +5263,8 @@ static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv,
tv_list_append_list(rettv->vval.v_list, tv_list_alloc(kListLenUnknown));
}
match_fuzzy(argvars[0].vval.v_list, (char_u *)tv_get_string(&argvars[1]), key, &cb, retmatchpos,
rettv->vval.v_list);
fuzzy_match_in_list(argvars[0].vval.v_list, (char_u *)tv_get_string(&argvars[1]), matchseq, key,
&cb, retmatchpos, rettv->vval.v_list);
callback_free(&cb);
}

View File

@ -24,16 +24,15 @@ func Test_matchfuzzy()
call assert_equal(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], matchfuzzy(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], 'aa'))
call assert_equal(256, matchfuzzy([repeat('a', 256)], repeat('a', 256))[0]->len())
call assert_equal([], matchfuzzy([repeat('a', 300)], repeat('a', 257)))
" matches with same score should not be reordered
let l = ['abc1', 'abc2', 'abc3']
call assert_equal(l, l->matchfuzzy('abc'))
" Tests for match preferences
" preference for camel case match
call assert_equal(['oneTwo', 'onetwo'], ['onetwo', 'oneTwo']->matchfuzzy('onetwo'))
" preference for match after a separator (_ or space)
if has("win32")
call assert_equal(['onetwo', 'one two', 'one_two'], ['onetwo', 'one_two', 'one two']->matchfuzzy('onetwo'))
else
call assert_equal(['onetwo', 'one_two', 'one two'], ['onetwo', 'one_two', 'one two']->matchfuzzy('onetwo'))
endif
" preference for leading letter match
call assert_equal(['onetwo', 'xonetwo'], ['xonetwo', 'onetwo']->matchfuzzy('onetwo'))
" preference for sequential match
@ -44,6 +43,17 @@ func Test_matchfuzzy()
call assert_equal(['one', 'onex', 'onexx'], ['onexx', 'one', 'onex']->matchfuzzy('one'))
" prefer complete matches over separator matches
call assert_equal(['.vim/vimrc', '.vim/vimrc_colors', '.vim/v_i_m_r_c'], ['.vim/vimrc', '.vim/vimrc_colors', '.vim/v_i_m_r_c']->matchfuzzy('vimrc'))
" gap penalty
call assert_equal(['xxayybxxxx', 'xxayyybxxx', 'xxayyyybxx'], ['xxayyyybxx', 'xxayyybxxx', 'xxayybxxxx']->matchfuzzy('ab'))
" match multiple words (separated by space)
call assert_equal(['foo bar baz'], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzy('baz foo'))
call assert_equal([], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzy('one two'))
call assert_equal([], ['foo bar']->matchfuzzy(" \t "))
" test for matching a sequence of words
call assert_equal(['bar foo'], ['foo bar', 'bar foo', 'foobar', 'barfoo']->matchfuzzy('bar foo', {'matchseq' : 1}))
call assert_equal([#{text: 'two one'}], [#{text: 'one two'}, #{text: 'two one'}]->matchfuzzy('two one', #{key: 'text', matchseq: v:true}))
%bw!
eval ['somebuf', 'anotherone', 'needle', 'yetanotherone']->map({_, v -> bufadd(v) + bufload(v)})
@ -51,6 +61,7 @@ func Test_matchfuzzy()
call assert_equal(1, len(l))
call assert_match('needle', l[0])
" Test for fuzzy matching dicts
let l = [{'id' : 5, 'val' : 'crayon'}, {'id' : 6, 'val' : 'camera'}]
call assert_equal([{'id' : 6, 'val' : 'camera'}], matchfuzzy(l, 'cam', {'text_cb' : {v -> v.val}}))
call assert_equal([{'id' : 6, 'val' : 'camera'}], matchfuzzy(l, 'cam', {'key' : 'val'}))
@ -72,6 +83,9 @@ func Test_matchfuzzy()
call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : v:_null_string})", 'E475:')
" Nvim doesn't have null functions
" call assert_fails("let x = matchfuzzy(l, 'foo', {'text_cb' : test_null_function()})", 'E475:')
" matches with same score should not be reordered
let l = [#{text: 'abc', id: 1}, #{text: 'abc', id: 2}, #{text: 'abc', id: 3}]
call assert_equal(l, l->matchfuzzy('abc', #{key: 'text'}))
let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}]
call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : 'name'})", 'E730:')
@ -84,7 +98,7 @@ func Test_matchfuzzy()
let &encoding = save_enc
endfunc
" Test for the fuzzymatchpos() function
" Test for the matchfuzzypos() function
func Test_matchfuzzypos()
call assert_equal([['curl', 'world'], [[2,3], [2,3]]], matchfuzzypos(['world', 'curl'], 'rl'))
call assert_equal([['curl', 'world'], [[2,3], [2,3]]], matchfuzzypos(['world', 'one', 'curl'], 'rl'))
@ -92,6 +106,10 @@ func Test_matchfuzzypos()
\ [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]],
\ matchfuzzypos(['hello world hello world', 'hello', 'world'], 'hello'))
call assert_equal([['aaaaaaa'], [[0, 1, 2]]], matchfuzzypos(['aaaaaaa'], 'aaa'))
call assert_equal([['a b'], [[0, 3]]], matchfuzzypos(['a b'], 'a b'))
call assert_equal([['a b'], [[0, 3]]], matchfuzzypos(['a b'], 'a b'))
call assert_equal([['a b'], [[0]]], matchfuzzypos(['a b'], ' a '))
call assert_equal([[], []], matchfuzzypos(['a b'], ' '))
call assert_equal([[], []], matchfuzzypos(['world', 'curl'], 'ab'))
let x = matchfuzzypos([repeat('a', 256)], repeat('a', 256))
call assert_equal(range(256), x[1][0])
@ -113,6 +131,12 @@ func Test_matchfuzzypos()
" best recursive match
call assert_equal([['xoone'], [[2, 3, 4]]], matchfuzzypos(['xoone'], 'one'))
" match multiple words (separated by space)
call assert_equal([['foo bar baz'], [[8, 9, 10, 0, 1, 2]]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('baz foo'))
call assert_equal([[], []], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('one two'))
call assert_equal([[], []], ['foo bar']->matchfuzzypos(" \t "))
call assert_equal([['grace'], [[1, 2, 3, 4, 2, 3, 4, 0, 1, 2, 3, 4]]], ['grace']->matchfuzzypos('race ace grace'))
let l = [{'id' : 5, 'val' : 'crayon'}, {'id' : 6, 'val' : 'camera'}]
call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]]],
\ matchfuzzypos(l, 'cam', {'text_cb' : {v -> v.val}}))
@ -141,6 +165,7 @@ func Test_matchfuzzypos()
call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : 'name'})", 'E730:')
endfunc
" Test for matchfuzzy() with multibyte characters
func Test_matchfuzzy_mbyte()
CheckFeature multi_lang
call assert_equal(['ンヹㄇヺヴ'], matchfuzzy(['ンヹㄇヺヴ'], 'ヹヺ'))
@ -151,19 +176,19 @@ func Test_matchfuzzy_mbyte()
call assert_equal(['ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ', 'πbπ'],
\ matchfuzzy(['πbπ', 'ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ'], 'ππ'))
" match multiple words (separated by space)
call assert_equal(['세 마리의 작은 돼지'], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzy('돼지 마리의'))
call assert_equal([], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzy('파란 하늘'))
" preference for camel case match
call assert_equal(['oneĄwo', 'oneąwo'],
\ ['oneąwo', 'oneĄwo']->matchfuzzy('oneąwo'))
" preference for complete match then match after separator (_ or space)
if has("win32")
" order is different between Windows and Unix :(
" It's important that the complete match is first
call assert_equal(['Ⅱabㄟㄠ', 'Ⅱa bㄟㄠ', 'Ⅱa_bㄟㄠ'],
\ ['Ⅱabㄟㄠ', 'Ⅱa_bㄟㄠ', 'Ⅱa bㄟㄠ']->matchfuzzy('Ⅱabㄟㄠ'))
else
call assert_equal(['Ⅱabㄟㄠ'] + sort(['Ⅱa_bㄟㄠ', 'Ⅱa bㄟㄠ']),
\ ['Ⅱabㄟㄠ', 'Ⅱa bㄟㄠ', 'Ⅱa_bㄟㄠ']->matchfuzzy('Ⅱabㄟㄠ'))
endif
" preference for match after a separator (_ or space)
call assert_equal(['ㄓㄔabㄟㄠ', 'ㄓㄔa_bㄟㄠ', 'ㄓㄔa bㄟㄠ'],
\ ['ㄓㄔa_bㄟㄠ', 'ㄓㄔa bㄟㄠ', 'ㄓㄔabㄟㄠ']->matchfuzzy('ㄓㄔabㄟㄠ'))
" preference for leading letter match
call assert_equal(['ŗŝţũŵż', 'xŗŝţũŵż'],
\ ['xŗŝţũŵż', 'ŗŝţũŵż']->matchfuzzy('ŗŝţũŵż'))
@ -178,6 +203,7 @@ func Test_matchfuzzy_mbyte()
\ ['ŗŝţxx', 'ŗŝţ', 'ŗŝţx']->matchfuzzy('ŗŝţ'))
endfunc
" Test for matchfuzzypos() with multibyte characters
func Test_matchfuzzypos_mbyte()
CheckFeature multi_lang
call assert_equal([['こんにちは世界'], [[0, 1, 2, 3, 4]]],
@ -198,9 +224,13 @@ func Test_matchfuzzypos_mbyte()
call assert_equal(range(256), x[1][0])
call assert_equal([[], []], matchfuzzypos([repeat('✓', 300)], repeat('✓', 257)))
" match multiple words (separated by space)
call assert_equal([['세 마리의 작은 돼지'], [[9, 10, 2, 3, 4]]], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzypos('돼지 마리의'))
call assert_equal([[], []], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzypos('파란 하늘'))
" match in a long string
call assert_equal([[repeat('♪', 300) .. '✗✗✗'], [[300, 301, 302]]],
\ matchfuzzypos([repeat('♪', 300) .. '✗✗✗'], '✗✗✗'))
call assert_equal([[repeat('ぶ', 300) .. 'ẼẼẼ'], [[300, 301, 302]]],
\ matchfuzzypos([repeat('ぶ', 300) .. 'ẼẼẼ'], 'ẼẼẼ'))
" preference for camel case match
call assert_equal([['xѳѵҁxxѳѴҁ'], [[6, 7, 8]]], matchfuzzypos(['xѳѵҁxxѳѴҁ'], 'ѳѵҁ'))
" preference for match after a separator (_ or space)