From 6c5f0cf29f3499cc011b83998f3521b4cb0de8f7 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 30 Jun 2026 20:00:31 +0800 Subject: [PATCH] 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. --- runtime/lua/vim/_core/cmdwin.lua | 10 ++++++++- test/functional/editor/cmdwin_spec.lua | 29 +++++++++++++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/runtime/lua/vim/_core/cmdwin.lua b/runtime/lua/vim/_core/cmdwin.lua index 05bc112b33..06475f29e9 100644 --- a/runtime/lua/vim/_core/cmdwin.lua +++ b/runtime/lua/vim/_core/cmdwin.lua @@ -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', '', '', { buf = buf }) + vim.keymap.set('n', '', 'a', { 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(''), '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 diff --git a/test/functional/editor/cmdwin_spec.lua b/test/functional/editor/cmdwin_spec.lua index a78a9cae3c..0deb4040c9 100644 --- a/test/functional/editor/cmdwin_spec.lua +++ b/test/functional/editor/cmdwin_spec.lua @@ -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(' in normal mode cancels without executing', function() feed('q:') feed('ilet g:executed = 1') @@ -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('') + eq(cmdline, fn.getcmdline()) + eq(':', fn.getcmdtype()) + eq({ firstbuf }, fn.tabpagebuflist()) + feed('') + n.poke_eventloop() + eq(':', fn.getcmdwintype()) + eq({ bufline, bufline }, api.nvim_buf_get_lines(0, 0, -1, false)) + feed('') + 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())