patch 9.1.1526: completion: search completion match may differ in case

Problem:  completion: search completion match may differ in case
          (techntools)
Solution: add "exacttext" to 'wildoptions' value (Girish Palya)

This flag does the following:

exacttext
      When this flag is present, search pattern completion
      (e.g., in |/|, |?|, |:s|, |:g|, |:v|, and |:vim|)
      shows exact buffer text as menu items, without
      preserving regex artifacts like position
      anchors (e.g., |/\<|). This provides more intuitive
      menu items that match the actual buffer text. However,
      searches may be less accurate since the pattern is not
      preserved exactly.
      By default, Vim preserves the typed pattern (with
      anchors) and appends the matched word. This preserves
      search correctness, especially when using regular
      expressions or with 'smartcase' enabled. However, the
      case of the appended matched word may not exactly
      match the case of the word in the buffer.

fixes: #17654
closes: #17667

Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
This commit is contained in:
Girish Palya
2025-07-08 21:29:02 +02:00
committed by Christian Brabandt
parent b3eaae21b9
commit 93c2d5bf7f
7 changed files with 87 additions and 23 deletions

View File

@ -1,4 +1,4 @@
*options.txt* For Vim version 9.1. Last change: 2025 Jul 05 *options.txt* For Vim version 9.1. Last change: 2025 Jul 08
VIM REFERENCE MANUAL by Bram Moolenaar VIM REFERENCE MANUAL by Bram Moolenaar
@ -9759,6 +9759,7 @@ A jump table for the options with a short description can be found at |Q_op|.
< 'wildchar' also enables completion in search pattern contexts such as < 'wildchar' also enables completion in search pattern contexts such as
|/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal <Tab> |/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal <Tab>
instead of triggering completion, type <C-V><Tab> or "\t". instead of triggering completion, type <C-V><Tab> or "\t".
See also |'wildoptions'|.
NOTE: This option is set to the Vi default value when 'compatible' is NOTE: This option is set to the Vi default value when 'compatible' is
set and to the Vim default value when 'compatible' is reset. set and to the Vim default value when 'compatible' is reset.
@ -9926,6 +9927,20 @@ A jump table for the options with a short description can be found at |Q_op|.
A list of words that change how |cmdline-completion| is done. A list of words that change how |cmdline-completion| is done.
The following values are supported: The following values are supported:
exacttext When this flag is present, search pattern completion
(e.g., in |/|, |?|, |:s|, |:g|, |:v|, and |:vim|)
shows exact buffer text as menu items, without
preserving regex artifacts like position
anchors (e.g., |/\<|). This provides more intuitive
menu items that match the actual buffer text.
However, searches may be less accurate since the
pattern is not preserved exactly.
By default, Vim preserves the typed pattern (with
anchors) and appends the matched word. This preserves
search correctness, especially when using regular
expressions or with 'smartcase' enabled. However, the
case of the appended matched word may not exactly
match the case of the word in the buffer.
fuzzy Use |fuzzy-matching| to find completion matches. When fuzzy Use |fuzzy-matching| to find completion matches. When
this value is specified, wildcard expansion will not this value is specified, wildcard expansion will not
be used for completion. The matches will be sorted by be used for completion. The matches will be sorted by

View File

@ -1,4 +1,4 @@
*version9.txt* For Vim version 9.1. Last change: 2025 Jul 05 *version9.txt* For Vim version 9.1. Last change: 2025 Jul 08
VIM REFERENCE MANUAL by Bram Moolenaar VIM REFERENCE MANUAL by Bram Moolenaar
@ -41623,6 +41623,8 @@ Completion: ~
- improved commandline completion for the |:hi| command - improved commandline completion for the |:hi| command
- New option value for 'wildmode': - New option value for 'wildmode':
"noselect" - do not auto select an entry in the wildmenu "noselect" - do not auto select an entry in the wildmenu
"exacttext" - show exact matches in wildmenu with search
completion
- New flags for 'complete': - New flags for 'complete':
"F{func}" - complete using given function "F{func}" - complete using given function
"F" - complete using 'completefunc' "F" - complete using 'completefunc'

View File

