patch 9.1.1520: completion: search completion doesn't handle 'smartcase' well
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>
diff --git a/src/cmdexpand.c b/src/cmdexpand.c
index b7597ea..13d540e 100644
--- a/src/cmdexpand.c
+++ b/src/cmdexpand.c
@@ -4687,6 +4687,82 @@
}
/*
+ * 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 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 @@
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 @@
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 @@
}
// 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)
@@ -4842,14 +4912,11 @@
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;
}