From 301c7065cacfe785a367dee3f03ea42d6be8691e Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Fri, 6 Mar 2026 19:29:20 +0100 Subject: [PATCH] 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. --- runtime/lua/vim/_core/ui2/cmdline.lua | 62 ++++++++++++++++++-------- runtime/lua/vim/_core/ui2/messages.lua | 25 ++++------- test/functional/ui/cmdline2_spec.lua | 32 ++++++------- test/functional/ui/messages2_spec.lua | 6 ++- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/runtime/lua/vim/_core/ui2/cmdline.lua b/runtime/lua/vim/_core/ui2/cmdline.lua index b175e368ae..1a31152fd2 100644 --- a/runtime/lua/vim/_core/ui2/cmdline.lua +++ b/runtime/lua/vim/_core/ui2/cmdline.lua @@ -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 diff --git a/runtime/lua/vim/_core/ui2/messages.lua b/runtime/lua/vim/_core/ui2/messages.lua index 9132ce4464..4bbcad58d6 100644 --- a/runtime/lua/vim/_core/ui2/messages.lua +++ b/runtime/lua/vim/_core/ui2/messages.lua @@ -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(''), '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 == '' then + if not typed or typed == '' 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 == '' diff --git a/test/functional/ui/cmdline2_spec.lua b/test/functional/ui/cmdline2_spec.lua index 8a6503572f..b6879c860c 100644 --- a/test/functional/ui/cmdline2_spec.lua +++ b/test/functional/ui/cmdline2_spec.lua @@ -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:")]]) 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('') @@ -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) diff --git a/test/functional/ui/messages2_spec.lua b/test/functional/ui/messages2_spec.lua index f1e90085ce..244841075a 100644 --- a/test/functional/ui/messages2_spec.lua +++ b/test/functional/ui/messages2_spec.lua @@ -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)