patch 9.1.1214: matchfuzzy() can be improved for camel case matches Commit: https://github.com/vim/vim/commit/28e40a7b55ce471656cccc2260c11a29d5da447e Author: glepnir <glephun...@gmail.com> Date: Sun Mar 16 21:24:22 2025 +0100
patch 9.1.1214: matchfuzzy() can be improved for camel case matches Problem: When searching for "Cur", CamelCase matches like "lCursor" score higher than exact prefix matches like Cursor, which is counter-intuitive (Maxim Kim). Solution: Add a 'camelcase' option to matchfuzzy() that lets users disable CamelCase bonuses when needed, making prefix matches rank higher. (glepnir) fixes: #16504 closes: #16797 Signed-off-by: glepnir <glephun...@gmail.com> Signed-off-by: Christian Brabandt <c...@256bit.org> diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index b7df7c561..3456093d2 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -1,4 +1,4 @@ -*builtin.txt* For Vim version 9.1. Last change: 2025 Mar 11 +*builtin.txt* For Vim version 9.1. Last change: 2025 Mar 16 VIM REFERENCE MANUAL by Bram Moolenaar @@ -7275,6 +7275,9 @@ matchfuzzy({list}, {str} [, {dict}]) *matchfuzzy()* given sequence. limit Maximum number of matches in {list} to be returned. Zero means no limit. + camelcase Use enhanced camel case scoring making results + better suited for completion related to + programming languages. Default is v:true If {list} is a list of dictionaries, then the optional {dict} argument supports the following additional items: diff --git a/src/proto/search.pro b/src/proto/search.pro index e9be4533a..1927738d7 100644 --- a/src/proto/search.pro +++ b/src/proto/search.pro @@ -37,7 +37,7 @@ void find_pattern_in_path(char_u *ptr, int dir, int len, int whole, int skip_com spat_T *get_spat(int idx); int get_spat_last_idx(void); void f_searchcount(typval_T *argvars, typval_T *rettv); -int fuzzy_match(char_u *str, char_u *pat_arg, int matchseq, int *outScore, int_u *matches, int maxMatches); +int fuzzy_match(char_u *str, char_u *pat_arg, int matchseq, int *outScore, int_u *matches, int maxMatches, int camelcase); void f_matchfuzzy(typval_T *argvars, typval_T *rettv); void f_matchfuzzypos(typval_T *argvars, typval_T *rettv); int fuzzy_match_str(char_u *str, char_u *pat); diff --git a/src/quickfix.c b/src/quickfix.c index 1008dd5ab..e498d5eba 100644 --- a/src/quickfix.c +++ b/src/quickfix.c @@ -6246,7 +6246,8 @@ vgr_match_buflines( // Fuzzy string match CLEAR_FIELD(matches); - while (fuzzy_match(str + col, spat, FALSE, &score, matches, sz) > 0) + while (fuzzy_match(str + col, spat, FALSE, &score, + matches, sz, TRUE) > 0) { // Pass the buffer number so that it gets used even for a // dummy buffer, unless duplicate_name is set, then the diff --git a/src/search.c b/src/search.c index 064ed3e1e..c7ae4f8e2 100644 --- a/src/search.c +++ b/src/search.c @@ -42,11 +42,11 @@ static void find_mps_values(int *initc, int *findc, int *backwards, int switchit static int is_zero_width(char_u *pattern, size_t patternlen, int move, pos_T *cur, int direction); 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); static void update_search_stat(int dirc, pos_T *pos, pos_T *cursor_pos, searchstat_T *stat, int recompute, int maxcount, long timeout); -static int fuzzy_match_compute_score(char_u *fuzpat, char_u *str, int strSz, int_u *matches, int numMatches); -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); +static int fuzzy_match_compute_score(char_u *fuzpat, char_u *str, int strSz, int_u *matches, int numMatches, int camelcase); +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, int camelcase); #if defined(FEAT_EVAL) || defined(FEAT_PROTO) static int fuzzy_match_item_compare(const void *s1, const void *s2); -static void fuzzy_match_in_list(list_T *l, char_u *str, int matchseq, char_u *key, callback_T *item_cb, int retmatchpos, list_T *fmatchlist, long max_matches); +static void fuzzy_match_in_list(list_T *l, char_u *str, int matchseq, char_u *key, callback_T *item_cb, int retmatchpos, list_T *fmatchlist, long max_matches, int camelcase); static void do_fuzzymatch(typval_T *argvars, typval_T *rettv, int retmatchpos); #endif static int fuzzy_match_str_compare(const void *s1, const void *s2); @@ -4388,7 +4388,8 @@ fuzzy_match_compute_score( char_u *str, int strSz, int_u *matches, - int numMatches) + int numMatches, + int camelcase) { int score; int penalty; @@ -4461,7 +4462,7 @@ fuzzy_match_compute_score( } // Enhanced camel case scoring - if (vim_islower(neighbor) && vim_isupper(curr)) + if (camelcase && vim_islower(neighbor) && vim_isupper(curr)) { score += CAMEL_BONUS * 2; // Double the camel case bonus is_camel = TRUE; @@ -4544,7 +4545,8 @@ fuzzy_match_recursive( int_u *matches, int maxMatches, int nextMatch, - int *recursionCount) + int *recursionCount, + int camelcase) { // Recursion params int recursiveMatch = FALSE; @@ -4596,7 +4598,7 @@ fuzzy_match_recursive( &recursiveScore, strBegin, strLen, matches, recursiveMatches, ARRAY_LENGTH(recursiveMatches), - nextMatch, recursionCount)) + nextMatch, recursionCount, camelcase)) { // Pick best recursive score if (!recursiveMatch || recursiveScore > bestRecursiveScore) @@ -4628,7 +4630,7 @@ fuzzy_match_recursive( // Calculate score if (matched) *outScore = fuzzy_match_compute_score(fuzpat, strBegin, strLen, matches, - nextMatch); + nextMatch, camelcase); // Return best result if (recursiveMatch && (!matched || bestRecursiveScore > *outScore)) @@ -4666,7 +4668,8 @@ fuzzy_match( int matchseq, int *outScore, int_u *matches, - int maxMatches) + int maxMatches, + int camelcase) { int recursionCount = 0; int len = MB_CHARLEN(str); @@ -4714,7 +4717,7 @@ fuzzy_match( recursionCount = 0; matchCount = fuzzy_match_recursive(pat, str, 0, &score, str, len, NULL, matches + numMatches, maxMatches - numMatches, - 0, &recursionCount); + 0, &recursionCount, camelcase); if (matchCount == 0) { numMatches = 0; @@ -4775,7 +4778,8 @@ fuzzy_match_in_list( callback_T *item_cb, int retmatchpos, list_T *fmatchlist, - long max_matches) + long max_matches, + int camelcase) { long len; fuzzyItem_T *items; @@ -4836,7 +4840,7 @@ fuzzy_match_in_list( if (itemstr != NULL && fuzzy_match(itemstr, str, matchseq, &score, matches, - MAX_FUZZY_MATCHES)) + MAX_FUZZY_MATCHES, camelcase)) { items[match_count].idx = match_count; items[match_count].item = li; @@ -4955,6 +4959,7 @@ do_fuzzymatch(typval_T *argvars, typval_T *rettv, int retmatchpos) int ret; int matchseq = FALSE; long max_matches = 0; + int camelcase = TRUE; if (in_vim9script() && (check_for_list_arg(argvars, 0) == FAIL @@ -5020,6 +5025,16 @@ do_fuzzymatch(typval_T *argvars, typval_T *rettv, int retmatchpos) max_matches = (long)tv_get_number_chk(&di->di_tv, NULL); } + if ((di = dict_find(d, (char_u *)"camelcase", -1)) != NULL) + { + if (di->di_tv.v_type != VAR_BOOL) + { + semsg(_(e_invalid_argument_str), "camelcase"); + return; + } + camelcase = tv_get_bool_chk(&di->di_tv, NULL); + } + if (dict_has_key(d, "matchseq")) matchseq = TRUE; } @@ -5063,7 +5078,8 @@ do_fuzzymatch(typval_T *argvars, typval_T *rettv, int retmatchpos) } fuzzy_match_in_list(argvars[0].vval.v_list, tv_get_string(&argvars[1]), - matchseq, key, &cb, retmatchpos, rettv->vval.v_list, max_matches); + matchseq, key, &cb, retmatchpos, rettv->vval.v_list, max_matches, + camelcase); done: free_callback(&cb); @@ -5166,7 +5182,7 @@ fuzzy_match_str(char_u *str, char_u *pat) return 0; fuzzy_match(str, pat, TRUE, &score, matchpos, - sizeof(matchpos) / sizeof(matchpos[0])); + sizeof(matchpos) / sizeof(matchpos[0]), TRUE); return score; } @@ -5193,7 +5209,7 @@ fuzzy_match_str_with_pos(char_u *str UNUSED, char_u *pat UNUSED) return NULL; ga_init2(match_positions, sizeof(int_u), 10); - if (!fuzzy_match(str, pat, FALSE, &score, matches, MAX_FUZZY_MATCHES) + if (!fuzzy_match(str, pat, FALSE, &score, matches, MAX_FUZZY_MATCHES, TRUE) || score == 0) { ga_clear(match_positions); diff --git a/src/testdir/test_matchfuzzy.vim b/src/testdir/test_matchfuzzy.vim index cba08446b..5280f2fa3 100644 --- a/src/testdir/test_matchfuzzy.vim +++ b/src/testdir/test_matchfuzzy.vim @@ -87,6 +87,10 @@ func Test_matchfuzzy() let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}] call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : 'name'})", 'E730:') + " camcelcase + call assert_equal(['Cursor', 'CurSearch', 'CursorLine', 'lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor'], matchfuzzy(['Cursor', 'lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor', 'CurSearch', 'CursorLine'], 'Cur', {"camelcase": v:false})) + call assert_equal(['lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor', 'Cursor', 'CurSearch', 'CursorLine'], matchfuzzy(['Cursor', 'lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor', 'CurSearch', 'CursorLine'], 'Cur')) + " Test in latin1 encoding let save_enc = &encoding set encoding=latin1 @@ -168,6 +172,15 @@ func Test_matchfuzzypos() let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}] call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : 'name'})", 'E730:') + + "camelcase + call assert_equal([['lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor', 'Cursor', 'CurSearch', 'CursorLine'], [[1, 2, 3], [2, 3, 4], [2, 3, 4], [6, 7, 8], [0, 1, 2], [0, 1, 2], [0, 1, 2]], [318, 311, 308, 303, 267, 264, 263]], + \ matchfuzzypos(['Cursor', 'lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor', 'CurSearch', 'CursorLine'], 'Cur')) + + call assert_equal([['Cursor', 'CurSearch', 'CursorLine', 'lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor'], [[0, 1, 2], [0, 1, 2], [0, 1, 2], [1, 2, 3], [2, 3, 4], [2, 3, 4], [6, 7, 8]], [267, 264, 263, 246, 239, 236, 231]], + \ matchfuzzypos(['Cursor', 'lCursor', 'shCurlyIn', 'shCurlyError', 'TracesCursor', 'CurSearch', 'CursorLine'], 'Cur', {"camelcase": v:false})) + call assert_equal([['things', 'sThings', 'thisThings'], [[0, 1, 2, 3], [1, 2, 3, 4], [0, 1, 2, 7]], [333, 287, 279]], + \ matchfuzzypos(['things','sThings', 'thisThings'], 'thin', {'camelcase': v:false})) endfunc " Test for matchfuzzy() with multibyte characters diff --git a/src/version.c b/src/version.c index b69d34d26..b633dc847 100644 --- a/src/version.c +++ b/src/version.c @@ -704,6 +704,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1214, /**/ 1213, /**/ -- -- You received this message from the "vim_dev" maillist. Do not top-post! Type your reply below the text you are replying to. For more information, visit http://www.vim.org/maillist.php --- You received this message because you are subscribed to the Google Groups "vim_dev" group. To unsubscribe from this group and stop receiving emails from it, send an email to vim_dev+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1ttucY-00EdJQ-Do%40256bit.org.