From a4e0e0f1b054949de9b54efffdc32c73205a6d04 Mon Sep 17 00:00:00 2001 From: Shawon Date: Wed, 7 Oct 2026 21:17:46 +0600 Subject: [PATCH 01/16] refactor: Reworked how messages are shown --- lua/definitions/message.lua | 9 +- lua/ui/message.lua | 1640 ++++------------------------------- 2 files changed, 189 insertions(+), 1460 deletions(-) diff --git a/lua/definitions/message.lua b/lua/definitions/message.lua index 0cf55f5..5ec0769 100644 --- a/lua/definitions/message.lua +++ b/lua/definitions/message.lua @@ -23,14 +23,6 @@ ---@field confirm boolean ---@class ui.message.type --- ---@field normal boolean ---@field hidden boolean ---@field list boolean ---@field confirm boolean - - ---@class ui.message.style Message pstyle --- ---@field condition? fun(msg: ui.message.entry, lines: string[], extmarks: ui.message.extmarks): boolean Condition for this processor. @@ -185,6 +177,7 @@ ---@class ui.message.entry A message entry. --- +---@field id string ---@field type? ui.message.type --- ---@field kind ui.message.kind diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 7f56aee..ceb2522 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -8,1529 +8,265 @@ local utils = require("ui.utils"); ------------------------------------------------------------------------------ ----@type integer Namespace for decorations in messages. -message.namespace = vim.api.nvim_create_namespace("ui.message") - ----@type integer, integer Buffer & window for messages. -message.msg_buffer, message.msg_window = nil, nil; - ----@type integer, integer Buffer & window for showing larger messages. -message.list_buffer, message.list_window = nil, nil; - ----@type integer, integer Buffer & window for confirmation messages. -message.confirm_buffer, message.confirm_window = nil, nil; - ----@type integer, integer Buffer & window for message history. -message.history_buffer, message.history_window = nil, nil; - ----@type integer, integer Buffer & window for showmode. -message.show_buffer, message.show_window = nil, nil; - ------------------------------------------------------------------------------- - ----@type integer Current message ID. -message.id = 2000; - ----@type ui.message.entry[] Message history(stores messages not available in `:messages`). +---@class ui.message +--- +---@field namespace integer +---@field current_order integer Current messages order. +--- +---@field buffer? integer +---@field window? integer +--- +---@field confirm_buffer? integer +---@field confirm_window? integer +--- +---@field list_buffer? integer +---@field list_window? integer +--- +---@field showmode_buffer? integer +---@field showmode_window? integer +--- +---@field history_buffer? integer +---@field history_window? integer +message.data = { + namespace = vim.api.nvim_create_namespace("ui.message"), + + buffer = nil, + window = nil, + + confirm_buffer = nil, + confirm_window = nil, + + list_buffer = nil, + list_window = nil, + + showmode_buffer = nil, + showmode_window = nil, + + history_buffer = nil, + history_window = nil, +}; + +---@type ui.message.entry[] message.history = {}; ----@type ui.message.entry[] Currently visible message. -message.visible = {}; - ----@type ui.message.decorations[] Decorations to show in the statuscolumn. -message.decorations = {}; - ----@type ui.message.decorations[] Decorations to show in the statuscolumn for history. -message.history_decorations = {}; - ---- Custom statuscolumn for the message window. ----@return string -message.statuscolumn = function () - ---|fS - - local win = vim.g.statusline_winid; - - if win ~= message.msg_window and win ~= message.history_window then - -- Wrong window. - return ""; - elseif not message.decorations and not message.history_decorations then - -- Decorations not available. - return ""; - end - - ---@type integer Current line-number(0-indexed). - local lnum = vim.v.lnum - 1; - - for _, entry in ipairs(win == message.history_window and (message.history_decorations or {}) or (message.decorations or {})) do - if lnum >= entry.from and lnum <= entry.to then - if lnum == entry.from and vim.v.virtnum == 0 then - return "%=" .. utils.to_statuscolumn(entry.icon); - elseif lnum == entry.to and vim.v.virtnum == 0 then - return "%=" .. utils.to_statuscolumn( - entry.tail or entry.padding or entry.icon - ); - else - return "%=" .. utils.to_statuscolumn( - entry.padding or entry.icon - ); - end - - break; - end - end - - return ""; - - ---|fE -end - --- Export the statuscolumn so that we can use it in 'statuscolumn' option. -_G.__ui_statuscolumn = message.statuscolumn; - ------------------------------------------------------------------------------- - ----@type boolean Have we passed UIEnter event? -message.ui_attached = false; ----@type ui.message.entry[] List of messages to echo after UIEnter. -message.ui_echo = {}; - ---- Caches given message. ----@param kind ui.message.kind ----@param content ui.message.fragment[] ----@param replace_last boolean ----@param add_to_history boolean -message.cache = function (kind, content, replace_last, add_to_history) - ---|fS - - if #message.ui_echo > 0 and replace_last == true then - message.ui_echo[#message.ui_echo] = { - kind = kind, - content = content, - replace_last = replace_last, - - add_to_history = add_to_history - }; - else - table.insert(message.ui_echo, { - kind = kind, - content = content, - replace_last = replace_last, - - add_to_history = add_to_history - }); - end - - ---|fE -end - -vim.api.nvim_create_autocmd("UIEnter", { - callback = function () - message.ui_attached = true; - - for _, item in ipairs(message.ui_echo) do - message.__add(item.kind, item.content, item.add_to_history or true); - end - end -}); - ------------------------------------------------------------------------------- - ---- Prepares various window & buffers. -message.__prepare = function () - ---|fS - - local win_config = { - relative = "editor", - - row = 0, col = 0, - width = 1, height = 1, - - border = "none", - - style = "minimal", - hide = true, - focusable = false - }; - - if not message.msg_buffer or vim.api.nvim_buf_is_valid(message.msg_buffer) == false then - message.msg_buffer = vim.api.nvim_create_buf(false, true); - end - - if not message.msg_window or vim.api.nvim_win_is_valid(message.msg_window) == false then - message.msg_window = utils.open_win(message.msg_buffer, false, win_config); - vim.api.nvim_win_set_var(message.msg_window, "ui_window", true); - utils.set("w", message.msg_window, "foldmethod", "manual"); - - utils.set("w", message.msg_window, "numberwidth", 1); - utils.set("w", message.msg_window, "statuscolumn", "%!v:lua.__ui_statuscolumn()"); - end - - ---------- - - if not message.list_buffer or vim.api.nvim_buf_is_valid(message.list_buffer) == false then - message.list_buffer = vim.api.nvim_create_buf(false, true); - end - - ---------- - - if not message.confirm_buffer or vim.api.nvim_buf_is_valid(message.confirm_buffer) == false then - message.confirm_buffer = vim.api.nvim_create_buf(false, true); - end - - ---------- - - if not message.history_buffer or vim.api.nvim_buf_is_valid(message.history_buffer) == false then - message.history_buffer = vim.api.nvim_create_buf(false, true); - end - - ---------- - - if not message.show_buffer or vim.api.nvim_buf_is_valid(message.show_buffer) == false then - message.show_buffer = vim.api.nvim_create_buf(false, true); - end - - if not message.show_window or vim.api.nvim_win_is_valid(message.show_window) == false then - message.show_window = utils.open_win(message.show_buffer, false, win_config); - vim.api.nvim_win_set_var(message.show_window, "ui_window", true); - - utils.set("w", message.show_window, "foldmethod", "manual"); - utils.set("w", message.show_window, "sidescrolloff", 0); - end +---@type ui.message.entry[] +message.visible = {}; - ---|fE -end +---@type table +message.visible_special = {}; ---- Wrapper for `vim.uv.new_timer()`. ----@param callback function ----@param duration? integer ----@param interval? integer ----@return table -message.timer = function (callback, duration, interval) - ---|fS +---@type ui.message.decorations[] +message.statuscolumn_decorations = {} - local timer = vim.uv.new_timer(); ---@diagnostic disable-line +message.timer = function (id, duration, interval) + local timer = vim.uv.new_timer(); - if interval then - timer:start(0, duration, vim.schedule_wrap(callback)); - else - timer:start(duration, 0, vim.schedule_wrap(callback)); + if timer then + timer:start(duration, interval, vim.schedule_wrap(function () + message.free(id); + end)); end return timer; - - ---|fE end ---- Removes message with `ID`. ----@param id integer -message.__remove = function (id) - ---|fS - - if message.visible[id] then +message.free = function (id) + if type(id) == "string" then + message.visible_special[id] = nil; + else message.visible[id] = nil; - - vim.schedule(function () - log.assert( - "ui/message.lua → remove_render", - pcall(message.__render) - ) - end) - end - - ---|fE -end - ---- Adds a new message. ----@param kind ui.message.kind ----@param content ui.message.fragment[] ----@param add_to_history boolean -message.__add = function (kind, content, add_to_history) - ---|fS - - if kind == "" and vim.tbl_isempty(message.history) == false then - -- Calling `nvim__redraw()` causes messages to duplicate - -- with incorrect kind. - -- So, we check if that's the case and handle accordingly. - - --- Last shown message. - local last_shown = message.history[message.id - 1]; - - --- Last visible message. - local last_visible = message.visible[message.id - 1]; - - if last_visible and vim.deep_equal(last_visible.content, content) then - -- Message is still visible, extend duration. - message.__replace(last_visible.kind, content, false); - return; - elseif last_shown and vim.deep_equal(last_shown.content, content) then - -- Message is not visible, discard this one. - return; - end end vim.schedule(function () - ---|fS - - ---@type boolean Should this message be ignored? - local condition = utils.eval(spec.config.message.ignore, kind, content); - - if condition == true then - return; - end - - local lines = utils.to_lines(content); - - ---@type boolean, boolean? - local is_list, _add_to_history = spec.is_list(kind, content, add_to_history); - local max_lines = spec.config.message.max_lines or math.floor(vim.o.lines * 0.5); - - if is_list == true or #lines > max_lines then - -- The message should be shown as a list. - -- It either, - -- 1. Is a list message with inaccurate `kind`. - -- 2. Is too long to show. - log.assert( - "ui/message.lua → add_list", - pcall(message.__list, { - -- If the message is too long, it should be - -- added to history. - type = _add_to_history and "normal" or "list", - - kind = kind, - content = content, - }) - ); - return; - elseif kind == "list_cmd" then - -- If the message isn't considered a list command, - -- we should change it's kind even if Neovim tells - -- us otherwise. - kind = "not_list_cmd"; - end - - ---@type integer Current message's ID. - local current_id = message.id; - - ---@type ui.message.style__static - local style = spec.get_msg_style({ kind = kind, content = content }, lines, {}) or {}; - - ---@type integer Message visibility duration. - local duration = math.min( - style.duration or 5000, - spec.config.message.max_duration or 5000 - ); - - -- Store the message in history & visible - -- message table. - message.history[message.id] = { - type = add_to_history and "normal" or "hidden", - - kind = kind, - content = content - }; - - -- The visible message has a `timer` - -- thta shows/hides the message. - message.visible[message.id] = vim.tbl_extend("force", { - kind = kind, - content = content - }, { - timer = message.timer(function () - message.__remove(current_id); - end, duration) - }); - - message.id = message.id + 1; - log.assert( - "ui/message.lua → add_render", - pcall(message.__render) + "ui/message.lua → remove_render", + pcall(message.render) ); - - ---|fE end); - - ---|fE end ---- Replaces the last visible message. ---@param kind ui.message.kind ---@param content ui.message.fragment[] ----@param add_to_history boolean -message.__replace = function (kind, content, add_to_history) - ---|fS - - vim.schedule(function () - ---@type boolean Should this message be ignored? - local condition = utils.eval(spec.config.message.ignore, kind, content, true); - - if condition == true then - return; - end - - ---@type integer[] - local keys = vim.tbl_keys(message.visible); - - local lines = utils.to_lines(content); - - ---@type boolean, boolean? - local is_list, _add_to_history = spec.is_list(kind, content, add_to_history); - local max_lines = spec.config.message.max_lines or math.floor(vim.o.lines * 0.5); - - if is_list == true or #lines > max_lines then - log.assert( - "ui/message.lua → replace_list", - pcall(message.__list, { - -- If the message is too long, it should be - -- added to history. - type = _add_to_history and "normal" or "list", - - kind = kind, - content = content, - }) - ); - return; - elseif #keys == 0 or not message.visible[keys[#keys]] then - -- No last visible message available. - -- Add new message. - message.__add(kind, content, add_to_history); - return; - elseif add_to_history then - -- Certain replace type messages need - -- to be added to the history. - message.history[message.id] = { - type = "normal", - - kind = kind, - content = content - }; - message.id = message.id + 1; - else - message.history[keys[#keys]] = { - type = "normal", - - kind = kind, - content = content - }; - end - - --- Last visible message. - local last = message.visible[keys[#keys]]; - - last.timer:stop(); - - last.kind = kind; - last.content = content; - - ---@type ui.message.style__static - local style = spec.get_msg_style({ kind = kind, content = content }, lines, {}) or {}; - local duration = math.min( - style.duration or 5000, - spec.config.message.max_duration or 5000 - ); - - last.timer:start(duration, 0, vim.schedule_wrap(function () - message.__remove(keys[#keys]); - end)); - - log.assert( - "ui/message.lua → replace_render", - pcall(message.__render) - ); - end); - - ---|fE +---@param replace_last boolean +---@param history boolean +---@param _append boolean +---@param id string +---@param _trigger string +---@return ui.message.entry +message.new = function (kind, content, replace_last, history, _append, id, _trigger) + return { + id = id, + kind = kind, + type = "normal", + + content = content, + + replace_last = replace_last, + add_to_history = history, + + timer = nil, + } --[[@as ui.message.entry]]; end ------------------------------------------------------------------------------ ---- Confirmation message. ----@param obj ui.message.entry -message.__confirm = function (obj) - ---|fS - - message.history[message.id] = obj; - message.id = message.id + 1; - - --- All logic must be run outside of - --- fast event. +message.msg_show = function (kind, content, replace_last, history, append, id, trigger) vim.schedule(function () - vim.g.__ui_confirm_msg = obj; - local lines, exts = utils.process_content(obj.content); - - message.__prepare(); - - ---@type ui.message.confirm__static - local config = spec.get_confirm_style(obj, lines, exts); + local msg = message.new(kind, content, replace_last, history, append, id, trigger); + local lines = utils.to_lines(content); + local style = spec.get_msg_style(msg, lines, {}) or {}; - if config.modifier then - lines = config.modifier.lines or lines; - exts = config.modifier.extmarks or exts; + -- NOTE: Make sure to reset the freeing timer. + if message.visible_special[id] then + message.visible_special[id].timer:stop(); + elseif message.visible[id] then + message.visible[id].timer:stop(); end - local window_config = vim.tbl_extend("force", { - relative = "editor", - - row = config.row or math.ceil((vim.o.lines - #lines) / 2), - col = config.col or math.ceil((vim.o.columns - utils.max_len(lines)) / 2), - - width = config.width or utils.max_len(lines), - height = config.height or utils.wrapped_height(lines, config.width), - - border = config.border or "none", - style = "minimal", + msg.timer = message.timer(id, style.duration or 5000); - zindex = 90, - hide = false - }, spec.config.message.confirm_winconfig or {}); - - if message.confirm_window and vim.api.nvim_win_is_valid(message.confirm_window) then - vim.api.nvim_win_set_config(message.confirm_window, window_config); + if type(id) == "string" then + message.visible_special[id] = msg; else - message.confirm_window = utils.open_win(message.confirm_buffer, false, window_config); - vim.api.nvim_win_set_var(message.confirm_window, "ui_window", true); - end - - pcall(vim.api.nvim_win_set_cursor, message.confirm_window, { 1, 0 }); - vim.api.nvim_buf_clear_namespace(message.confirm_buffer, message.namespace, 0, -1); - vim.api.nvim_buf_set_lines(message.confirm_buffer, 0, -1, false, lines); - - for l, line in ipairs(exts) do - for _, ext in ipairs(line) do - pcall(vim.api.nvim_buf_set_extmark, message.confirm_buffer, message.namespace, l - 1, ext[1], { - end_col = ext[2], - hl_group = ext[3] - }); - end - end - - utils.set("w", message.confirm_window, "foldmethod", "manual"); - - utils.set("w", message.confirm_window, "wrap", true); - utils.set("w", message.confirm_window, "linebreak", true); - utils.set("w", message.confirm_window, "cursorline", #lines > 1); - - if config.winhl then - utils.set("w", message.confirm_window, "winhl", config.winhl); + message.visible[id] = msg; end + message.history[id] = msg; - ---|fS "feat: Allow moving in the confirm window." - - ---@type string[] Various movement keys - local movememt_keys = { - vim.api.nvim_replace_termcodes("", true, true, true), - vim.api.nvim_replace_termcodes("", true, true, true), - - vim.api.nvim_replace_termcodes("", true, true, true), - vim.api.nvim_replace_termcodes("", true, true, true), - - vim.api.nvim_replace_termcodes("h", true, true, true), - vim.api.nvim_replace_termcodes("l", true, true, true), - - vim.api.nvim_replace_termcodes("j", true, true, true), - vim.api.nvim_replace_termcodes("k", true, true, true), - }; - - --- Handles cursor movements. - ---@param key string - local function handle_movement (key) - ---|fS - - local pos = vim.api.nvim_win_get_cursor(message.confirm_window); - local X, Y = pos[2], pos[1]; - - if key == movememt_keys[1] or key == movememt_keys[5] then - X = math.max(0, X - 1); - elseif key == movememt_keys[2] or key == movememt_keys[6] then - X = math.min(string.len(lines[Y] or ""), X + 1); - elseif key == movememt_keys[3] or key == movememt_keys[7] then - Y = math.min(#lines, Y + 1); - else - Y = math.max(1, Y - 1); - end - - pcall(vim.api.nvim_win_set_cursor, message.confirm_window, { Y, X }); - - ---|fE - end - - ---|fE - - --- Auto hide on next key press. - vim.on_key(function (key) - if vim.list_contains(vim.g.__confirm_keys or {}, string.lower(key)) == false then - if vim.list_contains(movememt_keys, key) then - -- If the key is a movement key then - -- we try to do the movement and - -- redraw the entire screen. - -- `nvim__redraw()` doesn't work - -- here. - pcall(handle_movement, key); - pcall(vim.cmd, "mode"); ---@diagnostic disable-line - end - - return; - end - - pcall(vim.api.nvim_win_close, message.confirm_window, true); - vim.on_key(nil, message.namespace); - - vim.g.__ui_confirm_msg = nil; - end, message.namespace); - - if package.loaded["ui.cmdline"] and vim.v.vim_did_enter ~= 1 then - -- The cmdline is initially hidden when Neovim starts. - -- So, we make the next `cmdline_show` fire a `:mode`. - -- - -- We only need to call this for messages before `VimEnter`. - package.loaded["ui.cmdline"].__use_mode = true; - package.loaded["ui.cmdline"].__render(); - - -- The cmdline isn't visible in some cases. - -- Manually call `:mode` to make it visible. - vim.defer_fn(function () - pcall(vim.cmd, "mode"); ---@diagnostic disable-line - end, 100); - end - end); - - ---|fE + log.assert( + "ui/message.lua → add_render", + pcall(message.render) + ); + end) end ---- List message. ----@param obj ui.message.entry -message.__list = function (obj) - ---|fS - - message.history[message.id] = obj; - message.id = message.id + 1; - - --- All logic must be run outside of - --- fast event. - vim.schedule(function () - message.__prepare(); - vim.g.__ui_list_msg = obj; - - local lines, exts = utils.process_content(obj.content); - - ---@type ui.message.list__static - local config = spec.get_listmsg_style(obj, lines, exts); - - if config.modifier then - lines = config.modifier.lines or lines; - exts = config.modifier.extmarks or exts; - end - - ---|fS "feat: Keymap(s)" - - vim.api.nvim_buf_set_keymap(message.list_buffer, "n", "q", "", { - callback = function () - vim.api.nvim_set_current_win( - utils.last_win() - ); - pcall(vim.api.nvim_win_close, message.list_window, true); - - vim.g.__ui_list_msg = nil; - end - }); - - ---|fE - - ---@type integer - local W = math.min(utils.max_len(lines), math.floor(vim.o.columns * 0.75)); - ---@type integer - local H = math.min(#lines, vim.o.lines - 2); - - local window_config = vim.tbl_extend("force", { - relative = "editor", - - row = config.row or math.ceil((vim.o.lines - H) / 2), - col = config.col or math.ceil((vim.o.columns - W) / 2), - - width = config.width or W, - height = config.height or H, - - border = config.border or "none", - style = "minimal", - - zindex = 50, - hide = false, - focusable = true - }, spec.config.message.list_winconfig or {}); - - if message.list_window and vim.api.nvim_win_is_valid(message.list_window) then - vim.api.nvim_win_set_config(message.list_window, window_config); - else - message.list_window = utils.open_win(message.list_buffer, false, window_config); - vim.api.nvim_win_set_var(message.list_window, "ui_window", true); - - vim.api.nvim_create_autocmd("WinClosed", { - pattern = tostring(message.list_window), - callback = function () - message.list_window = nil; - vim.g.__ui_list_msg = nil; - end - }) - end - - ---|fS - - vim.bo[message.list_buffer].modifiable = true; - - vim.api.nvim_buf_clear_namespace(message.list_buffer, message.namespace, 0, -1); - vim.api.nvim_buf_set_lines(message.list_buffer, 0, -1, false, lines); - - for l, line in ipairs(exts) do - for _, ext in ipairs(line) do - log.assert( - "ui/message.lua → list_highlights", - pcall( - vim.api.nvim_buf_set_extmark, - message.list_buffer, - message.namespace, - - l - 1, - ext[1], - - { - end_col = ext[2], - hl_group = ext[3] - } - ) - ); - end - end - - vim.bo[message.list_buffer].modifiable = false; - - ---|fE +------------------------------------------------------------------------------ - vim.api.nvim_set_current_win(message.list_window); - utils.set("w", message.list_window, "foldmethod", "manual"); - utils.set("w", message.list_window, "wrap", false); +message.set_buf_win = function (buf, win) + ---@type vim.api.keyset.win_config + local window_opts = { + relative = "editor", - if config.winhl then - utils.set("w", message.list_window, "winhl", config.winhl); - end - end); + row = 0, col = 0, + width = 1, height = 1, - ---|fE -end + border = "none", + style = "minimal", -message.__list_resize = function () - ---|fS + hide = true, focusable = false + }; - if not vim.g.__ui_list_msg then - return; - elseif not message.list_window or not vim.api.nvim_win_is_valid(message.list_window) then - return; + if not message.data[buf] or not vim.api.nvim_buf_is_valid(message.data[buf]) then + message.data[buf] = vim.api.nvim_create_buf(false, true); end - local lines, exts = utils.process_content(vim.g.__ui_list_msg.content); + if not message.data[win] or not vim.api.nvim_win_is_valid(message.data[win]) then + message.data[win] = vim.api.nvim_open_win(message.data[buf], false, window_opts); + vim.api.nvim_win_set_var(message.data[win], "ui_window", true); - ---@type ui.message.list__static - local config = spec.get_listmsg_style(vim.g.__ui_list_msg, lines, exts); - - if config.modifier then - lines = config.modifier.lines or lines; - exts = config.modifier.extmarks or exts; + utils.set("w", message.data[win], "foldmethod", "manual"); + utils.set("w", message.data[win], "numberwidth", 1); end - - ---@type integer - local W = math.min(utils.max_len(lines), math.floor(vim.o.columns * 0.75)); - ---@type integer - local H = math.min(#lines, vim.o.lines - 2); - - local window_config = vim.tbl_extend("force", { - relative = "editor", - - row = config.row or math.ceil((vim.o.lines - H) / 2), - col = config.col or math.ceil((vim.o.columns - W) / 2), - - width = config.width or W, - height = config.height or H, - - border = config.border or "none", - style = "minimal", - - zindex = 90, - hide = false, - focusable = true - }, spec.config.message.list_winconfig or {}); - - vim.api.nvim_win_set_config(message.list_window, window_config); - - ---|fE end ---- Hides the message window. -message.__hide = function () - ---|fS - - local keys = vim.tbl_keys(message.visible); - if #keys ~= 0 then return; end - - pcall(vim.api.nvim_win_set_config, message.msg_window, { hide = true }); - - ---|fE +message.prepare = function () + message.set_buf_win("buffer", "window"); + message.set_buf_win("confirm_buffer", "confirm_window"); + message.set_buf_win("list_buffer", "list_window"); + message.set_buf_win("history_buffer", "history_window"); + message.set_buf_win("showmode_buffer", "showmode_window"); end -message.__get_cmdline_offset = function () +message.render = function () ---|fS - local cmdline_offset = 0 - - if vim.g.__ui_cmd_height and vim.g.__ui_cmd_height > 0 then - cmdline_offset = vim.g.__ui_cmd_height + spec.config.cmdline.row_offset - 1 - end - - return cmdline_offset - - ---|fE -end + message.prepare(); ---- Renders visible messages. -message.__render = function () - ---|fS + local lines = {}; + local extmarks = {}; - local keys = vim.tbl_keys(message.visible); - table.sort(keys); + -- Visible messages + local msg_orders = vim.tbl_keys(message.visible); + local sp_msg_orders = vim.tbl_keys(message.visible_special); - if #keys == 0 then - message.__hide(); + if #msg_orders == 0 and #sp_msg_orders == 0 then + vim.api.nvim_win_close(message.data.window, true); return; end - message.__prepare(); + table.sort(msg_orders); - ---@type integer - local last_decor_size = 0; + for _, msg_order in ipairs(msg_orders) do + local msg = message.visible[msg_order]; + local m_lines, m_exts = utils.process_content(msg.content); - for _, entry in ipairs(message.decorations) do - if entry.icon then - last_decor_size = math.max(last_decor_size, utils.virt_len(entry.icon)); + local style = spec.get_msg_style(msg, m_lines, m_exts) or {}; + if style.modifier then + m_lines = style.modifier.lines or m_lines; + m_exts = style.modifier.extmarks or m_exts; end - end - local lines, exts = {}, {}; - message.decorations = {}; + lines = vim.list_extend(lines, m_lines); + extmarks = vim.list_extend(extmarks, m_exts); + end - for _, key in ipairs(keys) do - local value = message.visible[key]; - local m_lines, m_exts = utils.process_content(value.content); + table.sort(sp_msg_orders); - ---@type ui.message.style__static - local style = spec.get_msg_style(value, m_lines, m_exts) or {}; + for _, msg_order in ipairs(sp_msg_orders) do + local msg = message.visible_special[msg_order]; + local m_lines, m_exts = utils.process_content(msg.content); + local style = spec.get_msg_style(msg, m_lines, m_exts) or {}; if style.modifier then m_lines = style.modifier.lines or m_lines; m_exts = style.modifier.extmarks or m_exts; end - if style.decorations then - table.insert(message.decorations, vim.tbl_extend("force", style.decorations, { - from = #lines, - to = #lines + (#m_lines - 1) - })); - end - - lines = vim.list_extend(lines, m_lines) - exts = vim.list_extend(exts, m_exts) + lines = vim.list_extend(lines, m_lines); + extmarks = vim.list_extend(extmarks, m_exts); end - -- Remove trailing newlines. - -- These are typically used before a `return_prompt`. while lines[#lines] == "" do table.remove(lines); end - vim.api.nvim_buf_clear_namespace(message.msg_buffer, message.namespace, 0, -1); - vim.api.nvim_buf_set_lines(message.msg_buffer, 0, -1, false, lines); - - for l, line in ipairs(exts) do - for _, ext in ipairs(line) do - if ext[3] == "" then - goto continue; - end + vim.api.nvim_buf_clear_namespace(message.data.buffer, message.data.namespace, 0, -1); + vim.api.nvim_buf_set_lines(message.data.buffer, 0, -1, false, lines); - vim.api.nvim_buf_set_extmark(message.msg_buffer, message.namespace, l - 1, ext[1], { - end_col = ext[2], - hl_group = ext[3] - }); - - ::continue:: - end - end - - ---@type integer Number of columns decorations take. - local decor_size = 0; - - for _, entry in ipairs(message.decorations) do - if entry.icon then - decor_size = math.max(decor_size, utils.virt_len(entry.icon)); - end - - if entry.line_hl_group then - pcall(vim.api.nvim_buf_set_extmark, message.msg_buffer, message.namespace, entry.from, 0, { - end_row = entry.to, - line_hl_group = entry.line_hl_group - }); - end - end - - local W = math.min(math.floor(vim.o.columns * 0.5), utils.max_len(lines)); - local H = utils.wrapped_height(lines, W); + local width = math.min( + math.floor(vim.o.columns * 0.5), + utils.max_len(lines) + ); + local height = utils.wrapped_height(lines, width); - local window_config = vim.tbl_extend("keep", spec.config.message.message_winconfig or {}, { + local window_opts = vim.tbl_extend("force", spec.config.message.message_winconfig, { relative = "editor", + anchor = "SE", - -- Original: row = total_lines - height; - -- Offset: command_height + message_height + 1(statusline_height); - -- - -- Result = Original - Offset; - row = (vim.o.lines - H) - (vim.o.cmdheight + message.__get_cmdline_offset() + 1), + row = vim.o.lines - 1, col = vim.o.columns, - width = W + decor_size, - height = H, + width = width, + height = height, border = "none", - zindex = 80, - hide = false + zindex = 200, + hide = false, }); - if message.msg_window and vim.api.nvim_win_is_valid(message.msg_window) then - vim.api.nvim_win_set_config(message.msg_window, window_config); - else - message.msg_window = utils.open_win(message.msg_buffer, false, window_config); - vim.api.nvim_win_set_var(message.msg_window, "ui_window", true); - end - - utils.set("w", message.msg_window, "winhl", "Normal:Normal"); - utils.set("w", message.msg_window, "statuscolumn", "%!v:lua.__ui_statuscolumn()"); - - utils.set("w", message.msg_window, "wrap", true); - utils.set("w", message.msg_window, "linebreak", true); - utils.set("w", message.msg_window, "breakindent", true); - + vim.api.nvim_win_set_config(message.data.window, window_opts); utils.redraw({ flush = true, statuscolumn = true, - win = message.msg_window + win = message.data.window }, { - --[[ - BUG: Visual artifacts are shown if the statuscolumn size is updated repeatedly. - Solution: Update only when the decoration size changes. - ]] - ignore = last_decor_size ~= decor_size - }); - - ---|fE -end - ---- Loads history in a window. ----@param entries ( ui.message.fragment[] )[] -message.__history = function (entries) - ---|fS - - local function get_msg_radio () - local types = _G.__ui_history_types or { - normal = true, - hidden = false, - list = false, - confirm = false, - }; - - local items = { - normal = " [N]ormal ", - hidden = " [H]idden ", - list = " [L]ist ", - confirm = " [C]onfirm " - }; - - local keys = vim.tbl_keys(items); - table.sort(keys); - - local line, exts = "Filters: ", {}; - table.insert(exts, { 0, #line, "@comment" }); - - for k, key in ipairs(keys) do - local enabled = types[key]; - local item = items[key]; - - local before = #line; - line = line .. item .. (k ~= #keys and " " or ""); - - if enabled == true then - table.insert(exts, { before, before + #item, "UICmdlineDefaultIcon" }) - else - table.insert(exts, { before, before + #item, "UICmdlineSearchDownIcon" }) - end - end - - return line, exts; - end - - ---@type ui.message.source - vim.g.__ui_history_pref = vim.g.__ui_history_pref or spec.config.message.history_preference or "vim"; - - _G.__ui_history_types = _G.__ui_history_types or spec.config.message.history_types or { - normal = true, - hidden = false, - list = false, - confirm = false, - }; - - vim.g.__ui_history = true; - - message.__prepare(); - message.history_decorations = {}; - - ---|fS "feat: Keymaps" - - vim.api.nvim_buf_set_keymap(message.history_buffer, "n", "u", "", { - desc = "[u]pdates message history.", - callback = function () - if vim.g.__ui_history_pref == "vim" then - vim.cmd("messages"); - else - message.__history(entries); - end - end, - }); - vim.api.nvim_buf_set_keymap(message.history_buffer, "n", "t", "", { - desc = "[t]oggles between `vim` and `ui.nvim`'s message history.", - callback = function () - vim.g.__ui_history_pref = vim.g.__ui_history_pref == "vim" and "ui" or "vim"; - message.__history(entries); - end - }); - - vim.api.nvim_buf_set_keymap(message.history_buffer, "n", "q", "", { - desc = "[q]uits message window.", - callback = function () - ---|fS - - -- Instead of closing the window, we hide it. - -- - -- Only floating windows can be hidden so we - -- turn it into a floating window. - vim.api.nvim_set_current_win( - utils.last_win() - ); - log.assert( - "ui/message.lua → history_quit", - pcall(vim.api.nvim_win_close, message.history_window, true) - ); - - ---|fE - end - }); - - vim.api.nvim_buf_set_keymap(message.history_buffer, "n", "N", "", { - desc = "Toggles [N]ormal msssags visiblity.", - callback = function () - if vim.g.__ui_history_pref == "vim" then - return; - end - - _G.__ui_history_types.normal = not _G.__ui_history_types.normal; - message.__history(entries); - end - }); - - vim.api.nvim_buf_set_keymap(message.history_buffer, "n", "H", "", { - desc = "Toggles [H]idden msssags visiblity.", - callback = function () - if vim.g.__ui_history_pref == "vim" then - return; - end - - _G.__ui_history_types.hidden = not _G.__ui_history_types.hidden; - message.__history(entries); - end - }); - - vim.api.nvim_buf_set_keymap(message.history_buffer, "n", "L", "", { - desc = "Toggles [L]ist msssags visiblity.", - callback = function () - if vim.g.__ui_history_pref == "vim" then - return; - end - - _G.__ui_history_types.list = not _G.__ui_history_types.list; - message.__history(entries); - end - }); - - vim.api.nvim_buf_set_keymap(message.history_buffer, "n", "C", "", { - desc = "Toggles [C]onfirm msssags visiblity.", - callback = function () - if vim.g.__ui_history_pref == "vim" then - return; - end - - _G.__ui_history_types.confirm = not _G.__ui_history_types.confirm; - message.__history(entries); - end - }); - - ---|fE - - ---@type string[], ( ui.message.hl_fragment[] )[] - local lines, exts = {}, {}; - - if vim.g.__ui_history_pref == "vim" then - table.insert(lines, " History:"); - table.insert(exts, { - { 0, #" ", "DiagnosticOk" }, - { #" ", #lines[1], "@comment" }, - }); - else - local l, x = get_msg_radio(); - - table.insert(lines, "󰊌 History:"); - table.insert(lines, l); - - table.insert(exts, { - { 0, #"󰊌 ", "DiagnosticHint" }, - { #"󰊌 ", #lines[1], "@comment" }, - }); - table.insert(exts, x); - end - - --- Equalizes line & extmark count. - ---@param _lines string[] - ---@param _exts ( ui.message.hl_fragment[] )[] - local function lines_exts_equal (_lines, _exts) - if #_lines < #_exts then - for _ = 1, #_exts - #_lines do - table.insert(lines, ""); - end - elseif #_lines > #_exts then - for _ = 1, #_lines - #_exts do - table.insert(exts, {}); - end - end - end - - ---|fS "code: Create lines & extmarks for messages" - - if vim.g.__ui_history_pref == "vim" and entries then - -- Show raw history from Vim. - for _, entry in ipairs(entries) do - local _lines, _exts = utils.process_content(entry[2]); - - lines = vim.list_extend(lines, _lines); - exts = vim.list_extend(exts, _exts); - - lines_exts_equal(lines, exts); - end - else - -- Show history from `ui.nvim`. - - ---@type integer[] List of message IDs. - local keys = vim.tbl_keys(message.history); - table.sort(keys); - - for _, key in ipairs(keys) do - local value = message.history[key]; - - if _G.__ui_history_types[value.type or "normal"] ~= true then - -- Ignore messages if their level is lower - -- then the preferred message level - goto ignore_message; - end - - local m_lines, m_exts = utils.process_content(value.content); - local processor; - - if not value.type or value.type == "normal" then - -- Regular message. - processor = spec.get_msg_style(value, m_lines, m_exts) or {}; - elseif value.type == "hidden" then - -- Regular message's whose `history = false`. - processor = spec.get_msg_style(value, m_lines, m_exts) or {}; - elseif value.type == "list" then - -- List message. - processor = spec.get_listmsg_style(value, m_lines, m_exts) or {}; - elseif value.type == "confirm" then - -- Confirm message. - processor = spec.get_confirm_style(value, lines, exts) or {}; - end - - if processor.modifier then - m_lines = processor.modifier.lines or m_lines; - m_exts = processor.modifier.extmarks or m_exts; - end - - if processor.history_decorations then - table.insert(message.history_decorations, vim.tbl_extend("force", processor.history_decorations, { - from = #lines, - to = #lines + (#m_lines - 1) - })); - elseif processor.decorations then - table.insert(message.history_decorations, vim.tbl_extend("force", processor.decorations, { - from = #lines, - to = #lines + (#m_lines - 1) - })); - end - - lines = vim.list_extend(lines, m_lines) - exts = vim.list_extend(exts, m_exts) - - lines_exts_equal(lines, exts); - - ::ignore_message:: - end - end - - ---|fE - - vim.bo[message.history_buffer].modifiable = true; - - vim.api.nvim_buf_clear_namespace(message.history_buffer, message.namespace, 0, -1); - vim.api.nvim_buf_set_lines(message.history_buffer, 0, -1, false, lines); - - ---|fS "doc: Add keymap hints" - - vim.api.nvim_buf_set_extmark(message.history_buffer, message.namespace, 0, 0, { - virt_text_pos = "right_align", - virt_text = { - { " u ", "UIHistoryKeymap" }, - { " Update ", "UIHistoryDesc" }, - { " " }, - { " t ", "UIHistoryKeymap" }, - { " Toggle source ", "UIHistoryDesc" }, - { " " }, - { " q ", "UIHistoryKeymap" }, - { " Quit ", "UIHistoryDesc" }, - }, - - hl_mode = "combine" - }); - - ---|fE - - -- Highlight lines of the buffer. - for l, line in ipairs(exts) do - for _, ext in ipairs(line) do - vim.api.nvim_buf_set_extmark(message.history_buffer, message.namespace, l - 1, ext[1], { - end_col = ext[2], - hl_group = ext[3] - }); - end - end - - -- Highlight lines with decorations. - for _, entry in ipairs(message.history_decorations) do - if entry.line_hl_group then - vim.api.nvim_buf_set_extmark(message.history_buffer, message.namespace, entry.from, 0, { - end_row = entry.to, - line_hl_group = entry.line_hl_group - }); - end - end - - vim.bo[message.history_buffer].modifiable = false; - - local window_config = vim.tbl_extend("force", { - split = "below", - win = -1, -- creates top-level split - height = 10, - - hide = false - }, spec.config.message.history_winconfig or {}); - - if message.history_window and vim.api.nvim_win_is_valid(message.history_window) then - pcall(vim.api.nvim_win_set_config, message.history_window, window_config); - else - message.history_window = vim.api.nvim_open_win(message.history_buffer, true, window_config); - vim.api.nvim_win_set_var(message.history_window, "ui_window", true); - end - - vim.api.nvim_set_current_win(message.history_window); - - utils.set("w", message.history_window, "number", true); - utils.set("w", message.history_window, "relativenumber", true); - utils.set("w", message.history_window, "numberwidth", 1); - utils.set("w", message.history_window, "statuscolumn", "%!v:lua.__ui_statuscolumn()"); - - utils.set("w", message.history_window, "wrap", true); - utils.set("w", message.history_window, "linebreak", true); - - utils.redraw({ - flush = true, - - win = message.history_window, - statuscolumn = true, - }); - - vim.g.__ui_history = false; - - ---|fE -end - ------------------------------------------------------------------------------- - -message.__showcmd = function (content) - ---|fS - - content = content or vim.g.__ui_showcmd or {}; - - message.__prepare(); - vim.g.__ui_showcmd = content; - - local window_config = vim.tbl_extend("keep", spec.config.message.showcmd_winconfig or {}, { - relative = "editor", - - row = vim.o.lines - (vim.o.cmdheight + message.__get_cmdline_offset() + 2) , - col = 0, - - width = 10, - height = 1, - - border = "none", - - zindex = 80, - hide = false - }); - - local text, extmarks = utils.process_content(content); - - if #text == 0 or text[1] == "" then - -- Close the window if there is no text. - vim.g.__ui_showcmd = {}; - window_config.hide = true; - - pcall(vim.api.nvim_win_set_config, message.show_window, window_config); - return; - elseif spec.config.message.showcmd.modifier then - local modifier = utils.eval(spec.config.message.showcmd.modifier, content, text, extmarks); - - if type(modifier) == "table" then - text = modifier.lines or text; - extmarks = modifier.extmarks or extmarks; - end - end - - --- Change window width. - window_config.width = math.min( - vim.fn.strdisplaywidth(text[1]), - spec.config.message.showcmd.max_width or 0 - ); - - vim.api.nvim_buf_clear_namespace(message.show_buffer, message.namespace, 0, -1); - vim.api.nvim_buf_set_lines(message.show_buffer, 0, -1, false, text); - - for l, line in ipairs(extmarks) do - for _, ext in ipairs(line) do - if ext[3] == "" then - goto continue; - end - - vim.api.nvim_buf_set_extmark(message.show_buffer, message.namespace, l - 1, ext[1], { - end_col = ext[2], - hl_group = ext[3] - }); - - ::continue:: - end - end - - if message.show_window and vim.api.nvim_win_is_valid(message.show_window) then - vim.api.nvim_win_set_config(message.show_window, window_config); - else - message.show_window = utils.open_win(message.show_buffer, false, window_config); - vim.api.nvim_win_set_var(message.show_window, "ui_window", true); - - --- Always horizontally center the cursor. - --- We can't dynamically set this without bombing users with - --- `OptionSet` events. - utils.set("w", message.show_window, "sidescrolloff", 999); - end - - -- Use display width instead of byte length as there are - -- multi-byte characters. - -- If all else fails, `pcall()` should save us. - pcall(vim.api.nvim_win_set_cursor, message.show_window, { - 1, math.floor(window_config.width / 2) - }); - - -- `Showcmd` messages should always be updated! - vim.api.nvim__redraw({ - flush = true, - win = message.show_window + ignore = false, }); ---|fE end ------------------------------------------------------------------------------- - ----@param kind ui.message.kind ----@param content ui.message.fragment[] ----@param replace_last boolean ----@param add_to_history boolean -message.msg_show = function (kind, content, replace_last, add_to_history) - ---|fS - - local _replace_last = replace_last; - - if spec.config.message.respect_replace_last == false then - _replace_last = false; - end - - if kind == "confirm" then - -- Confirm messages need to be - -- handled first. - - log.assert( - "ui/message.lua → __confirm", - pcall(message.__confirm, { - type = "confirm", - - kind = kind, - content = content, - }) - ); - elseif message.ui_attached == false then - -- Cache messages if the UI hasn't been attached - -- to yet. - message.cache(kind, content, _replace_last, add_to_history); - elseif kind == "search_count" then - message.__replace(kind, content, add_to_history); - elseif kind == "return_prompt" then - --- Hit `` on hit-enter prompts. - --- or else we get stuck. - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", false); - elseif _replace_last and vim.tbl_isempty(message.visible) == false then - message.__replace(kind, content, add_to_history); - else - message.__add(kind, content, add_to_history) - end - - ---|fE -end - ----@param entries ( ui.message.fragment[] )[] -message.msg_history_show = function (entries) - ---|fS - - -- Escape hit-enter from opening messages. - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", false); - - log.assert( - "ui/message.lua → history_show", - pcall(message.__history, entries) - ); - - ---|fE -end - --- message.msg_showmode = function (content) --- table.insert(log.entries, vim.inspect(content)) --- end - -message.msg_showcmd = function (content) - message.__showcmd(content); -end - -message.msg_clear = function () - ---|fS - - if not vim.g.__confirm_keys or #vim.g.__confirm_keys == 0 then - return; - end - - for k, v in pairs(message.visible) do - v.timer:stop(); - message.__remove(k); - end - - message.__render(); - - ---|fE -end - ------------------------------------------------------------------------------- - -message.on_attach = function () - ---|fS - - if spec.config.message.wrap_notify then - _G.__vim_notify = vim.notify; - _G.__vim_notify_once = vim.notify_once; - - ---@diagnostic disable-next-line: duplicate-set-field - vim.notify = function (msg, level, opts) - ---|fS "refactor: Custom vim.notify" - - msg = tostring(msg); - level = level or vim.log.levels.INFO; - opts = opts or {}; - - ---@type ui.message.fragment[] - local chunks = { - { - 0, - msg, - level == vim.log.levels.WARN and vim.fn.hlID("WarningMsg") or (level == vim.log.levels.ERROR and vim.fn.hlID("ErrorMsg") or 0) - } - }; - - if type(opts.title) == "string" then - table.insert(chunks, 1, { - 0, - string.format(" %s ", opts.title), - vim.fn.hlID("UIMessageDefault") - }); - table.insert(chunks, 2 ,{ - 0, ": ", vim.fn.hlID("@comment") - }); - end - - message.msg_show("", chunks, false, true) - - ---|fE - end - - ---@type string[] - local showed = {}; - - ---@diagnostic disable-next-line: duplicate-set-field - vim.notify_once = function (msg, level, opts) - ---|fS "refactor: Custom vim.notify_once" - - if vim.list_contains(showed, msg) then - return; - else - table.insert(showed, msg); - vim.notify(msg, level, opts); - end - - ---|fE - end - end - - ---|fE -end - -message.on_detach = function () - ---|fS - - if spec.config.message.wrap_notify then - vim.notify = _G.__vim_notify; - vim.notify_once = _G.__vim_notify_once; - end - - ---|fE -end - ------------------------------------------------------------------------------- - --- Handles message events. ---@param event string ---@param ... any @@ -1555,34 +291,34 @@ message.setup = function () vim.api.nvim_create_autocmd("VimResized", { callback = function () - message.__list_resize(); - - if vim.g.__ui_showcmd then - message.__showcmd(vim.g.__ui_showcmd); - end - - message.__render(); - end - }); - - vim.api.nvim_create_autocmd("TabLeave", { - callback = function () - pcall(vim.api.nvim_win_close, message.msg_window, true); - pcall(vim.api.nvim_win_close, message.show_window, true); - - message.msg_window = nil; - message.show_window = nil; - end - }); + -- message.__list_resize(); + -- + -- if vim.g.__ui_showcmd then + -- message.__showcmd(vim.g.__ui_showcmd); + -- end - vim.api.nvim_create_autocmd({ - "VimEnter", - "TabEnter" - }, { - callback = function () - message.__render(); + message.render(); end }); + -- + -- vim.api.nvim_create_autocmd("TabLeave", { + -- callback = function () + -- pcall(vim.api.nvim_win_close, message.msg_window, true); + -- pcall(vim.api.nvim_win_close, message.show_window, true); + -- + -- message.msg_window = nil; + -- message.show_window = nil; + -- end + -- }); + -- + -- vim.api.nvim_create_autocmd({ + -- "VimEnter", + -- "TabEnter" + -- }, { + -- callback = function () + -- message.__render(); + -- end + -- }); ---|fE end From 69b1dc06d41bacf1c9673a7b0f69c0226f36b76e Mon Sep 17 00:00:00 2001 From: Shawon Date: Wed, 7 Oct 2026 22:31:38 +0600 Subject: [PATCH 02/16] refactor: Added back decoration support --- lua/ui/message.lua | 174 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 26 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index ceb2522..71e24fa 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -55,6 +55,9 @@ message.visible = {}; ---@type table message.visible_special = {}; +---@type ui.message.decorations[] +message.visible_decorations = {}; + ---@type ui.message.decorations[] message.statuscolumn_decorations = {} @@ -62,7 +65,7 @@ message.timer = function (id, duration, interval) local timer = vim.uv.new_timer(); if timer then - timer:start(duration, interval, vim.schedule_wrap(function () + timer:start(duration or 5000, interval or 0, vim.schedule_wrap(function () message.free(id); end)); end @@ -110,10 +113,79 @@ end ------------------------------------------------------------------------------ +message.msg_list = function (kind, content, replace_last, history, append, id, trigger) + ---|fS + + local msg = message.new(kind, content, replace_last, history, append, id, trigger); + message.history[id] = msg; + message.prepare(); + + vim.api.nvim_buf_set_keymap(message.data.list_buffer, "n", "q", "", { + callback = function () + pcall(vim.api.nvim_win_close, message.data.list_window, true); + end + }); + + local lines, extmarks = utils.process_content(content); + local style = spec.get_listmsg_style(msg, lines, extmarks); + + log.print(style, "HERE"); + + if style.modifier then + lines = style.modifier.lines or lines; + extmarks = style.modifier.extmarks or extmarks; + end + + vim.api.nvim_buf_clear_namespace(message.data.list_buffer, message.data.namespace, 0, -1); + vim.api.nvim_buf_set_lines(message.data.list_buffer, 0, -1, false, lines); + + local width = math.min( + math.floor(vim.o.columns * 0.5), + utils.max_len(lines) + ); + local height = utils.wrapped_height(lines, width); + + local window_opts = vim.tbl_extend("force", spec.config.message.list_winconfig, { + relative = "editor", + + row = style.row or math.ceil((vim.o.lines - height) / 2), + col = style.col or math.ceil((vim.o.columns - width) / 2), + + width = width, + height = height, + + border = "none", + + zindex = 200, + hide = false, + }); + + vim.api.nvim_win_set_config(message.data.list_window, window_opts); + vim.api.nvim_set_current_win(message.data.list_window); + + utils.redraw({ + flush = true, + statuscolumn = true, + + win = message.data.list_window + }, { + ignore = false, + }); + + ---|fE +end + message.msg_show = function (kind, content, replace_last, history, append, id, trigger) vim.schedule(function () + local is_list = spec.is_list(kind, content, history); local msg = message.new(kind, content, replace_last, history, append, id, trigger); local lines = utils.to_lines(content); + + if is_list or #lines > (spec.config.message.max_lines or math.floor(vim.o.lines * 0.5)) then + message.msg_list(kind, content, replace_last, history, append, id, trigger); + return; + end + local style = spec.get_msg_style(msg, lines, {}) or {}; -- NOTE: Make sure to reset the freeing timer. @@ -176,10 +248,59 @@ message.prepare = function () message.set_buf_win("showmode_buffer", "showmode_window"); end +message.apply_extmarks = function (src, buffer, extmarks) + ---|fS + + for l, line in ipairs(extmarks) do + for _, item in ipairs(line) do + if item[3] == "" then + goto continue; + end + + log.assert( + "ui/message.lua → " .. src, + pcall( + vim.api.nvim_buf_set_extmark, + buffer, + message.data.namespace, + + l - 1, + item[1], + + { + end_col = item[2], + hl_group = item[3], + } + ) + ); + + ::continue:: + end + end + + ---|fE +end + +message.apply_msg_decorations = function () + ---|fS + + for _, item in ipairs(message.visible_decorations) do + if item.line_hl_group then + pcall(vim.api.nvim_buf_set_extmark, message.data.buffer, message.data.namespace, item.from, 0, { + end_row = item.to, + line_hl_group = item.line_hl_group, + }); + end + end + + ---|fE +end + message.render = function () ---|fS message.prepare(); + message.visible_decorations = {}; local lines = {}; local extmarks = {}; @@ -195,35 +316,33 @@ message.render = function () table.sort(msg_orders); - for _, msg_order in ipairs(msg_orders) do - local msg = message.visible[msg_order]; - local m_lines, m_exts = utils.process_content(msg.content); - - local style = spec.get_msg_style(msg, m_lines, m_exts) or {}; - if style.modifier then - m_lines = style.modifier.lines or m_lines; - m_exts = style.modifier.extmarks or m_exts; + local function handle_msg_orders (orders, msgs) + for _, msg_order in ipairs(orders) do + local msg = msgs[msg_order]; + local m_lines, m_exts = utils.process_content(msg.content); + + local style = spec.get_msg_style(msg, m_lines, m_exts) or {}; + if style.modifier then + m_lines = style.modifier.lines or m_lines; + m_exts = style.modifier.extmarks or m_exts; + end + + if style.decorations then + table.insert(message.visible_decorations, vim.tbl_extend("force", style.decorations, { + from = #lines, + to = (#lines + #m_lines) - 1, + })); + end + + lines = vim.list_extend(lines, m_lines); + extmarks = vim.list_extend(extmarks, m_exts); end - - lines = vim.list_extend(lines, m_lines); - extmarks = vim.list_extend(extmarks, m_exts); end - table.sort(sp_msg_orders); + handle_msg_orders(msg_orders, message.visible) - for _, msg_order in ipairs(sp_msg_orders) do - local msg = message.visible_special[msg_order]; - local m_lines, m_exts = utils.process_content(msg.content); - - local style = spec.get_msg_style(msg, m_lines, m_exts) or {}; - if style.modifier then - m_lines = style.modifier.lines or m_lines; - m_exts = style.modifier.extmarks or m_exts; - end - - lines = vim.list_extend(lines, m_lines); - extmarks = vim.list_extend(extmarks, m_exts); - end + table.sort(sp_msg_orders); + handle_msg_orders(sp_msg_orders, message.visible_special) while lines[#lines] == "" do table.remove(lines); @@ -232,6 +351,9 @@ message.render = function () vim.api.nvim_buf_clear_namespace(message.data.buffer, message.data.namespace, 0, -1); vim.api.nvim_buf_set_lines(message.data.buffer, 0, -1, false, lines); + message.apply_extmarks("HERE", message.data.buffer, extmarks); + message.apply_msg_decorations(); + local width = math.min( math.floor(vim.o.columns * 0.5), utils.max_len(lines) From f2508af935feb70374aa39de687b1dc4fff427c8 Mon Sep 17 00:00:00 2001 From: Shawon Date: Wed, 7 Oct 2026 22:31:49 +0600 Subject: [PATCH 03/16] fix: Fixed write message conditions --- lua/ui/spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/spec.lua b/lua/ui/spec.lua index 9a276c1..02b8b3e 100644 --- a/lua/ui/spec.lua +++ b/lua/ui/spec.lua @@ -1280,7 +1280,7 @@ spec.default = { ---|fS condition = function (msg) - return msg.kind == "bufwrite"; + return msg.id == "bufwrite"; end, modifier = function (_, lines) From b0789fe9b7151257cf432d7b310e90265385b390 Mon Sep 17 00:00:00 2001 From: Shawon Date: Wed, 7 Oct 2026 23:59:01 +0600 Subject: [PATCH 04/16] refactor: Added back icon decorations --- lua/ui/message.lua | 51 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 71e24fa..645bd16 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -213,6 +213,42 @@ end ------------------------------------------------------------------------------ +message.statuscolumn = function () + ---|fS + + local win = vim.g.statusline_winid; + + if win ~= message.data.window and win ~= message.data.history_window then + return ""; + end + + local row = vim.v.lnum - 1; + + for _, item in ipairs(message.visible_decorations) do + if row >= item.from and row <= item.to then + if vim.v.virtnum == 0 then + if row == item.from then + return "%=" .. utils.to_statuscolumn(item.icon); + elseif row == item.to then + return "%=" .. utils.to_statuscolumn(item.tail or item.padding or item.icon); + else + return "%=" .. utils.to_statuscolumn(item.padding or item.icon); + end + else + return "%="; + end + + break; + end + end + + return ""; + + ---|fE +end + +_G.ui_statuscolumn = message.statuscolumn; + message.set_buf_win = function (buf, win) ---@type vim.api.keyset.win_config local window_opts = { @@ -246,6 +282,8 @@ message.prepare = function () message.set_buf_win("list_buffer", "list_window"); message.set_buf_win("history_buffer", "history_window"); message.set_buf_win("showmode_buffer", "showmode_window"); + + utils.set("w", message.data.window, "statuscolumn", "%!v:lua.ui_statuscolumn()"); end message.apply_extmarks = function (src, buffer, extmarks) @@ -281,10 +319,17 @@ message.apply_extmarks = function (src, buffer, extmarks) ---|fE end +---@return integer message.apply_msg_decorations = function () ---|fS + local decor_width = 0; + for _, item in ipairs(message.visible_decorations) do + if item.icon then + decor_width = math.max(decor_width, utils.virt_len(item.icon)); + end + if item.line_hl_group then pcall(vim.api.nvim_buf_set_extmark, message.data.buffer, message.data.namespace, item.from, 0, { end_row = item.to, @@ -293,6 +338,8 @@ message.apply_msg_decorations = function () end end + return decor_width; + ---|fE end @@ -352,7 +399,7 @@ message.render = function () vim.api.nvim_buf_set_lines(message.data.buffer, 0, -1, false, lines); message.apply_extmarks("HERE", message.data.buffer, extmarks); - message.apply_msg_decorations(); + local decor_size = message.apply_msg_decorations(); local width = math.min( math.floor(vim.o.columns * 0.5), @@ -367,7 +414,7 @@ message.render = function () row = vim.o.lines - 1, col = vim.o.columns, - width = width, + width = width + decor_size, height = height, border = "none", From ec1d9c2627e20fff0fb4733ca681bc8c8b067a72 Mon Sep 17 00:00:00 2001 From: Shawon Date: Thu, 8 Oct 2026 08:23:18 +0600 Subject: [PATCH 05/16] refactor: Added back confirm message support --- lua/ui/message.lua | 139 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 4 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 645bd16..7c9a536 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -113,6 +113,126 @@ end ------------------------------------------------------------------------------ +local movement_keys = { + vim.api.nvim_replace_termcodes("", true, true, true), + vim.api.nvim_replace_termcodes("", true, true, true), + vim.api.nvim_replace_termcodes("", true, true, true), + vim.api.nvim_replace_termcodes("", true, true, true), + + vim.api.nvim_replace_termcodes("h", true, true, true), + vim.api.nvim_replace_termcodes("j", true, true, true), + vim.api.nvim_replace_termcodes("k", true, true, true), + vim.api.nvim_replace_termcodes("l", true, true, true), +}; + +message.confirm_movement = function (key) + ---|fS + + local success, pos = pcall(vim.api.nvim_win_get_cursor, message.data.confirm_window); + if not success then return; end + + if key == movement_keys[1] or key == movement_keys[5] then + pos[1] = math.max(0, pos[1] - 1); + elseif key == movement_keys[2] or key == movement_keys[6] then + pos[2] = math.max(0, pos[2] - 1); + elseif key == movement_keys[3] or key == movement_keys[7] then + pos[2] = pos[2] + 1; + elseif key == movement_keys[4] or key == movement_keys[8] then + pos[1] = pos[1] + 1; + end + + pcall(vim.api.nvim_win_set_cursor, message.data.confirm_window, pos); + + ---|fE +end + +---@param kind ui.message.kind +---@param content ui.message.fragment[] +---@param replace_last boolean +---@param history boolean +---@param append boolean +---@param id string +---@param trigger string +message.msg_confirm = function (kind, content, replace_last, history, append, id, trigger) + ---|fS + + local msg = message.new(kind, content, replace_last, history, append, id, trigger); + vim.g.__ui_confirm_msg = msg; + + message.history[id] = msg; + message.prepare(); + + local lines, extmarks = utils.process_content(content); + local style = spec.get_confirm_style(msg, lines, extmarks); + + if style.modifier then + lines = style.modifier.lines or lines; + extmarks = style.modifier.extmarks or extmarks; + end + + vim.api.nvim_buf_clear_namespace(message.data.confirm_buffer, message.data.namespace, 0, -1); + vim.api.nvim_buf_set_lines(message.data.confirm_buffer, 0, -1, false, lines); + message.apply_extmarks("msg_confirm", message.data.confirm_buffer, extmarks); + + local width = math.min( + math.floor(vim.o.columns * 0.5), + utils.max_len(lines) + ); + local height = utils.wrapped_height(lines, width); + + local window_opts = vim.tbl_extend("force", spec.config.message.confirm_winconfig or {}, { + relative = "editor", + + row = style.row or math.ceil((vim.o.lines - height) / 2), + col = style.col or math.ceil((vim.o.columns - width) / 2), + + width = width, + height = height, + + border = "none", + + zindex = 200, + hide = false, + }); + + vim.api.nvim_win_set_config(message.data.confirm_window, window_opts); + vim.api.nvim_set_current_win(message.data.confirm_window); + + utils.redraw({ + flush = true, + statuscolumn = true, + + win = message.data.confirm_window + }, { + ignore = false, + }); + + vim.on_key(function (key) + if not vim.list_contains(vim.g.__confirm_keys or {}, string.lower(key)) then + if vim.list_contains(movement_keys, key) then + pcall(message.confirm_movement, key); + pcall(vim.cmd, "mode"); + end + + return; + end + + pcall(vim.api.nvim_win_close, message.data.confirm_window, true); + vim.on_key(nil, message.data.namespace); + + vim.g.__ui_confirm_msg = nil; + end, message.data.namespace) + + ---|fE +end + +---@param kind ui.message.kind +---@param content ui.message.fragment[] +---@param replace_last boolean +---@param history boolean +---@param append boolean +---@param id string +---@param trigger string message.msg_list = function (kind, content, replace_last, history, append, id, trigger) ---|fS @@ -129,8 +249,6 @@ message.msg_list = function (kind, content, replace_last, history, append, id, t local lines, extmarks = utils.process_content(content); local style = spec.get_listmsg_style(msg, lines, extmarks); - log.print(style, "HERE"); - if style.modifier then lines = style.modifier.lines or lines; extmarks = style.modifier.extmarks or extmarks; @@ -138,6 +256,7 @@ message.msg_list = function (kind, content, replace_last, history, append, id, t vim.api.nvim_buf_clear_namespace(message.data.list_buffer, message.data.namespace, 0, -1); vim.api.nvim_buf_set_lines(message.data.list_buffer, 0, -1, false, lines); + message.apply_extmarks("msg_list", message.data.list_buffer, extmarks); local width = math.min( math.floor(vim.o.columns * 0.5), @@ -145,7 +264,7 @@ message.msg_list = function (kind, content, replace_last, history, append, id, t ); local height = utils.wrapped_height(lines, width); - local window_opts = vim.tbl_extend("force", spec.config.message.list_winconfig, { + local window_opts = vim.tbl_extend("force", spec.config.message.list_winconfig or {}, { relative = "editor", row = style.row or math.ceil((vim.o.lines - height) / 2), @@ -175,8 +294,20 @@ message.msg_list = function (kind, content, replace_last, history, append, id, t ---|fE end +---@param kind ui.message.kind +---@param content ui.message.fragment[] +---@param replace_last boolean +---@param history boolean +---@param append boolean +---@param id string +---@param trigger string message.msg_show = function (kind, content, replace_last, history, append, id, trigger) vim.schedule(function () + if kind == "confirm" then + message.msg_confirm(kind, content, replace_last, history, append, id, trigger); + return; + end + local is_list = spec.is_list(kind, content, history); local msg = message.new(kind, content, replace_last, history, append, id, trigger); local lines = utils.to_lines(content); @@ -407,7 +538,7 @@ message.render = function () ); local height = utils.wrapped_height(lines, width); - local window_opts = vim.tbl_extend("force", spec.config.message.message_winconfig, { + local window_opts = vim.tbl_extend("force", spec.config.message.message_winconfig or {}, { relative = "editor", anchor = "SE", From 09af170efa0263918cc950f873b2cdb77fcc7dc8 Mon Sep 17 00:00:00 2001 From: Shawon Date: Thu, 8 Oct 2026 09:49:14 +0600 Subject: [PATCH 06/16] refactor: Added back showcmd --- lua/ui/message.lua | 155 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 25 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 7c9a536..c819f4a 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -342,6 +342,111 @@ message.msg_show = function (kind, content, replace_last, history, append, id, t end) end +local showmode_hide_timer = vim.uv.new_timer(); + +message.showmode_hide = function () + if not showmode_hide_timer then + showmode_hide_timer = vim.uv.new_timer(); + end + + if showmode_hide_timer then + showmode_hide_timer:stop(); + showmode_hide_timer:start(500, 0, vim.schedule_wrap(function () + pcall(vim.api.nvim_win_close, message.data.showmode_window, true); + end)); + end +end + +message.showmode_resize = function () + ---|fS + + if vim.api.nvim_win_is_valid(message.data.showmode_window) then + ---@type vim.api.keyset.win_config + local window_opts = { + relative = "editor", + anchor = "SW", + + row = vim.o.lines - (1 + message.cmdline_offset()), + col = 0, + + border = "none", + + zindex = 200, + hide = false, + }; + + vim.api.nvim_win_set_config(message.data.showmode_window, window_opts); + utils.redraw({ + flush = true, + win = message.data.showmode_window + }, { + ignore = false, + }); + end + + ---|fE +end + +message.msg_showcmd = function (content) + ---|fS + + vim.schedule(function () + if #content == 0 then + message.showmode_hide(); + return; + end + + message.prepare(); + + local lines, extmarks = utils.process_content(content); + local modifier = utils.eval(spec.config.message.showcmd.modifier, content, lines, extmarks); + + if modifier then + lines = modifier.lines or lines; + extmarks = modifier.extmarks or extmarks; + end + + vim.api.nvim_buf_clear_namespace(message.data.showmode_buffer, message.data.namespace, 0, -1); + vim.api.nvim_buf_set_lines(message.data.showmode_buffer, 0, -1, false, lines); + message.apply_extmarks("msg_list", message.data.showmode_buffer, extmarks); + + local width = utils.max_len(lines); + local height = 1; + + ---@type vim.api.keyset.win_config + local window_opts = { + relative = "editor", + anchor = "SW", + + row = vim.o.lines - (1 + message.cmdline_offset()), + col = 0, + + width = width, + height = height, + + border = "none", + + zindex = 200, + hide = false, + }; + + vim.api.nvim_win_set_config(message.data.showmode_window, window_opts); + utils.set("w", message.data.showmode_window, "sidescrolloff", 999) + pcall(vim.api.nvim_win_set_cursor, message.data.showmode_window, { 0, width }) + + utils.redraw({ + flush = true, + statuscolumn = true, + + win = message.data.showmode_window + }, { + ignore = false, + }); + end); + + ---|fE +end + ------------------------------------------------------------------------------ message.statuscolumn = function () @@ -474,6 +579,14 @@ message.apply_msg_decorations = function () ---|fE end +message.cmdline_offset = function () + if (vim.g.__ui_cmd_height or 0) > 0 then + return (vim.g.__ui_cmd_height or 0) + (spec.config.cmdline.row_offset or 1) + end + + return (vim.g.__ui_cmd_height or 0); +end + message.render = function () ---|fS @@ -542,7 +655,7 @@ message.render = function () relative = "editor", anchor = "SE", - row = vim.o.lines - 1, + row = vim.o.lines - (1 + message.cmdline_offset()), col = vim.o.columns, width = width + decor_size, @@ -591,34 +704,26 @@ message.setup = function () vim.api.nvim_create_autocmd("VimResized", { callback = function () - -- message.__list_resize(); - -- - -- if vim.g.__ui_showcmd then - -- message.__showcmd(vim.g.__ui_showcmd); - -- end + message.showmode_resize(); + message.render(); + end + }); + vim.api.nvim_create_autocmd("TabLeave", { + callback = function () + pcall(vim.api.nvim_win_close, message.data.window, true); + pcall(vim.api.nvim_win_close, message.data.showmode_window, true); + end + }); + + vim.api.nvim_create_autocmd({ + "VimEnter", + "TabEnter" + }, { + callback = function () message.render(); end }); - -- - -- vim.api.nvim_create_autocmd("TabLeave", { - -- callback = function () - -- pcall(vim.api.nvim_win_close, message.msg_window, true); - -- pcall(vim.api.nvim_win_close, message.show_window, true); - -- - -- message.msg_window = nil; - -- message.show_window = nil; - -- end - -- }); - -- - -- vim.api.nvim_create_autocmd({ - -- "VimEnter", - -- "TabEnter" - -- }, { - -- callback = function () - -- message.__render(); - -- end - -- }); ---|fE end From 1e372eeaa9d86a2a7803b6681ae62139ae459905 Mon Sep 17 00:00:00 2001 From: Shawon Date: Thu, 8 Oct 2026 10:04:21 +0600 Subject: [PATCH 07/16] refactor: Removed `schedule()` for showcmd --- lua/ui/message.lua | 78 ++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index c819f4a..f08066d 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -390,59 +390,55 @@ end message.msg_showcmd = function (content) ---|fS - vim.schedule(function () - if #content == 0 then - message.showmode_hide(); - return; - end - - message.prepare(); + if #content == 0 then + message.showmode_hide(); + return; + end - local lines, extmarks = utils.process_content(content); - local modifier = utils.eval(spec.config.message.showcmd.modifier, content, lines, extmarks); + message.prepare(); - if modifier then - lines = modifier.lines or lines; - extmarks = modifier.extmarks or extmarks; - end + local lines, extmarks = utils.process_content(content); + local modifier = utils.eval(spec.config.message.showcmd.modifier, content, lines, extmarks); - vim.api.nvim_buf_clear_namespace(message.data.showmode_buffer, message.data.namespace, 0, -1); - vim.api.nvim_buf_set_lines(message.data.showmode_buffer, 0, -1, false, lines); - message.apply_extmarks("msg_list", message.data.showmode_buffer, extmarks); + if modifier then + lines = modifier.lines or lines; + extmarks = modifier.extmarks or extmarks; + end - local width = utils.max_len(lines); - local height = 1; + vim.api.nvim_buf_clear_namespace(message.data.showmode_buffer, message.data.namespace, 0, -1); + vim.api.nvim_buf_set_lines(message.data.showmode_buffer, 0, -1, false, lines); + message.apply_extmarks("msg_list", message.data.showmode_buffer, extmarks); - ---@type vim.api.keyset.win_config - local window_opts = { - relative = "editor", - anchor = "SW", + local width = utils.max_len(lines); + local height = 1; - row = vim.o.lines - (1 + message.cmdline_offset()), - col = 0, + ---@type vim.api.keyset.win_config + local window_opts = { + relative = "editor", + anchor = "SW", - width = width, - height = height, + row = vim.o.lines - (1 + message.cmdline_offset()), + col = 0, - border = "none", + width = width, + height = height, - zindex = 200, - hide = false, - }; + border = "none", - vim.api.nvim_win_set_config(message.data.showmode_window, window_opts); - utils.set("w", message.data.showmode_window, "sidescrolloff", 999) - pcall(vim.api.nvim_win_set_cursor, message.data.showmode_window, { 0, width }) + zindex = 200, + hide = false, + }; - utils.redraw({ - flush = true, - statuscolumn = true, + vim.api.nvim_win_set_config(message.data.showmode_window, window_opts); + utils.set("w", message.data.showmode_window, "sidescrolloff", 999) + pcall(vim.api.nvim_win_set_cursor, message.data.showmode_window, { 0, width }) - win = message.data.showmode_window - }, { - ignore = false, - }); - end); + utils.redraw({ + flush = true, + win = message.data.showmode_window + }, { + ignore = false, + }); ---|fE end From 8ad3045cd42729e5359f02790552c1ade066ca29 Mon Sep 17 00:00:00 2001 From: Shawon Date: Thu, 8 Oct 2026 13:10:14 +0600 Subject: [PATCH 08/16] refator: Added back message history --- lua/ui/message.lua | 297 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 259 insertions(+), 38 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index f08066d..c339464 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -59,13 +59,13 @@ message.visible_special = {}; message.visible_decorations = {}; ---@type ui.message.decorations[] -message.statuscolumn_decorations = {} +message.history_decorations = {} -message.timer = function (id, duration, interval) +message.timer = function(id, duration, interval) local timer = vim.uv.new_timer(); if timer then - timer:start(duration or 5000, interval or 0, vim.schedule_wrap(function () + timer:start(duration or 5000, interval or 0, vim.schedule_wrap(function() message.free(id); end)); end @@ -73,14 +73,14 @@ message.timer = function (id, duration, interval) return timer; end -message.free = function (id) +message.free = function(id) if type(id) == "string" then message.visible_special[id] = nil; else message.visible[id] = nil; end - vim.schedule(function () + vim.schedule(function() log.assert( "ui/message.lua → remove_render", pcall(message.render) @@ -96,7 +96,7 @@ end ---@param id string ---@param _trigger string ---@return ui.message.entry -message.new = function (kind, content, replace_last, history, _append, id, _trigger) +message.new = function(kind, content, replace_last, history, _append, id, _trigger) return { id = id, kind = kind, @@ -125,7 +125,7 @@ local movement_keys = { vim.api.nvim_replace_termcodes("l", true, true, true), }; -message.confirm_movement = function (key) +message.confirm_movement = function(key) ---|fS local success, pos = pcall(vim.api.nvim_win_get_cursor, message.data.confirm_window); @@ -153,7 +153,7 @@ end ---@param append boolean ---@param id string ---@param trigger string -message.msg_confirm = function (kind, content, replace_last, history, append, id, trigger) +message.msg_confirm = function(kind, content, replace_last, history, append, id, trigger) ---|fS local msg = message.new(kind, content, replace_last, history, append, id, trigger); @@ -207,7 +207,7 @@ message.msg_confirm = function (kind, content, replace_last, history, append, id ignore = false, }); - vim.on_key(function (key) + vim.on_key(function(key) if not vim.list_contains(vim.g.__confirm_keys or {}, string.lower(key)) then if vim.list_contains(movement_keys, key) then pcall(message.confirm_movement, key); @@ -233,7 +233,7 @@ end ---@param append boolean ---@param id string ---@param trigger string -message.msg_list = function (kind, content, replace_last, history, append, id, trigger) +message.msg_list = function(kind, content, replace_last, history, append, id, trigger) ---|fS local msg = message.new(kind, content, replace_last, history, append, id, trigger); @@ -241,7 +241,7 @@ message.msg_list = function (kind, content, replace_last, history, append, id, t message.prepare(); vim.api.nvim_buf_set_keymap(message.data.list_buffer, "n", "q", "", { - callback = function () + callback = function() pcall(vim.api.nvim_win_close, message.data.list_window, true); end }); @@ -301,8 +301,8 @@ end ---@param append boolean ---@param id string ---@param trigger string -message.msg_show = function (kind, content, replace_last, history, append, id, trigger) - vim.schedule(function () +message.msg_show = function(kind, content, replace_last, history, append, id, trigger) + vim.schedule(function() if kind == "confirm" then message.msg_confirm(kind, content, replace_last, history, append, id, trigger); return; @@ -344,20 +344,20 @@ end local showmode_hide_timer = vim.uv.new_timer(); -message.showmode_hide = function () +message.showmode_hide = function() if not showmode_hide_timer then showmode_hide_timer = vim.uv.new_timer(); end if showmode_hide_timer then showmode_hide_timer:stop(); - showmode_hide_timer:start(500, 0, vim.schedule_wrap(function () + showmode_hide_timer:start(500, 0, vim.schedule_wrap(function() pcall(vim.api.nvim_win_close, message.data.showmode_window, true); end)); end end -message.showmode_resize = function () +message.showmode_resize = function() ---|fS if vim.api.nvim_win_is_valid(message.data.showmode_window) then @@ -387,7 +387,7 @@ message.showmode_resize = function () ---|fE end -message.msg_showcmd = function (content) +message.msg_showcmd = function(content) ---|fS if #content == 0 then @@ -443,9 +443,227 @@ message.msg_showcmd = function (content) ---|fE end +message.history_keymaps = function(items) + if not message.data.history_buffer then + return; + end + + vim.api.nvim_buf_set_keymap(message.data.history_buffer, "n", "u", "", { + desc = "[u]pdates message history.", + callback = function() + if vim.g.history_source == "vim" then + vim.cmd("message"); + else + message.msg_history_show(items); + end + end + }); + vim.api.nvim_buf_set_keymap(message.data.history_buffer, "n", "t", "", { + desc = "[t]oggles between `vim` and `ui.nvim`'s message history.", + callback = function() + if vim.g.history_source == "vim" then + vim.g.history_source = "ui.nvim"; + else + vim.g.history_source = "vim"; + end + + message.msg_history_show(items); + end + }); + + vim.api.nvim_buf_set_keymap(message.data.history_buffer, "n", "q", "", { + desc = "[q]uit", + callback = function() + log.assert( + "ui/message.lua → history_quit", + pcall(vim.api.nvim_win_close, message.data.history_window, true) + ) + end + }); + + vim.api.nvim_buf_set_keymap(message.data.history_buffer, "n", "N", "", { + desc = "Toggles [N]ormal msssags visiblity.", + callback = function() + if _G.history_show then + _G.history_show.normal = not _G.history_show.normal; + end + + message.msg_history_show(items); + end + }); + vim.api.nvim_buf_set_keymap(message.data.history_buffer, "n", "H", "", { + desc = "Toggles [H]idden msssags visiblity.", + callback = function() + if _G.history_show then + _G.history_show.hidden = not _G.history_show.hidden; + end + + message.msg_history_show(items); + end + }); + vim.api.nvim_buf_set_keymap(message.data.history_buffer, "n", "L", "", { + desc = "Toggles [L]ist msssags visiblity.", + callback = function() + if _G.history_show then + _G.history_show.list = not _G.history_show.list; + end + + message.msg_history_show(items); + end + }); + vim.api.nvim_buf_set_keymap(message.data.history_buffer, "n", "C", "", { + desc = "Toggles [C]onfirm msssags visiblity.", + callback = function() + if _G.history_show then + _G.history_show.confirm = not _G.history_show.confirm; + end + + message.msg_history_show(items); + end + }); +end + +message.msg_history_show = function(items) + vim.g.history_source = vim.g.history_source or "vim"; + _G.history_show = _G.history_show or { + normal = true, + + hidden = false, + list = false, + confirm = false, + }; + + message.prepare(); + message.history_keymaps(items); + + message.history_decorations = {}; + + local lines, extmarks = {}, {}; + + if vim.g.history_source == "vim" then + for _, item in ipairs(items) do + local i_lines, i_extmarks = utils.process_content(item[2]); + + lines = vim.list_extend(lines, i_lines); + extmarks = vim.list_extend(extmarks, i_extmarks); + end + else + local ui_chips = { + { " [N]ormal ", _G.history_show.normal }, + { " [H]idden ", _G.history_show.hidden }, + { " [L]ist ", _G.history_show.list }, + { " [C]onfirm ", _G.history_show.confirm }, + }; + + local line, extmark = "", {}; + + for i, item in ipairs(ui_chips) do + local before = #line; + line = line .. item[1] .. (i ~= #ui_chips and " " or ""); + + table.insert(extmark, + { before, before + #item[1], item[2] and "UICmdlineDefaultIcon" or "UICmdlineSearchUpIcon" }) + end + + table.insert(lines, line); + table.insert(extmarks, extmark); + + local msg_orders = vim.tbl_keys(message.history); + table.sort(msg_orders); + + for _, order in ipairs(msg_orders) do + local msg = message.history[order]; + + if _G.history_show[msg.type or "normal"] then + local m_lines, m_exts = utils.process_content(msg.content); + + local style = spec.get_msg_style(msg, m_lines, m_exts) or {}; + if style.modifier then + m_lines = style.modifier.lines or m_lines; + m_exts = style.modifier.extmarks or m_exts; + end + + if style.decorations then + table.insert(message.history_decorations, vim.tbl_extend("force", style.decorations, { + from = #lines, + to = (#lines + #m_lines) - 1, + })); + end + + lines = vim.list_extend(lines, m_lines); + extmarks = vim.list_extend(extmarks, m_exts); + end + end + + utils.set("w", message.data.history_window, "statuscolumn", "%!v:lua.ui_statuscolumn()"); + end + + vim.bo[message.data.history_buffer].modifiable = true; + + vim.api.nvim_buf_clear_namespace(message.data.history_buffer, message.data.namespace, 0, -1); + vim.api.nvim_buf_set_lines(message.data.history_buffer, 0, -1, false, lines); + + vim.bo[message.data.history_buffer].modifiable = false; + + if vim.g.history_source == "vim" then + vim.wo[message.data.history_window].statusline = table.concat({ + "%#UILSBufname#", + " VIM ", + "%#Normal#", + "%=", + "%#UIHistoryKeymap#", + " t ", + "%#UIHistoryDesc#", + " Toggle source ", + "%#Normal#", + " ", + "%#UIHistoryKeymap#", + " q ", + "%#UIHistoryDesc#", + " Quit ", + }, ""); + else + vim.wo[message.data.history_window].statusline = table.concat({ + "%#UILSBuffer#", + " UI.nvim ", + "%#Normal#", + "%=", + "%#UIHistoryKeymap#", + " u ", + "%#UIHistoryDesc#", + " Update ", + "%#Normal#", + " ", + "%#UIHistoryKeymap#", + " t ", + "%#UIHistoryDesc#", + " Toggle source ", + "%#Normal#", + " ", + "%#UIHistoryKeymap#", + " q ", + "%#UIHistoryDesc#", + " Quit ", + }, ""); + end + + message.apply_extmarks("HERE", message.data.history_buffer, extmarks); + message.apply_msg_decorations(message.history_decorations, message.data.history_buffer); + + local window_opts = vim.tbl_extend("force", spec.config.message.history_winconfig or {}, { + split = "below", + win = -1, + + height = 10, + }); + + vim.api.nvim_win_set_config(message.data.history_window, window_opts); + vim.api.nvim_set_current_win(message.data.history_window); +end + ------------------------------------------------------------------------------ -message.statuscolumn = function () +message.statuscolumn = function() ---|fS local win = vim.g.statusline_winid; @@ -456,7 +674,7 @@ message.statuscolumn = function () local row = vim.v.lnum - 1; - for _, item in ipairs(message.visible_decorations) do + for _, item in ipairs(win == message.data.window and message.visible_decorations or message.history_decorations) do if row >= item.from and row <= item.to then if vim.v.virtnum == 0 then if row == item.from then @@ -481,18 +699,21 @@ end _G.ui_statuscolumn = message.statuscolumn; -message.set_buf_win = function (buf, win) +message.set_buf_win = function(buf, win) ---@type vim.api.keyset.win_config local window_opts = { relative = "editor", - row = 0, col = 0, - width = 1, height = 1, + row = 0, + col = 0, + width = 1, + height = 1, border = "none", style = "minimal", - hide = true, focusable = false + hide = true, + focusable = false }; if not message.data[buf] or not vim.api.nvim_buf_is_valid(message.data[buf]) then @@ -508,7 +729,7 @@ message.set_buf_win = function (buf, win) end end -message.prepare = function () +message.prepare = function() message.set_buf_win("buffer", "window"); message.set_buf_win("confirm_buffer", "confirm_window"); message.set_buf_win("list_buffer", "list_window"); @@ -518,7 +739,7 @@ message.prepare = function () utils.set("w", message.data.window, "statuscolumn", "%!v:lua.ui_statuscolumn()"); end -message.apply_extmarks = function (src, buffer, extmarks) +message.apply_extmarks = function(src, buffer, extmarks) ---|fS for l, line in ipairs(extmarks) do @@ -552,18 +773,18 @@ message.apply_extmarks = function (src, buffer, extmarks) end ---@return integer -message.apply_msg_decorations = function () +message.apply_msg_decorations = function(src, buffer) ---|fS local decor_width = 0; - for _, item in ipairs(message.visible_decorations) do + for _, item in ipairs(src) do if item.icon then decor_width = math.max(decor_width, utils.virt_len(item.icon)); end if item.line_hl_group then - pcall(vim.api.nvim_buf_set_extmark, message.data.buffer, message.data.namespace, item.from, 0, { + pcall(vim.api.nvim_buf_set_extmark, buffer, message.data.namespace, item.from, 0, { end_row = item.to, line_hl_group = item.line_hl_group, }); @@ -575,7 +796,7 @@ message.apply_msg_decorations = function () ---|fE end -message.cmdline_offset = function () +message.cmdline_offset = function() if (vim.g.__ui_cmd_height or 0) > 0 then return (vim.g.__ui_cmd_height or 0) + (spec.config.cmdline.row_offset or 1) end @@ -583,7 +804,7 @@ message.cmdline_offset = function () return (vim.g.__ui_cmd_height or 0); end -message.render = function () +message.render = function() ---|fS message.prepare(); @@ -603,7 +824,7 @@ message.render = function () table.sort(msg_orders); - local function handle_msg_orders (orders, msgs) + local function handle_msg_orders(orders, msgs) for _, msg_order in ipairs(orders) do local msg = msgs[msg_order]; local m_lines, m_exts = utils.process_content(msg.content); @@ -638,8 +859,8 @@ message.render = function () vim.api.nvim_buf_clear_namespace(message.data.buffer, message.data.namespace, 0, -1); vim.api.nvim_buf_set_lines(message.data.buffer, 0, -1, false, lines); - message.apply_extmarks("HERE", message.data.buffer, extmarks); - local decor_size = message.apply_msg_decorations(); + message.apply_extmarks("msg_render", message.data.buffer, extmarks); + local decor_size = message.apply_msg_decorations(message.visible_decorations, message.data.buffer); local width = math.min( math.floor(vim.o.columns * 0.5), @@ -679,7 +900,7 @@ end --- Handles message events. ---@param event string ---@param ... any -message.handle = function (event, ...) +message.handle = function(event, ...) ---|fS log.level_inc(); @@ -695,18 +916,18 @@ message.handle = function (event, ...) ---|fE end -message.setup = function () +message.setup = function() ---|fS vim.api.nvim_create_autocmd("VimResized", { - callback = function () + callback = function() message.showmode_resize(); message.render(); end }); vim.api.nvim_create_autocmd("TabLeave", { - callback = function () + callback = function() pcall(vim.api.nvim_win_close, message.data.window, true); pcall(vim.api.nvim_win_close, message.data.showmode_window, true); end @@ -716,7 +937,7 @@ message.setup = function () "VimEnter", "TabEnter" }, { - callback = function () + callback = function() message.render(); end }); From 8a92d13460428bd194b51588546550d24ea01061 Mon Sep 17 00:00:00 2001 From: Shawon Date: Thu, 8 Oct 2026 15:34:32 +0600 Subject: [PATCH 09/16] refactor: Fixed showcmd not showing up sometimes --- lua/ui/message.lua | 78 +++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index c339464..9d4e258 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -22,8 +22,8 @@ local utils = require("ui.utils"); ---@field list_buffer? integer ---@field list_window? integer --- ----@field showmode_buffer? integer ----@field showmode_window? integer +---@field showcmd_buffer? integer +---@field showcmd_window? integer --- ---@field history_buffer? integer ---@field history_window? integer @@ -39,8 +39,8 @@ message.data = { list_buffer = nil, list_window = nil, - showmode_buffer = nil, - showmode_window = nil, + showcmd_buffer = nil, + showcmd_window = nil, history_buffer = nil, history_window = nil, @@ -342,25 +342,38 @@ message.msg_show = function(kind, content, replace_last, history, append, id, tr end) end -local showmode_hide_timer = vim.uv.new_timer(); +local showcmd_hide_timer = vim.uv.new_timer(); -message.showmode_hide = function() - if not showmode_hide_timer then - showmode_hide_timer = vim.uv.new_timer(); +message.showcmd_hide = function() + if not showcmd_hide_timer then + showcmd_hide_timer = vim.uv.new_timer(); end - if showmode_hide_timer then - showmode_hide_timer:stop(); - showmode_hide_timer:start(500, 0, vim.schedule_wrap(function() - pcall(vim.api.nvim_win_close, message.data.showmode_window, true); + if showcmd_hide_timer then + showcmd_hide_timer:stop(); + showcmd_hide_timer:start(500, 0, vim.schedule_wrap(function() + pcall(vim.api.nvim_win_close, message.data.showcmd_window, true); + pcall(vim.api.nvim_buf_set_lines, message.data.showcmd_buffer, 0, -1, false, {}) end)); end end -message.showmode_resize = function() +message.showcmd_resize = function() ---|fS - if vim.api.nvim_win_is_valid(message.data.showmode_window) then + local success, valid = pcall(vim.api.nvim_buf_is_valid, message.data.showcmd_buffer) + + if not success or not valid then + return; + end + + local lines = vim.api.nvim_buf_get_lines(message.data.showcmd_buffer, 0, -1, false); + + if not lines[1] or lines[1] == "" then + return; + end + + if vim.api.nvim_win_is_valid(message.data.showcmd_window) then ---@type vim.api.keyset.win_config local window_opts = { relative = "editor", @@ -375,10 +388,10 @@ message.showmode_resize = function() hide = false, }; - vim.api.nvim_win_set_config(message.data.showmode_window, window_opts); + vim.api.nvim_win_set_config(message.data.showcmd_window, window_opts); utils.redraw({ flush = true, - win = message.data.showmode_window + win = message.data.showcmd_window }, { ignore = false, }); @@ -391,7 +404,7 @@ message.msg_showcmd = function(content) ---|fS if #content == 0 then - message.showmode_hide(); + message.showcmd_hide(); return; end @@ -405,9 +418,9 @@ message.msg_showcmd = function(content) extmarks = modifier.extmarks or extmarks; end - vim.api.nvim_buf_clear_namespace(message.data.showmode_buffer, message.data.namespace, 0, -1); - vim.api.nvim_buf_set_lines(message.data.showmode_buffer, 0, -1, false, lines); - message.apply_extmarks("msg_list", message.data.showmode_buffer, extmarks); + vim.api.nvim_buf_clear_namespace(message.data.showcmd_buffer, message.data.namespace, 0, -1); + vim.api.nvim_buf_set_lines(message.data.showcmd_buffer, 0, -1, false, lines); + message.apply_extmarks("msg_list", message.data.showcmd_buffer, extmarks); local width = utils.max_len(lines); local height = 1; @@ -429,16 +442,15 @@ message.msg_showcmd = function(content) hide = false, }; - vim.api.nvim_win_set_config(message.data.showmode_window, window_opts); - utils.set("w", message.data.showmode_window, "sidescrolloff", 999) - pcall(vim.api.nvim_win_set_cursor, message.data.showmode_window, { 0, width }) + vim.api.nvim_win_set_config(message.data.showcmd_window, window_opts); + utils.set("w", message.data.showcmd_window, "sidescrolloff", 999) + pcall(vim.api.nvim_win_set_cursor, message.data.showcmd_window, { 0, width }) - utils.redraw({ + + vim.api.nvim__redraw({ flush = true, - win = message.data.showmode_window - }, { - ignore = false, - }); + win = message.data.showcmd_window + }) ---|fE end @@ -685,7 +697,7 @@ message.statuscolumn = function() return "%=" .. utils.to_statuscolumn(item.padding or item.icon); end else - return "%="; + return utils.to_statuscolumn(item.padding or item.icon); end break; @@ -734,7 +746,7 @@ message.prepare = function() message.set_buf_win("confirm_buffer", "confirm_window"); message.set_buf_win("list_buffer", "list_window"); message.set_buf_win("history_buffer", "history_window"); - message.set_buf_win("showmode_buffer", "showmode_window"); + message.set_buf_win("showcmd_buffer", "showcmd_window"); utils.set("w", message.data.window, "statuscolumn", "%!v:lua.ui_statuscolumn()"); end @@ -859,6 +871,8 @@ message.render = function() vim.api.nvim_buf_clear_namespace(message.data.buffer, message.data.namespace, 0, -1); vim.api.nvim_buf_set_lines(message.data.buffer, 0, -1, false, lines); + utils.set("w", message.data.window, "wrap", true); + message.apply_extmarks("msg_render", message.data.buffer, extmarks); local decor_size = message.apply_msg_decorations(message.visible_decorations, message.data.buffer); @@ -921,7 +935,7 @@ message.setup = function() vim.api.nvim_create_autocmd("VimResized", { callback = function() - message.showmode_resize(); + message.showcmd_resize(); message.render(); end }); @@ -929,7 +943,7 @@ message.setup = function() vim.api.nvim_create_autocmd("TabLeave", { callback = function() pcall(vim.api.nvim_win_close, message.data.window, true); - pcall(vim.api.nvim_win_close, message.data.showmode_window, true); + pcall(vim.api.nvim_win_close, message.data.showcmd_window, true); end }); From 651ef283149677876db44a13364fcd44681a33f8 Mon Sep 17 00:00:00 2001 From: Shawon Date: Thu, 8 Oct 2026 18:03:12 +0600 Subject: [PATCH 10/16] refactor: Fixed warnings & added type definitions --- lua/definitions/message.lua | 2 +- lua/ui/message.lua | 86 ++++++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/lua/definitions/message.lua b/lua/definitions/message.lua index 5ec0769..f0fddde 100644 --- a/lua/definitions/message.lua +++ b/lua/definitions/message.lua @@ -177,7 +177,7 @@ ---@class ui.message.entry A message entry. --- ----@field id string +---@field id string | integer ---@field type? ui.message.type --- ---@field kind ui.message.kind diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 9d4e258..0d67325 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -1,5 +1,4 @@ ---- Custom message for ---- Neovim. +--- Custom message for Neovim. local message = {}; local log = require("ui.log"); @@ -61,7 +60,12 @@ message.visible_decorations = {}; ---@type ui.message.decorations[] message.history_decorations = {} +---@param id string | integer Item id to remove. +---@param duration? integer Duration(in ms) until activation. +---@param interval? integer Delay between repeats(`nil` disables repeating). message.timer = function(id, duration, interval) + ---|fS + local timer = vim.uv.new_timer(); if timer then @@ -71,9 +75,14 @@ message.timer = function(id, duration, interval) end return timer; + + ---|fE end +---@param id string | integer Item `id` to free. message.free = function(id) + ---|fS + if type(id) == "string" then message.visible_special[id] = nil; else @@ -86,17 +95,19 @@ message.free = function(id) pcall(message.render) ); end); + + ---|fE end ---@param kind ui.message.kind ---@param content ui.message.fragment[] ---@param replace_last boolean ---@param history boolean ----@param _append boolean ----@param id string ----@param _trigger string +---@param id string | integer ---@return ui.message.entry -message.new = function(kind, content, replace_last, history, _append, id, _trigger) +message.new = function(kind, content, replace_last, history, _, id, _) + ---|fS + return { id = id, kind = kind, @@ -109,6 +120,8 @@ message.new = function(kind, content, replace_last, history, _append, id, _trigg timer = nil, } --[[@as ui.message.entry]]; + + ---|fE end ------------------------------------------------------------------------------ @@ -125,6 +138,7 @@ local movement_keys = { vim.api.nvim_replace_termcodes("l", true, true, true), }; +---@param key string Key to handle. message.confirm_movement = function(key) ---|fS @@ -151,7 +165,7 @@ end ---@param replace_last boolean ---@param history boolean ---@param append boolean ----@param id string +---@param id string | integer ---@param trigger string message.msg_confirm = function(kind, content, replace_last, history, append, id, trigger) ---|fS @@ -211,7 +225,7 @@ message.msg_confirm = function(kind, content, replace_last, history, append, id, if not vim.list_contains(vim.g.__confirm_keys or {}, string.lower(key)) then if vim.list_contains(movement_keys, key) then pcall(message.confirm_movement, key); - pcall(vim.cmd, "mode"); + pcall(vim.cmd, "mode"); ---@diagnostic disable-line end return; @@ -231,7 +245,7 @@ end ---@param replace_last boolean ---@param history boolean ---@param append boolean ----@param id string +---@param id string | integer ---@param trigger string message.msg_list = function(kind, content, replace_last, history, append, id, trigger) ---|fS @@ -299,9 +313,11 @@ end ---@param replace_last boolean ---@param history boolean ---@param append boolean ----@param id string +---@param id string | integer ---@param trigger string message.msg_show = function(kind, content, replace_last, history, append, id, trigger) + ---|fS + vim.schedule(function() if kind == "confirm" then message.msg_confirm(kind, content, replace_last, history, append, id, trigger); @@ -339,12 +355,16 @@ message.msg_show = function(kind, content, replace_last, history, append, id, tr "ui/message.lua → add_render", pcall(message.render) ); - end) + end); + + ---|fE end local showcmd_hide_timer = vim.uv.new_timer(); message.showcmd_hide = function() + ---|fS + if not showcmd_hide_timer then showcmd_hide_timer = vim.uv.new_timer(); end @@ -356,6 +376,8 @@ message.showcmd_hide = function() pcall(vim.api.nvim_buf_set_lines, message.data.showcmd_buffer, 0, -1, false, {}) end)); end + + ---|fE end message.showcmd_resize = function() @@ -400,6 +422,7 @@ message.showcmd_resize = function() ---|fE end +---@param content ui.message.fragment[] message.msg_showcmd = function(content) ---|fS @@ -443,7 +466,6 @@ message.msg_showcmd = function(content) }; vim.api.nvim_win_set_config(message.data.showcmd_window, window_opts); - utils.set("w", message.data.showcmd_window, "sidescrolloff", 999) pcall(vim.api.nvim_win_set_cursor, message.data.showcmd_window, { 0, width }) @@ -455,7 +477,10 @@ message.msg_showcmd = function(content) ---|fE end +---@param items ui.message.entry[] Items used for reloading the history window. message.history_keymaps = function(items) + ---|fS + if not message.data.history_buffer then return; end @@ -533,9 +558,14 @@ message.history_keymaps = function(items) message.msg_history_show(items); end }); + + ---|fE end +---@param items ui.message.entry[] message.msg_history_show = function(items) + ---|fS + vim.g.history_source = vim.g.history_source or "vim"; _G.history_show = _G.history_show or { normal = true, @@ -606,8 +636,6 @@ message.msg_history_show = function(items) extmarks = vim.list_extend(extmarks, m_exts); end end - - utils.set("w", message.data.history_window, "statuscolumn", "%!v:lua.ui_statuscolumn()"); end vim.bo[message.data.history_buffer].modifiable = true; @@ -671,10 +699,13 @@ message.msg_history_show = function(items) vim.api.nvim_win_set_config(message.data.history_window, window_opts); vim.api.nvim_set_current_win(message.data.history_window); + + ---|fE end ------------------------------------------------------------------------------ +---@return string message.statuscolumn = function() ---|fS @@ -711,7 +742,12 @@ end _G.ui_statuscolumn = message.statuscolumn; +--- Creates a buffer & window pair & assign them to `message.data` +---@param buf string Buffer name +---@param win string Window name message.set_buf_win = function(buf, win) + ---|fS + ---@type vim.api.keyset.win_config local window_opts = { relative = "editor", @@ -739,9 +775,14 @@ message.set_buf_win = function(buf, win) utils.set("w", message.data[win], "foldmethod", "manual"); utils.set("w", message.data[win], "numberwidth", 1); end + + ---|fE end +--- Prepare all buffers & windows used for messages. message.prepare = function() + ---|fS + message.set_buf_win("buffer", "window"); message.set_buf_win("confirm_buffer", "confirm_window"); message.set_buf_win("list_buffer", "list_window"); @@ -749,8 +790,17 @@ message.prepare = function() message.set_buf_win("showcmd_buffer", "showcmd_window"); utils.set("w", message.data.window, "statuscolumn", "%!v:lua.ui_statuscolumn()"); + utils.set("w", message.data.window, "wrap", true); + + utils.set("w", message.data.history_window, "statuscolumn", "%!v:lua.ui_statuscolumn()"); + utils.set("w", message.data.showcmd_window, "sidescrolloff", 999) + + ---|fE end +---@param src string Source used for logs +---@param buffer integer Buffer ID +---@param extmarks ui.message.hl_fragment[][] message.apply_extmarks = function(src, buffer, extmarks) ---|fS @@ -784,6 +834,7 @@ message.apply_extmarks = function(src, buffer, extmarks) ---|fE end +---@param src ui.message.decorations[] ---@return integer message.apply_msg_decorations = function(src, buffer) ---|fS @@ -808,12 +859,17 @@ message.apply_msg_decorations = function(src, buffer) ---|fE end +---@return integer RowOffset Rows used by the command-line message.cmdline_offset = function() + ---|fS + if (vim.g.__ui_cmd_height or 0) > 0 then return (vim.g.__ui_cmd_height or 0) + (spec.config.cmdline.row_offset or 1) end return (vim.g.__ui_cmd_height or 0); + + ---|fE end message.render = function() @@ -871,8 +927,6 @@ message.render = function() vim.api.nvim_buf_clear_namespace(message.data.buffer, message.data.namespace, 0, -1); vim.api.nvim_buf_set_lines(message.data.buffer, 0, -1, false, lines); - utils.set("w", message.data.window, "wrap", true); - message.apply_extmarks("msg_render", message.data.buffer, extmarks); local decor_size = message.apply_msg_decorations(message.visible_decorations, message.data.buffer); From 76c3bf8a5fb3d308837a789ec1df30d8bee1dfd1 Mon Sep 17 00:00:00 2001 From: Shawon Date: Thu, 8 Oct 2026 21:46:04 +0600 Subject: [PATCH 11/16] refactor: Changed variable name --- lua/ui/cmdline.lua | 12 ++++++------ lua/ui/message.lua | 10 +--------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lua/ui/cmdline.lua b/lua/ui/cmdline.lua index f18eff9..ed686dc 100644 --- a/lua/ui/cmdline.lua +++ b/lua/ui/cmdline.lua @@ -310,7 +310,7 @@ cmdline.__render = function () local lines, extmarks, _ = cmdline.__lines(); local H = #lines; - vim.g.__ui_cmd_height = H; + vim.g.ui_cmd_height = H; local win_config = { ---|fS @@ -406,12 +406,12 @@ cmdline.__render = function () if spec.config.message.enable and package.loaded["ui.message"] then log.assert( "ui/cmdline.lua → message_refresh", - pcall(package.loaded["ui.message"].__render) + pcall(package.loaded["ui.message"].render) ); log.assert( "ui/cmdline.lua → showcmd_refresh", - pcall(package.loaded["ui.message"].__showcmd) + pcall(package.loaded["ui.message"].showcmd_resize) ); end @@ -540,7 +540,7 @@ cmdline.cmdline_hide = function () -- Reset exported height. -- Also reset state. - vim.g.__ui_cmd_height = 0; + vim.g.ui_cmd_height = 0; cmdline.old_state = {}; ---@diagnostic disable-line local function close_callback () @@ -558,7 +558,7 @@ cmdline.cmdline_hide = function () if spec.config.message.enable and package.loaded["ui.message"] then log.assert( "ui/cmdline.lua", - pcall(package.loaded["ui.message"].__render) + pcall(package.loaded["ui.message"].render) ); end @@ -663,7 +663,7 @@ cmdline.setup = function () callback = function () -- Only redraw if the cmdline -- window is visible. - if vim.g.__ui_cmd_height and vim.g.__ui_cmd_height > 0 then + if vim.g.ui_cmd_height and vim.g.ui_cmd_height > 0 then log.assert( "ui/cmdline.lua", pcall(cmdline.__render) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 0d67325..a3e8e58 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -861,15 +861,7 @@ end ---@return integer RowOffset Rows used by the command-line message.cmdline_offset = function() - ---|fS - - if (vim.g.__ui_cmd_height or 0) > 0 then - return (vim.g.__ui_cmd_height or 0) + (spec.config.cmdline.row_offset or 1) - end - - return (vim.g.__ui_cmd_height or 0); - - ---|fE + return (vim.g.ui_cmd_height or 0) + (spec.config.cmdline.row_offset or 0) - 1 end message.render = function() From df2da1f46a8c68630d7e70e620e993e78ded6790 Mon Sep 17 00:00:00 2001 From: Shawon Date: Fri, 9 Oct 2026 11:17:50 +0600 Subject: [PATCH 12/16] fix: Fixed incorrect message type --- lua/ui/message.lua | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index a3e8e58..37fd174 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -104,14 +104,26 @@ end ---@param replace_last boolean ---@param history boolean ---@param id string | integer +---@param _type? ui.message.type ---@return ui.message.entry -message.new = function(kind, content, replace_last, history, _, id, _) +message.new = function(kind, content, replace_last, history, _, id, _, _type) ---|fS + ---@type ui.message.type + local type = "normal"; + + if _type then + type = _type; + elseif kind == "confirm" then + type = "confirm"; + elseif not history then + type = "hidden"; + end + return { id = id, kind = kind, - type = "normal", + type = type, content = content, @@ -170,7 +182,7 @@ end message.msg_confirm = function(kind, content, replace_last, history, append, id, trigger) ---|fS - local msg = message.new(kind, content, replace_last, history, append, id, trigger); + local msg = message.new(kind, content, replace_last, history, append, id, trigger, "confirm"); vim.g.__ui_confirm_msg = msg; message.history[id] = msg; @@ -250,7 +262,7 @@ end message.msg_list = function(kind, content, replace_last, history, append, id, trigger) ---|fS - local msg = message.new(kind, content, replace_last, history, append, id, trigger); + local msg = message.new(kind, content, replace_last, history, append, id, trigger, "list"); message.history[id] = msg; message.prepare(); @@ -597,6 +609,11 @@ message.msg_history_show = function(items) { " [C]onfirm ", _G.history_show.confirm }, }; + -- Taken from `ZeroBrane` + local function padnum(d) + return ("%03d%s"):format(#d, d) + end + local line, extmark = "", {}; for i, item in ipairs(ui_chips) do @@ -611,7 +628,14 @@ message.msg_history_show = function(items) table.insert(extmarks, extmark); local msg_orders = vim.tbl_keys(message.history); - table.sort(msg_orders); + table.sort(msg_orders, function (a, b) + if type(a) == "number" and type(b) == "number" then + return a < b; + else + -- There may be special message order(e.g. `bufwrite`), this is an Overkill BTW. + return tostring(a):gsub("%d+", padnum) < tostring(b):gsub("%d+", padnum); + end + end); for _, order in ipairs(msg_orders) do local msg = message.history[order]; From 85cc349cd6af6a820364ee7da42bc69583098fde Mon Sep 17 00:00:00 2001 From: Shawon Date: Fri, 9 Oct 2026 16:10:00 +0600 Subject: [PATCH 13/16] refactor: Moved general message modifiers to separate file --- lua/ui/spec.lua | 322 ++++++++++++++++---------------------- lua/ui/spec/modifiers.lua | 128 +++++++++++++++ 2 files changed, 259 insertions(+), 191 deletions(-) create mode 100644 lua/ui/spec/modifiers.lua diff --git a/lua/ui/spec.lua b/lua/ui/spec.lua index 02b8b3e..1be9329 100644 --- a/lua/ui/spec.lua +++ b/lua/ui/spec.lua @@ -1,16 +1,17 @@ local spec = {}; local utils = require("ui.utils"); +local modifiers = require("ui.spec.modifiers"); --- Creates message text from highlight group properties. ---@param props table ---@return string[] ---@return ( ui.message.hl_fragment[] )[] -local function hl_prop_txt (props) +local function hl_prop_txt(props) ---|fS local lines, exts = {}, {}; - local function new_property (name, value, hl) + local function new_property(name, value, hl) table.insert(lines, string.format(" %s: %s", name, value)); table.insert(exts, {}); @@ -108,7 +109,7 @@ end --- Checks if a message is a list message. ---@param lines string[] ---@return boolean -spec.generic_list_msg = function (lines) +spec.generic_list_msg = function(lines) ---|fS local start_marker = ""; @@ -188,7 +189,7 @@ spec.generic_list_msg = function (lines) }; local additional_condition = { - [9] = function (_lines) + [9] = function(_lines) if #_lines <= 2 then return false; end @@ -219,7 +220,7 @@ end --- Checks if a message is a list message is valid. ---@param lines string[] ---@return boolean -spec.ignore_list = function (lines) +spec.ignore_list = function(lines) ---|fS local _lines = utils.trim_lines(lines); @@ -250,7 +251,7 @@ spec.default = { enable = true, - winconfig = function (_, position) + winconfig = function(_, position) ---|fS if position == "top_left" then @@ -311,7 +312,7 @@ spec.default = { ---|fE end, - tooltip = function () + tooltip = function() local mode = vim.api.nvim_get_mode().mode; return mode == "c" and { { " 󰸾 󰹀 ", "UIMenuKeymap" }, @@ -335,7 +336,7 @@ spec.default = { -- Buffer variables(b:). buffer_variable = { - condition = function (word) + condition = function(word) return string.match(word, "^b:") ~= nil; end, @@ -345,7 +346,7 @@ spec.default = { -- Global variables(g:). global_variable = { - condition = function (word) + condition = function(word) return string.match(word, "^g:") ~= nil; end, @@ -355,7 +356,7 @@ spec.default = { -- Local/argument variables(l:,a:,s:). local_variable = { - condition = function (word) + condition = function(word) return string.match(word, "^[las]:") ~= nil; end, @@ -365,7 +366,7 @@ spec.default = { -- Tab variables(t:). tabpage_variable = { - condition = function (word) + condition = function(word) return string.match(word, "^t:") ~= nil; end, @@ -375,7 +376,7 @@ spec.default = { -- Vim variables(v:). vim_variable = { - condition = function (word) + condition = function(word) return string.match(word, "^v:") ~= nil; end, @@ -385,7 +386,7 @@ spec.default = { -- Window variables(w:). window_variable = { - condition = function (word) + condition = function(word) return string.match(word, "^w:") ~= nil; end, @@ -395,7 +396,7 @@ spec.default = { class = { - condition = function (_, kind) + condition = function(_, kind) return kind == "m"; end, @@ -403,7 +404,7 @@ spec.default = { }, ["function"] = { - condition = function (word, kind) + condition = function(word, kind) return kind == "f" or string.match(word, "%(%)?$") ~= nil; end, @@ -412,7 +413,7 @@ spec.default = { }, macro = { - condition = function (_, kind) + condition = function(_, kind) return kind == "d"; end, @@ -421,7 +422,7 @@ spec.default = { }, type_definiton = { - condition = function (_, kind) + condition = function(_, kind) return kind == "t"; end, @@ -430,7 +431,7 @@ spec.default = { }, variable = { - condition = function (_, kind) + condition = function(_, kind) return kind == "v"; end, @@ -468,12 +469,12 @@ spec.default = { search_up = { ---|fS - condition = function (state) + condition = function(state) return state.firstc == "/"; end, winhl = "Normal:UICmdlineSearchUp", - filetype ="cmdline_search", + filetype = "cmdline_search", icon = { { "  ", "UICmdlineSearchUpIcon" } }, @@ -483,12 +484,12 @@ spec.default = { search_down = { ---|fS - condition = function (state) + condition = function(state) return state.firstc == "?"; end, winhl = "Normal:UICmdlineSearchDown", - filetype ="cmdline_search", + filetype = "cmdline_search", icon = { { "  ", "UICmdlineSearchDownIcon" } }, @@ -498,7 +499,7 @@ spec.default = { set = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[#lines], "^set ") ~= nil; end, @@ -515,14 +516,14 @@ spec.default = { shell = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[#lines], "^!") ~= nil; end, winhl = "Normal:UICmdlineEval", offset = 1, - filetype = function (state) + filetype = function(state) ---@type string? Shell name. local shell = string.match(vim.o.shell, "%w+$"); @@ -535,7 +536,7 @@ spec.default = { end end, - icon = function () + icon = function() ---@diagnostic disable:undefined-field if not _G.is_within_termux then return { @@ -559,7 +560,7 @@ spec.default = { substitute = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[#lines], "^%S*s/") ~= nil; end, @@ -567,7 +568,7 @@ spec.default = { filetype = "vim", - icon = function (_, lines) + icon = function(_, lines) if string.match(lines[#lines], "^s/") then return { { "  ", "UICmdlineSubstituteIcon" } @@ -585,14 +586,14 @@ spec.default = { lua_eval = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[#lines], "^=") ~= nil; end, winhl = "Normal:UICmdlineEval", offset = 1, - filetype = function (state) + filetype = function(state) return state.pos < 1 and "vim" or "lua"; end, @@ -606,14 +607,14 @@ spec.default = { lua = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[#lines], "^lua ") ~= nil; end, winhl = "Normal:UICmdlineLua", offset = 4, - filetype = function (state) + filetype = function(state) return state.pos < 4 and "vim" or "lua"; end, @@ -627,11 +628,11 @@ spec.default = { __keymap = { ---|fS - condition = function (state) + condition = function(state) return string.match(state.prompt or "", "[%[%(].[%]%)]%w+") ~= nil; end, - title = function (state) + title = function(state) ---@type ( [ string, string? ][] )[] local title = {}; local is_first = true; @@ -658,11 +659,11 @@ spec.default = { prompt = { ---|fS - condition = function (state) + condition = function(state) return state.prompt ~= ""; end, - title = function (state) + title = function(state) ---@type ( [ string, string? ][] )[] local output = {}; local lines = utils.text_wrap({ state.prompt or "" }, math.floor(vim.o.columns * 0.8)); @@ -715,7 +716,7 @@ spec.default = { list_winconfig = {}, history_winconfig = {}, - ignore = function (kind, content) + ignore = function(kind, content) ---|fS if kind == "bufwrite" then @@ -735,7 +736,7 @@ spec.default = { showcmd = { max_width = math.floor(vim.o.columns * 0.4), - modifier = function (_, lines) + modifier = function(_, lines) ---|fS local mode = vim.api.nvim_get_mode().mode; @@ -813,7 +814,7 @@ spec.default = { default = { ---|fS - duration = function (msg, lines) + duration = function(msg, lines) local duration = 2500; if msg.kind == "write" then @@ -830,7 +831,7 @@ spec.default = { return duration + utils.read_time(lines); end, - decorations = function (msg) + decorations = function(msg) local config = { icon = { { "▍󰍡 ", "UIMessageDefaultSign" } @@ -875,7 +876,7 @@ spec.default = { end elseif string.match(content[2] or "", " .+ %w+%.nvim ") or string.match(content[2] or "", " [%w/\\]+%.%w+ ") then -- Error message format used by *plugins*. - -- e plugin.nvim : Some message. + -- e plugin.nvim : Some message. return {}; end @@ -888,7 +889,7 @@ spec.default = { __swap = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[2], "Found a swap file") ~= nil; end, @@ -908,7 +909,7 @@ spec.default = { __spell = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[1], "Word (.+) added to .-%.add$") ~= nil; end, @@ -927,7 +928,7 @@ spec.default = { __lua_error = { ---|fS - condition = function (_, lines) + condition = function(_, lines) for _, line in ipairs(lines) do if string.match(line, ".-Error.-:%d+: ?.-$") then return true; @@ -937,7 +938,7 @@ spec.default = { return false; end, - modifier = function (_, lines) + modifier = function(_, lines) if vim.g.__ui_history then return {}; end @@ -991,20 +992,20 @@ spec.default = { { 0, #actual_error, "DiagnosticError" }, }, { - { 0, 5, "Comment" }, + { 0, 5, "Comment" }, { 6, 6 + #path, "DiagnosticHint" }, }, { - { 0, 5, "Comment" }, + { 0, 5, "Comment" }, { 6, 6 + #line, "DiagnosticHint" }, }, (code or exec) and {} or nil, code and { - { 0, 5, "Comment" }, + { 0, 5, "Comment" }, { 6, 6 + #code, "DiagnosticError" }, } or nil, exec and { - { 0, 5, "Comment" }, + { 0, 5, "Comment" }, { 6, 6 + #exec, "DiagnosticError" }, } or nil, } @@ -1024,10 +1025,34 @@ spec.default = { ---|fE }, + __ls = { + ---|fS + + condition = function(...) + return modifiers.is_ls(...); + end, + + modifier = function (...) + return modifiers.ls(...); + end, + + decorations = { + icon = { + { " 󰙅 ", "Constant" } + }, + padding = { + { " ", "Normal" } + }, + line_hl_group = "", + } + + ---|fE + }, + option = { ---|fS - condition = function (_, lines) + condition = function(_, lines) if string.match(lines[1], "^%s+%w+=") then local opt, val = string.match(lines[1], "^%s+(%w+)=(.*)$"); @@ -1050,7 +1075,7 @@ spec.default = { return vim.o[option] ~= nil and type(vim.o[option]) == "boolean"; end, - modifier = function (_, lines) + modifier = function(_, lines) if string.match(lines[1], "^%s*(%w+)$") then local key = string.match(lines[1], "^%s*(.*)$"); local state = string.match(key, "^no") == nil; @@ -1063,8 +1088,8 @@ spec.default = { }, extmarks = { { - { 0, #key, "@property" }, - { #key, #key + 1, "@punctuation" }, + { 0, #key, "@property" }, + { #key, #key + 1, "@punctuation" }, { #key + 2, #key + 2 + #tostring(state), "@boolean" }, } } @@ -1082,8 +1107,8 @@ spec.default = { }, extmarks = { { - { 0, #key, "@property" }, - { #key, #key + 1, "@punctuation" }, + { 0, #key, "@property" }, + { #key, #key + 1, "@punctuation" }, { #key + 2, #key + 2 + #value, "@" .. utils.get_type(value) }, } } @@ -1105,11 +1130,11 @@ spec.default = { search = { ---|fS - condition = function (msg) + condition = function(msg) return msg.kind == "search_cmd"; end, - modifier = function (_, lines) + modifier = function(_, lines) local term = string.match(lines[1], "^[%?/](.*)$"); return { @@ -1119,7 +1144,7 @@ spec.default = { } }; end, - decorations = function (_, lines) + decorations = function(_, lines) if string.match(lines[#lines], "^/") then return { icon = { @@ -1149,7 +1174,7 @@ spec.default = { error_msg = { ---|fS - condition = function (msg) + condition = function(msg) return msg.kind == "emsg"; end, @@ -1170,12 +1195,12 @@ spec.default = { highlight_link = { ---|fS - condition = function (_, lines) + condition = function(_, lines) local is_hl, data = utils.is_hl_line(lines[2] or "") return lines[1] == "" and is_hl and data.value.link; end, - modifier = function (_, lines) + modifier = function(_, lines) local _, data = utils.is_hl_line(lines[2] or "") local definition = vim.api.nvim_exec2("hi " .. data.value.link, { output = true }).output; @@ -1198,18 +1223,18 @@ spec.default = { { 0, 10, data.group_name } }, { - { 0, 4, "@property" }, - { 4, 5, "@punctuation" }, + { 0, 4, "@property" }, + { 4, 5, "@punctuation" }, { 6, 6 + #data.group_name, "@string" }, }, { - { 0, 6, "@property" }, - { 6, 7, "@punctuation" }, + { 0, 6, "@property" }, + { 6, 7, "@punctuation" }, { 8, 8 + #data.value.link, "@constant" }, }, {}, { - { 0, #"Raw definition", "DiagnosticHint" }, + { 0, #"Raw definition", "DiagnosticHint" }, { #"Raw definition", #"Raw definition" + 1, "@punctuation" }, }, }; @@ -1237,12 +1262,12 @@ spec.default = { highlight_group = { ---|fS - condition = function (_, lines) + condition = function(_, lines) local is_hl, data = utils.is_hl_line(lines[2] or "") return lines[1] == "" and is_hl and data.value.link == nil; end, - modifier = function (_, lines) + modifier = function(_, lines) local _, data = utils.is_hl_line(lines[2] or "") local d_lines, d_exts = hl_prop_txt(data); @@ -1279,11 +1304,11 @@ spec.default = { write = { ---|fS - condition = function (msg) + condition = function(msg) return msg.id == "bufwrite"; end, - modifier = function (_, lines) + modifier = function(_, lines) local filename, _, bytes; if string.match(lines[#lines], "%[New%]") then @@ -1299,13 +1324,13 @@ spec.default = { }, extmarks = { { - { 0, 9, "Comment" }, - { 9, 9 + #filename, "Special" }, + { 0, 9, "Comment" }, + { 9, 9 + #filename, "Special" }, { 9 + #filename, 10 + #filename, "Comment" } }, { - { 0, 7, "Comment" }, - { 7, 7 + #bytes, "@constant" }, + { 0, 7, "Comment" }, + { 7, 7 + #bytes, "@constant" }, { 8 + #bytes, 13 + #bytes, "Comment" } }, } @@ -1327,14 +1352,15 @@ spec.default = { undo_redo = { ---|fS - condition = function (_, lines) + condition = function(_, lines) if lines[1] == "Already at oldest change" then return true; elseif lines[1] == "Already at newest change" then return true; end - return string.match(lines[1], "%d+ .-; %w+ #%d+") ~= nil or string.match(lines[1], "%d+ line less; %w+ #%d+") ~= nil; + return string.match(lines[1], "%d+ .-; %w+ #%d+") ~= nil or + string.match(lines[1], "%d+ line less; %w+ #%d+") ~= nil; end, decorations = { @@ -1355,17 +1381,17 @@ spec.default = { terminal_command = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[1] or "", "^:!") ~= nil end, - modifier = function (_, lines) + modifier = function(_, lines) ---@type string Removed trailing `^M`. local removed_tail = string.gsub(lines[1] or "", ".$", ""); return { extmarks = { - { { 0, #removed_tail, "UICmdlineEvalIcon" } } + { { 0, #removed_tail, "UICmdlineEvalIcon" } } }, lines = { removed_tail } }; @@ -1388,7 +1414,7 @@ spec.default = { shell_output = { ---|fS - condition = function (entry) + condition = function(entry) return entry.kind == "shell_out"; end, @@ -1409,7 +1435,7 @@ spec.default = { ---|fE }, - is_list = function (kind, content, add_to_history) + is_list = function(kind, content, add_to_history) local lines = utils.to_lines(content); if kind == "list_cmd" then @@ -1431,98 +1457,12 @@ spec.default = { ls = { ---|fS - condition = function (msg, lines) - if msg.kind ~= "list_cmd" then - return false; - elseif string.match(lines[2], '^%s*(%d+)%s*([u%%#ah%-=RF%?%+x]+)%s*"(.+)"%s*line (%d+)$') == nil then - return false; - end - - return true; + condition = function(...) + return modifiers.is_ls(...); end, - modifier = function (_, lines) - local _lines, exts = {}, {}; - local entries = {}; - - ---@type table - local widths = { - id = 6, - name = 6, - lnum = 4, - indicators = 10 - }; - - for l, line in ipairs(lines) do - if l == 1 then - goto continue; - end - - local ID, indicators, name, lnum = string.match(line, '^%s*(%d+)%s*([u%%#ah%-=RF%?%+x]+)%s*"(.+)"%s*line (%d+)$'); - - table.insert(entries, { - id = ID or "", - name = name or "", - lnum = lnum or "", - - indicators = indicators or "", - }); - - widths.id = math.max(widths.id, vim.fn.strdisplaywidth(ID or "")); - widths.name = math.max(widths.name, vim.fn.strdisplaywidth(name or "")); - widths.lnum = math.max(widths.lnum, vim.fn.strdisplaywidth(lnum or "")); - widths.indicators = math.max(widths.indicators, vim.fn.strdisplaywidth(indicators or "")); - - ::continue:: - end - - local title, title_exts = utils.to_row({ - { - string.format(" %-" .. widths.id .. "s ", "Buffer"), - "UILSBuffer" - }, - { - string.format(" %-" .. widths.name .. "s ", "Name"), - "UILSBufname" - }, - { - string.format(" %-" .. widths.indicators .. "s ", "Indicators"), - "UILSIndicator" - }, - { - string.format(" %-" .. widths.lnum .. "s ", "Line"), - "UILSLmum" - }, - }); - - table.insert(_lines, title); - table.insert(exts, title_exts); - - for e, entry in ipairs(entries) do - local row, row_exts = utils.to_row({ - { - string.format(" %-" .. widths.id .. "s ", entry.id), - e % 2 == 0 and "@comment" or "UICmdlineSearchDown" - }, - { - string.format(" %-" .. widths.name .. "s ", entry.name), - e % 2 == 0 and "@comment" or "UICmdlineDefault" - }, - { - string.format(" %-" .. widths.indicators .. "s ", entry.indicators), - e % 2 == 0 and "@comment" or "UICmdlineLua" - }, - { - string.format(" %-" .. widths.lnum .. "s ", entry.lnum), - e % 2 == 0 and "@comment" or "UICmdlineSubstitute" - }, - }); - - table.insert(_lines, row); - table.insert(exts, row_exts); - end - - return { lines = _lines, extmarks = exts }; + modifier = function(...) + return modifiers.ls(...); end, winhl = "Normal:@comment", @@ -1534,12 +1474,12 @@ spec.default = { hi = { ---|fS - condition = function (_, lines) + condition = function(_, lines) local is_hl = utils.is_hl_line(lines[2] or ""); return lines[1] == "" and is_hl; end, - modifier = function (_, lines, exts) + modifier = function(_, lines, exts) local _lines = vim.deepcopy(lines); local _exts = vim.deepcopy(exts); @@ -1561,7 +1501,7 @@ spec.default = { swap_alert = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[2] or "", '^Swap') ~= nil end, @@ -1575,7 +1515,7 @@ spec.default = { { 0, 26, "DiagnosticWarn" } }, { - { 6, 10, "@comment" }, + { 6, 10, "@comment" }, { 10, 17, "DiagnosticVirtualTextHint" } } } @@ -1590,11 +1530,11 @@ spec.default = { write_confirm = { ---|fS - condition = function (_, lines) + condition = function(_, lines) return string.match(lines[2] or "", '^Save changes to "([^"]+)"') ~= nil end, - modifier = function (_, lines) + modifier = function(_, lines) local file = string.match(lines[2] or "", 'Save changes to "([^"]+)"') return { @@ -1603,8 +1543,8 @@ spec.default = { }, extmarks = { { - { 0, 13, "@comment" }, - { 13, 15 + #file, "DiagnosticVirtualTextHint" }, + { 0, 13, "@comment" }, + { 13, 15 + #file, "DiagnosticVirtualTextHint" }, { 15 + #file, 16 + #file, "@comment" }, } } @@ -1628,7 +1568,7 @@ spec.config = vim.deepcopy(spec.default); ---@param state ui.cmdline.state ---@param lines string[] ---@return ui.cmdline.style__static -spec.get_cmdline_style = function (state, lines) +spec.get_cmdline_style = function(state, lines) ---|fS local styles = spec.config.cmdline.styles or {}; @@ -1652,7 +1592,7 @@ spec.get_cmdline_style = function (state, lines) break; end - ::continue:: + ::continue:: end ---@type ui.cmdline.style__static @@ -1684,7 +1624,7 @@ end ---@param lines string[] ---@param extmarks ui.message.extmarks ---@return ui.message.confirm__static -spec.get_confirm_style = function (msg, lines, extmarks) +spec.get_confirm_style = function(msg, lines, extmarks) ---|fS local styles = spec.config.message.confirm_styles or {}; @@ -1707,7 +1647,7 @@ spec.get_confirm_style = function (msg, lines, extmarks) break; end - ::continue:: + ::continue:: end ---@type ui.message.confirm__static @@ -1740,7 +1680,7 @@ end ---@param lines string[] ---@param extmarks ui.message.extmarks ---@return ui.message.list__static -spec.get_listmsg_style = function (msg, lines, extmarks) +spec.get_listmsg_style = function(msg, lines, extmarks) ---|fS local styles = spec.config.message.list_styles or {}; @@ -1763,7 +1703,7 @@ spec.get_listmsg_style = function (msg, lines, extmarks) break; end - ::continue:: + ::continue:: end ---@type ui.message.list__static @@ -1795,10 +1735,10 @@ end ---@param lines string[] ---@param extmarks ui.message.extmarks ---@return ui.message.style__static -spec.get_msg_style = function (msg, lines, extmarks) +spec.get_msg_style = function(msg, lines, extmarks) ---|fS - local processors= spec.config.message.msg_styles or {}; + local processors = spec.config.message.msg_styles or {}; local _output = processors.default or {}; ---@type string[] @@ -1817,7 +1757,7 @@ spec.get_msg_style = function (msg, lines, extmarks) break; end - ::continue:: + ::continue:: end local output = {}; @@ -1872,7 +1812,7 @@ end ---@param history boolean ---@return boolean ---@return boolean -spec.is_list = function (kind, content, history) +spec.is_list = function(kind, content, history) ---|fS if not spec.config.message.is_list then @@ -1896,7 +1836,7 @@ end ---@param menu string ---@param info string ---@return ui.popupmenu.style__static -spec.get_item_style = function (word, kind, menu, info) +spec.get_item_style = function(word, kind, menu, info) ---|fS local styles = spec.default.popupmenu.styles or {}; @@ -1919,7 +1859,7 @@ spec.get_item_style = function (word, kind, menu, info) break; end - ::continue:: + ::continue:: end ---@type ui.popupmenu.style__static diff --git a/lua/ui/spec/modifiers.lua b/lua/ui/spec/modifiers.lua new file mode 100644 index 0000000..a13ab9f --- /dev/null +++ b/lua/ui/spec/modifiers.lua @@ -0,0 +1,128 @@ +local modifiers = {}; +local utils = require("ui.utils"); + +modifiers.lpeg = { + ls = vim.lpeg.Ct( + vim.lpeg.P(" ") ^ 0 * + vim.lpeg.Cg( + vim.lpeg.R("09") ^ 1, + "buffer_id" + ) * + vim.lpeg.P(" ") ^ 0 * + vim.lpeg.Cg( + vim.lpeg.S("u%#ah-=RF?+x ") ^ 1 / function(str) return string.gsub(str, "%s*$", ""); end, + "indicators" + ) * + vim.lpeg.P(" ") ^ 0 * + vim.lpeg.P('"') * + vim.lpeg.Cg( + (1 - vim.lpeg.P('"')) ^ 0, + "name" + ) * + vim.lpeg.P('"') * + vim.lpeg.P(" ") ^ 0 * + vim.lpeg.P("line") * + vim.lpeg.P(" ") ^ 0 * + vim.lpeg.Cg( + vim.lpeg.R("09") ^ 1, + "line" + ) + ), +}; + +modifiers.is_ls = function (msg, lines, _) + if msg.kind ~= "list_cmd" then + return false; + elseif not modifiers.lpeg.ls:match(lines[1] or "") then + return false; + end + + return true; +end + +modifiers.ls = function (_, lines) + local _lines, exts = {}, {}; + local entries = {}; + + ---@type table + local widths = { + id = 6, + name = 6, + lnum = 4, + indicators = 10 + }; + + for l, line in ipairs(lines) do + if #lines > 1 and l == 1 then + goto continue; + end + + local matches = modifiers.lpeg.ls:match(line); + local entry = { + id = matches.buffer_id or "", + name = matches.name or "", + lnum = matches.line or "", + + indicators = matches.indicators or "", + }; + + table.insert(entries, entry); + + widths.id = math.max(widths.id, vim.fn.strdisplaywidth(entry.id)); + widths.name = math.max(widths.name, vim.fn.strdisplaywidth(entry.name)); + widths.lnum = math.max(widths.lnum, vim.fn.strdisplaywidth(entry.lnum)); + widths.indicators = math.max(widths.indicators, vim.fn.strdisplaywidth(entry.indicators)); + + ::continue:: + end + + local title, title_exts = utils.to_row({ + { + string.format(" %-" .. widths.id .. "s ", "Buffer"), + "UILSBuffer" + }, + { + string.format(" %-" .. widths.name .. "s ", "Name"), + "UILSBufname" + }, + { + string.format(" %-" .. widths.indicators .. "s ", "Indicators"), + "UILSIndicator" + }, + { + string.format(" %-" .. widths.lnum .. "s ", "Line"), + "UILSLmum" + }, + }); + + table.insert(_lines, title); + table.insert(exts, title_exts); + + for e, entry in ipairs(entries) do + local row, row_exts = utils.to_row({ + { + string.format(" %-" .. widths.id .. "s ", entry.id), + e % 2 == 0 and "@comment" or "UICmdlineSearchDown" + }, + { + string.format(" %-" .. widths.name .. "s ", entry.name), + e % 2 == 0 and "@comment" or "UICmdlineDefault" + }, + { + string.format(" %-" .. widths.indicators .. "s ", entry.indicators), + e % 2 == 0 and "@comment" or "UICmdlineLua" + }, + { + string.format(" %-" .. widths.lnum .. "s ", entry.lnum), + e % 2 == 0 and "@comment" or "UICmdlineSubstitute" + }, + }); + + table.insert(_lines, row); + table.insert(exts, row_exts); + end + + return { lines = _lines, extmarks = exts }; +end + +return modifiers; From abde68f7cbf1f1fbbea973e8bf02427a425fcda9 Mon Sep 17 00:00:00 2001 From: Shawon Date: Fri, 9 Oct 2026 16:10:20 +0600 Subject: [PATCH 14/16] fix: Message windows now use `Normal` as background --- lua/ui/message.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 37fd174..6965fc8 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -798,6 +798,7 @@ message.set_buf_win = function(buf, win) utils.set("w", message.data[win], "foldmethod", "manual"); utils.set("w", message.data[win], "numberwidth", 1); + utils.set("w", message.data[win], "winhl", "Normal:Normal"); end ---|fE From 1b32861fc5b3ae59d87d9cb4b6ac9c15b9ebf023 Mon Sep 17 00:00:00 2001 From: Shawon Date: Fri, 9 Oct 2026 16:24:25 +0600 Subject: [PATCH 15/16] refactor(spec): Removed unnecessary `:hi` msg modifications --- lua/ui/spec.lua | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/lua/ui/spec.lua b/lua/ui/spec.lua index 1be9329..33fa76e 100644 --- a/lua/ui/spec.lua +++ b/lua/ui/spec.lua @@ -1470,27 +1470,6 @@ spec.default = { ---|fE }, - - hi = { - ---|fS - - condition = function(_, lines) - local is_hl = utils.is_hl_line(lines[2] or ""); - return lines[1] == "" and is_hl; - end, - - modifier = function(_, lines, exts) - local _lines = vim.deepcopy(lines); - local _exts = vim.deepcopy(exts); - - table.remove(_lines, 1); - table.remove(_exts, 1); - - return { lines = _lines, extmarks = _exts }; - end - - ---|fE - }, }, confirm_styles = { From e92457afc3ee3bb3b142a9060393ef37f533f009 Mon Sep 17 00:00:00 2001 From: Shawon Date: Fri, 9 Oct 2026 16:50:50 +0600 Subject: [PATCH 16/16] chore: Added some decorations for the message filter bar --- lua/ui/message.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/ui/message.lua b/lua/ui/message.lua index 6965fc8..bc5e8d5 100644 --- a/lua/ui/message.lua +++ b/lua/ui/message.lua @@ -285,7 +285,7 @@ message.msg_list = function(kind, content, replace_last, history, append, id, tr message.apply_extmarks("msg_list", message.data.list_buffer, extmarks); local width = math.min( - math.floor(vim.o.columns * 0.5), + math.floor(vim.o.columns * 0.75), utils.max_len(lines) ); local height = utils.wrapped_height(lines, width); @@ -511,7 +511,7 @@ message.history_keymaps = function(items) desc = "[t]oggles between `vim` and `ui.nvim`'s message history.", callback = function() if vim.g.history_source == "vim" then - vim.g.history_source = "ui.nvim"; + vim.g.history_source = "ui"; else vim.g.history_source = "vim"; end @@ -614,7 +614,9 @@ message.msg_history_show = function(items) return ("%03d%s"):format(#d, d) end - local line, extmark = "", {}; + local line, extmark = " Filters ", { + { 0, 11, "Comment" } + }; for i, item in ipairs(ui_chips) do local before = #line;