@ -4631,6 +4631,7 @@ copy_substring_from_pos(pos_T *start, pos_T *end, char_u **match,
int segment_len; int segment_len;
linenr_T lnum; linenr_T lnum;
garray_T ga; garray_T ga;
int exacttext = vim_strchr(p_wop, WOP_EXACTTEXT) != NULL;
if (start->lnum > end->lnum if (start->lnum > end->lnum
|| (start->lnum == end->lnum && start->col >= end->col)) || (start->lnum == end->lnum && start->col >= end->col))
@ -4646,12 +4647,17 @@ copy_substring_from_pos(pos_T *start, pos_T *end, char_u **match,
segment_len = is_single_line ? (end->col - start->col) segment_len = is_single_line ? (end->col - start->col)
: (int)STRLEN(start_ptr); : (int)STRLEN(start_ptr);
if (ga_grow(&ga, segment_len + 1) != OK) if (ga_grow(&ga, segment_len + 2) != OK)
return FAIL; return FAIL;
ga_concat_len(&ga, start_ptr, segment_len); ga_concat_len(&ga, start_ptr, segment_len);
if (!is_single_line) if (!is_single_line)
ga_append(&ga, '\n'); {
if (exacttext)
ga_concat_len(&ga, (char_u *)"\\n", 2);
else
ga_append(&ga, '\n');
}
// Append full lines between start and end // Append full lines between start and end
if (!is_single_line) if (!is_single_line)
@ -4659,10 +4665,13 @@ copy_substring_from_pos(pos_T *start, pos_T *end, char_u **match,
for (lnum = start->lnum + 1; lnum < end->lnum; lnum++) for (lnum = start->lnum + 1; lnum < end->lnum; lnum++)
{ {
line = ml_get(lnum); line = ml_get(lnum);
if (ga_grow(&ga, ml_get_len(lnum) + 1) != OK) if (ga_grow(&ga, ml_get_len(lnum) + 2) != OK)
return FAIL; return FAIL;
ga_concat(&ga, line); ga_concat(&ga, line);
ga_append(&ga, '\n'); if (exacttext)
ga_concat_len(&ga, (char_u *)"\\n", 2);
else
ga_append(&ga, '\n');
} }
} }
@ -4783,6 +4792,7 @@ expand_pattern_in_buf(
int compl_started = FALSE; int compl_started = FALSE;
int search_flags; int search_flags;
char_u *match, *full_match; char_u *match, *full_match;
int exacttext = vim_strchr(p_wop, WOP_EXACTTEXT) != NULL;
#ifdef FEAT_SEARCH_EXTRA #ifdef FEAT_SEARCH_EXTRA
has_range = search_first_line != 0; has_range = search_first_line != 0;
@ -4871,26 +4881,31 @@ expand_pattern_in_buf(
&word_end_pos)) &word_end_pos))
break; break;
// Construct a new match from completed word appended to pattern itself if (exacttext)
match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos, match = full_match;
FALSE); else
// 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); // Construct a new match from completed word appended to pattern itself
match = concat_pattern_with_buffer_match(pat, pat_len, match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos,
&end_match_pos, TRUE); 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)) if (match == NULL || !is_regex_match(match, full_match))
{ {
vim_free(match); vim_free(match);
vim_free(full_match); match = concat_pattern_with_buffer_match(pat, pat_len,
continue; &end_match_pos, TRUE);
if (match == NULL || !is_regex_match(match, full_match))
{
vim_free(match);
vim_free(full_match);
continue;
}
} }
vim_free(full_match);
} }
vim_free(full_match);
// Include this match if it is not a duplicate // Include this match if it is not a duplicate
for (int i = 0; i < ga.ga_len; ++i) for (int i = 0; i < ga.ga_len; ++i)

View File

