diff --git a/lua/astroui/config.lua b/lua/astroui/config.lua index 855d0a9..0ec71fa 100644 --- a/lua/astroui/config.lua +++ b/lua/astroui/config.lua @@ -9,8 +9,11 @@ ---@alias StringMap table ---@alias AstroUIIconHighlight (fun(self:table):boolean)|boolean ----@alias AstroUIHighlight vim.api.keyset.highlight ----@alias AstroUIHighlights table +---@class AstroUIHighlight: vim.api.keyset.highlight +---@field fg? integer|string +---@field bg? integer|string +---@field reverse? boolean +---@alias AstroUIHighlights table ---@class AstroUIFileIconHighlights ---@field tabline AstroUIIconHighlight? enable or disabling file icon highlighting in the tabline diff --git a/lua/astroui/folding.lua b/lua/astroui/folding.lua index 4e0b5b2..ca679c5 100644 --- a/lua/astroui/folding.lua +++ b/lua/astroui/folding.lua @@ -42,6 +42,20 @@ function M.is_enabled(bufnr) return enabled == true end +--- Refresh LSP folding availability for a buffer +---@param bufnr? integer The buffer to refresh (defaults to the current buffer) +function M.refresh(bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + if not vim.api.nvim_buf_is_valid(bufnr) then return end + for _, client in pairs(vim.lsp.get_clients { bufnr = bufnr }) do + if client:supports_method("textDocument/foldingRange", bufnr) then + lsp_bufs[bufnr] = true + return + end + end + lsp_bufs[bufnr] = nil +end + --- A fold expression for doing LSP and Treesitter based folding ---@param lnum? integer the current line number ---@return string foldlevel the calculated fold level @@ -96,10 +110,7 @@ function M.setup() vim.api.nvim_create_autocmd("LspAttach", { desc = "Monitor attached LSP clients with fold providers", group = augroup, - callback = function(args) - local client = assert(vim.lsp.get_client_by_id(args.data.client_id)) - if client:supports_method("textDocument/foldingRange", args.buf) then lsp_bufs[args.buf] = true end - end, + callback = function(args) M.refresh(args.buf) end, }) vim.api.nvim_create_autocmd("LspDetach", { group = augroup, @@ -114,6 +125,22 @@ function M.setup() lsp_bufs[args.buf] = nil end, }) + vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, { + group = augroup, + desc = "Remove LSP folding providers for deleted buffers", + callback = function(args) lsp_bufs[args.buf] = nil end, + }) + vim.api.nvim_create_autocmd("User", { + pattern = "AstroLspCapability", + group = augroup, + desc = "Refresh folding providers after LSP capability changes", + callback = function(args) + local data = args.data + if type(data) == "table" and type(data.client_id) == "number" and type(data.bufnr) == "number" then + M.refresh(data.bufnr) + end + end, + }) is_setup = true end diff --git a/lua/astroui/init.lua b/lua/astroui/init.lua index 7d1e24e..aae7333 100644 --- a/lua/astroui/init.lua +++ b/lua/astroui/init.lua @@ -48,10 +48,10 @@ end --- Get highlight properties for a given highlight name ---@param name string The highlight group name ---@param fallback? table The fallback highlight properties ----@return table properties # the highlight group properties +---@return AstroUIHighlight properties # the highlight group properties function M.get_hlgroup(name, fallback) if vim.fn.hlexists(name) == 1 then - local hl = vim.api.nvim_get_hl(0, { name = name, link = false }) + local hl = vim.api.nvim_get_hl(0, { name = name, link = false }) --[[@as AstroUIHighlight]] if not hl.fg then hl.fg = "NONE" end if not hl.bg then hl.bg = "NONE" end if hl.reverse then @@ -70,7 +70,7 @@ end function M.get_icon(kind, padding, no_fallback) local icons_enabled = vim.g.icons_enabled ~= false if not icons_enabled and no_fallback then return "" end - local icon_pack = assert(M.config[icons_enabled and "icons" or "text_icons"]) + local icon_pack = assert(M.config[icons_enabled and "icons" or "text_icons"]) --[[@as StringMap]] local icon = icon_pack[kind] return icon and icon .. (" "):rep(padding or 0) or "" end diff --git a/lua/astroui/lazygit.lua b/lua/astroui/lazygit.lua index ad79bd8..4115c7b 100644 --- a/lua/astroui/lazygit.lua +++ b/lua/astroui/lazygit.lua @@ -5,11 +5,62 @@ local M = {} local astroui = require "astroui" local config = astroui.config.lazygit +local function yaml_scalar(value) + if type(value) == "string" then return vim.json.encode(value) end + if type(value) == "number" or type(value) == "boolean" then return tostring(value) end + if value == nil then return "null" end + error(("Cannot serialize %s as YAML"):format(type(value))) +end + +local function yaml_key(key) + if type(key) == "number" then return tostring(key) end + if key:match "^[%a_][%w_-]*$" then return key end + return vim.json.encode(key) +end + +local function yaml_keys(tbl) + local keys = vim.tbl_keys(tbl) + table.sort(keys, function(left, right) return tostring(left) < tostring(right) end) + return keys +end + +local function yaml_empty_collection(tbl) return vim.islist(tbl) and "[]" or "{}" end + +local function to_yaml(value, indent) + local lines = {} + local padding = string.rep(" ", indent) + if vim.islist(value) then + for _, item in ipairs(value) do + if type(item) ~= "table" then + table.insert(lines, padding .. "- " .. yaml_scalar(item)) + elseif next(item) == nil then + table.insert(lines, padding .. "- " .. yaml_empty_collection(item)) + else + table.insert(lines, padding .. "-") + vim.list_extend(lines, to_yaml(item, indent + 2)) + end + end + else + for _, key in ipairs(yaml_keys(value)) do + local item = value[key] + if type(item) ~= "table" then + table.insert(lines, padding .. yaml_key(key) .. ": " .. yaml_scalar(item)) + elseif next(item) == nil then + table.insert(lines, padding .. yaml_key(key) .. ": " .. yaml_empty_collection(item)) + else + table.insert(lines, padding .. yaml_key(key) .. ":") + vim.list_extend(lines, to_yaml(item, indent + 2)) + end + end + end + return lines +end + -- Build theme configuration file function M.update_config() if not config then return end ---@type table - local theme = {} + local theme = vim.empty_dict() -- Check if TUI is attached -- TODO: REMOVE WHEN DROPPING SUPPORT FOR NEOVIM v0.11 @@ -42,27 +93,7 @@ function M.update_config() end local lg_config = vim.tbl_deep_extend("force", { gui = { theme = theme } }, config.config or {}) - - local function yaml_val(val) return type(val) == "string" and not val:find "^\"'`" and ("%q"):format(val) or val end - - local function to_yaml(tbl, indent) - indent = indent or 0 - local lines = {} - for k, v in pairs(tbl) do - table.insert(lines, string.rep(" ", indent) .. k .. (type(v) == "table" and ":" or ": " .. yaml_val(v))) - if type(v) == "table" then - if vim.islist(v) then - for _, item in ipairs(v) do - table.insert(lines, string.rep(" ", indent + 2) .. "- " .. yaml_val(item)) - end - else - vim.list_extend(lines, to_yaml(v, indent + 2)) - end - end - end - return lines - end - vim.fn.writefile(to_yaml(lg_config), config.theme_path) + vim.fn.writefile(to_yaml(lg_config, 0), config.theme_path) end function M.setup() diff --git a/lua/astroui/status/component.lua b/lua/astroui/status/component.lua index 719c38c..34366d6 100644 --- a/lua/astroui/status/component.lua +++ b/lua/astroui/status/component.lua @@ -174,14 +174,13 @@ function M.lsp(opts) )) end ---- A function to build a set of children components for a git branch section +--- A function to build a set of children components for a virtual environment section ---@param opts? AstroUIComponentVirtualEnvOpts provider options ---@return table # The Heirline component table --- @usage local heirline_component = require("astroui.status").component.git_branch() +-- @usage local heirline_component = require("astroui.status").component.virtual_env() function M.virtual_env(opts) opts = extend_tbl(vim.tbl_get(config, "components", "virtual_env"), opts) if opts.surround and opts.surround.condition == nil then - opts.surround = vim.tbl_extend("force", {}, opts.surround) local virtual_env_opts = opts.virtual_env and extend_tbl(vim.tbl_get(config, "providers", "virtual_env"), opts.virtual_env) opts.surround.condition = function() diff --git a/lua/astroui/status/config.lua b/lua/astroui/status/config.lua index 48e3d2c..79c0c7a 100644 --- a/lua/astroui/status/config.lua +++ b/lua/astroui/status/config.lua @@ -373,6 +373,19 @@ ---A collection of click handlers for internal heirline components such as `gitsigns`, `diagnostics`, and `dap` ---@field sign_handlers table? +local function handle_sign_click(args) + if not args.sign then return false end + local config = assert(require("astroui").config.status) + local handler = args.sign.name ~= "" and config.sign_handlers[args.sign.name] + if not handler then handler = config.sign_handlers[args.sign.namespace] end + if not handler then handler = config.sign_handlers[args.sign.texthl] end + if handler then + handler(args) + return true + end + return false +end + ---@type AstroUIStatusOpts return { attributes = {}, @@ -682,6 +695,8 @@ return { if args.mods:find "c" then local dap_avail, dap = pcall(require, "dap") if dap_avail then dap.toggle_breakpoint() end + else + handle_sign_click(args) end end, }, @@ -693,13 +708,7 @@ return { name = "sign_click", callback = function(...) local args = require("astroui.status.utils").statuscolumn_clickargs(...) - local config = assert(require("astroui").config.status) - if args.sign then - local handler = args.sign.name ~= "" and config.sign_handlers[args.sign.name] - if not handler then handler = config.sign_handlers[args.sign.namespace] end - if not handler then handler = config.sign_handlers[args.sign.texthl] end - if handler then handler(args) end - end + handle_sign_click(args) end, }, }, diff --git a/lua/astroui/status/heirline.lua b/lua/astroui/status/heirline.lua index f4cf03a..14efa2b 100644 --- a/lua/astroui/status/heirline.lua +++ b/lua/astroui/status/heirline.lua @@ -16,6 +16,8 @@ local status_utils = require "astroui.status.utils" local buf_utils = require "astrocore.buffer" local get_icon = require("astroui").get_icon +local picker_fallback_labels = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + --- A helper function to get the type a tab or buffer is ---@param self table the self table from a heirline component function ---@param prefix? string the prefix of the type, either "tab" or "buffer" (Default: "buffer") @@ -66,7 +68,8 @@ function M.make_buflist(component) condition = function(self) return self._show_picker end, update = false, init = function(self) - if not vim.tbl_get(self._picker_labels, self.label) then + if vim.tbl_get(self._picker_labels, self.label) ~= self.bufnr then + self.label = nil local bufname = provider.filename()(self) local label = bufname:sub(1, 1) local i = 2 @@ -75,11 +78,25 @@ function M.make_buflist(component) label = bufname:sub(i, i) i = i + 1 end - self._picker_labels[label] = self.bufnr - self.label = label + if #label ~= 1 or self._picker_labels[label] then + for j = 1, #picker_fallback_labels do + local fallback_label = picker_fallback_labels:sub(j, j) + if not self._picker_labels[fallback_label] then + label = fallback_label + break + end + end + end + if #label == 1 and not self._picker_labels[label] then + self._picker_labels[label] = self.bufnr + self.label = label + end end end, - provider = function(self) return provider.str { str = self.label, padding = { left = 1, right = 1 } } end, + provider = function(self) + if not self.label then return "" end + return provider.str { str = self.label, padding = { left = 1, right = 1 } } + end, hl = cached_func(hl.get_attributes, "buffer_picker"), }, component, -- create buffer component @@ -103,24 +120,26 @@ function M.make_tablist(...) return require("heirline.utils").make_tablist(...) ---@param callback function with a single parameter of the buffer number function M.buffer_picker(callback) local tabline = require("heirline").tabline - -- if buflist then local prev_showtabline = vim.opt.showtabline:get() if prev_showtabline ~= 2 then vim.opt.showtabline = 2 end - vim.cmd.redrawtabline() - ---@diagnostic disable-next-line: undefined-field - local buflist = vim.tbl_get(tabline, "_buflist", 1) - if buflist then - buflist._picker_labels = {} - buflist._show_picker = true + local buflist + local ok, err = pcall(function() vim.cmd.redrawtabline() - local char = vim.fn.getcharstr() - local bufnr = buflist._picker_labels[char] - if bufnr then callback(bufnr) end - buflist._show_picker = false - end + ---@diagnostic disable-next-line: undefined-field + buflist = vim.tbl_get(tabline, "_buflist", 1) + if buflist then + buflist._picker_labels = {} + buflist._show_picker = true + vim.cmd.redrawtabline() + local char = vim.fn.getcharstr() + local bufnr = buflist._picker_labels[char] + if bufnr then callback(bufnr) end + end + end) + if buflist then buflist._show_picker = false end if prev_showtabline ~= 2 then vim.opt.showtabline = prev_showtabline end vim.cmd.redrawtabline() - -- end + if not ok then error(err, 0) end end --- Refresh heirline colors diff --git a/lua/astroui/status/hl.lua b/lua/astroui/status/hl.lua index 4c68cac..40654f2 100644 --- a/lua/astroui/status/hl.lua +++ b/lua/astroui/status/hl.lua @@ -48,7 +48,7 @@ end ---@return table # a table of highlight information -- @usage local heirline_component = { provider = "Example Provider", hl = require("astroui.status").hl.get_attributes("treesitter") }, function M.get_attributes(name, include_bg) - local hl = vim.deepcopy(config.attributes[name] or {}) + local hl = config.attributes[name] or {} local loaded_colors = require("heirline.highlights").get_loaded_colors() if loaded_colors[name .. "_fg"] then hl.fg = name .. "_fg" end if include_bg and loaded_colors[name .. "_bg"] then hl.bg = name .. "_bg" end diff --git a/lua/astroui/status/init.lua b/lua/astroui/status/init.lua index a5d59d2..4eadad4 100644 --- a/lua/astroui/status/init.lua +++ b/lua/astroui/status/init.lua @@ -17,6 +17,7 @@ local extend_tbl = astro.extend_tbl local provider = require "astroui.status.provider" local status_utils = require "astroui.status.utils" +local breadcrumbs_id = 0 --- An `init` function to build a set of children components for LSP breadcrumbs ---@param opts? AstroUIInitBreadcrumbsOpts component init options @@ -29,7 +30,13 @@ function M.breadcrumbs(opts) icon = { enabled = true, hl = config.icon_highlights.breadcrumbs }, padding = { left = 0, right = 0 }, }, opts) + breadcrumbs_id = breadcrumbs_id + 1 + local click_name = "heirline_breadcrumbs_" .. breadcrumbs_id .. "_" + local positions = {} return function(self) + local winid = vim.fn.win_getid(self.winnr) + positions[winid] = {} + local window_positions = positions[winid] local data = require("aerial").get_location(true) or {} local children = {} -- add prefix if needed, use the separator if true, or use the provided character @@ -44,15 +51,20 @@ function M.breadcrumbs(opts) -- create a child for each level for i, d in ipairs(data) do if i > start_idx then + local click_id = #window_positions + 1 + window_positions[click_id] = { lnum = d.lnum, col = d.col } local child = { { provider = d.name:gsub("%%", "%%%%"):gsub("%s*->%s*", "") }, -- add symbol name on_click = { -- add on click function - minwid = status_utils.encode_pos(d.lnum, d.col, self.winnr), + minwid = click_id, callback = function(_, minwid) - local lnum, col, winnr = status_utils.decode_pos(minwid) - vim.api.nvim_win_set_cursor(assert(vim.fn.win_getid(winnr)), { lnum, col }) + local position = positions[winid] and positions[winid][minwid] + if position and vim.api.nvim_win_is_valid(winid) then + vim.api.nvim_win_set_cursor(winid, { position.lnum, position.col }) + end end, - name = "heirline_breadcrumbs", + name = click_name .. winid, + update = true, }, } if opts.icon.enabled then -- add icon and highlight if enabled @@ -129,6 +141,7 @@ end ---@class AstroUIUpdateEvent: vim.api.keyset.create_autocmd ---@field [1] any +---@field callback? fun(instance:table,args:vim.api.keyset.create_autocmd.callback_args):boolean? ---@alias AstroUIUpdateEvents AstroUIUpdateEvent|AstroUIUpdateEvent[] @@ -141,16 +154,23 @@ function M.update_events(opts) ---@cast opts AstroUIUpdateEvent[] return function(self) if not rawget(self, "once") then - local function clear_cache() self._win_cache = nil end + local component = setmetatable({ self }, { __mode = "v" }) + local function clear_cache() + local instance = component[1] + if not instance then return true end + instance._win_cache = nil + end for _, event in ipairs(opts) do local event_opts = { callback = clear_cache } if type(event) == "table" then event_opts.pattern = event.pattern if event.callback then - local callback = event.callback + local callback = assert(event.callback) event_opts.callback = function(args) - clear_cache() - callback(self, args) + local instance = component[1] + if not instance then return true end + instance._win_cache = nil + callback(instance, args) end end event = event[1] diff --git a/lua/astroui/status/provider.lua b/lua/astroui/status/provider.lua index 8a1c871..63af3e3 100644 --- a/lua/astroui/status/provider.lua +++ b/lua/astroui/status/provider.lua @@ -19,6 +19,27 @@ local get_icon = ui.get_icon local condition = require "astroui.status.condition" local status_utils = require "astroui.status.utils" +local function truncate_display(str, max_width, suffix) + if max_width <= 0 then return "" end + if vim.fn.strdisplaywidth(str) <= max_width then return str end + + suffix = suffix or "" + local suffix_width = vim.fn.strdisplaywidth(suffix) + local available_width = math.max(max_width - suffix_width, 0) + local truncated = {} + local width = 0 + for index = 0, vim.fn.strchars(str) - 1 do + local char = vim.fn.strcharpart(str, index, 1) + local char_width = vim.fn.strdisplaywidth(char) + if width + char_width > available_width then break end + table.insert(truncated, char) + width = width + char_width + end + + if suffix_width > max_width then return truncate_display(suffix, max_width) end + return table.concat(truncated) .. suffix +end + --- A provider function for the fill string ---@return string # the statusline string for filling the empty space -- @usage local heirline_component = { provider = require("astroui.status").provider.fill } @@ -34,19 +55,6 @@ function M.signcolumn(opts) return status_utils.stylize("%s", opts) end --- local function to resolve the first sign in the signcolumn --- specifically for usage when `signcolumn=number` -local function resolve_sign(bufnr, lnum) - local row = lnum - 1 - local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, { row, 0 }, { row, -1 }, { details = true, type = "sign" }) - local ret - for _, extmark in pairs(extmarks) do - local sign_def = extmark[4] - if sign_def.sign_text and (not ret or (ret.priority < sign_def.priority)) then ret = sign_def end - end - if ret then return { text = ret.sign_text, texthl = ret.sign_hl_group } end -end - --- A provider function for the numbercolumn string ---@param opts? AstroUIProviderNumbercolumnOpts provider options ---@return function # the statuscolumn string for adding the numbercolumn @@ -58,22 +66,29 @@ function M.numbercolumn(opts) local lnum, rnum, virtnum = vim.v.lnum, vim.v.relnum, vim.v.virtnum local num, relnum = vim.opt.number:get(), vim.opt.relativenumber:get() local bufnr = self and self.bufnr or 0 - local sign = vim.opt.signcolumn:get():find "nu" and resolve_sign(bufnr, lnum) + local signs = vim.opt.signcolumn:get():find "nu" and status_utils.get_signs(bufnr, lnum - 1) + local has_number_sign = false + for _, sign in ipairs(signs or {}) do + if sign.sign_text or sign.number_hl_group then + has_number_sign = true + break + end + end local str if virtnum ~= 0 then str = "%=" - elseif sign then - str = sign.text - if sign.texthl then str = "%#" .. sign.texthl .. "#" .. str .. "%*" end - str = "%=" .. str + elseif has_number_sign then + str = "%=%l" elseif not num and not relnum then str = "%=" else local cur = relnum and (rnum > 0 and rnum or (num and lnum or 0)) or lnum + local formatted = tostring(cur) if opts.thousands and cur > 999 then - cur = tostring(cur):reverse():gsub("%d%d%d", "%1" .. opts.thousands):reverse():gsub("^%" .. opts.thousands, "") + formatted = + formatted:reverse():gsub("%d%d%d", "%1" .. opts.thousands):reverse():gsub("^%" .. opts.thousands, "") end - str = (rnum == 0 and not opts.culright and relnum) and cur .. "%=" or "%=" .. cur + str = (rnum == 0 and not opts.culright and relnum) and formatted .. "%=" or "%=" .. formatted end return status_utils.stylize(str, opts) end @@ -89,11 +104,11 @@ function M.foldcolumn(opts) if vim.fn.has "nvim-0.12" == 1 then return status_utils.stylize("%C", opts) end -- TODO: Remove code when deprecating Neovim 0.11 support local ffi = require "astroui.ffi" -- get AstroUI C extensions - local fillchars = vim.opt.fillchars:get() - local foldopen = fillchars.foldopen or get_icon "FoldOpened" - local foldclosed = fillchars.foldclose or get_icon "FoldClosed" - local foldsep = fillchars.foldsep or get_icon "FoldSeparator" return function() -- move to M.fold_indicator + local fillchars = vim.opt_local.fillchars:get() + local foldopen = fillchars.foldopen or get_icon "FoldOpened" + local foldclosed = fillchars.foldclose or get_icon "FoldClosed" + local foldsep = fillchars.foldsep or get_icon "FoldSeparator" local wp = ffi.C.find_window_by_handle(0, ffi.new "Error") -- get window handler local width = ffi.C.compute_foldcolumn(wp, 0) -- get foldcolumn width -- get fold info of current line @@ -363,39 +378,54 @@ end function M.unique_path(opts) opts = extend_tbl(vim.tbl_get(config, "providers", "unique_path"), opts) local function path_parts(bufnr) + local path = vim.api.nvim_buf_get_name(bufnr):gsub("\\", "/") + local root = "" + if path:sub(1, 1) == "/" then + root, path = "/", path:gsub("^/+", "") + elseif path:match "^%a:/" then + root, path = path:sub(1, 2), path:sub(4) + end local parts = {} - for match in (vim.api.nvim_buf_get_name(bufnr) .. "/"):gmatch("(.-)" .. "/") do + for match in path:gmatch "[^/]+" do table.insert(parts, match) end - return parts + return { root = root, parts = parts } + end + local function path_suffix(path, length, include_root) + local suffix = table.concat(path.parts, "/", math.max(#path.parts - length, 1), #path.parts - 1) + if include_root then suffix = path.root .. (path.root == "/" and "" or "/") .. suffix end + return suffix ~= "" and (suffix:sub(-1) == "/" and suffix or suffix .. "/") or suffix end return function(self) local bufnr = self and self.bufnr or opts.bufnr local name = opts.buf_name(bufnr) local unique_path = "" -- check for same buffer names under different dirs - local current + local current, unique_length for _, value in ipairs(vim.t.bufs or {}) do if name == opts.buf_name(value) and value ~= bufnr then if not current then current = path_parts(bufnr) end local other = path_parts(value) - - for i = #current - 1, 1, -1 do - if current[i] ~= other[i] then - unique_path = current[i] .. "/" - break + local current_dirs, other_dirs = #current.parts - 1, #other.parts - 1 + local common_dirs = 0 + while common_dirs < current_dirs and common_dirs < other_dirs do + if current.parts[current_dirs - common_dirs] ~= other.parts[other_dirs - common_dirs] then break end + common_dirs = common_dirs + 1 + end + if common_dirs < current_dirs or common_dirs < other_dirs or current.root ~= other.root then + local include_root = current.root ~= other.root and common_dirs == current_dirs + local length = math.min(common_dirs + 1, current_dirs) + (include_root and 1 or 0) + if not unique_length or length >= unique_length then + unique_length = length + unique_path = path_suffix(current, length - (include_root and 1 or 0), include_root) end end end end - return status_utils.stylize( - ( - opts.max_length > 0 - and #unique_path > opts.max_length - and unique_path:sub(1, opts.max_length - 2) .. get_icon "Ellipsis" .. "/" - ) or unique_path, - opts - ) + if opts.max_length > 0 and vim.fn.strdisplaywidth(unique_path) > opts.max_length then + unique_path = truncate_display(unique_path, opts.max_length, get_icon "Ellipsis" .. "/") + end + return status_utils.stylize(unique_path, opts) end end @@ -500,7 +530,7 @@ end --- A provider function for showing the connected LSP client names ---@param opts? AstroUIProviderLspClientNamesOpts provider options ---@return function # the function for outputting the LSP client names --- @usage local heirline_component = { provider = require("astroui.status").provider.lsp_client_names({ integrations = { null_ls = true, conform = true, lint = true }, truncate = 0.25 }) } +-- @usage local heirline_component = { provider = require("astroui.status").provider.lsp_client_names({ integrations = { null_ls = true, conform = true, ["nvim-lint"] = true }, truncate = 0.25 }) } -- @see astroui.status.utils.stylize function M.lsp_client_names(opts) opts = extend_tbl(vim.tbl_get(config, "providers", "lsp_client_names"), opts) @@ -544,13 +574,12 @@ function M.lsp_client_names(opts) end local buf_client_names_set, client_name_lookup = {}, {} for _, client in ipairs(buf_client_names) do - local mapping = opts.mappings and (opts.mappings[client] or opts.mappings["*"]) - if mapping then - if type(mapping) == "function" then - client = mapping(client) - else - client = mapping - end + local mapping = opts.mappings and opts.mappings[client] + if mapping == nil and opts.mappings then mapping = opts.mappings["*"] end + if type(mapping) == "function" then + client = mapping(client) + elseif mapping ~= nil then + client = mapping end if client and client ~= "" and not client_name_lookup[client] then client_name_lookup[client] = true @@ -560,7 +589,7 @@ function M.lsp_client_names(opts) local str = table.concat(buf_client_names_set, ", ") if type(opts.truncate) == "number" then local max_width = math.floor(status_utils.width() * opts.truncate) - if #str > max_width then str = str:sub(0, max_width) .. "…" end + if vim.fn.strdisplaywidth(str) > max_width then str = truncate_display(str, max_width, "…") end end return status_utils.stylize(str, opts) end @@ -578,7 +607,8 @@ function M.virtual_env(opts) local venv = vim.env.VIRTUAL_ENV local env_str if venv and venv ~= "" then - local path = vim.fn.split(venv, "/") + venv = venv:gsub([[[/\\]+$]], "") + local path = vim.split(venv, [[[/\\]+]]) env_str = path[#path] if #path > 1 and vim.tbl_contains(opts.env_names, env_str) then env_str = path[#path - 1] end elseif opts.conda.enabled and conda and conda ~= "" then diff --git a/lua/astroui/status/utils.lua b/lua/astroui/status/utils.lua index 49a0be3..2e06e20 100644 --- a/lua/astroui/status/utils.lua +++ b/lua/astroui/status/utils.lua @@ -165,6 +165,7 @@ function M.icon_provider(bufnr) end local color = require("astroui").get_hlgroup(hl).fg if type(color) == "number" then color = string.format("#%06x", color) end + ---@cast color string? return icon, color end return cached_icon_provider(bufname, filetype, bufname) @@ -192,13 +193,87 @@ end ---@param col integer column number of position ---@param winnr integer a window number ---@return integer the encoded position -function M.encode_pos(line, col, winnr) return bit.bor(bit.lshift(line, 16), bit.lshift(col, 6), winnr) end +function M.encode_pos(line, col, winnr) + local function validate(value, maximum, name) + if type(value) ~= "number" or value % 1 ~= 0 or value < 0 or value > maximum then + error(("%s must be an integer between 0 and %d"):format(name, maximum), 3) + end + end + validate(line, 65535, "line") + validate(col, 1023, "col") + validate(winnr, 63, "winnr") + return bit.bor(bit.lshift(line, 16), bit.lshift(col, 6), winnr) +end --- Decode a previously encoded position to it's sub parts ---@param c integer the encoded position ---@return integer line, integer column, integer window function M.decode_pos(c) return bit.rshift(c, 16), bit.band(bit.rshift(c, 6), 1023), bit.band(c, 63) end +---@private +function M.sign_is_higher(current, sign) + if not current then return true end + local current_priority, priority = current.priority or 0, sign.priority or 0 + if priority ~= current_priority then return priority > current_priority end + local current_id, id = current.id or 0, sign.id or 0 + if id ~= current_id then return id > current_id end + local current_rank, rank = current.rank or math.huge, sign.rank or math.huge + local current_ranked, ranked = current_rank ~= math.huge, rank ~= math.huge + if ranked ~= current_ranked then return ranked end + if ranked and rank ~= current_rank then return rank < current_rank end + return (sign.order or 0) > (current.order or 0) +end + +---@class AstroUIStatusSign: vim.api.keyset.extmark_details +---@field anonymous boolean +---@field id integer +---@field namespace string +---@field order integer +---@field rank number + +function M.get_signs(bufnr, row) + local namespaces = {} + for namespace, ns_id in pairs(vim.api.nvim_get_namespaces()) do + namespaces[ns_id] = namespace + end + + local placements = {} + local placed = vim.fn.sign_getplaced(bufnr, { group = "*", lnum = row + 1 })[1] + for rank, sign in ipairs(placed and placed.signs or {}) do + local key = sign.group .. "\0" .. sign.id + if not placements[key] then placements[key] = {} end + table.insert(placements[key], rank) + end + + local placement_indices, signs = {}, {} + for order, extmark in + ipairs( + vim.api.nvim_buf_get_extmarks( + bufnr, + -1, + { row, 0 }, + { row, -1 }, + { details = true, type = "sign", overlap = true } + ) + ) + do + local details = extmark[4] --[[@as AstroUIStatusSign]] + local namespace_name = details.ns_id and namespaces[details.ns_id] + local namespace = namespace_name or (details.ns_id and ("#" .. details.ns_id)) or "" + local placement_key = namespace_name and (namespace_name .. "\0" .. extmark[1]) + or not details.ns_id and ("\0" .. extmark[1]) + if placement_key then placement_indices[placement_key] = (placement_indices[placement_key] or 0) + 1 end + local ranks = placement_key and placements[placement_key] + details.anonymous = details.ns_id ~= nil and namespace_name == nil + details.id = extmark[1] + details.namespace = namespace + details.order = order + details.rank = ranks and ranks[placement_indices[placement_key]] or math.huge + table.insert(signs, details) + end + return signs +end + --- Get a list of registered null-ls providers for a given filetype ---@param params table parameters to use for null-ls providers ---@return table # a table of null-ls sources @@ -256,30 +331,111 @@ function M.statuscolumn_clickargs(self, minwid, clicks, button, mods) args.char = vim.fn.screenstring(args.mousepos.screenrow, args.mousepos.screencol) if args.char == " " then args.char = vim.fn.screenstring(args.mousepos.screenrow, args.mousepos.screencol - 1) end - if not self.signs then self.signs = {} end - args.sign = self.signs[args.char] - if not args.sign then -- update signs if not found on first click - if not self.bufnr then self.bufnr = vim.api.nvim_get_current_buf() end - local row = args.mousepos.line - 1 - for _, extmark in - ipairs(vim.api.nvim_buf_get_extmarks(self.bufnr, -1, { row, 0 }, { row, -1 }, { details = true, type = "sign" })) - do - local sign = extmark[4] - if not (self.namespaces and self.namespaces[sign.ns_id]) then - self.namespaces = {} - for ns, ns_id in pairs(vim.api.nvim_get_namespaces()) do - self.namespaces[ns_id] = ns - end + self.bufnr = vim.api.nvim_win_get_buf(args.mousepos.winid) + self.signs = {} + local ambiguous = {} + local rendered_signs = {} + local function set_sign(key, sign) + local conflict = ambiguous[key] + if conflict then + if sign.priority > conflict.priority or (sign.priority == conflict.priority and sign.id > conflict.id) then + ambiguous[key], self.signs[key] = nil, sign end - if sign.sign_text then - self.signs[sign.sign_text:gsub("%s", "")] = { - name = sign.sign_name, - text = sign.sign_text, - texthl = sign.sign_hl_group or "NoTexthl", - namespace = sign.ns_id and self.namespaces[sign.ns_id], - } + return + end + local current = self.signs[key] + if + current + and (current.anonymous or sign.anonymous) + and current.priority == sign.priority + and current.id == sign.id + and (current.ns_id ~= sign.ns_id or current.anonymous ~= sign.anonymous) + then + self.signs[key], ambiguous[key] = nil, { priority = sign.priority, id = sign.id } + elseif M.sign_is_higher(current, sign) then + self.signs[key] = sign + end + end + local function insert_rendered_sign(sign) + for index, current in ipairs(rendered_signs) do + if M.sign_is_higher(current, sign) then + table.insert(rendered_signs, index, sign) + return + end + end + table.insert(rendered_signs, sign) + end + local row = args.mousepos.line - 1 + for _, sign in ipairs(M.get_signs(self.bufnr, row)) do + if sign.sign_text then + local text = sign.sign_text:gsub("%s", "") + local sign_info = { + name = sign.sign_name, + anonymous = sign.anonymous, + text = sign.sign_text, + texthl = sign.sign_hl_group or "NoTexthl", + namespace = sign.namespace, + priority = sign.priority or 0, + id = sign.id, + ns_id = sign.ns_id, + order = sign.order, + rank = sign.rank, + matches = {}, + } + set_sign(text, sign_info) + sign_info.matches[text] = true + for index = 0, vim.fn.strchars(text) - 1 do + local char = vim.fn.strcharpart(text, index, 1) + set_sign(char, sign_info) + sign_info.matches[char] = true + end + insert_rendered_sign(sign_info) + end + end + local signcolumn = vim.wo[args.mousepos.winid].signcolumn + local sign_slots + if signcolumn == "yes" or signcolumn == "number" then + sign_slots = 1 + elseif signcolumn:find "^yes:" then + sign_slots = tonumber(signcolumn:match "%d+") + elseif signcolumn == "auto" then + sign_slots = #rendered_signs > 0 and 1 or 0 + else + local minimum, maximum = signcolumn:match "^auto:(%d+)%-(%d+)$" + if minimum then + local min = assert(tonumber(minimum)) + local max = assert(tonumber(maximum)) + sign_slots = math.max(min, math.min(#rendered_signs, max)) + else + maximum = tonumber(signcolumn:match "^auto:(%d+)$") + sign_slots = maximum and math.min(#rendered_signs, maximum) or 0 + end + end + local wininfo = vim.fn.getwininfo(args.mousepos.winid)[1] + local signcolumn_start = wininfo and wininfo.textoff - sign_slots * 2 + 1 + local slot = signcolumn_start + and args.mousepos.wincol + and args.mousepos.wincol >= signcolumn_start + and args.mousepos.wincol < signcolumn_start + sign_slots * 2 + and math.ceil((args.mousepos.wincol - signcolumn_start + 1) / 2) + local slot_sign = slot and rendered_signs[slot] + if slot_sign and slot_sign.matches[args.char] then + local slot_ambiguous = false + for _, sign in ipairs(rendered_signs) do + if + sign ~= slot_sign + and sign.matches[args.char] + and (sign.anonymous or slot_sign.anonymous) + and sign.priority == slot_sign.priority + and sign.id == slot_sign.id + and (sign.ns_id ~= slot_sign.ns_id or sign.anonymous ~= slot_sign.anonymous) + then + slot_ambiguous = true + break end end + args.sign = not slot_ambiguous and slot_sign or nil + else args.sign = self.signs[args.char] end vim.api.nvim_set_current_win(args.mousepos.winid)