fix(ui2): only highlight Ex command lines in the cmdline #38182

Problem:  Prompts and message text (in block mode) in the cmdline are
          parsed and highlighted as if it is Vimscript.
          Entering the cmdline while it is expanded can work more like
          it does with UI1, where the press enter prompt is replaced
          and previous messages stay on the message grid, while
          subsequent messages are placed below it.
Solution: Highlight manually with string parser on lines starting with ':'.
          Spoof cmdline block mode when the cmdline is entered while it
          is expanded.
This commit is contained in:
luukvbaal
2026-03-06 19:29:20 +01:00
committed by GitHub
parent ac6cf5b03b
commit 301c7065ca
4 changed files with 74 additions and 51 deletions

View File

@@ -2,7 +2,6 @@ local ui = require('vim._core.ui2')
local api, fn = vim.api, vim.fn
---@class vim._core.ui2.cmdline
local M = {
highlighter = nil, ---@type vim.treesitter.highlighter?
indent = 0, -- Current indent for block event.
prompt = false, -- Whether a prompt is active; route to dialog regardless of ui.cfg.msg.target.
dialog = false, -- Whether a dialog window was opened.
@@ -44,7 +43,8 @@ local promptlen = 0 -- Current length of the last line in the prompt.
---@alias CmdContent CmdChunk[]
---@param content CmdContent
---@param prompt string
local function set_text(content, prompt)
---@param hl_id integer Prompt highlight group.
local function set_text(content, prompt, hl_id)
local lines = {} ---@type string[]
for line in (prompt .. '\n'):gmatch('(.-)\n') do
lines[#lines + 1] = fn.strtrans(line)
@@ -55,6 +55,29 @@ local function set_text(content, prompt)
end
lines[#lines] = ('%s%s '):format(lines[#lines], fn.strtrans(cmdbuff))
api.nvim_buf_set_lines(ui.bufs.cmd, M.srow, -1, false, lines)
-- Highlight prompt, or parse and highlight line starting with ':' as Vimscript.
if promptlen > 0 and hl_id > 0 then
local opts = { invalidate = true, undo_restore = false, end_col = promptlen, hl_group = hl_id }
opts.end_line = M.erow
api.nvim_buf_set_extmark(ui.bufs.cmd, ui.ns, M.srow, 0, opts)
elseif lines[1]:sub(1, 1) == ':' then
local parser = vim.treesitter.get_string_parser(lines[1], 'vim')
parser:parse(true)
parser:for_each_tree(function(tstree, tree)
local query = tstree and vim.treesitter.query.get(tree:lang(), 'highlights')
if query then
for capture, node in query:iter_captures(tstree:root(), lines[1]) do
local _, start_col, _, end_col = node:range()
if query.captures[capture]:sub(1, 1) ~= '_' then
local opts = { invalidate = true, undo_restore = false, end_col = end_col }
opts.hl_group = ('@%s.%s'):format(query.captures[capture], query.lang)
api.nvim_buf_set_extmark(ui.bufs.cmd, ui.ns, M.srow, start_col, opts)
end
end
end
end)
end
end
--- Set the cmdline buffer text and cursor position.
@@ -67,22 +90,17 @@ end
---@param level integer
---@param hl_id integer
function M.cmdline_show(content, pos, firstc, prompt, indent, level, hl_id)
M.level, M.indent, M.prompt = level, indent, #prompt > 0
if M.highlighter == nil or M.highlighter.bufnr ~= ui.bufs.cmd then
local parser = assert(vim.treesitter.get_parser(ui.bufs.cmd, 'vim', {}))
M.highlighter = vim.treesitter.highlighter.new(parser)
end
-- Only enable TS highlighter for Ex commands (not search or filter commands).
M.highlighter.active[ui.bufs.cmd] = firstc == ':' and M.highlighter or nil
if ui.msg.cmd.msg_row ~= -1 then
-- When entering the cmdline while it is expanded, place cmdline below messages.
if M.level == 0 and ui.msg.cmd_on_key then
M.srow = api.nvim_buf_line_count(ui.bufs.cmd)
vim.on_key(nil, ui.msg.cmd_on_key)
elseif ui.msg.cmd.msg_row ~= -1 and not ui.msg.cmd_on_key then
ui.msg.msg_clear()
end
ui.msg.virt.last = { {}, {}, {}, {} }
set_text(content, ('%s%s%s'):format(firstc, prompt, (' '):rep(indent)))
if promptlen > 0 and hl_id > 0 then
api.nvim_buf_set_extmark(ui.bufs.cmd, ui.ns, 0, 0, { hl_group = hl_id, end_col = promptlen })
end
M.level, M.indent, M.prompt = level, indent, #prompt > 0
set_text(content, ('%s%s%s'):format(firstc, prompt, (' '):rep(indent)), hl_id)
ui.msg.virt.last = { {}, {}, {}, {} }
local height = math.max(ui.cmdheight, api.nvim_win_text_height(ui.wins.cmd, {}).all)
win_config(ui.wins.cmd, false, height)
@@ -125,7 +143,15 @@ end
---@param level integer
---@param abort boolean
function M.cmdline_hide(level, abort)
if M.srow > 0 or level > (fn.getcmdwintype() == '' and 1 or 2) then
if ui.msg.cmd_on_key then
if abort then
api.nvim_win_close(ui.wins.cmd, true)
ui.check_targets()
else
ui.msg.set_pos('cmd')
end
ui.msg.cmd_on_key, M.srow = nil, 0
elseif M.srow > 0 or level > (fn.getcmdwintype() == '' and 1 or 2) then
return -- No need to hide when still in nested cmdline or cmdline_block.
end
@@ -156,7 +182,7 @@ end
---@param lines CmdContent[]
function M.cmdline_block_show(lines)
for _, content in ipairs(lines) do
set_text(content, ':')
set_text(content, ':', 0)
M.srow = M.srow + 1
end
end
@@ -165,7 +191,7 @@ end
---
---@param line CmdContent
function M.cmdline_block_append(line)
set_text(line, ':')
set_text(line, ':', 0)
M.srow = M.srow + 1
end

View File

@@ -36,13 +36,13 @@ local M = {
delayed = false, -- Whether placement of 'last' virt_text is delayed.
},
dialog_on_key = nil, ---@type integer? vim.on_key namespace for paging in the dialog window.
cmd_on_key = nil, ---@type integer? vim.on_key namespace for paging in the dialog window.
}
local cmd_on_key ---@type integer? Set to vim.on_key namespace while cmdline is expanded.
-- An external redraw indicates the start of a new batch of messages in the cmdline.
api.nvim_set_decoration_provider(ui.ns, {
on_start = function()
M.cmd.ids = (ui.redrawing or cmd_on_key) and M.cmd.ids or {}
M.cmd.ids = (ui.redrawing or M.cmd_on_key) and M.cmd.ids or {}
end,
})
@@ -86,7 +86,7 @@ end
---@param type 'last'|'msg'|'top'|'bot'
---@param tgt? 'cmd'|'msg'|'dialog'
local function set_virttext(type, tgt)
if (type == 'last' and (ui.cmdheight == 0 or M.virt.delayed)) or cmd_on_key then
if (type == 'last' and (ui.cmdheight == 0 or M.virt.delayed)) or M.cmd_on_key then
return -- Don't show virtual text while cmdline is expanded or delaying for error.
end
@@ -213,10 +213,6 @@ local function expand_msg(src)
hlopts.end_col, hlopts.hl_group = mark[4].end_col, mark[4].hl_group
api.nvim_buf_set_extmark(ui.bufs[tgt], ui.ns, srow + mark[2], mark[3], hlopts)
end
if tgt == 'cmd' and ui.cmd.highlighter then
ui.cmd.highlighter.active[ui.bufs.cmd] = nil
end
else
M.virt.msg[M.virt.idx.dupe][1] = nil
for _, id in pairs(M.virt.ids) do
@@ -338,9 +334,6 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
ui.cmd.srow = row + 1
else
api.nvim_win_set_cursor(ui.wins.cmd, { 1, 0 }) -- ensure first line is visible
if ui.cmd.highlighter then
ui.cmd.highlighter.active[buf] = nil
end
-- Place [+x] indicator for lines that spill over 'cmdheight'.
local texth = api.nvim_win_text_height(ui.wins.cmd, {})
local spill = texth.all > ui.cmdheight and (' [+%d]'):format(texth.all - ui.cmdheight)
@@ -371,7 +364,7 @@ function M.show_msg(tgt, kind, content, replace_last, append, id)
-- Reset message state the next event loop iteration.
if not cmd_timer and (col > 0 or next(M.cmd.ids) ~= nil) then
cmd_timer = vim.defer_fn(function()
M.cmd.ids, cmd_timer, col = cmd_on_key and M.cmd.ids or {}, nil, 0
M.cmd.ids, cmd_timer, col = M.cmd_on_key and M.cmd.ids or {}, nil, 0
end, 0)
end
end
@@ -480,7 +473,7 @@ function M.msg_history_show(entries, prev_cmd)
end
-- Showing output of previous command, clear in case still visible.
if cmd_on_key or prev_cmd then
if M.cmd_on_key or prev_cmd then
M.msg_clear()
api.nvim_feedkeys(vim.keycode('<Esc>'), 'n', false)
end
@@ -511,20 +504,20 @@ function M.set_pos(tgt)
cfg.title = tgt == 'dialog' and cfg.height < texth.all and { title } or nil
api.nvim_win_set_config(win, cfg)
if tgt == 'cmd' and not cmd_on_key then
if tgt == 'cmd' and not M.cmd_on_key then
-- Temporarily expand the cmdline, until next key press.
local save_spill = M.virt.msg[M.virt.idx.spill][1]
local spill = texth.all > cfg.height and (' [+%d]'):format(texth.all - cfg.height)
M.virt.msg[M.virt.idx.spill][1] = spill and { 0, spill } or nil
set_virttext('msg', 'cmd')
M.virt.msg[M.virt.idx.spill][1] = save_spill
cmd_on_key = vim.on_key(function(_, typed)
M.cmd_on_key = vim.on_key(function(_, typed)
typed = typed and fn.keytrans(typed)
if not typed or typed == '<MouseMove>' then
if not typed or typed == '<MouseMove>' or typed == ':' then
return
end
vim.on_key(nil, ui.ns)
cmd_on_key, M.cmd.ids = nil, {}
M.cmd_on_key, M.cmd.ids = nil, {}
-- Check if window was entered and reopen with original config.
local entered = typed == '<CR>'

View File

@@ -78,17 +78,17 @@ describe('cmdline2', function()
{1:~ }|*9
{16::}{15:if} {26:1} |
{16::} {15:echo} {26:"foo"} |
{15:foo} |
foo |
{16::} ^ |
]])
feed([[echo input("foo\nbar:")<CR>]])
screen:expect([[
|
{1:~ }|*7
:if 1 |
: echo "foo" |
{16::}{15:if} {26:1} |
{16::} {15:echo} {26:"foo"} |
foo |
: echo input("foo\nbar:") |
{16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} |
foo |
bar:^ |
]])
@@ -96,10 +96,10 @@ describe('cmdline2', function()
screen:expect([[
|
{1:~ }|*7
:if 1 |
: echo "foo" |
{16::}{15:if} {26:1} |
{16::} {15:echo} {26:"foo"} |
foo |
: echo input("foo\nbar:") |
{16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} |
foo |
bar:baz^ |
]])
@@ -109,11 +109,11 @@ describe('cmdline2', function()
{1:~ }|*5
{16::}{15:if} {26:1} |
{16::} {15:echo} {26:"foo"} |
{15:foo} |
foo |
{16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} |
{15:foo} |
{15:bar}:baz |
{15:baz} |
foo |
bar:baz |
baz |
{16::} ^ |
]])
feed('endif')
@@ -122,11 +122,11 @@ describe('cmdline2', function()
{1:~ }|*5
{16::}{15:if} {26:1} |
{16::} {15:echo} {26:"foo"} |
{15:foo} |
foo |
{16::} {15:echo} {25:input}{16:(}{26:"foo\nbar:"}{16:)} |
{15:foo} |
{15:bar}:baz |
{15:baz} |
foo |
bar:baz |
baz |
{16::} {15:endif}^ |
]])
feed('<CR>')
@@ -178,7 +178,7 @@ describe('cmdline2', function()
screen:expect([[
{10:f}oo |
{1:~ }|*12
{16::}{15:s}{16:/f}^ |
{16::}{15:s}{16:/f^ } |
]])
end)

View File

@@ -334,10 +334,14 @@ describe('messages2', function()
]])
command('echo "foo\nbar"')
screen:expect_unchanged()
-- Place cmdline below expanded cmdline instead: #37653.
feed(':')
screen:expect([[
|
{1:~ }|*12
{1:~ }|*9
{3: }|
foo |
bar |
{16::}^ |
]])
end)