mirror of
https://github.com/neovim/neovim.git
synced 2026-07-22 17:02:59 +00:00
Problem:
cmdwin (the `:q` cmdline buffer) has various limitations which require
special-casing all over the codebase.
Besides complicating the code, it also breaks async plugins if they try
to create buffers/windows after some work is done, if the user happens
to open cmdwin at the wrong the moment:
Lua callback: …/guh.nvim/lua/guh/util.lua:531:
E11: Invalid in command-line window; <CR> executes, CTRL-C quits
stack traceback:
[C]: in function 'nvim_buf_delete'
…/guh.nvim/lua/guh/util.lua:531: in function <…/guh.nvim/lua/guh/util.lua:526>
Solution:
Just say no to "inception". Reimplement cmdwin as a normal buffer+window.
All of the cmdwin contortions (in both core, and innocent plugins) exist
literally only to support "inception": recursive
cmdwin-in-cmdline-things, like `<c-r>=`, `/`, search-during-substitute,
`:input()`, etc. So we just won't support that (though I have
a potential plan for that later, which I call "modal parking lot").
The benefit is that plugins, and core, no longer have to care about
cmdwin.
BONUS:
- mouse-drag on vertical separators works (it only worked for
horizontal/statusline before)
- inccommand-in-cmdwin now works correctly, for free (thus don't need
#40077).
POTENTIAL FOLLOWUPS
- Drop `CHECK_CMDWIN` ("E11: Invalid in command-line window"), allow chaos.
- Unify `BUFLOCK_OK` / `LOCK_OK` ?
DESIGN:
- Eliminate lots of C globals, `EX_CMDWIN`, etc.
- `text_locked()` no longer reports true for cmdwin.
- cmdwin = a normal window with 'winfixbuf', 'bufhidden=wipe',
'buftype=nofile'. Invariants come from those options rather than
special cases throughout the codebase.
- `nv_record` for q:/q//q? calls Lua
`nlua_call_vimfn("vim._core.cmdwin", …)`. No `K_CMDWIN`
/ cmdline-reader detour.
- `cedit_key` (`c_CTRL-F`) schedules a deferred event that calls
`vim._core.cmdwin.open(type, content, pos)` and returns `Ctrl_C` so
the in-flight cmdline cancels. Reader state is not serialized; instead
the captured `(type, line, col)` is replayed via
`nvim_feedkeys(type..line.."<CR>", "nt", …)` after user confirms.
- On confirm/cancel: `<CR>` / `<C-C>` calls into Lua which closes the
window and re-feeds the cmdline.
BREAKING CHANGES:
- Expression-register cmdline (`<C-R>=` from insert-mode) no longer
supports cmdwin. Same applies to `input()` / `inputlist()` (already
covered by `text_locked`).
- Usage of cmdwin in macros/mappings will probably break (assuming they
ever worked).
278 lines
7.9 KiB
VimL
278 lines
7.9 KiB
VimL
" Tests for editing the command line.
|
||
|
||
source check.vim
|
||
source screendump.vim
|
||
|
||
|
||
func Test_cant_open_cmdwin_in_cmdwin()
|
||
try
|
||
call feedkeys("q:q::q\<CR>", "x!")
|
||
catch
|
||
let caught = v:exception
|
||
endtry
|
||
call assert_match('E1292:', caught)
|
||
endfunc
|
||
|
||
func Test_cmdwin_virtual_edit()
|
||
enew!
|
||
set ve=all cpo+=$
|
||
silent normal q/s
|
||
|
||
set ve= cpo-=$
|
||
endfunc
|
||
|
||
" Check that a :normal command can be used to stop Visual mode without side
|
||
" effects.
|
||
func Test_normal_escape()
|
||
throw 'Skipped: Nvim supports cmdwin freedom #40312'
|
||
call feedkeys("q:i\" foo\<Esc>:normal! \<C-V>\<Esc>\<CR>:\" bar\<CR>", 'ntx')
|
||
call assert_equal('" bar', @:)
|
||
endfunc
|
||
|
||
" This was using a pointer to a freed buffer
|
||
func Test_cmdwin_freed_buffer_ptr()
|
||
throw "Skipped: Nvim supports cmdwin freedom #40312"
|
||
" this does not work on MS-Windows because renaming an open file fails
|
||
CheckNotMSWindows
|
||
|
||
au BufEnter * next 0| file
|
||
edit 0
|
||
silent! norm q/
|
||
|
||
au! BufEnter
|
||
bwipe!
|
||
endfunc
|
||
|
||
" This was resulting in a window with negative width.
|
||
" The test doesn't reproduce the illegal memory access though...
|
||
func Test_cmdwin_split_often()
|
||
let lines = &lines
|
||
let columns = &columns
|
||
set t_WS=
|
||
|
||
try
|
||
" set encoding=iso8859
|
||
set ruler
|
||
winsize 0 0
|
||
noremap 0 H
|
||
sil norm 0000000q:
|
||
catch /E36:/
|
||
endtry
|
||
|
||
bwipe!
|
||
set encoding=utf8
|
||
let &lines = lines
|
||
let &columns = columns
|
||
endfunc
|
||
|
||
func Test_cmdwin_restore_heights()
|
||
set showtabline=0 cmdheight=2 laststatus=0
|
||
call feedkeys("q::set cmdheight=1\<CR>:q\<CR>", 'ntx')
|
||
call assert_equal(&lines - 1, winheight(0))
|
||
|
||
set showtabline=2 cmdheight=3
|
||
call feedkeys("q::set showtabline=0\<CR>:q\<CR>", 'ntx')
|
||
call assert_equal(&lines - 3, winheight(0))
|
||
|
||
set cmdheight=1 laststatus=2
|
||
call feedkeys("q::set laststatus=0\<CR>:q\<CR>", 'ntx')
|
||
call assert_equal(&lines - 1, winheight(0))
|
||
|
||
set laststatus=2
|
||
call feedkeys("q::set laststatus=1\<CR>:q\<CR>", 'ntx')
|
||
call assert_equal(&lines - 1, winheight(0))
|
||
|
||
set laststatus=2
|
||
belowright vsplit
|
||
wincmd _
|
||
let restcmds = winrestcmd()
|
||
call feedkeys("q::set laststatus=1\<CR>:q\<CR>", 'ntx')
|
||
" As we have 2 windows, &ls = 1 should still have a statusline on the last
|
||
" window. As such, the number of available rows hasn't changed and the window
|
||
" sizes should be restored.
|
||
call assert_equal(restcmds, winrestcmd())
|
||
|
||
set cmdheight& showtabline& laststatus&
|
||
endfunc
|
||
|
||
func Test_cmdwin_temp_curwin()
|
||
throw "Skipped: Nvim supports cmdwin freedom #40312"
|
||
func CheckWraps(expect_wrap)
|
||
setlocal textwidth=0 wrapmargin=1
|
||
|
||
call deletebufline('', 1, '$')
|
||
let as = repeat('a', winwidth(0) - 2 - &wrapmargin)
|
||
call setline(1, as .. ' b')
|
||
normal! gww
|
||
|
||
setlocal textwidth& wrapmargin&
|
||
call assert_equal(a:expect_wrap ? [as, 'b'] : [as .. ' b'], getline(1, '$'))
|
||
endfunc
|
||
|
||
func CheckCmdWin()
|
||
call assert_equal('command', win_gettype())
|
||
" textoff and &wrapmargin formatting considers the cmdwin_type char.
|
||
call assert_equal(1, getwininfo(win_getid())[0].textoff)
|
||
call CheckWraps(1)
|
||
endfunc
|
||
|
||
func CheckOtherWin()
|
||
call assert_equal('', win_gettype())
|
||
call assert_equal(0, getwininfo(win_getid())[0].textoff)
|
||
call CheckWraps(0)
|
||
endfunc
|
||
|
||
call feedkeys("q::call CheckCmdWin()\<CR>:call win_execute(win_getid(winnr('#')), 'call CheckOtherWin()')\<CR>:q<CR>", 'ntx')
|
||
|
||
%bwipe!
|
||
delfunc CheckWraps
|
||
delfunc CheckCmdWin
|
||
delfunc CheckOtherWin
|
||
endfunc
|
||
|
||
func Test_cmdwin_interrupted()
|
||
throw "Skipped: Nvim supports cmdwin freedom #40312"
|
||
func CheckInterrupted()
|
||
call feedkeys("q::call assert_equal('', getcmdwintype())\<CR>:call assert_equal('', getcmdtype())\<CR>:q<CR>", 'ntx')
|
||
endfunc
|
||
|
||
augroup CmdWin
|
||
|
||
" While opening the cmdwin's split:
|
||
" Close the cmdwin's window.
|
||
au WinEnter * ++once quit
|
||
call CheckInterrupted()
|
||
|
||
" Close the old window.
|
||
au WinEnter * ++once execute winnr('#') 'quit'
|
||
call CheckInterrupted()
|
||
|
||
" Switch back to the old window.
|
||
au WinEnter * ++once wincmd p
|
||
call CheckInterrupted()
|
||
|
||
" Change the old window's buffer.
|
||
au WinEnter * ++once call win_execute(win_getid(winnr('#')), 'enew')
|
||
call CheckInterrupted()
|
||
|
||
" Using BufLeave autocmds as cmdwin restrictions do not apply to them when
|
||
" fired from opening the cmdwin...
|
||
" After opening the cmdwin's split, while creating the cmdwin's buffer:
|
||
" Delete the cmdwin's buffer.
|
||
au BufLeave * ++once bwipe
|
||
call CheckInterrupted()
|
||
|
||
" Close the cmdwin's window.
|
||
au BufLeave * ++once quit
|
||
call CheckInterrupted()
|
||
|
||
" Close the old window.
|
||
au BufLeave * ++once execute winnr('#') 'quit'
|
||
call CheckInterrupted()
|
||
|
||
" Switch to a different window.
|
||
au BufLeave * ++once split
|
||
call CheckInterrupted()
|
||
|
||
" Change the old window's buffer.
|
||
au BufLeave * ++once call win_execute(win_getid(winnr('#')), 'enew')
|
||
call CheckInterrupted()
|
||
|
||
" However, changing the current buffer is OK and does not interrupt.
|
||
au BufLeave * ++once edit other
|
||
call feedkeys("q::let t=getcmdwintype()\<CR>:let b=bufnr()\<CR>:clo<CR>", 'ntx')
|
||
call assert_equal(':', t)
|
||
call assert_equal(1, bufloaded('other'))
|
||
call assert_notequal(b, bufnr('other'))
|
||
|
||
augroup END
|
||
|
||
" No autocmds should remain, but clear the augroup to be sure.
|
||
augroup CmdWin
|
||
au!
|
||
augroup END
|
||
|
||
%bwipe!
|
||
delfunc CheckInterrupted
|
||
endfunc
|
||
|
||
func Test_cmdwin_existing_bufname()
|
||
throw "Skipped: Nvim supports cmdwin freedom #40312"
|
||
func CheckName()
|
||
call assert_equal(1, getbufinfo('')[0].command)
|
||
call assert_equal(0, getbufinfo('[Command Line]')[0].command)
|
||
call assert_match('#a\s*"\[Command Line\]"', execute('ls'))
|
||
call assert_match('%a\s*"\[Command Line\]"', execute('ls'))
|
||
endfunc
|
||
file [Command Line]
|
||
call feedkeys("q::call CheckName()\<CR>:q\<CR>", 'ntx')
|
||
0file
|
||
delfunc CheckName
|
||
endfunc
|
||
|
||
func Test_cmdwin_showcmd()
|
||
CheckScreendump
|
||
|
||
let lines =<< trim [SCRIPT]
|
||
augroup vimHints | au! | augroup END
|
||
set showcmd
|
||
[SCRIPT]
|
||
call writefile(lines, 'XTest_cmdwin_showcmd', 'D')
|
||
let buf = RunVimInTerminal('-S XTest_cmdwin_showcmd', {'rows': 18})
|
||
|
||
for keys in ['q:', ":\<C-F>"]
|
||
call term_sendkeys(buf, keys)
|
||
call VerifyScreenDump(buf, 'Test_cmdwin_showcmd_1', {})
|
||
call term_sendkeys(buf, '"')
|
||
call WaitForAssert({-> assert_match('^: \+" *$', term_getline(buf, 18))})
|
||
call term_sendkeys(buf, 'x')
|
||
call WaitForAssert({-> assert_match('^: \+"x *$', term_getline(buf, 18))})
|
||
call term_sendkeys(buf, 'y')
|
||
call WaitForAssert({-> assert_match('^: \+"xy *$', term_getline(buf, 18))})
|
||
call term_sendkeys(buf, 'y')
|
||
call WaitForAssert({-> assert_match('^: \+$', term_getline(buf, 18))})
|
||
call term_sendkeys(buf, "\<C-C>\<C-C>")
|
||
call VerifyScreenDump(buf, 'Test_cmdwin_showcmd_2', {})
|
||
endfor
|
||
|
||
" clean up
|
||
call StopVimInTerminal(buf)
|
||
endfunc
|
||
|
||
func Test_cmdwin_cursor_position()
|
||
throw "Skipped: Nvim supports cmdwin freedom #40312"
|
||
" When the cmdline fills the screen width exactly, pressing CTRL-F to open
|
||
" the cmdwin should place the cursor on the last character, not past it.
|
||
let cmd = 'echo "' .. repeat('a', &columns - 8) .. '"'
|
||
call assert_equal(&columns - 1, len(cmd))
|
||
let g:cmdwin_col = 0
|
||
let g:cmdwin_line = ''
|
||
call feedkeys(':' .. cmd .. "\<C-F>" ..
|
||
\ ":let g:cmdwin_col = col('.')\<CR>" ..
|
||
\ ":let g:cmdwin_line = getline('.')\<CR>" ..
|
||
\ ":q\<CR>", 'x!')
|
||
call assert_equal(len(cmd), g:cmdwin_col)
|
||
call assert_equal(cmd, g:cmdwin_line)
|
||
unlet g:cmdwin_col g:cmdwin_line
|
||
endfunc
|
||
|
||
func Test_cmdwin_no_prefix_on_wrapped_line()
|
||
CheckScreendump
|
||
|
||
let lines =<< trim END
|
||
augroup vimHints | au! | augroup END
|
||
END
|
||
call writefile(lines, 'Xcmdwin_wrap', 'D')
|
||
|
||
let buf = RunVimInTerminal('-S Xcmdwin_wrap', #{rows: 12, cols: 40})
|
||
let cmd = 'echo "' .. repeat('a', 40 - 8) .. '"XYZ'
|
||
call term_sendkeys(buf, ':' .. cmd .. "\<C-F>")
|
||
call TermWait(buf, 100)
|
||
call VerifyScreenDump(buf, 'Test_cmdwin_wrap_prefix', {})
|
||
|
||
call term_sendkeys(buf, ":q\<CR>")
|
||
call StopVimInTerminal(buf)
|
||
endfunc
|
||
|
||
" vim: shiftwidth=2 sts=2 expandtab
|