From 29e3e0c0463a5862be5f19bac2cf3a6f9ebda982 Mon Sep 17 00:00:00 2001 From: Girish Palya Date: Fri, 4 Jul 2025 19:34:05 +0200 Subject: [PATCH 1/3] Fix: Improve smartcase handling in search pattern completion **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. --- src/cmdexpand.c | 120 ++++++++++++++++++++++++++--------- src/testdir/test_cmdline.vim | 10 ++- 2 files changed, 100 insertions(+), 30 deletions(-) diff --git a/src/cmdexpand.c b/src/cmdexpand.c index f7a5f5cc63..e294576125 100644 --- a/src/cmdexpand.c +++ b/src/cmdexpand.c @@ -4662,6 +4662,77 @@ copy_substring_from_pos(pos_T *start, pos_T *end, char_u **match, 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(®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_u * +concat_pattern_with_buffer_match( + char_u *pat, + int pat_len, + pos_T *end_match_pos, + int lowercase) +{ + 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 (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 + mch_memmove(match + pat_len, line + end_match_pos->col, match_len); + } + match[pat_len + match_len] = NUL; + return match; +cleanup: + vim_free(match); + return NULL; +} + /* * Search for strings matching "pat" in the specified range and return them. * Returns OK on success, FAIL otherwise. @@ -4677,12 +4748,11 @@ expand_pattern_in_buf( garray_T ga; int found_new_match; int looped_around = FALSE; - int pat_len, match_len; + int pat_len; int has_range = FALSE; int compl_started = FALSE; int search_flags; - char_u *match, *line, *word_end; - regmatch_T regmatch; + char_u *match, *full_match; #ifdef FEAT_SEARCH_EXTRA has_range = search_first_line != 0; @@ -4707,11 +4777,6 @@ expand_pattern_in_buf( search_flags = SEARCH_OPT | SEARCH_NOOF | SEARCH_PEEK | SEARCH_NFMSG | (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* for (;;) @@ -4772,30 +4837,30 @@ expand_pattern_in_buf( } // 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)) + // Construct a new match from completed word appended to pattern itself + 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 (match == NULL || !is_regex_match(match, full_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); - - // 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; + vim_free(full_match); // Include this match if it is not a duplicate for (int i = 0; i < ga.ga_len; ++i) @@ -4818,14 +4883,11 @@ expand_pattern_in_buf( cur_match_pos = word_end_pos; } - vim_regfree(regmatch.regprog); - *matches = (char_u **)ga.ga_data; *numMatches = ga.ga_len; return OK; cleanup: - vim_regfree(regmatch.regprog); ga_clear_strings(&ga); return FAIL; } diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim index 9ebeb0f44a..d2d47a150e 100644 --- a/src/testdir/test_cmdline.vim +++ b/src/testdir/test_cmdline.vim @@ -4467,6 +4467,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) @@ -4474,13 +4476,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 test_override("char_avail", 0) From a9f540a8c68bbacd8bc2170166170a850eef8935 Mon Sep 17 00:00:00 2001 From: Girish Palya Date: Fri, 4 Jul 2025 20:59:46 +0200 Subject: [PATCH 2/3] Fix compile error M src/cmdexpand.c --- src/cmdexpand.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cmdexpand.c b/src/cmdexpand.c index e294576125..4b957072b3 100644 --- a/src/cmdexpand.c +++ b/src/cmdexpand.c @@ -4698,7 +4698,7 @@ concat_pattern_with_buffer_match( char_u *pat, int pat_len, pos_T *end_match_pos, - int lowercase) + int lowercase UNUSED) { char_u *line = ml_get(end_match_pos->lnum); char_u *word_end = find_word_end(line + end_match_pos->col); @@ -4710,6 +4710,7 @@ concat_pattern_with_buffer_match( 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, @@ -4724,6 +4725,7 @@ concat_pattern_with_buffer_match( vim_free(lower); } else +#endif mch_memmove(match + pat_len, line + end_match_pos->col, match_len); } match[pat_len + match_len] = NUL; From f4e301ec353c17f8b6200d80211385cf58b928df Mon Sep 17 00:00:00 2001 From: Girish Palya Date: Fri, 4 Jul 2025 21:11:45 +0200 Subject: [PATCH 3/3] Fix compile issue M src/cmdexpand.c --- src/cmdexpand.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cmdexpand.c b/src/cmdexpand.c index 4b957072b3..b1a478971d 100644 --- a/src/cmdexpand.c +++ b/src/cmdexpand.c @@ -4730,9 +4730,12 @@ concat_pattern_with_buffer_match( } match[pat_len + match_len] = NUL; return match; + +#if defined(FEAT_EVAL) || defined(FEAT_SPELL) || defined(PROTO) cleanup: vim_free(match); return NULL; +#endif } /*