From ef0ec7edac5b7923955998ebddbfe02a1146bbf3 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 9 Jul 2025 07:38:43 +0800 Subject: [PATCH] vim-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: vim/vim#17654 closes: vim/vim#17667 https://github.com/vim/vim/commit/93c2d5bf7f01db594a3f5ebecbd5a31dfd411544 Co-authored-by: Girish Palya --- runtime/doc/options.txt | 15 +++++++++++ runtime/lua/vim/_meta/options.lua | 15 +++++++++++ src/nvim/cmdexpand.c | 45 ++++++++++++++++++++----------- src/nvim/options.lua | 17 +++++++++++- test/old/testdir/test_cmdline.vim | 31 ++++++++++++++++++++- 5 files changed, 106 insertions(+), 17 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 55f8ea2158..1fd793f436 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -7173,6 +7173,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 |/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal instead of triggering completion, type or "\t". + See also |'wildoptions'|. *'wildcharm'* *'wcm'* 'wildcharm' 'wcm' number (default 0) @@ -7319,6 +7320,20 @@ A jump table for the options with a short description can be found at |Q_op|. global A list of words that change how |cmdline-completion| is done. 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 this value is specified, wildcard expansion will not be used for completion. The matches will be sorted by diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 60715def3e..c529a87811 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -7818,6 +7818,7 @@ vim.go.ww = vim.go.whichwrap --- 'wildchar' also enables completion in search pattern contexts such as --- `/`, `?`, `:s`, `:g`, `:v`, and `:vim`. To insert a literal --- instead of triggering completion, type or "\t". +--- See also `'wildoptions'`. --- --- @type integer vim.o.wildchar = 9 @@ -8012,6 +8013,20 @@ vim.go.wim = vim.go.wildmode --- A list of words that change how `cmdline-completion` is done. --- 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 --- this value is specified, wildcard expansion will not --- be used for completion. The matches will be sorted by diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index 83e70f7245..3ce314f1d9 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -3893,6 +3893,8 @@ void f_cmdcomplete_info(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) /// matched text is returned in '*match_end'. static int copy_substring_from_pos(pos_T *start, pos_T *end, char **match, pos_T *match_end) { + bool exacttext = wop_flags & kOptWopFlagExacttext; + if (start->lnum > end->lnum || (start->lnum == end->lnum && start->col >= end->col)) { return FAIL; // invalid range @@ -3909,19 +3911,27 @@ static int copy_substring_from_pos(pos_T *start, pos_T *end, char **match, pos_T int segment_len = is_single_line ? (int)(end->col - start->col) : (int)strlen(start_ptr); - ga_grow(&ga, segment_len + 1); + ga_grow(&ga, segment_len + 2); ga_concat_len(&ga, start_ptr, (size_t)segment_len); if (!is_single_line) { - ga_append(&ga, '\n'); + if (exacttext) { + ga_concat_len(&ga, "\\n", 2); + } else { + ga_append(&ga, '\n'); + } } // Append full lines between start and end if (!is_single_line) { for (linenr_T lnum = start->lnum + 1; lnum < end->lnum; lnum++) { char *line = ml_get(lnum); - ga_grow(&ga, ml_get_len(lnum) + 1); + ga_grow(&ga, ml_get_len(lnum) + 2); ga_concat(&ga, line); - ga_append(&ga, '\n'); + if (exacttext) { + ga_concat_len(&ga, "\\n", 2); + } else { + ga_append(&ga, '\n'); + } } } @@ -4004,6 +4014,7 @@ static char *concat_pattern_with_buffer_match(char *pat, int pat_len, pos_T *end /// @param[out] numMatches number of matches static int expand_pattern_in_buf(char *pat, Direction dir, char ***matches, int *numMatches) { + bool exacttext = wop_flags & kOptWopFlagExacttext; bool has_range = search_first_line != 0; *matches = NULL; @@ -4090,22 +4101,26 @@ static int expand_pattern_in_buf(char *pat, Direction dir, char ***matches, int break; } - // Construct a new match from completed word appended to pattern itself - match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos, false); + if (exacttext) { + match = full_match; + } else { + // 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 (!is_regex_match(match, full_match)) { - xfree(match); - match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos, true); + // 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); - xfree(full_match); - continue; + 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; + } } + xfree(full_match); } - xfree(full_match); // Include this match if it is not a duplicate for (int i = 0; i < ga.ga_len; i++) { diff --git a/src/nvim/options.lua b/src/nvim/options.lua index d320e0928a..570d0a6d35 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -10117,6 +10117,7 @@ local options = { < 'wildchar' also enables completion in search pattern contexts such as |/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal instead of triggering completion, type or "\t". + See also |'wildoptions'|. ]=], full_name = 'wildchar', scope = { 'global' }, @@ -10311,12 +10312,26 @@ local options = { { abbreviation = 'wop', defaults = 'pum,tagfile', - values = { 'fuzzy', 'tagfile', 'pum' }, + values = { 'fuzzy', 'tagfile', 'pum', 'exacttext' }, flags = true, deny_duplicates = true, desc = [=[ A list of words that change how |cmdline-completion| is done. 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 this value is specified, wildcard expansion will not be used for completion. The matches will be sorted by diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index 742bac0127..1391f5be6e 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -4499,6 +4499,7 @@ func Test_search_complete() " Match case correctly %d call setline(1, ["foobar", "Foobar", "fooBAr", "FooBARR"]) + call feedkeys("gg/f\\", 'tx') call assert_equal(['fooBAr', 'foobar'], g:compl_info.matches) call feedkeys("gg/Fo\\", 'tx') @@ -4507,6 +4508,7 @@ func Test_search_complete() 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) @@ -4516,6 +4518,7 @@ func Test_search_complete() 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) @@ -4523,16 +4526,42 @@ 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/foob\\", 'tx') + call assert_equal(['foobar', 'foobarr'], g:compl_info.matches) 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) + set wildoptions+=exacttext ignorecase& smartcase& + call feedkeys("gg/F\\", 'tx') + call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) + call feedkeys("gg/foob\\", 'tx') + call assert_equal([], g:compl_info.matches) + call feedkeys("gg/r\\n.\\", 'tx') + call assert_equal(['r\nFoobar', 'r\nfooBAr', 'r\nFooBARR'], g:compl_info.matches) + + set ignorecase + call feedkeys("gg/F\\", 'tx') + call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches) + call feedkeys("gg/R\\n.\\", 'tx') + call assert_equal(['r\nFoobar', 'r\nfooBAr', 'r\nFooBARR'], g:compl_info.matches) + + set smartcase + call feedkeys("gg/f\\", 'tx') + call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches) + call feedkeys("gg/foob\\", 'tx') + call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches) + call feedkeys("gg/R\\n.\\", 'tx') + call assert_equal({}, g:compl_info) + call feedkeys("gg/r\\n.*\\n\\", 'tx') + call assert_equal(['r\nFoobar\nfooBAr', 'r\nfooBAr\nFooBARR'], g:compl_info.matches) + bw! call Ntest_override("char_avail", 0) delfunc GetComplInfo unlet! g:compl_info - set wildcharm=0 incsearch& ignorecase& smartcase& + set wildcharm=0 incsearch& ignorecase& smartcase& wildoptions& endfunc func Test_search_wildmenu_screendump()