Files
neovim/test/functional/legacy/cmdline_spec.lua
zeertzjq dd707246fd vim-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: vim/vim#17570

6b49fba8c8

Co-authored-by: Girish Palya <girishji@gmail.com>
2025-07-05 21:58:38 +08:00

490 lines
18 KiB
Lua

local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local clear = n.clear
local command = n.command
local feed = n.feed
local feed_command = n.feed_command
local exec = n.exec
local api = n.api
local pesc = vim.pesc
describe('cmdline', function()
before_each(clear)
-- oldtest: Test_cmdlineclear_tabenter()
it('is cleared when switching tabs', function()
local screen = Screen.new(30, 10)
feed_command([[call setline(1, range(30))]])
screen:expect([[
^0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
:call setline(1, range(30)) |
]])
feed [[:tabnew<cr>]]
screen:expect {
grid = [[
{24: + [No Name] }{5: [No Name] }{2: }{24:X}|
^ |
{1:~ }|*7
:tabnew |
]],
}
feed [[<C-w>-<C-w>-]]
screen:expect {
grid = [[
{24: + [No Name] }{5: [No Name] }{2: }{24:X}|
^ |
{1:~ }|*5
|*3
]],
}
feed [[gt]]
screen:expect {
grid = [[
{5: + [No Name] }{24: [No Name] }{2: }{24:X}|
^0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
|
]],
}
feed [[gt]]
screen:expect([[
{24: + [No Name] }{5: [No Name] }{2: }{24:X}|
^ |
{1:~ }|*5
|*3
]])
end)
-- oldtest: Test_verbose_option()
it('prints every executed Ex command if verbose >= 16', function()
local screen = Screen.new(60, 12)
exec([[
command DoSomething echo 'hello' |set ts=4 |let v = '123' |echo v
call feedkeys("\r", 't') " for the hit-enter prompt
set verbose=20
]])
feed_command('DoSomething')
screen:expect([[
|
{1:~ }|*2
{3: }|
Executing: DoSomething |
Executing: echo 'hello' |set ts=4 |let v = '123' |echo v |
hello |
Executing: set ts=4 |let v = '123' |echo v |
Executing: let v = '123' |echo v |
Executing: echo v |
123 |
{6:Press ENTER or type command to continue}^ |
]])
end)
-- oldtest: Test_cmdline_redraw_tabline()
it('tabline is redrawn on entering cmdline', function()
local screen = Screen.new(30, 6)
exec([[
set showtabline=2
autocmd CmdlineEnter * set tabline=foo
]])
feed(':')
screen:expect([[
{2:foo }|
|
{1:~ }|*3
:^ |
]])
end)
-- oldtest: Test_wildmenu_with_input_func()
it('wildmenu works with input() function', function()
local screen = Screen.new(60, 8)
screen:add_extra_attr_ids({
[100] = { background = Screen.colors.Yellow, foreground = Screen.colors.Black },
})
feed(":call input('Command? ', '', 'command')<CR>")
screen:expect([[
|
{1:~ }|*6
Command? ^ |
]])
feed('ech<Tab>')
screen:expect([[
|
{1:~ }|*5
{100:echo}{3: echoerr echohl echomsg echon }|
Command? echo^ |
]])
feed('<Space>')
screen:expect([[
|
{1:~ }|*6
Command? echo ^ |
]])
feed('bufn<Tab>')
screen:expect([[
|
{1:~ }|*5
{100:bufname(}{3: bufnr( }|
Command? echo bufname(^ |
]])
feed('<CR>')
command('set wildoptions+=pum')
feed(":call input('Command? ', '', 'command')<CR>")
screen:expect([[
|
{1:~ }|*6
Command? ^ |
]])
feed('ech<Tab>')
screen:expect([[
|
{1:~ }|
{1:~ }{12: echo }{1: }|
{1:~ }{4: echoerr }{1: }|
{1:~ }{4: echohl }{1: }|
{1:~ }{4: echomsg }{1: }|
{1:~ }{4: echon }{1: }|
Command? echo^ |
]])
feed('<Space>')
screen:expect([[
|
{1:~ }|*6
Command? echo ^ |
]])
feed('bufn<Tab>')
screen:expect([[
|
{1:~ }|*4
{1:~ }{12: bufname( }{1: }|
{1:~ }{4: bufnr( }{1: }|
Command? echo bufname(^ |
]])
feed('<CR>')
end)
-- oldtest: Test_redraw_in_autocmd()
it('cmdline cursor position is correct after :redraw with cmdheight=2', function()
local screen = Screen.new(30, 6)
exec([[
set cmdheight=2
autocmd CmdlineChanged * redraw
]])
feed(':for i in range(3)<CR>')
screen:expect([[
|
{1:~ }|*3
:for i in range(3) |
: ^ |
]])
feed(':let i =')
-- Note: this may still be considered broken, ref #18140
screen:expect([[
|
{1:~ }|*3
: :let i =^ |
|
]])
end)
-- oldtest: Test_changing_cmdheight()
it("changing 'cmdheight'", function()
local screen = Screen.new(60, 8)
exec([[
set cmdheight=1 laststatus=2
func EchoOne()
set laststatus=2 cmdheight=1
echo 'foo'
echo 'bar'
set cmdheight=2
endfunc
func EchoTwo()
set laststatus=2
set cmdheight=5
echo 'foo'
echo 'bar'
set cmdheight=1
endfunc
]])
feed(':resize -3<CR>')
screen:expect([[
^ |
{1:~ }|*2
{3:[No Name] }|
|*4
]])
-- :resize now also changes 'cmdheight' accordingly
feed(':set cmdheight+=1<CR>')
screen:expect([[
^ |
{1:~ }|
{3:[No Name] }|
|*5
]])
-- using more space moves the status line up
feed(':set cmdheight+=1<CR>')
screen:expect([[
^ |
{3:[No Name] }|
|*6
]])
-- reducing cmdheight moves status line down
feed(':set cmdheight-=3<CR>')
screen:expect([[
^ |
{1:~ }|*3
{3:[No Name] }|
|*3
]])
-- reducing window size and then setting cmdheight
feed(':resize -1<CR>')
feed(':set cmdheight=1<CR>')
screen:expect([[
^ |
{1:~ }|*5
{3:[No Name] }|
|
]])
-- setting 'cmdheight' works after outputting two messages
feed(':call EchoTwo()')
screen:expect([[
|
{1:~ }|*5
{3:[No Name] }|
:call EchoTwo()^ |
]])
feed('<CR>')
screen:expect([[
^ |
{1:~ }|*5
{3:[No Name] }|
|
]])
-- increasing 'cmdheight' doesn't clear the messages that need hit-enter
feed(':call EchoOne()<CR>')
screen:expect([[
|
{1:~ }|*3
{3: }|
foo |
bar |
{6:Press ENTER or type command to continue}^ |
]])
-- window commands do not reduce 'cmdheight' to value lower than :set by user
feed('<CR>:wincmd _<CR>')
screen:expect([[
^ |
{1:~ }|*4
{3:[No Name] }|
:wincmd _ |
|
]])
end)
-- oldtest: Test_cmdheight_tabline()
it("changing 'cmdheight' when there is a tabline", function()
local screen = Screen.new(60, 8)
api.nvim_set_option_value('laststatus', 2, {})
api.nvim_set_option_value('showtabline', 2, {})
api.nvim_set_option_value('cmdheight', 1, {})
screen:expect([[
{5: [No Name] }{2: }|
^ |
{1:~ }|*4
{3:[No Name] }|
|
]])
end)
-- oldtest: Test_rulerformat_position()
it("ruler has correct position with 'rulerformat' set", function()
local screen = Screen.new(20, 3)
api.nvim_set_option_value('ruler', true, {})
api.nvim_set_option_value('rulerformat', 'longish', {})
api.nvim_set_option_value('laststatus', 0, {})
api.nvim_set_option_value('winwidth', 1, {})
feed [[<C-W>v<C-W>|<C-W>p]]
screen:expect [[
│^ |
{1:~ }│{1:~}|
longish |
]]
end)
-- oldtest: Test_rulerformat_function()
it("'rulerformat' can use %!", function()
local screen = Screen.new(40, 2)
exec([[
func TestRulerFn()
return '10,20%=30%%'
endfunc
]])
api.nvim_set_option_value('ruler', true, {})
api.nvim_set_option_value('rulerformat', '%!TestRulerFn()', {})
screen:expect([[
^ |
10,20 30% |
]])
end)
-- oldtest: Test_search_wildmenu_screendump()
it('wildmenu for search completion', function()
local screen = Screen.new(60, 10)
screen:add_extra_attr_ids({
[100] = { background = Screen.colors.Yellow, foreground = Screen.colors.Black },
})
exec([[
set wildmenu wildcharm=<f5> wildoptions-=pum
call setline(1, ['the', 'these', 'the', 'foobar', 'thethe', 'thethere'])
]])
-- Pattern has newline at EOF
feed('gg2j/e\\n<f5>')
screen:expect([[
the |
these |
the |
foobar |
thethe |
thethere |
{1:~ }|*2
{100:e\nfoobar}{3: e\nthethere e\nthese e\nthe }|
/e\nfoobar^ |
]])
-- longest:full
feed('<esc>')
command('set wim=longest,full')
feed('gg/t<f5>')
screen:expect([[
the |
these |
the |
foobar |
thethe |
thethere |
{1:~ }|*3
/the^ |
]])
-- list:full
feed('<esc>')
command('set wim=list,full')
feed('gg/t<f5>')
screen:expect([[
{10:t}he |
{10:t}hese |
{10:t}he |
foobar |
{10:t}he{10:t}he |
{10:t}he{10:t}here |
{3: }|
/t |
these the thethe thethere there |
/t^ |
]])
-- noselect:full
feed('<esc>')
command('set wim=noselect,full')
feed('gg/t<f5>')
screen:expect([[
the |
these |
the |
foobar |
thethe |
thethere |
{1:~ }|*2
{3:these the thethe thethere there }|
/t^ |
]])
-- Multiline
feed('<esc>gg/t.*\\n.*\\n.<tab>')
screen:expect([[
the |
these |
the |
foobar |
thethe |
thethere |
{1:~ }|*2
{3:t.*\n.*\n.oobar t.*\n.*\n.hethe t.*\n.*\n.he }|
/t.*\n.*\n.^ |
]])
feed('<esc>')
end)
end)
describe('cmdwin', function()
before_each(clear)
-- oldtest: Test_cmdwin_interrupted()
it('still uses a new buffer when interrupting more prompt on open', function()
local screen = Screen.new(30, 16)
command('set more')
command('autocmd WinNew * highlight')
feed('q:')
screen:expect({ any = pesc('{6:-- More --}^') })
feed('q')
screen:expect([[
|
{1:~ }|*5
{2:[No Name] }|
{1::}^ |
{1:~ }|*6
{3:[Command Line] }|
|
]])
feed([[aecho 'done']])
screen:expect([[
|
{1:~ }|*5
{2:[No Name] }|
{1::}echo 'done'^ |
{1:~ }|*6
{3:[Command Line] }|
{5:-- INSERT --} |
]])
feed('<CR>')
screen:expect([[
^ |
{1:~ }|*14
done |
]])
end)
end)