fix(cmdwin): handle control characters properly #40488

- Replace newlines in the current cmdline with NULs when opening cmdwin,
  and do the reverse when putting a cmdwin line back into the cmdline.
- Escape control characters with Ctrl-V when feeding cmdline.
This commit is contained in:
zeertzjq
2026-06-30 20:00:31 +08:00
committed by GitHub
parent f89972e13a
commit 6c5f0cf29f
2 changed files with 31 additions and 8 deletions

View File

@@ -81,9 +81,10 @@ function M.open(type, init_line, init_col)
vim.wo[win][0].statuscolumn = '%#NonText#' .. type
local filled = fill_history(buf, type)
init_line = init_line and init_line:gsub('\n', '\0') or ''
-- Append the in-flight cmdline as the last line (or only line if history is empty).
vim.api.nvim_buf_set_lines(buf, filled and -1 or 0, -1, false, { init_line or '' })
vim.api.nvim_buf_set_lines(buf, filled and -1 or 0, -1, false, { init_line })
local last = vim.api.nvim_buf_line_count(buf)
vim.api.nvim_win_set_cursor(win, { last, math.max(0, (init_col or 1) - 1) })
@@ -92,6 +93,11 @@ function M.open(type, init_line, init_col)
vim.bo[buf].filetype = 'vim'
end
if vim.o.wildchar == ('\t'):byte() then
vim.keymap.set('i', '<Tab>', '<C-X><C-V>', { buf = buf })
vim.keymap.set('n', '<Tab>', 'a<C-X><C-V>', { buf = buf })
end
vim.api.nvim__cmdwin_set(type, buf) -- Update the C-side globals.
state = {
@@ -155,6 +161,7 @@ function M.confirm()
return
end
local line, type = _close()
line = line:gsub('%z', '\n'):gsub('(%c)', '\022%1') -- Escape control characters.
vim.api.nvim_feedkeys(type .. line .. vim.keycode('<CR>'), 'nt', false)
end
@@ -164,6 +171,7 @@ function M.cancel()
return
end
local line, type = _close()
line = line:gsub('%z', '\n'):gsub('(%c)', '\022%1') -- Escape control characters.
vim.api.nvim_feedkeys(type .. line, 'nt', false)
end

View File

@@ -81,13 +81,6 @@ describe('cmdwin', function()
eq(2, fn.histnr('cmd')) -- History unchanged (the in-flight cmdline was not added).
end)
it('history entry with literal newline char', function()
fn.histadd(':', 'echo \n x')
feed('q:')
eq(':', fn.getcmdwintype())
eq('echo \0 x', api.nvim_buf_get_lines(0, 0, 1, false)[1])
end)
it('<C-C> in normal mode cancels without executing', function()
feed('q:')
feed('ilet g:executed = 1<Esc>')
@@ -102,6 +95,28 @@ describe('cmdwin', function()
eq(':', fn.getcmdtype())
end)
it('history entry or current cmdline with control chars', function()
local firstbuf = api.nvim_get_current_buf()
local cmdline = 'normal! \023\022ifoo\nbar\027' -- Ctrl-W Ctrl-V ifoo\nbar Esc
local bufline = cmdline:gsub('\n', '\0')
fn.histadd(':', cmdline)
feed('q:')
eq(':', fn.getcmdwintype())
eq({ bufline, '' }, api.nvim_buf_get_lines(0, 0, -1, false))
api.nvim_win_set_cursor(0, { 1, 0 })
feed('<C-C>')
eq(cmdline, fn.getcmdline())
eq(':', fn.getcmdtype())
eq({ firstbuf }, fn.tabpagebuflist())
feed('<C-F>')
n.poke_eventloop()
eq(':', fn.getcmdwintype())
eq({ bufline, bufline }, api.nvim_buf_get_lines(0, 0, -1, false))
feed('<CR>')
eq({ firstbuf, firstbuf }, fn.tabpagebuflist())
eq({ 'foo', 'bar' }, api.nvim_buf_get_lines(0, 0, -1, false))
end)
it('async API calls work while cmdwin is open #40312', function()
feed('q:')
eq(':', fn.getcmdwintype())