@ -375,8 +375,9 @@ typedef enum {
// flags for the 'wildoptions' option // flags for the 'wildoptions' option
// each defined char should be unique over all values. // each defined char should be unique over all values.
#define WOP_FUZZY 'z' #define WOP_FUZZY 'z'
#define WOP_TAGFILE 't' #define WOP_TAGFILE 'g'
#define WOP_PUM 'p' #define WOP_PUM 'p'
#define WOP_EXACTTEXT 'x'
// arguments for can_bs() // arguments for can_bs()
// each defined char should be unique over all values // each defined char should be unique over all values

View File

@ -103,7 +103,7 @@ static char *(p_ttym_values[]) = {"xterm", "xterm2", "dec", "netterm", "jsbterm"
static char *(p_ve_values[]) = {"block", "insert", "all", "onemore", "none", "NONE", NULL}; static char *(p_ve_values[]) = {"block", "insert", "all", "onemore", "none", "NONE", NULL};
// Note: Keep this in sync with check_opt_wim() // Note: Keep this in sync with check_opt_wim()
static char *(p_wim_values[]) = {"full", "longest", "list", "lastused", "noselect", NULL}; static char *(p_wim_values[]) = {"full", "longest", "list", "lastused", "noselect", NULL};
static char *(p_wop_values[]) = {"fuzzy", "tagfile", "pum", NULL}; static char *(p_wop_values[]) = {"fuzzy", "tagfile", "pum", "exacttext", NULL};
#ifdef FEAT_WAK #ifdef FEAT_WAK
static char *(p_wak_values[]) = {"yes", "menu", "no", NULL}; static char *(p_wak_values[]) = {"yes", "menu", "no", NULL};
#endif #endif

View File

@ -4472,6 +4472,7 @@ func Test_search_complete()
" Match case correctly " Match case correctly
%d %d
call setline(1, ["foobar", "Foobar", "fooBAr", "FooBARR"]) call setline(1, ["foobar", "Foobar", "fooBAr", "FooBARR"])
call feedkeys("gg/f\<tab>\<f9>", 'tx') call feedkeys("gg/f\<tab>\<f9>", 'tx')
call assert_equal(['fooBAr', 'foobar'], g:compl_info.matches) call assert_equal(['fooBAr', 'foobar'], g:compl_info.matches)
call feedkeys("gg/Fo\<tab>\<f9>", 'tx') call feedkeys("gg/Fo\<tab>\<f9>", 'tx')
@ -4480,6 +4481,7 @@ func Test_search_complete()
call assert_equal({}, g:compl_info) call assert_equal({}, g:compl_info)
call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx') call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx')
call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches) call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches)
set ignorecase set ignorecase
call feedkeys("gg/f\<tab>\<f9>", 'tx') 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)
@ -4489,6 +4491,7 @@ func Test_search_complete()
call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches) call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches)
call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx') call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx')
call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches) call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches)
set smartcase set smartcase
call feedkeys("gg/f\<tab>\<f9>", 'tx') 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)
@ -4496,16 +4499,42 @@ func Test_search_complete()
call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
call feedkeys("gg/FO\<tab>\<f9>", 'tx') call feedkeys("gg/FO\<tab>\<f9>", 'tx')
call assert_equal({}, g:compl_info) call assert_equal({}, g:compl_info)
call feedkeys("gg/foob\<tab>\<f9>", 'tx')
call assert_equal(['foobar', 'foobarr'], g:compl_info.matches)
call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx') call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx')
call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches) call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches)
call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx') call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx')
call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches) call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches)
set wildoptions+=exacttext ignorecase& smartcase&
call feedkeys("gg/F\<tab>\<f9>", 'tx')
call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
call feedkeys("gg/foob\<tab>\<f9>", 'tx')
call assert_equal([], g:compl_info.matches)
call feedkeys("gg/r\\n.\<tab>\<f9>", 'tx')
call assert_equal(['r\nFoobar', 'r\nfooBAr', 'r\nFooBARR'], g:compl_info.matches)
set ignorecase
call feedkeys("gg/F\<tab>\<f9>", 'tx')
call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches)
call feedkeys("gg/R\\n.\<tab>\<f9>", 'tx')
call assert_equal(['r\nFoobar', 'r\nfooBAr', 'r\nFooBARR'], g:compl_info.matches)
set smartcase
call feedkeys("gg/f\<tab>\<f9>", 'tx')
call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches)
call feedkeys("gg/foob\<tab>\<f9>", 'tx')
call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches)
call feedkeys("gg/R\\n.\<tab>\<f9>", 'tx')
call assert_equal({}, g:compl_info)
call feedkeys("gg/r\\n.*\\n\<tab>\<f9>", 'tx')
call assert_equal(['r\nFoobar\nfooBAr', 'r\nfooBAr\nFooBARR'], g:compl_info.matches)
bw! bw!
call test_override("char_avail", 0) call test_override("char_avail", 0)
delfunc GetComplInfo delfunc GetComplInfo
unlet! g:compl_info unlet! g:compl_info
set wildcharm=0 incsearch& ignorecase& smartcase& set wildcharm=0 incsearch& ignorecase& smartcase& wildoptions&
endfunc endfunc
func Test_search_wildmenu_screendump() func Test_search_wildmenu_screendump()

View File

@ -719,6 +719,8 @@ static char *(features[]) =
static int included_patches[] = static int included_patches[] =
{ /* Add new patch number below this line */ { /* Add new patch number below this line */
/**/
1526,
/**/ /**/
1525, 1525,
/**/ /**/