]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.1.1520: completion: search completion doesn't handle 'smartcase' well v9.1.1520
authorGirish Palya <girishji@gmail.com>
Mon, 7 Jul 2025 17:42:10 +0000 (19:42 +0200)
committerChristian Brabandt <cb@256bit.org>
Mon, 7 Jul 2025 17:42:10 +0000 (19:42 +0200)
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>
src/cmdexpand.c
src/testdir/test_cmdline.vim
src/version.c

index b7597ea31efad008b8d28e9889fcfde6bda4b1fd..13d540e77dc55241373880f6d7fe7a76efe33f19 100644 (file)
@@ -4686,6 +4686,82 @@ 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(&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.
  * Returns OK on success, FAIL otherwise.
@@ -4701,12 +4777,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;
@@ -4731,11 +4806,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 (;;)
@@ -4796,30 +4866,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(&regmatch, 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)
@@ -4842,14 +4912,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;
 }
index b3d293639aacb62d083bfef09754155b21b84f7f..c5a7d85c76e200c18028ec588dd362949ed47973 100644 (file)
@@ -4481,6 +4481,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)
@@ -4488,13 +4490,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 test_override("char_avail", 0)
index 76c51ab75f21b85400c9ad93b01de8a69703d8e5..a892dcb4a28c51371dd2bbaf872d1e7e70d2c4c6 100644 (file)
@@ -719,6 +719,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1520,
 /**/
     1519,
 /**/