commit 27daeb0d688e77ab45a3d4b0774db4e893de0b3d
parent ed8b022633de83c8c7ef965b628c89195f2a35e2
Author: zeertzjq <zeertzjq@outlook.com>
Date: Tue, 8 Jul 2025 08:07:42 +0800
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
https://github.com/vim/vim/commit/af220077848dd5d0d303c1ac262692351b90f212
Co-authored-by: Girish Palya <girishji@gmail.com>
Diffstat:
2 files changed, 76 insertions(+), 29 deletions(-)
diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c
@@ -3944,6 +3944,57 @@ static int copy_substring_from_pos(pos_T *start, pos_T *end, char **match, pos_T
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.
/// 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
| (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;
ga_init(&ga, sizeof(char *), 10); // Use growable array of char *
pos_T end_match_pos, word_end_pos;
bool looped_around = false;
bool compl_started = false;
- char *match;
+ char *match, *full_match;
while (true) {
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
- 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)) {
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
- char *line = ml_get(end_match_pos.lnum);
- 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));
- 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) {
- memmove(match + (size_t)pat_len, line + end_match_pos.col, (size_t)match_len);
+ match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos, 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 (!is_regex_match(match, full_match)) {
+ 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
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;
*numMatches = ga.ga_len;
return OK;
cleanup:
- vim_regfree(regmatch.regprog);
ga_clear_strings(&ga);
return FAIL;
}
diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim
@@ -4505,6 +4505,8 @@ func Test_search_complete()
call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
call feedkeys("gg/FO\<tab>\<f9>", 'tx')
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
call feedkeys("gg/f\<tab>\<f9>", 'tx')
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 feedkeys("gg/FO\<tab>\<f9>", 'tx')
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
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 assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
call feedkeys("gg/FO\<tab>\<f9>", 'tx')
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!
call Ntest_override("char_avail", 0)