Files
neovim/test/functional/core/main_spec.lua
Justin M. Keyes b2bf7bcfb1 feat(cmdwin): implement cmdwin as a normal buf+win
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).
2026-06-25 18:36:24 +02:00

266 lines
7.9 KiB
Lua

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local uv = vim.uv
local eq = t.eq
local matches = t.matches
local eval = n.eval
local clear = n.clear
local fn = n.fn
local write_file = t.write_file
local is_os = t.is_os
describe('command-line option', function()
describe('-s', function()
local fname = 'Xtest-functional-core-main-s'
local fname_2 = fname .. '.2'
local nonexistent_fname = fname .. '.nonexistent'
local dollar_fname = '$' .. fname
before_each(function()
clear()
os.remove(fname)
os.remove(dollar_fname)
end)
after_each(function()
os.remove(fname)
os.remove(dollar_fname)
end)
it('treats - as stdin', function()
eq(nil, uv.fs_stat(fname))
fn.system({
n.nvim_prog,
'-u',
'NONE',
'-i',
'NONE',
'--headless',
'--cmd',
'set noswapfile shortmess+=IFW fileformats=unix',
'-s',
'-',
fname,
}, { ':call setline(1, "42")', ':wqall!', '' })
eq(0, eval('v:shell_error'))
local attrs = uv.fs_stat(fname)
eq(#'42\n', attrs.size)
end)
it('does not expand $VAR', function()
eq(nil, uv.fs_stat(fname))
eq(true, not not dollar_fname:find('%$%w+'))
write_file(dollar_fname, ':call setline(1, "100500")\n:wqall!\n')
local p = n.spawn_wait(
'--cmd',
'set noswapfile shortmess+=IFW fileformats=unix',
'-s',
dollar_fname,
fname
)
eq(0, p.status)
local attrs = uv.fs_stat(fname)
eq(#'100500\n', attrs.size)
end)
it('does not crash when running completion in Ex mode', function()
local p =
n.spawn_wait('--clean', '-e', '-s', '--cmd', 'exe "norm! i\\<C-X>\\<C-V>"', '--cmd', 'qa!')
eq(0, p.status)
end)
it('does not crash when running completion from -l script', function()
local lua_fname = 'Xinscompl.lua'
write_file(lua_fname, [=[vim.cmd([[exe "norm! i\<C-X>\<C-V>"]])]=])
finally(function()
os.remove(lua_fname)
end)
local p = n.spawn_wait('--clean', '-l', lua_fname)
eq(0, p.status)
end)
it('does not crash after reading from stdin in non-headless mode', function()
-- Repro from fdfa1ed (2017): the empty `echo ""` pipe makes scriptin (`-s -`) hit EOF
-- immediately and call closescript(); if scriptin held fd 0 directly, libuv asserts.
--
-- Note: can't use nvim_chan_send because pty stdin never EOFs.
local nvim_set = 'set noswapfile shortmess+=IFW fileformats=unix notermguicolors'
local shell_cmd = ([[echo "" | %s "%s" --clean --cmd "%s" -s -]]):format(
is_os('win') and '&' or '',
n.nvim_prog,
nvim_set
)
if is_os('win') then
-- Use PowerShell; cmd.exe mis-parses libuv's `\"…\"` (MSVCRT quoting).
n.set_shell_powershell()
end
local screen = Screen.new(40, 8)
fn.jobstart(shell_cmd, {
term = true,
env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
})
-- First screen confirms Nvim reached TUI (didn't crash closing scriptin).
screen:expect(
[[
^ |
~ |*4
{1:[No Name] 0,0-1 All}|
|*2
]],
{ [1] = { reverse = true } }
)
-- Exit with code 42 to avoid false positives.
n.feed('i:cq 42<CR>')
if is_os('win') then
-- XXX: `:cq 42` isn't reaching Nvim, or Nvim exits 1 anyway?
screen:expect({ any = '%[Process exited 1%]' })
else
screen:expect({ any = '%[Process exited 42%]' })
end
--[=[ Example of incorrect output:
screen:expect([[
^nvim: /var/tmp/portage/dev-libs/libuv-1.|
10.2/work/libuv-1.10.2/src/unix/core.c:5|
19: uv__close: Assertion `fd > STDERR_FI|
LENO' failed. |
|
[Process exited 6] |
|*2
]])
]=]
end)
it('fails when trying to use nonexistent file with -s', function()
local p = n.spawn_wait(
'--cmd',
'set noswapfile shortmess+=IFW fileformats=unix',
'--cmd',
'language C',
'-s',
nonexistent_fname
)
eq(
'Cannot open for reading: "' .. nonexistent_fname .. '": no such file or directory\n',
--- TODO(justinmk): using `p.output` because Nvim emits CRLF even on non-Win. Emit LF instead?
p:output()
)
eq(2, p.status)
end)
it('errors out when trying to use -s twice', function()
write_file(fname, ':call setline(1, "1")\n:wqall!\n')
write_file(dollar_fname, ':call setline(1, "2")\n:wqall!\n')
local p = n.spawn_wait(
'--cmd',
'set noswapfile shortmess+=IFW fileformats=unix',
'--cmd',
'language C',
'-s',
fname,
'-s',
dollar_fname,
fname_2
)
--- TODO(justinmk): using `p.output` because Nvim emits CRLF even on non-Win. Emit LF instead?
eq('Attempt to open script file again: "-s ' .. dollar_fname .. '"\n', p:output())
eq(2, p.status)
eq(nil, uv.fs_stat(fname_2))
end)
end)
it('nvim -v, :version', function()
matches('Run ":verbose version"', fn.execute(':version'))
matches('fall%-back for %$VIM: .*Run :checkhealth', fn.execute(':verbose version'))
matches('Run "nvim %-V1 %-v"', n.spawn_wait('-v').stdout)
matches('fall%-back for %$VIM: .*Run :checkhealth', n.spawn_wait('-V1', '-v').stdout)
end)
if is_os('win') then
for _, prefix in ipairs({ '~/', '~\\' }) do
it('expands ' .. prefix .. ' on Windows', function()
local fname = os.getenv('USERPROFILE') .. '\\nvim_test.txt'
finally(function()
os.remove(fname)
end)
write_file(fname, 'some text')
eq(
'some text',
fn.system({
n.nvim_prog,
'-es',
'+%print',
'+q',
prefix .. 'nvim_test.txt',
}):gsub('\n', '')
)
end)
end
end
end)
describe('vim._core', function()
it('works with "-u NONE" and no VIMRUNTIME', function()
clear {
args_rm = { '-u' },
args = { '-u', 'NONE' },
env = { VIMRUNTIME = 'non-existent' },
}
-- `vim.hl` is NOT a builtin module.
t.matches("^module 'vim%.hl' not found:", t.pcall_err(n.exec_lua, [[require('vim.hl')]]))
-- All `vim._core.*` modules are builtin.
t.eq(
{ 'rebind_after_restart', 'serverlist' },
n.exec_lua([[local k = vim.tbl_keys(require('vim._core.server')); table.sort(k); return k]])
)
local expected = {
'vim.F',
'vim._core.cmdwin',
'vim._core.defaults',
'vim._core.editor',
'vim._core.ex_cmd',
'vim._core.exrc',
'vim._core.help',
'vim._core.log',
'vim._core.options',
'vim._core.server',
'vim._core.shared',
'vim._core.spell',
'vim._core.stringbuffer',
'vim._core.swapfile',
'vim._core.system',
'vim._core.table',
'vim._core.tag',
'vim._core.time',
'vim._core.ui',
'vim._core.ui2',
'vim._core.util',
'vim._core.vimfn',
'vim._init_packages',
'vim.filetype',
'vim.fs',
'vim.inspect',
'vim.keymap',
'vim.loader',
'vim.text',
'vim.tty',
}
if n.exec_lua [[return not not _G.jit]] then
expected = vim.list_extend({
'ffi',
'jit.profile',
'jit.util',
'string.buffer',
'table.clear',
'table.new',
}, expected)
end
t.eq(expected, n.exec_lua([[local t = vim.tbl_keys(package.preload); table.sort(t); return t]]))
end)
end)