From 27daeb0d688e77ab45a3d4b0774db4e893de0b3d Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 8 Jul 2025 08:07:42 +0800 Subject: [PATCH] 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 --- src/nvim/cmdexpand.c | 95 ++++++++++++++++++++++--------- test/old/testdir/test_cmdline.vim | 10 +++- 2 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index 472476a05b..83e70f7245 100644 --- 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 index 34caeea303..742bac0127 100644 --- 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\\", 'tx') call assert_equal({}, g:compl_info) + call feedkeys("gg/\\cFo\\", 'tx') + call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches) set ignorecase call feedkeys("gg/f\\", '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\\", 'tx') call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches) + call feedkeys("gg/\\Cfo\\", 'tx') + call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches) set smartcase call feedkeys("gg/f\\", '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\\", 'tx') call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) call feedkeys("gg/FO\\", 'tx') call assert_equal({}, g:compl_info) + call feedkeys("gg/\\Cfo\\", 'tx') + call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches) + call feedkeys("gg/\\cFo\\", 'tx') + call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches) bw! call Ntest_override("char_avail", 0)