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..b6a0047cf6 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -249,13 +249,14 @@ int nextwild(expand_T *xp, int type, int options, bool escape) char *p; if (xp->xp_numfiles == -1) { - may_expand_pattern = options & WILD_MAY_EXPAND_PATTERN; pre_incsearch_pos = xp->xp_pre_incsearch_pos; if (ccline->input_fn && ccline->xp_context == EXPAND_COMMANDS) { // Expand commands typed in input() function set_cmd_context(xp, ccline->cmdbuff, ccline->cmdlen, ccline->cmdpos, false); } else { + may_expand_pattern = options & WILD_MAY_EXPAND_PATTERN; set_expand_context(xp); + may_expand_pattern = false; } if (xp->xp_context == EXPAND_LUA) { nlua_expand_pat(xp); @@ -3893,6 +3894,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 +3912,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 +4015,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 +4102,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..a2ecc57388 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -4499,14 +4499,16 @@ 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') call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) call feedkeys("gg/FO\\", 'tx') - call assert_equal({}, g:compl_info) + 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,23 +4518,55 @@ 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) 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 assert_equal({}, g:compl_info) + + " Issue #17680 (getcompletion() does not support search completion) + let result = getcompletion('%s/', 'cmdline') + call assert_equal([], result) + + 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() @@ -4601,44 +4635,44 @@ func Test_range_complete() for trig in ["\", "\"] call feedkeys($":%s/a{trig}\", 'xt') - call assert_equal(['ab', 'a', 'af'], g:compl_info.matches) + call assert_equal(['ab', 'a', 'af'], g:compl_info.matches) " call feedkeys($":vim9cmd :%s/a{trig}\", 'xt') call feedkeys($":verbose :%s/a{trig}\", 'xt') - call assert_equal(['ab', 'a', 'af'], g:compl_info.matches) + call assert_equal(['ab', 'a', 'af'], g:compl_info.matches) endfor call feedkeys(":%s/\\", 'xt') - call assert_equal({}, g:compl_info) + call assert_equal({}, g:compl_info) for cmd in ['s', 'g'] - call feedkeys(":1,2" . cmd . "/a\\", 'xt') - call assert_equal(['ab', 'a'], g:compl_info.matches) + call feedkeys($":1,2{cmd}/a\\", 'xt') + call assert_equal(['ab', 'a'], g:compl_info.matches) endfor 1 call feedkeys(":.,+2s/a\\", 'xt') - call assert_equal(['ab', 'a'], g:compl_info.matches) + call assert_equal(['ab', 'a'], g:compl_info.matches) /f call feedkeys(":1,s/b\\", 'xt') - call assert_equal(['b', 'ba'], g:compl_info.matches) + call assert_equal(['b', 'ba'], g:compl_info.matches) /c call feedkeys(":\\?,4s/a\\", 'xt') - call assert_equal(['a', 'af'], g:compl_info.matches) + call assert_equal(['a', 'af'], g:compl_info.matches) %s/c/c/ call feedkeys(":1,\\&s/a\\", 'xt') - call assert_equal(['ab', 'a'], g:compl_info.matches) + call assert_equal(['ab', 'a'], g:compl_info.matches) 3 normal! ma call feedkeys(":'a,$s/a\\", 'xt') - call assert_equal(['a', 'af'], g:compl_info.matches) + call assert_equal(['a', 'af'], g:compl_info.matches) " Line number followed by a search pattern ([start]/pattern/[command]) call feedkeys("3/a\\", 'xt') - call assert_equal(['a', 'af', 'ab'], g:compl_info.matches) + call assert_equal(['a', 'af', 'ab'], g:compl_info.matches) bw! call Ntest_override("char_avail", 0)