Skip to content

Commit 9dfc7e5

Browse files
glepnirchrisbra
authored andcommitted
patch 9.1.1046: fuzzymatching doesn't prefer matching camelcase
Problem: fuzzymatching doesn't prefer matching camelcase (Tomasz N) Solution: Add extra score when case matches (glepnir) fixes: #16434 closes: #16439 Signed-off-by: glepnir <[email protected]> Signed-off-by: Christian Brabandt <[email protected]>
1 parent c04334c commit 9dfc7e5

3 files changed

Lines changed: 102 additions & 30 deletions

File tree

src/search.c

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ static void find_mps_values(int *initc, int *findc, int *backwards, int switchit
4242
static int is_zero_width(char_u *pattern, size_t patternlen, int move, pos_T *cur, int direction);
4343
static void cmdline_search_stat(int dirc, pos_T *pos, pos_T *cursor_pos, int show_top_bot_msg, char_u *msgbuf, size_t msgbuflen, int recompute, int maxcount, long timeout);
4444
static void update_search_stat(int dirc, pos_T *pos, pos_T *cursor_pos, searchstat_T *stat, int recompute, int maxcount, long timeout);
45-
static int fuzzy_match_compute_score(char_u *str, int strSz, int_u *matches, int numMatches);
45+
static int fuzzy_match_compute_score(char_u *fuzpat, char_u *str, int strSz, int_u *matches, int numMatches);
4646
static int fuzzy_match_recursive(char_u *fuzpat, char_u *str, int_u strIdx, int *outScore, char_u *strBegin, int strLen, int_u *srcMatches, int_u *matches, int maxMatches, int nextMatch, int *recursionCount);
4747
#if defined(FEAT_EVAL) || defined(FEAT_PROTO)
4848
static int fuzzy_match_item_compare(const void *s1, const void *s2);
@@ -4355,6 +4355,10 @@ typedef struct
43554355
#define CAMEL_BONUS 30
43564356
// bonus if the first letter is matched
43574357
#define FIRST_LETTER_BONUS 15
4358+
// bonus if exact match
4359+
#define EXACT_MATCH_BONUS 100
4360+
// bonus if case match when no ignorecase
4361+
#define CASE_MATCH_BONUS 25
43584362
// penalty applied for every letter in str before the first match
43594363
#define LEADING_LETTER_PENALTY (-5)
43604364
// maximum penalty for leading letters
@@ -4374,6 +4378,7 @@ typedef struct
43744378
*/
43754379
static int
43764380
fuzzy_match_compute_score(
4381+
char_u *fuzpat,
43774382
char_u *str,
43784383
int strSz,
43794384
int_u *matches,
@@ -4386,6 +4391,11 @@ fuzzy_match_compute_score(
43864391
char_u *p = str;
43874392
int_u sidx = 0;
43884393
int is_exact_match = TRUE;
4394+
char_u *orig_fuzpat = fuzpat - numMatches;
4395+
char_u *curpat = orig_fuzpat;
4396+
int pat_idx = 0;
4397+
// Track consecutive camel case matches
4398+
int consecutive_camel = 0;
43894399

43904400
// Initialize score
43914401
score = 100;
@@ -4404,6 +4414,8 @@ fuzzy_match_compute_score(
44044414
for (i = 0; i < numMatches; ++i)
44054415
{
44064416
int_u currIdx = matches[i];
4417+
int curr;
4418+
int is_camel = FALSE;
44074419

44084420
if (i > 0)
44094421
{
@@ -4413,15 +4425,18 @@ fuzzy_match_compute_score(
44134425
if (currIdx == (prevIdx + 1))
44144426
score += SEQUENTIAL_BONUS;
44154427
else
4428+
{
44164429
score += GAP_PENALTY * (currIdx - prevIdx);
4430+
// Reset consecutive camel count on gap
4431+
consecutive_camel = 0;
4432+
}
44174433
}
44184434

44194435
// Check for bonuses based on neighbor character value
44204436
if (currIdx > 0)
44214437
{
44224438
// Camel case
4423-
int neighbor = ' ';
4424-
int curr;
4439+
int neighbor = ' ';
44254440

44264441
if (has_mbyte)
44274442
{
@@ -4439,8 +4454,18 @@ fuzzy_match_compute_score(
44394454
curr = str[currIdx];
44404455
}
44414456

4457+
// Enhanced camel case scoring
44424458
if (vim_islower(neighbor) && vim_isupper(curr))
4443-
score += CAMEL_BONUS;
4459+
{
4460+
score += CAMEL_BONUS * 2; // Double the camel case bonus
4461+
is_camel = TRUE;
4462+
consecutive_camel++;
4463+
// Additional bonus for consecutive camel
4464+
if (consecutive_camel > 1)
4465+
score += CAMEL_BONUS;
4466+
}
4467+
else
4468+
consecutive_camel = 0;
44444469

44454470
// Bonus if the match follows a separator character
44464471
if (neighbor == '/' || neighbor == '\\')
@@ -4452,14 +4477,47 @@ fuzzy_match_compute_score(
44524477
{
44534478
// First letter
44544479
score += FIRST_LETTER_BONUS;
4480+
curr = has_mbyte ? (*mb_ptr2char)(p) : str[currIdx];
44554481
}
4482+
4483+
// Case matching bonus
4484+
if (vim_isalpha(curr))
4485+
{
4486+
while (pat_idx < i && *curpat)
4487+
{
4488+
if (has_mbyte)
4489+
MB_PTR_ADV(curpat);
4490+
else
4491+
curpat++;
4492+
pat_idx++;
4493+
}
4494+
4495+
if (has_mbyte)
4496+
{
4497+
if (curr == (*mb_ptr2char)(curpat))
4498+
{
4499+
score += CASE_MATCH_BONUS;
4500+
// Extra bonus for exact case match in camel
4501+
if (is_camel)
4502+
score += CASE_MATCH_BONUS / 2;
4503+
}
4504+
}
4505+
else if (curr == *curpat)
4506+
{
4507+
score += CASE_MATCH_BONUS;
4508+
if (is_camel)
4509+
score += CASE_MATCH_BONUS / 2;
4510+
}
4511+
}
4512+
44564513
// Check exact match condition
44574514
if (currIdx != (int_u)i)
44584515
is_exact_match = FALSE;
44594516
}
4517+
44604518
// Boost score for exact matches
44614519
if (is_exact_match && numMatches == strSz)
4462-
score += 100;
4520+
score += EXACT_MATCH_BONUS;
44634521

44644522
return score;
44654523
}
@@ -4563,7 +4621,7 @@ fuzzy_match_recursive(
45634621

45644622
// Calculate score
45654623
if (matched)
4566-
*outScore = fuzzy_match_compute_score(strBegin, strLen, matches,
4624+
*outScore = fuzzy_match_compute_score(fuzpat, strBegin, strLen, matches,
45674625
nextMatch);
45684626

45694627
// Return best result

src/testdir/test_matchfuzzy.vim

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,15 @@ endfunc
9696

9797
" Test for the matchfuzzypos() function
9898
func Test_matchfuzzypos()
99-
call assert_equal([['curl', 'world'], [[2,3], [2,3]], [128, 127]], matchfuzzypos(['world', 'curl'], 'rl'))
100-
call assert_equal([['curl', 'world'], [[2,3], [2,3]], [128, 127]], matchfuzzypos(['world', 'one', 'curl'], 'rl'))
99+
call assert_equal([['curl', 'world'], [[2,3], [2,3]], [178, 177]], matchfuzzypos(['world', 'curl'], 'rl'))
100+
call assert_equal([['curl', 'world'], [[2,3], [2,3]], [178, 177]], matchfuzzypos(['world', 'one', 'curl'], 'rl'))
101101
call assert_equal([['hello', 'hello world hello world'],
102-
\ [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [375, 257]],
102+
\ [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [500, 382]],
103103
\ matchfuzzypos(['hello world hello world', 'hello', 'world'], 'hello'))
104-
call assert_equal([['aaaaaaa'], [[0, 1, 2]], [191]], matchfuzzypos(['aaaaaaa'], 'aaa'))
105-
call assert_equal([['a b'], [[0, 3]], [219]], matchfuzzypos(['a b'], 'a b'))
106-
call assert_equal([['a b'], [[0, 3]], [219]], matchfuzzypos(['a b'], 'a b'))
107-
call assert_equal([['a b'], [[0]], [112]], matchfuzzypos(['a b'], ' a '))
104+
call assert_equal([['aaaaaaa'], [[0, 1, 2]], [266]], matchfuzzypos(['aaaaaaa'], 'aaa'))
105+
call assert_equal([['a b'], [[0, 3]], [269]], matchfuzzypos(['a b'], 'a b'))
106+
call assert_equal([['a b'], [[0, 3]], [269]], matchfuzzypos(['a b'], 'a b'))
107+
call assert_equal([['a b'], [[0]], [137]], matchfuzzypos(['a b'], ' a '))
108108
call assert_equal([[], [], []], matchfuzzypos(['a b'], ' '))
109109
call assert_equal([[], [], []], matchfuzzypos(['world', 'curl'], 'ab'))
110110
let x = matchfuzzypos([repeat('a', 256)], repeat('a', 256))
@@ -113,33 +113,33 @@ func Test_matchfuzzypos()
113113
call assert_equal([[], [], []], matchfuzzypos([], 'abc'))
114114

115115
" match in a long string
116-
call assert_equal([[repeat('x', 300) .. 'abc'], [[300, 301, 302]], [-135]],
116+
call assert_equal([[repeat('x', 300) .. 'abc'], [[300, 301, 302]], [-60]],
117117
\ matchfuzzypos([repeat('x', 300) .. 'abc'], 'abc'))
118118

119119
" preference for camel case match
120-
call assert_equal([['xabcxxaBc'], [[6, 7, 8]], [189]], matchfuzzypos(['xabcxxaBc'], 'abc'))
120+
call assert_equal([['xabcxxaBc'], [[6, 7, 8]], [269]], matchfuzzypos(['xabcxxaBc'], 'abc'))
121121
" preference for match after a separator (_ or space)
122-
call assert_equal([['xabx_ab'], [[5, 6]], [145]], matchfuzzypos(['xabx_ab'], 'ab'))
122+
call assert_equal([['xabx_ab'], [[5, 6]], [195]], matchfuzzypos(['xabx_ab'], 'ab'))
123123
" preference for leading letter match
124-
call assert_equal([['abcxabc'], [[0, 1]], [150]], matchfuzzypos(['abcxabc'], 'ab'))
124+
call assert_equal([['abcxabc'], [[0, 1]], [200]], matchfuzzypos(['abcxabc'], 'ab'))
125125
" preference for sequential match
126-
call assert_equal([['aobncedone'], [[7, 8, 9]], [158]], matchfuzzypos(['aobncedone'], 'one'))
126+
call assert_equal([['aobncedone'], [[7, 8, 9]], [233]], matchfuzzypos(['aobncedone'], 'one'))
127127
" best recursive match
128-
call assert_equal([['xoone'], [[2, 3, 4]], [168]], matchfuzzypos(['xoone'], 'one'))
128+
call assert_equal([['xoone'], [[2, 3, 4]], [243]], matchfuzzypos(['xoone'], 'one'))
129129

130130
" match multiple words (separated by space)
131-
call assert_equal([['foo bar baz'], [[8, 9, 10, 0, 1, 2]], [369]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('baz foo'))
131+
call assert_equal([['foo bar baz'], [[8, 9, 10, 0, 1, 2]], [519]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('baz foo'))
132132
call assert_equal([[], [], []], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('baz foo', {'matchseq': 1}))
133-
call assert_equal([['foo bar baz'], [[0, 1, 2, 8, 9, 10]], [369]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('foo baz'))
134-
call assert_equal([['foo bar baz'], [[0, 1, 2, 3, 4, 5, 10]], [326]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('foo baz', {'matchseq': 1}))
133+
call assert_equal([['foo bar baz'], [[0, 1, 2, 8, 9, 10]], [519]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('foo baz'))
134+
call assert_equal([['foo bar baz'], [[0, 1, 2, 3, 4, 5, 10]], [476]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('foo baz', {'matchseq': 1}))
135135
call assert_equal([[], [], []], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('one two'))
136136
call assert_equal([[], [], []], ['foo bar']->matchfuzzypos(" \t "))
137-
call assert_equal([['grace'], [[1, 2, 3, 4, 2, 3, 4, 0, 1, 2, 3, 4]], [757]], ['grace']->matchfuzzypos('race ace grace'))
137+
call assert_equal([['grace'], [[1, 2, 3, 4, 2, 3, 4, 0, 1, 2, 3, 4]], [1057]], ['grace']->matchfuzzypos('race ace grace'))
138138

139139
let l = [{'id' : 5, 'val' : 'crayon'}, {'id' : 6, 'val' : 'camera'}]
140-
call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [192]],
140+
call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [267]],
141141
\ matchfuzzypos(l, 'cam', {'text_cb' : {v -> v.val}}))
142-
call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [192]],
142+
call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [267]],
143143
\ matchfuzzypos(l, 'cam', {'key' : 'val'}))
144144
call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'text_cb' : {v -> v.val}}))
145145
call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'key' : 'val'}))
@@ -154,6 +154,18 @@ func Test_matchfuzzypos()
154154
call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : test_null_string()})", 'E475:')
155155
call assert_fails("let x = matchfuzzypos(l, 'foo', {'text_cb' : test_null_function()})", 'E475:')
156156

157+
" case match
158+
call assert_equal([['Match', 'match'], [[0, 1], [0, 1]], [202, 177]], matchfuzzypos(['match', 'Match'], 'Ma'))
159+
call assert_equal([['match', 'Match'], [[0, 1], [0, 1]], [202, 177]], matchfuzzypos(['Match', 'match'], 'ma'))
160+
" CamelCase has high weight even case match
161+
call assert_equal(['MyTestCase', 'mytestcase'], matchfuzzy(['mytestcase', 'MyTestCase'], 'mtc'))
162+
call assert_equal(['MyTestCase', 'mytestcase'], matchfuzzy(['MyTestCase', 'mytestcase'], 'mtc'))
163+
call assert_equal(['MyTest', 'Mytest', 'mytest', ],matchfuzzy(['Mytest', 'mytest', 'MyTest'], 'MyT'))
164+
call assert_equal(['CamelCaseMatchIngAlg', 'camelCaseMatchingAlg', 'camelcasematchingalg'],
165+
\ matchfuzzy(['CamelCaseMatchIngAlg', 'camelcasematchingalg', 'camelCaseMatchingAlg'], 'CamelCase'))
166+
call assert_equal(['CamelCaseMatchIngAlg', 'camelCaseMatchingAlg', 'camelcasematchingalg'],
167+
\ matchfuzzy(['CamelCaseMatchIngAlg', 'camelcasematchingalg', 'camelCaseMatchingAlg'], 'CamelcaseM'))
168+
157169
let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}]
158170
call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : 'name'})", 'E730:')
159171
endfunc
@@ -204,12 +216,12 @@ func Test_matchfuzzypos_mbyte()
204216
call assert_equal([['ンヹㄇヺヴ'], [[1, 3]], [88]], matchfuzzypos(['ンヹㄇヺヴ'], 'ヹヺ'))
205217
" reverse the order of characters
206218
call assert_equal([[], [], []], matchfuzzypos(['ンヹㄇヺヴ'], 'ヺヹ'))
207-
call assert_equal([['αβΩxxx', 'xαxβxΩx'], [[0, 1, 2], [1, 3, 5]], [222, 113]],
219+
call assert_equal([['αβΩxxx', 'xαxβxΩx'], [[0, 1, 2], [1, 3, 5]], [252, 143]],
208220
\ matchfuzzypos(['αβΩxxx', 'xαxβxΩx'], 'αβΩ'))
209221
call assert_equal([['ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ', 'πbπ'],
210-
\ [[0, 1], [0, 1], [0, 1], [0, 2]], [151, 148, 145, 110]],
222+
\ [[0, 1], [0, 1], [0, 1], [0, 2]], [176, 173, 170, 135]],
211223
\ matchfuzzypos(['πbπ', 'ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ'], 'ππ'))
212-
call assert_equal([['ααααααα'], [[0, 1, 2]], [191]],
224+
call assert_equal([['ααααααα'], [[0, 1, 2]], [216]],
213225
\ matchfuzzypos(['ααααααα'], 'ααα'))
214226

215227
call assert_equal([[], [], []], matchfuzzypos(['ンヹㄇ', 'ŗŝţ'], 'fffifl'))
@@ -222,10 +234,10 @@ func Test_matchfuzzypos_mbyte()
222234
call assert_equal([[], [], []], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzypos('파란 하늘'))
223235

224236
" match in a long string
225-
call assert_equal([[repeat('', 300) .. 'ẼẼẼ'], [[300, 301, 302]], [-135]],
237+
call assert_equal([[repeat('', 300) .. 'ẼẼẼ'], [[300, 301, 302]], [-110]],
226238
\ matchfuzzypos([repeat('', 300) .. 'ẼẼẼ'], 'ẼẼẼ'))
227239
" preference for camel case match
228-
call assert_equal([['xѳѵҁxxѳѴҁ'], [[6, 7, 8]], [189]], matchfuzzypos(['xѳѵҁxxѳѴҁ'], 'ѳѵҁ'))
240+
call assert_equal([['xѳѵҁxxѳѴҁ'], [[6, 7, 8]], [219]], matchfuzzypos(['xѳѵҁxxѳѴҁ'], 'ѳѵҁ'))
229241
" preference for match after a separator (_ or space)
230242
call assert_equal([['xちだx_ちだ'], [[5, 6]], [145]], matchfuzzypos(['xちだx_ちだ'], 'ちだ'))
231243
" preference for leading letter match

src/version.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,8 @@ static char *(features[]) =
704704

705705
static int included_patches[] =
706706
{ /* Add new patch number below this line */
707+
/**/
708+
1046,
707709
/**/
708710
1045,
709711
/**/

0 commit comments

Comments
 (0)