patch 9.1.1490: 'wildchar' does not work in search contexts
Problem: 'wildchar' does not work in search contexts
Solution: implement search completion when 'wildchar' is typed
(Girish Palya).
This change enhances Vim's command-line completion by extending
'wildmode' behavior to search pattern contexts, including:
- '/' and '?' search commands
- ':s', ':g', ':v', and ':vim' commands
Completions preserve the exact regex pattern typed by the user,
appending the completed word directly to the original input. This
ensures that all regex elements — such as '<', '^', grouping brackets
'()', wildcards '\*', '.', and other special characters — remain intact
and in their original positions.
---
**Use Case**
While searching (using `/` or `?`) for lines containing a pattern like
`"foobar"`, you can now type a partial pattern (e.g., `/f`) followed by
a trigger key (`wildchar`) to open a **popup completion menu** showing
all matching words.
This offers two key benefits:
1. **Precision**: Select the exact word you're looking for without
typing it fully.
2. **Memory aid**: When you can’t recall a full function or variable
name, typing a few letters helps you visually identify and complete the
correct symbol.
---
**What’s New**
Completion is now supported in the following contexts:
- `/` and `?` search commands
- `:s`, `:g`, `:v`, and `:vimgrep` ex-commands
---
**Design Notes**
- While `'wildchar'` (usually `<Tab>`) triggers completion, you'll have
to use `<CTRL-V><Tab>` or "\t" to search for a literal tab.
- **Responsiveness**: Search remains responsive because it checks for
user input frequently.
---
**Try It Out**
Basic setup using the default `<Tab>` as the completion trigger:
```vim
set wim=noselect,full wop=pum wmnu
```
Now type:
```
/foo<Tab>
```
This opens a completion popup for matches containing "foo".
For matches beginning with "foo" type `/\<foo<Tab>`.
---
**Optional: Autocompletion**
For automatic popup menu completion as you type in search or `:`
commands, include this in your `.vimrc`:
```vim
vim9script
set wim=noselect:lastused,full wop=pum wcm=<C-@> wmnu
autocmd CmdlineChanged [:/?] CmdComplete()
def CmdComplete()
var [cmdline, curpos, cmdmode] = [getcmdline(), getcmdpos(),
expand('<afile>') == ':']
var trigger_char = '\%(\w\|[*/:.-]\)$'
var not_trigger_char = '^\%(\d\|,\|+\|-\)\+$' # Exclude numeric range
if getchar(1, {number: true}) == 0 # Typehead is empty, no more
pasted input
&& !wildmenumode() && curpos == cmdline->len() + 1
&& (!cmdmode || (cmdline =~ trigger_char && cmdline !~
not_trigger_char))
SkipCmdlineChanged()
feedkeys("\<C-@>", "t")
timer_start(0, (_) => getcmdline()->substitute('\%x00', '',
'ge')->setcmdline()) # Remove <C-@>
endif
enddef
def SkipCmdlineChanged(key = ''): string
set ei+=CmdlineChanged
timer_start(0, (_) => execute('set ei-=CmdlineChanged'))
return key == '' ? '' : ((wildmenumode() ? "\<C-E>" : '') .. key)
enddef
**Optional: Preserve history recall behavior**
cnoremap <expr> <Up> SkipCmdlineChanged("\<Up>")
cnoremap <expr> <Down> SkipCmdlineChanged("\<Down>")
**Optional: Customize popup height**
autocmd CmdlineEnter : set bo+=error | exec $'set ph={max([10,
winheight(0) - 4])}'
autocmd CmdlineEnter [/?] set bo+=error | set ph=8
autocmd CmdlineLeave [:/?] set bo-=error ph&
```
closes: #17570
Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/testdir/dumps/Test_search_wildmenu_1.dump b/src/testdir/dumps/Test_search_wildmenu_1.dump
new file mode 100644
index 0000000..46ab9a5
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_1.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|e+0#0000001#ffff4012|\|n|f|o@1|b|a|r| +3#0000000#ffffff0@1|e|\|n|t|h|e|t|h|e|r|e| @1|e|\|n|t|h|e|s|e| @1|e|\|n|t|h|e| @34
+|/+0&&|e|\|n|f|o@1|b|a|r> @64
diff --git a/src/testdir/dumps/Test_search_wildmenu_2.dump b/src/testdir/dumps/Test_search_wildmenu_2.dump
new file mode 100644
index 0000000..52889cc
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_2.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|/+0#0000000&|t|h|e> @70
diff --git a/src/testdir/dumps/Test_search_wildmenu_3.dump b/src/testdir/dumps/Test_search_wildmenu_3.dump
new file mode 100644
index 0000000..69e38f8
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_3.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|/+0#0000000&|t| @72
+|t|h|e|s|e| @4|t|h|e| @6|t|h|e|t|h|e| @3|t|h|e|t|h|e|r|e| @1|t|h|e|r|e| @29
+|/|t> @72
diff --git a/src/testdir/dumps/Test_search_wildmenu_4.dump b/src/testdir/dumps/Test_search_wildmenu_4.dump
new file mode 100644
index 0000000..512f87e
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_4.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|t+3#0000000&|h|e|s|e| @1|t|h|e| @1|t|h|e|t|h|e| @1|t|h|e|t|h|e|r|e| @1|t|h|e|r|e| @39
+|/+0&&|t> @72
diff --git a/src/testdir/dumps/Test_search_wildmenu_5.dump b/src/testdir/dumps/Test_search_wildmenu_5.dump
new file mode 100644
index 0000000..533644f
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_5.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|t+3#0000000&|.|*|\|n|.|*|\|n|.|o@1|b|a|r| @1|t|.|*|\|n|.|*|\|n|.|h|e|t|h|e| @1|t|.|*|\|n|.|*|\|n|.|h|e| @28
+|/+0&&|t|.|*|\|n|.|*|\|n|.> @63
diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim
index 9a3fe20..59c25db 100644
--- a/src/testdir/test_cmdline.vim
+++ b/src/testdir/test_cmdline.vim
@@ -1651,8 +1651,10 @@
" completion after a :global command
call feedkeys(":g/a/chist\t\<C-B>\"\<CR>", 'xt')
call assert_equal('"g/a/chistory', @:)
+ set wildchar=0
call feedkeys(":g/a\\/chist\t\<C-B>\"\<CR>", 'xt')
call assert_equal("\"g/a\\/chist\t", @:)
+ set wildchar&
" use <Esc> as the 'wildchar' for completion
set wildchar=<Esc>
@@ -3094,12 +3096,14 @@
" Test for completion after a :substitute command followed by a pipe (|)
" character
func Test_cmdline_complete_substitute()
+ set wildchar=0
call feedkeys(":s | \t\<C-B>\"\<CR>", 'xt')
call assert_equal("\"s | \t", @:)
call feedkeys(":s/ | \t\<C-B>\"\<CR>", 'xt')
call assert_equal("\"s/ | \t", @:)
call feedkeys(":s/one | \t\<C-B>\"\<CR>", 'xt')
call assert_equal("\"s/one | \t", @:)
+ set wildchar&
call feedkeys(":s/one/ | \t\<C-B>\"\<CR>", 'xt')
call assert_equal("\"s/one/ | \t", @:)
call feedkeys(":s/one/two | \t\<C-B>\"\<CR>", 'xt')
@@ -4350,4 +4354,239 @@
call assert_fails(':redrawtabpanel', 'E1547:')
endfunc
+" Test wildcharm completion for '/' and '?' search
+func Test_search_complete()
+ CheckOption incsearch
+ set wildcharm=<c-z>
+
+ " Disable char_avail so that expansion of commandline works
+ call test_override("char_avail", 1)
+
+ func GetComplInfo()
+ let g:compl_info = cmdcomplete_info()
+ return ''
+ endfunc
+
+ new
+ cnoremap <buffer><expr> <F9> GetComplInfo()
+
+ " Pressing <Tab> inserts tab character
+ set wildchar=0
+ call setline(1, "x\t")
+ call feedkeys("/x\t\r", "tx")
+ call assert_equal("x\t", @/)
+ set wildchar&
+
+ call setline(1, ['the', 'these', 'thethe', 'thethere', 'foobar'])
+
+ for trig in ["\<tab>", "\<c-z>"]
+ " Test menu first item and order
+ call feedkeys($"gg2j/t{trig}\<f9>", 'tx')
+ call assert_equal(['the', 'thethere', 'there', 'these', 'thethe'], g:compl_info.matches)
+ call feedkeys($"gg2j?t{trig}\<f9>", 'tx')
+ call assert_equal(['these', 'the', 'there', 'thethere', 'thethe'], g:compl_info.matches)
+
+ " <c-n> and <c-p> cycle through menu items
+ call feedkeys($"gg/the{trig}\<cr>", 'tx')
+ call assert_equal('these', getline('.'))
+ call feedkeys($"gg/the{trig}\<c-n>\<cr>", 'tx')
+ call assert_equal('thethe', getline('.'))
+ call feedkeys($"gg/the{trig}".repeat("\<c-n>", 5)."\<cr>", 'tx')
+ call assert_equal('these', getline('.'))
+ call feedkeys($"G?the{trig}\<cr>", 'tx')
+ call assert_equal('thethere', getline('.'))
+ call feedkeys($"G?the{trig}".repeat("\<c-p>", 5)."\<cr>", 'tx')
+ call assert_equal('thethere', getline('.'))
+
+ " Beginning of word pattern (<) retains '<'
+ call feedkeys($"gg2j/\\<t{trig}\<f9>", 'tx')
+ call assert_equal(['\<thethere', '\<the', '\<these', '\<thethe'], g:compl_info.matches)
+ call feedkeys($"gg2j?\\<t{trig}\<f9>", 'tx')
+ call assert_equal(['\<these', '\<the', '\<thethere', '\<thethe'], g:compl_info.matches)
+ call feedkeys($"gg2j/\\v<t{trig}\<f9>", 'tx')
+ call assert_equal(['\v<thethere', '\v<the', '\v<these', '\v<thethe'], g:compl_info.matches)
+ call feedkeys($"gg2j?\\v<th{trig}\<f9>", 'tx')
+ call assert_equal(['\v<these', '\v<the', '\v<thethere', '\v<thethe'], g:compl_info.matches)
+ endfor
+
+ " Ctrl-G goes from one match to the next, after menu is opened
+ set incsearch
+ " first match
+ call feedkeys("gg/the\<c-z>\<c-n>\<c-g>\<cr>", 'tx')
+ call assert_equal('thethe', getline('.'))
+ " second match
+ call feedkeys("gg/the\<c-z>\<c-n>\<c-g>\<c-g>\<cr>", 'tx')
+ call assert_equal('thethere', getline('.'))
+ call assert_equal([0, 0, 0, 0], getpos('"'))
+
+ " CTRL-T goes to the previous match
+ " first match
+ call feedkeys("G?the\<c-z>".repeat("\<c-n>", 2)."\<c-t>\<cr>", 'tx')
+ call assert_equal('thethere', getline('.'))
+ " second match
+ call feedkeys("G?the\<c-z>".repeat("\<c-n>", 2).repeat("\<c-t>", 2)."\<cr>", 'tx')
+ call assert_equal('thethe', getline('.'))
+
+ " wild menu is cleared properly
+ call feedkeys("/the\<c-z>\<esc>/\<f9>", 'tx')
+ call assert_equal({}, g:compl_info)
+ call feedkeys("/the\<c-z>\<c-e>\<f9>", 'tx')
+ call assert_equal([], g:compl_info.matches)
+
+ " Do not expand if offset is present (/pattern/offset and ?pattern?offset)
+ for pat in ["/", "/2", "/-3", "\\/"]
+ call feedkeys("/the" . pat . "\<c-z>\<f9>", 'tx')
+ call assert_equal({}, g:compl_info)
+ endfor
+ for pat in ["?", "?2", "?-3", "\\\\?"]
+ call feedkeys("?the" . pat . "\<c-z>\<f9>", 'tx')
+ call assert_equal({}, g:compl_info)
+ endfor
+
+ " Last letter of match is multibyte
+ call setline('$', ['theΩ'])
+ call feedkeys("gg/th\<c-z>\<f9>", 'tx')
+ call assert_equal(['these', 'thethe', 'the', 'thethere', 'there', 'theΩ'],
+ \ g:compl_info.matches)
+
+ " Identical words
+ call setline(1, ["foo", "foo", "foo", "foobar"])
+ call feedkeys("gg/f\<c-z>\<f9>", 'tx')
+ call assert_equal(['foo', 'foobar'], g:compl_info.matches)
+
+ " Exact match
+ call feedkeys("/foo\<c-z>\<f9>", 'tx')
+ call assert_equal(['foo', 'foobar'], g:compl_info.matches)
+
+ " Match case correctly
+ %d
+ call setline(1, ["foobar", "Foobar", "fooBAr", "FooBARR"])
+ call feedkeys("gg/f\<tab>\<f9>", 'tx')
+ call assert_equal(['fooBAr', 'foobar'], 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)
+ set ignorecase
+ call feedkeys("gg/f\<tab>\<f9>", 'tx')
+ 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/FO\<tab>\<f9>", 'tx')
+ call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches)
+ set smartcase
+ call feedkeys("gg/f\<tab>\<f9>", 'tx')
+ 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)
+
+ bw!
+ call test_override("char_avail", 0)
+ delfunc GetComplInfo
+ unlet! g:compl_info
+ set wildcharm=0 incsearch& ignorecase& smartcase&
+endfunc
+
+func Test_search_wildmenu_screendump()
+ CheckScreendump
+
+ let lines =<< trim [SCRIPT]
+ set wildmenu wildcharm=<f5>
+ call setline(1, ['the', 'these', 'the', 'foobar', 'thethe', 'thethere'])
+ [SCRIPT]
+ call writefile(lines, 'XTest_search_wildmenu', 'D')
+ let buf = RunVimInTerminal('-S XTest_search_wildmenu', {'rows': 10})
+
+ " Pattern has newline at EOF
+ call term_sendkeys(buf, "gg2j/e\\n\<f5>")
+ call VerifyScreenDump(buf, 'Test_search_wildmenu_1', {})
+
+ " longest:full
+ call term_sendkeys(buf, "\<esc>:set wim=longest,full\<cr>")
+ call term_sendkeys(buf, "gg/t\<f5>")
+ call VerifyScreenDump(buf, 'Test_search_wildmenu_2', {})
+
+ " list:full
+ call term_sendkeys(buf, "\<esc>:set wim=list,full\<cr>")
+ call term_sendkeys(buf, "gg/t\<f5>")
+ call VerifyScreenDump(buf, 'Test_search_wildmenu_3', {})
+
+ " noselect:full
+ call term_sendkeys(buf, "\<esc>:set wim=noselect,full\<cr>")
+ call term_sendkeys(buf, "gg/t\<f5>")
+ call VerifyScreenDump(buf, 'Test_search_wildmenu_4', {})
+
+ " Multiline
+ call term_sendkeys(buf, "\<esc>gg/t.*\\n.*\\n.\<tab>")
+ call VerifyScreenDump(buf, 'Test_search_wildmenu_5', {})
+
+ call term_sendkeys(buf, "\<esc>")
+ call StopVimInTerminal(buf)
+endfunc
+
+" Test wildcharm completion for :s and :g with range
+func Test_range_complete()
+ set wildcharm=<c-z>
+
+ " Disable char_avail so that expansion of commandline works
+ call test_override("char_avail", 1)
+
+ func GetComplInfo()
+ let g:compl_info = cmdcomplete_info()
+ return ''
+ endfunc
+ new
+ cnoremap <buffer><expr> <F9> GetComplInfo()
+
+ call setline(1, ['ab', 'ba', 'ca', 'af'])
+
+ for trig in ["\<tab>", "\<c-z>"]
+ call feedkeys($":%s/a{trig}\<f9>", 'xt')
+ call assert_equal(['ab', 'a', 'af'], g:compl_info.matches)
+ call feedkeys($":vim9cmd :%s/a{trig}\<f9>", 'xt')
+ call assert_equal(['ab', 'a', 'af'], g:compl_info.matches)
+ endfor
+
+ call feedkeys(":%s/\<c-z>\<f9>", 'xt')
+ call assert_equal({}, g:compl_info)
+
+ for cmd in ['s', 'g']
+ call feedkeys(":1,2" . cmd . "/a\<c-z>\<f9>", 'xt')
+ call assert_equal(['ab', 'a'], g:compl_info.matches)
+ endfor
+
+ 1
+ call feedkeys(":.,+2s/a\<c-z>\<f9>", 'xt')
+ call assert_equal(['ab', 'a'], g:compl_info.matches)
+
+ /f
+ call feedkeys(":1,s/b\<c-z>\<f9>", 'xt')
+ call assert_equal(['b', 'ba'], g:compl_info.matches)
+
+ /c
+ call feedkeys(":\\?,4s/a\<c-z>\<f9>", 'xt')
+ call assert_equal(['a', 'af'], g:compl_info.matches)
+
+ %s/c/c/
+ call feedkeys(":1,\\&s/a\<c-z>\<f9>", 'xt')
+ call assert_equal(['ab', 'a'], g:compl_info.matches)
+
+ 3
+ normal! ma
+ call feedkeys(":'a,$s/a\<c-z>\<f9>", 'xt')
+ call assert_equal(['a', 'af'], g:compl_info.matches)
+
+ " Line number followed by a search pattern ([start]/pattern/[command])
+ call feedkeys("3/a\<c-z>\<f9>", 'xt')
+ call assert_equal(['a', 'af', 'ab'], g:compl_info.matches)
+
+ bw!
+ call test_override("char_avail", 0)
+ delfunc GetComplInfo
+ unlet! g:compl_info
+ set wildcharm=0
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab