patch 9.1.1520: completion: search completion doesn't handle 'smartcase' well

Problem:  When using `/` or `?` in command-line mode with 'ignorecase' and
          'smartcase' enabled, the completion menu could show items that
          don't actually match any text in the buffer due to case mismatches

Solution: Instead of validating menu items only against the user-typed
          pattern, the new logic also checks whether the completed item
          matches actual buffer content. If needed, it retries the match
          using a lowercased version of the candidate, respecting
          smartcase semantics.

closes: #17665

Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Girish Palya
2025-07-07 19:42:10 +02:00
committed by Christian Brabandt
parent faed074ab7
commit af22007784
3 changed files with 107 additions and 30 deletions

View File

@ -4686,6 +4686,82 @@ copy_substring_from_pos(pos_T *start, pos_T *end, char_u **match,
return OK; return OK;
} }
/*
* Returns TRUE if the given string `str` matches the regex pattern `pat`.
* Honors the 'ignorecase' (p_ic) and 'smartcase' (p_scs) settings to determine
* case sensitivity.
*/
static int
is_regex_match(char_u *pat, char_u *str)
{
regmatch_T regmatch;
int result;
regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
if (regmatch.regprog == NULL)
return FALSE;
regmatch.rm_ic = p_ic;
if (p_ic && p_scs)
regmatch.rm_ic = !pat_has_uppercase(pat);
result = vim_regexec_nl(&regmatch, str, (colnr_T)0);
vim_regfree(regmatch.regprog);
return result;
}
/*
* Constructs a new match string by appending text from the buffer (starting at
* end_match_pos) to the given pattern `pat`. The result is a concatenation of
* `pat` and the word following end_match_pos.
* If 'lowercase' is TRUE, the appended text is converted to lowercase before
* being combined. Returns the newly allocated match string, or NULL on failure.
*/
static char_u *
concat_pattern_with_buffer_match(
char_u *pat,
int pat_len,
pos_T *end_match_pos,
int lowercase UNUSED)
{
char_u *line = ml_get(end_match_pos->lnum);
char_u *word_end = find_word_end(line + end_match_pos->col);
int match_len = (int)(word_end - (line + end_match_pos->col));
char_u *match = alloc(match_len + pat_len + 1); // +1 for NUL
if (match == NULL)
return NULL;
mch_memmove(match, pat, pat_len);
if (match_len > 0)
{
#if defined(FEAT_EVAL) || defined(FEAT_SPELL) || defined(PROTO)
if (lowercase)
{
char_u *mword = vim_strnsave(line + end_match_pos->col,
match_len);
if (mword == NULL)
goto cleanup;
char_u *lower = strlow_save(mword);
vim_free(mword);
if (lower == NULL)
goto cleanup;
mch_memmove(match + pat_len, lower, match_len);
vim_free(lower);
}
else
#endif
mch_memmove(match + pat_len, line + end_match_pos->col, match_len);
}
match[pat_len + match_len] = NUL;
return match;
#if defined(FEAT_EVAL) || defined(FEAT_SPELL) || defined(PROTO)
cleanup:
vim_free(match);
return NULL;
#endif
}
/* /*
* Search for strings matching "pat" in the specified range and return them. * Search for strings matching "pat" in the specified range and return them.
* Returns OK on success, FAIL otherwise. * Returns OK on success, FAIL otherwise.
@ -4701,12 +4777,11 @@ expand_pattern_in_buf(
garray_T ga; garray_T ga;
int found_new_match; int found_new_match;
int looped_around = FALSE; int looped_around = FALSE;
int pat_len, match_len; int pat_len;
int has_range = FALSE; int has_range = FALSE;
int compl_started = FALSE; int compl_started = FALSE;
int search_flags; int search_flags;
char_u *match, *line, *word_end; char_u *match, *full_match;
regmatch_T regmatch;
#ifdef FEAT_SEARCH_EXTRA #ifdef FEAT_SEARCH_EXTRA
has_range = search_first_line != 0; has_range = search_first_line != 0;
@ -4731,11 +4806,6 @@ expand_pattern_in_buf(
search_flags = SEARCH_OPT | SEARCH_NOOF | SEARCH_PEEK | SEARCH_NFMSG search_flags = SEARCH_OPT | SEARCH_NOOF | SEARCH_PEEK | SEARCH_NFMSG
| (has_range ? SEARCH_START : 0); | (has_range ? SEARCH_START : 0);
regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
if (regmatch.regprog == NULL)
return FAIL;
regmatch.rm_ic = p_ic;
ga_init2(&ga, sizeof(char_u *), 10); // Use growable array of char_u* ga_init2(&ga, sizeof(char_u *), 10); // Use growable array of char_u*
for (;;) for (;;)
@ -4796,30 +4866,30 @@ expand_pattern_in_buf(
} }
// Extract the matching text prepended to completed word // Extract the matching text prepended to completed word
if (!copy_substring_from_pos(&cur_match_pos, &end_match_pos, &match, if (!copy_substring_from_pos(&cur_match_pos, &end_match_pos, &full_match,
&word_end_pos)) &word_end_pos))
break; break;
// Verify that the constructed match actually matches the pattern with // Construct a new match from completed word appended to pattern itself
// correct case sensitivity match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos,
if (!vim_regexec_nl(&regmatch, match, (colnr_T)0)) FALSE);
// The regex pattern may include '\C' or '\c'. First, try matching the
// buffer word as-is. If it doesn't match, try again with the lowercase
// version of the word to handle smartcase behavior.
if (match == NULL || !is_regex_match(match, full_match))
{ {
vim_free(match); vim_free(match);
continue; match = concat_pattern_with_buffer_match(pat, pat_len,
&end_match_pos, TRUE);
if (match == NULL || !is_regex_match(match, full_match))
{
vim_free(match);
vim_free(full_match);
continue;
}
} }
vim_free(match); vim_free(full_match);
// Construct a new match from completed word appended to pattern itself
line = ml_get(end_match_pos.lnum);
word_end = find_word_end(line + end_match_pos.col); // col starts from 0
match_len = (int)(word_end - (line + end_match_pos.col));
match = alloc(match_len + pat_len + 1); // +1 for NUL
if (match == NULL)
goto cleanup;
mch_memmove(match, pat, pat_len);
if (match_len > 0)
mch_memmove(match + pat_len, line + end_match_pos.col, match_len);
match[pat_len + match_len] = NUL;
// Include this match if it is not a duplicate // Include this match if it is not a duplicate
for (int i = 0; i < ga.ga_len; ++i) for (int i = 0; i < ga.ga_len; ++i)
@ -4842,14 +4912,11 @@ expand_pattern_in_buf(
cur_match_pos = word_end_pos; cur_match_pos = word_end_pos;
} }
vim_regfree(regmatch.regprog);
*matches = (char_u **)ga.ga_data; *matches = (char_u **)ga.ga_data;
*numMatches = ga.ga_len; *numMatches = ga.ga_len;
return OK; return OK;
cleanup: cleanup:
vim_regfree(regmatch.regprog);
ga_clear_strings(&ga); ga_clear_strings(&ga);
return FAIL; return FAIL;
} }

View File

@ -4481,6 +4481,8 @@ func Test_search_complete()
call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
call feedkeys("gg/FO\<tab>\<f9>", 'tx') call feedkeys("gg/FO\<tab>\<f9>", 'tx')
call assert_equal({}, g:compl_info) call assert_equal({}, g:compl_info)
call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx')
call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches)
set ignorecase set ignorecase
call feedkeys("gg/f\<tab>\<f9>", 'tx') call feedkeys("gg/f\<tab>\<f9>", 'tx')
call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches) call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches)
@ -4488,13 +4490,19 @@ func Test_search_complete()
call assert_equal(['Foobar', 'FooBAr', 'FooBARR'], g:compl_info.matches) call assert_equal(['Foobar', 'FooBAr', 'FooBARR'], g:compl_info.matches)
call feedkeys("gg/FO\<tab>\<f9>", 'tx') call feedkeys("gg/FO\<tab>\<f9>", 'tx')
call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches) call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches)
call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx')
call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches)
set smartcase set smartcase
call feedkeys("gg/f\<tab>\<f9>", 'tx') call feedkeys("gg/f\<tab>\<f9>", 'tx')
call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches) call assert_equal(['foobar', 'fooBAr', 'foobarr'], g:compl_info.matches)
call feedkeys("gg/Fo\<tab>\<f9>", 'tx') call feedkeys("gg/Fo\<tab>\<f9>", 'tx')
call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
call feedkeys("gg/FO\<tab>\<f9>", 'tx') call feedkeys("gg/FO\<tab>\<f9>", 'tx')
call assert_equal({}, g:compl_info) call assert_equal({}, g:compl_info)
call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx')
call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches)
call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx')
call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches)
bw! bw!
call test_override("char_avail", 0) call test_override("char_avail", 0)

View File

@ -719,6 +719,8 @@ static char *(features[]) =
static int included_patches[] = static int included_patches[] =
{ /* Add new patch number below this line */ { /* Add new patch number below this line */
/**/
1520,
/**/ /**/
1519, 1519,
/**/ /**/