mirror of
https://github.com/neovim/neovim.git
synced 2025-11-20 01:01:22 +00:00
vim-patch:9.1.1520: completion: search completion doesn't handle 'smartcase' well (#34840)
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: vim/vim#17665
af22007784
Co-authored-by: Girish Palya <girishji@gmail.com>
This commit is contained in:
@@ -3944,6 +3944,57 @@ static int copy_substring_from_pos(pos_T *start, pos_T *end, char **match, pos_T
|
|||||||
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 bool is_regex_match(char *pat, char *str)
|
||||||
|
{
|
||||||
|
regmatch_T regmatch;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = vim_regexec_nl(®match, 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 *concat_pattern_with_buffer_match(char *pat, int pat_len, pos_T *end_match_pos,
|
||||||
|
bool lowercase)
|
||||||
|
FUNC_ATTR_NONNULL_RET
|
||||||
|
{
|
||||||
|
char *line = ml_get(end_match_pos->lnum);
|
||||||
|
char *word_end = find_word_end(line + end_match_pos->col);
|
||||||
|
int match_len = (int)(word_end - (line + end_match_pos->col));
|
||||||
|
char *match = xmalloc((size_t)match_len + (size_t)pat_len + 1); // +1 for NUL
|
||||||
|
|
||||||
|
memmove(match, pat, (size_t)pat_len);
|
||||||
|
if (match_len > 0) {
|
||||||
|
if (lowercase) {
|
||||||
|
char *mword = xstrnsave(line + end_match_pos->col, (size_t)match_len);
|
||||||
|
char *lower = strcase_save(mword, false);
|
||||||
|
xfree(mword);
|
||||||
|
memmove(match + pat_len, lower, (size_t)match_len);
|
||||||
|
xfree(lower);
|
||||||
|
} else {
|
||||||
|
memmove(match + pat_len, line + end_match_pos->col, (size_t)match_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match[pat_len + match_len] = NUL;
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
///
|
///
|
||||||
@@ -3973,20 +4024,13 @@ static int expand_pattern_in_buf(char *pat, Direction dir, char ***matches, int
|
|||||||
int search_flags = SEARCH_OPT | SEARCH_NOOF | SEARCH_PEEK | SEARCH_NFMSG
|
int search_flags = SEARCH_OPT | SEARCH_NOOF | SEARCH_PEEK | SEARCH_NFMSG
|
||||||
| (has_range ? SEARCH_START : 0);
|
| (has_range ? SEARCH_START : 0);
|
||||||
|
|
||||||
regmatch_T regmatch;
|
|
||||||
regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
|
|
||||||
if (regmatch.regprog == NULL) {
|
|
||||||
return FAIL;
|
|
||||||
}
|
|
||||||
regmatch.rm_ic = p_ic;
|
|
||||||
|
|
||||||
garray_T ga;
|
garray_T ga;
|
||||||
ga_init(&ga, sizeof(char *), 10); // Use growable array of char *
|
ga_init(&ga, sizeof(char *), 10); // Use growable array of char *
|
||||||
|
|
||||||
pos_T end_match_pos, word_end_pos;
|
pos_T end_match_pos, word_end_pos;
|
||||||
bool looped_around = false;
|
bool looped_around = false;
|
||||||
bool compl_started = false;
|
bool compl_started = false;
|
||||||
char *match;
|
char *match, *full_match;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
emsg_off++;
|
emsg_off++;
|
||||||
@@ -4041,29 +4085,27 @@ static int expand_pattern_in_buf(char *pat, Direction dir, char ***matches, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
// correct case sensitivity
|
|
||||||
if (!vim_regexec_nl(®match, match, (colnr_T)0)) {
|
|
||||||
xfree(match);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
xfree(match);
|
|
||||||
|
|
||||||
// Construct a new match from completed word appended to pattern itself
|
// Construct a new match from completed word appended to pattern itself
|
||||||
char *line = ml_get(end_match_pos.lnum);
|
match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos, false);
|
||||||
char *word_end = find_word_end(line + end_match_pos.col); // col starts from 0
|
|
||||||
int match_len = (int)(word_end - (line + end_match_pos.col));
|
// The regex pattern may include '\C' or '\c'. First, try matching the
|
||||||
match = xmalloc((size_t)match_len + (size_t)pat_len + 1); // +1 for NUL
|
// buffer word as-is. If it doesn't match, try again with the lowercase
|
||||||
memmove(match, pat, (size_t)pat_len);
|
// version of the word to handle smartcase behavior.
|
||||||
if (match_len > 0) {
|
if (!is_regex_match(match, full_match)) {
|
||||||
memmove(match + (size_t)pat_len, line + end_match_pos.col, (size_t)match_len);
|
xfree(match);
|
||||||
|
match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos, true);
|
||||||
|
if (!is_regex_match(match, full_match)) {
|
||||||
|
xfree(match);
|
||||||
|
xfree(full_match);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
match[pat_len + match_len] = NUL;
|
xfree(full_match);
|
||||||
|
|
||||||
// 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++) {
|
||||||
@@ -4084,14 +4126,11 @@ static int expand_pattern_in_buf(char *pat, Direction dir, char ***matches, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vim_regfree(regmatch.regprog);
|
|
||||||
|
|
||||||
*matches = (char **)ga.ga_data;
|
*matches = (char **)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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4505,6 +4505,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)
|
||||||
@@ -4512,13 +4514,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 Ntest_override("char_avail", 0)
|
call Ntest_override("char_avail", 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user