diff options
author | Sean Dewar <seandewar@users.noreply.github.com> | 2022-01-01 21:25:41 +0000 |
---|---|---|
committer | Sean Dewar <seandewar@users.noreply.github.com> | 2022-02-07 17:20:50 +0000 |
commit | 30deb14f397e576aedfa54600ed7408b3e03459d (patch) | |
tree | a266fff8721e9f73eac9017cd5615fe6b5c6f20b | |
parent | 8313d31e4a5065ac0e945ebc689fa083a50e41dc (diff) | |
download | rneovim-30deb14f397e576aedfa54600ed7408b3e03459d.tar.gz rneovim-30deb14f397e576aedfa54600ed7408b3e03459d.tar.bz2 rneovim-30deb14f397e576aedfa54600ed7408b3e03459d.zip |
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)
https://github.com/vim/vim/commit/8ded5b647aa4b3338da721b343e0bce0f86655f6
-rw-r--r-- | runtime/doc/builtin.txt | 19 | ||||
-rw-r--r-- | src/nvim/search.c | 171 | ||||
-rw-r--r-- | src/nvim/testdir/test_matchfuzzy.vim | 62 |
3 files changed, 184 insertions, 68 deletions
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 14cf9f09fc..745f1d9116 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -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 diff --git a/src/nvim/search.c b/src/nvim/search.c index a5b7d8f5ee..a021466446 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -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 don’t have an intrinsic meaning. The -/// score range isn’t 0 to 100. It’s roughly [-50, 50]. Longer words have a +/// score range isn’t 0 to 100. It’s 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,11 +4908,13 @@ 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, - 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, - int *const recursionCount) +/// 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, + int *const recursionCount) FUNC_ATTR_NONNULL_ARG(1, 2, 4, 5, 8, 11) FUNC_ATTR_WARN_UNUSED_RESULT { // Recursion params @@ -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++) { - tv_list_append_number(ptrs[i].lmatchpos, matches[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); } diff --git a/src/nvim/testdir/test_matchfuzzy.vim b/src/nvim/testdir/test_matchfuzzy.vim index 293f7387b8..28367b878d 100644 --- a/src/nvim/testdir/test_matchfuzzy.vim +++ b/src/nvim/testdir/test_matchfuzzy.vim @@ -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 + call assert_equal(['onetwo', 'one_two', 'one two'], ['onetwo', 'one_two', 'one two']->matchfuzzy('onetwo')) " 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ㄟㄠ']), + 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) |