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)