diff --git a/.luarc.json b/.luarc.json index c8e55994..24c625c9 100644 --- a/.luarc.json +++ b/.luarc.json @@ -2,6 +2,7 @@ "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", "Lua.diagnostics.disable": [ "assign-type-mismatch", - "cast-local-type" + "cast-local-type", + "missing-parameter" ] } \ No newline at end of file diff --git a/README.md b/README.md index 9e434420..52e64689 100644 --- a/README.md +++ b/README.md @@ -738,19 +738,19 @@ custom_areas = { local hint = #vim.diagnostic.get(0, {severity = seve.HINT}) if error ~= 0 then - table.insert(result, {text = "  " .. error, guifg = "#EC5241"}) + table.insert(result, {text = "  " .. error, fg = "#EC5241"}) end if warning ~= 0 then - table.insert(result, {text = "  " .. warning, guifg = "#EFB839"}) + table.insert(result, {text = "  " .. warning, fg = "#EFB839"}) end if hint ~= 0 then - table.insert(result, {text = "  " .. hint, guifg = "#A3BA5E"}) + table.insert(result, {text = "  " .. hint, fg = "#A3BA5E"}) end if info ~= 0 then - table.insert(result, {text = "  " .. info, guifg = "#7EA9A7"}) + table.insert(result, {text = "  " .. info, fg = "#7EA9A7"}) end return result end, diff --git a/doc/bufferline.txt b/doc/bufferline.txt index 7513060c..2d6ddafb 100644 --- a/doc/bufferline.txt +++ b/doc/bufferline.txt @@ -227,7 +227,7 @@ In order to group buffers specify a list of groups in your config e.g. items = { { name = "Tests", -- Mandatory - highlight = {gui = "underline", guisp = "blue"}, -- Optional + highlight = {underline = true, sp = "blue"}, -- Optional priority = 2, -- determines where it will appear relative to other groups (Optional) icon = "", -- Optional matcher = function(buf) -- Mandatory @@ -236,7 +236,7 @@ In order to group buffers specify a list of groups in your config e.g. } { name = "Docs" - highlight = {gui = "undercurl", guisp = "green"}, + highlight = {undercurl = true, sp = green}, auto_close = false, -- whether or not close this group if it doesn't contain the current buffer matcher = function(buf) return buf.filename:match('%.md') or buf.filename:match('%.txt') @@ -631,7 +631,7 @@ tested for example: > highlights = { fill = { - guibg = { + bg = { attribute = "fg", highlight = "Pmenu" } @@ -642,242 +642,254 @@ This will automatically pull the value of `Pmenu` fg color and use it Any improperly specified tables will be set to `nil` and overriden with the default value for that key. -NOTE: you can specify colors the same way you specify `gui` colors for the -highlight command. See `:h highlight` . - > +NOTE: you can specify colors the same way you specify colors for `nvim_set_hl`. See `:h vim.api.nvim_set_hl` . +> lua require'bufferline'.setup{ highlights = { fill = { - guifg = '', - guibg = '', + fg = '', + bg = '', }, background = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, tab = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, tab_selected = { - guifg = tabline_sel_bg, - guibg = '' + fg = tabline_sel_bg, + bg = '' }, tab_close = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, close_button = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, close_button_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, close_button_selected = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, buffer_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, buffer_selected = { - guifg = normal_fg, - guibg = '', - gui = "bold,italic" + fg = normal_fg, + bg = '', + bold = true, + italic = true, }, diagnostic = { - guifg = '', - guibg = '', + fg = '', + bg = '', }, diagnostic_visible = { - guifg = '', - guibg = '', + fg = '', + bg = '', }, diagnostic_selected = { - guifg = '', - guibg = '', - gui = "bold,italic" + fg = '', + bg = '', + bold = true, + italic = true, }, hint = { - guifg = '', - guisp = '', - guibg = '' + fg = '', + sp = '', + bg = '' }, hint_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, hint_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = '' + fg = '', + bg = '', + sp = '' + bold = true, + italic = true, }, hint_diagnostic = { - guifg = '', - guisp = '', - guibg = '' + fg = '', + sp = '', + bg = '' }, hint_diagnostic_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, hint_diagnostic_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = '' + fg = '', + bg = '', + sp = '' + bold = true, + italic = true, }, info = { - guifg = '', - guisp = '', - guibg = '' + fg = '', + sp = '', + bg = '' }, info_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, info_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = '' + fg = '', + bg = '', + sp = '' + bold = true, + italic = true, }, info_diagnostic = { - guifg = '', - guisp = '', - guibg = '' + fg = '', + sp = '', + bg = '' }, info_diagnostic_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, info_diagnostic_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = '' + fg = '', + bg = '', + sp = '' + bold = true, + italic = true, }, warning = { - guifg = '', - guisp = '', - guibg = '' + fg = '', + sp = '', + bg = '' }, warning_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, warning_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = '' + fg = '', + bg = '', + sp = '' + bold = true, + italic = true, }, warning_diagnostic = { - guifg = '', - guisp = '', - guibg = '' + fg = '', + sp = '', + bg = '' }, warning_diagnostic_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, warning_diagnostic_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = warning_diagnostic_fg + fg = '', + bg = '', + sp = warning_diagnostic_fg + bold = true, + italic = true, }, error = { - guifg = '', - guibg = '', - guisp = '' + fg = '', + bg = '', + sp = '' }, error_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, error_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = '' + fg = '', + bg = '', + sp = '' + bold = true, + italic = true, }, error_diagnostic = { - guifg = '', - guibg = '', - guisp = '' + fg = '', + bg = '', + sp = '' }, error_diagnostic_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, error_diagnostic_selected = { - guifg = '', - guibg = '', - gui = "bold,italic", - guisp = '' + fg = '', + bg = '', + sp = '' + bold = true, + italic = true, }, modified = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, modified_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, modified_selected = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, duplicate_selected = { - guifg = '', - gui = "italic", - guibg = '' + fg = '', + bg = '' + italic = true, }, duplicate_visible = { - guifg = '', - gui = "italic", - guibg = '' + fg = '', + bg = '' + italic = true }, duplicate = { - guifg = '', - gui = "italic", - guibg = '' + fg = '', + bg = '' + italic = true }, separator_selected = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, separator_visible = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, separator = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, indicator_selected = { - guifg = '', - guibg = '' + fg = '', + bg = '' }, pick_selected = { - guifg = '', - guibg = '', - gui = "bold,italic" + fg = '', + bg = '', + bold = true, + italic = true, }, pick_visible = { - guifg = '', - guibg = '', - gui = "bold,italic" + fg = '', + bg = '', + bold = true, + italic = true, }, pick = { - guifg = '', - guibg = '', - gui = "bold,italic" + fg = '', + bg = '', + bold = true, + italic = true, } }; } diff --git a/lua/bufferline.lua b/lua/bufferline.lua index 0b6a0a65..0e27a7a2 100644 --- a/lua/bufferline.lua +++ b/lua/bufferline.lua @@ -121,7 +121,7 @@ end ---@alias group_actions "close" | "toggle" ---Execute an action on a group of buffers ---@param name string ----@param action group_actions | fun(b: Buffer) +---@param action group_actions | fun(b: NvimBuffer) function M.group_action(name, action) assert(name, "A name must be passed to execute a group action") if action == "close" then @@ -153,12 +153,12 @@ local function handle_group_enter() if options.groups.options.toggle_hidden_on_enter then if current_group.hidden then groups.set_hidden(current_group.id, false) end end - utils.for_each(state.components, function(tab) + utils.for_each(function(tab) local group = groups.get_by_id(tab.group) if group and group.auto_close and group.id ~= current_group.id then groups.set_hidden(group.id, true) end - end) + end, state.components) end ---@param conf BufferlineConfig @@ -208,49 +208,33 @@ local function complete_groups(arg_lead, cmd_line, cursor_pos) return groups.nam local function setup_commands() local cmd = api.nvim_create_user_command - cmd("BufferLinePick", function() M.pick_buffer() end, {}) - cmd("BufferLinePickClose", function() M.close_buffer_with_pick() end, {}) - cmd("BufferLineCycleNext", function() M.cycle(1) end, {}) - cmd("BufferLineCyclePrev", function() M.cycle(-1) end, {}) - cmd("BufferLineCloseRight", function() M.close_in_direction("right") end, {}) - cmd("BufferLineCloseLeft", function() M.close_in_direction("left") end, {}) - cmd("BufferLineMoveNext", function() M.move(1) end, {}) - cmd("BufferLineMovePrev", function() M.move(-1) end, {}) - cmd("BufferLineSortByExtension", function() M.sort_buffers_by("extension") end, {}) - cmd("BufferLineSortByDirectory", function() M.sort_buffers_by("directory") end, {}) - cmd( "BufferLineSortByRelativeDirectory", function() M.sort_buffers_by("relative_directory") end, {} ) - cmd("BufferLineSortByTabs", function() M.sort_buffers_by("tabs") end, {}) - cmd("BufferLineGoToBuffer", function(opts) M.go_to_buffer(opts.args) end, { nargs = 1 }) - cmd( "BufferLineGroupClose", function(opts) M.group_action(opts.args, "close") end, { nargs = 1, complete = complete_groups } ) - cmd( "BufferLineGroupToggle", function(opts) M.group_action(opts.args, "toggle") end, { nargs = 1, complete = complete_groups } ) - cmd("BufferLineTogglePin", function() M.toggle_pin() end, { nargs = 0 }) end @@ -271,7 +255,9 @@ function M.setup(conf) ) return end - config.set(conf or {}) + conf = conf or {} + config.set(conf) + groups.setup(conf) -- Groups must be set up before the config is applied local preferences = config.apply() -- on loading (and reloading) the plugin's config reset all the highlights highlights.set_all(preferences) diff --git a/lua/bufferline/buffers.lua b/lua/bufferline/buffers.lua index 385987af..8ef1a316 100644 --- a/lua/bufferline/buffers.lua +++ b/lua/bufferline/buffers.lua @@ -13,6 +13,8 @@ local pick = require("bufferline.pick") local duplicates = require("bufferline.duplicates") --- @module "bufferline.diagnostics" local diagnostics = require("bufferline.diagnostics") +--- @module "bufferline.models" +local models = require("bufferline.models") local M = {} @@ -54,7 +56,7 @@ end ---Return a list of the buffers open in nvim as Components ---@param state BufferlineState ----@return Buffer[] +---@return NvimBuffer[] function M.get_components(state) local options = config.options local buf_nums = utils.get_valid_buffers() @@ -64,10 +66,10 @@ function M.get_components(state) pick.reset() duplicates.reset() - ---@type Buffer[] + ---@type NvimBuffer[] local components = {} local all_diagnostics = diagnostics.get(options) - local Buffer = require("bufferline.models").Buffer + local Buffer = models.Buffer for i, buf_id in ipairs(buf_nums) do local buf = Buffer:new({ path = api.nvim_buf_get_name(buf_id), diff --git a/lua/bufferline/commands.lua b/lua/bufferline/commands.lua index 55a1a79f..f7ccaa6f 100644 --- a/lua/bufferline/commands.lua +++ b/lua/bufferline/commands.lua @@ -27,15 +27,11 @@ local fmt = string.format local api = vim.api ---@param ids number[] -local function save_positions(ids) - local positions = table.concat(ids, ",") - vim.g[positions_key] = positions -end +local function save_positions(ids) vim.g[positions_key] = table.concat(ids, ",") end --- @param elements TabElement[] --- @return number[] local function get_ids(elements) - ---@diagnostic disable-next-line: return-type-mismatch return vim.tbl_map(function(item) return item.id end, elements) end diff --git a/lua/bufferline/config.lua b/lua/bufferline/config.lua index 20a942a6..a5c2899f 100644 --- a/lua/bufferline/config.lua +++ b/lua/bufferline/config.lua @@ -65,11 +65,15 @@ local colors = lazy.require("bufferline.colors") ---@field public themable boolean ---@class BufferlineHLGroup ----@field guifg string ----@field guibg string ----@field guisp string ----@field gui string ----@field hl string +---@field fg string +---@field bg string +---@field sp string +---@field special string +---@field bold boolean +---@field italic boolean +---@field underline boolean +---@field undercurl boolean +---@field hl_group string ---@field hl_name string ---@alias BufferlineHighlights table @@ -77,16 +81,15 @@ local colors = lazy.require("bufferline.colors") ---@class BufferlineConfig ---@field public options BufferlineOptions ---@field public highlights BufferlineHighlights ----@field private original BufferlineConfig original copy of user preferences +---@field private user BufferlineConfig original copy of user preferences ---@field private merge fun(self: BufferlineConfig, defaults: BufferlineConfig): BufferlineConfig ----@field private validate fun(self: BufferlineConfig, defaults: BufferlineConfig): nil +---@field private validate fun(self: BufferlineConfig, defaults: BufferlineConfig, resolved: BufferlineHighlights): nil ---@field private resolve fun(self: BufferlineConfig, defaults: BufferlineConfig) ----@field private resolve_highlights fun(BufferlineConfig, BufferlineHighlights):BufferlineHighlights ---@field private is_tabline fun():boolean --- Convert highlights specified as tables to the correct existing colours ---@param map BufferlineHighlights -local function convert_highlights(map) +local function hl_table_to_color(map) if not map or vim.tbl_isempty(map) then return {} end -- we deep copy the highlights table as assigning the attributes -- will only pass the references so will mutate the original table otherwise @@ -125,7 +128,7 @@ function Config:new(o) -- save a copy of the user's preferences so we can reference exactly what they -- wanted after the config and defaults have been merged. Do this using a copy -- so that reference isn't unintentionally mutated - self.original = vim.deepcopy(o) + self.user = vim.deepcopy(o) setmetatable(o, self) return o end @@ -136,8 +139,7 @@ end function Config:merge(defaults) assert(defaults and type(defaults) == "table", "A valid config table must be passed to merge") self.options = vim.tbl_deep_extend("keep", self.options or {}, defaults.options or {}) - local hls = convert_highlights(self.original.highlights) - self.highlights = vim.tbl_deep_extend("force", defaults.highlights, hls) + self.highlights = vim.tbl_deep_extend("force", defaults.highlights, self.highlights or {}) return self end @@ -153,7 +155,7 @@ local deprecations = { } ---@param options BufferlineOptions -local function handle_deprecations(options) +local function validate_user_options(options) if not options then return end for key, _ in pairs(options) do local deprecation = deprecations[key] @@ -166,32 +168,81 @@ local function handle_deprecations(options) end end ----Ensure the user has only specified highlight groups that exist ----@param defaults BufferlineConfig -function Config:validate(defaults) - handle_deprecations(self.options) - if self.highlights then - local incorrect = {} - for k, _ in pairs(self.highlights) do - if not defaults.highlights[k] then table.insert(incorrect, k) end +---@param options BufferlineOptions +---@return table[] +local function get_offset_highlights(options) + if not options or not options.offsets then return {} end + return utils.fold(function(accum, offset, i) + if offset.highlight and type(offset.highlight) == "table" then + accum[fmt("offset_%d", i)] = offset.highlight + end + return accum + end, options.offsets) +end + +---@param options BufferlineOptions +---@return table[] +local function get_group_highlights(options) + if not options or not options.groups then return {} end + return utils.fold(function(accum, group) + if group.highlight then accum[group.name] = group.highlight end + return accum + end, options.groups.items) +end + +local function validate_user_highlights(opts, defaults, hls) + if not hls then return end + local incorrect = { invalid_hl = {}, invalid_attrs = {} } + + local offset_highlights = get_offset_highlights(opts) + local group_highlights = get_group_highlights(opts) + local all_hls = vim.tbl_extend("force", {}, hls, offset_highlights, group_highlights) + + for k, hl in pairs(all_hls) do + for key, _ in pairs(hl) do + if key:match("gui") then table.insert(incorrect.invalid_attrs, fmt("- %s", k)) end + end + if hls[k] then + if not defaults.highlights[k] then table.insert(incorrect.invalid_hl, k) end end - -- Don't continue if there are no incorrect highlights - if vim.tbl_isempty(incorrect) then return end + end + + -- Don't continue if there are no incorrect highlights + if next(incorrect.invalid_hl) then local is_plural = #incorrect > 1 - local verb = is_plural and " are " or " is " - local article = is_plural and " " or " a " - local object = is_plural and " groups. " or " group. " local msg = table.concat({ - table.concat(incorrect, ", "), - verb, + table.concat(incorrect.invalid_hl, ", "), + is_plural and " are " or " is ", "not", - article, + is_plural and " " or " a ", "valid highlight", - object, - "Please check the README for all valid highlights", + is_plural and " groups. " or " group. ", + "Please check :help bufferline-highlights for all valid highlights", }) utils.notify(msg, utils.E) end + if next(incorrect.invalid_attrs) then + local msg = table.concat({ + "Using `gui`, `guifg`, `guibg`, `guisp` is deprecated please, convert these as follows: ", + "- guifg -> fg", + "- guibg -> bg", + "- guisp -> sp", + "- gui -> underline = true, undercurl = true, italic = true", + " see :help bufferline-highlights for more details on how to update your highlights", + "", + "Please fix: ", + unpack(incorrect.invalid_attrs), + }, "\n") + utils.notify(msg, utils.E) + end +end + +---Ensure the user has only specified highlight groups that exist +---@param defaults BufferlineConfig +---@param resolved BufferlineHighlights +function Config:validate(defaults, resolved) + validate_user_options(self.user.options) + validate_user_highlights(self.user.options, defaults, resolved) end function Config:mode() @@ -282,265 +333,279 @@ local function derive_colors() return { fill = { - guifg = comment_fg, - guibg = separator_background_color, + fg = comment_fg, + bg = separator_background_color, }, group_separator = { - guifg = comment_fg, - guibg = separator_background_color, + fg = comment_fg, + bg = separator_background_color, }, group_label = { - guibg = comment_fg, - guifg = separator_background_color, + bg = comment_fg, + fg = separator_background_color, }, tab = { - guifg = comment_fg, - guibg = background_color, + fg = comment_fg, + bg = background_color, }, tab_selected = { - guifg = tabline_sel_bg, - guibg = normal_bg, + fg = tabline_sel_bg, + bg = normal_bg, }, tab_close = { - guifg = comment_fg, - guibg = background_color, + fg = comment_fg, + bg = background_color, }, close_button = { - guifg = comment_fg, - guibg = background_color, + fg = comment_fg, + bg = background_color, }, close_button_visible = { - guifg = comment_fg, - guibg = visible_bg, + fg = comment_fg, + bg = visible_bg, }, close_button_selected = { - guifg = normal_fg, - guibg = normal_bg, + fg = normal_fg, + bg = normal_bg, }, background = { - guifg = comment_fg, - guibg = background_color, + fg = comment_fg, + bg = background_color, }, buffer = { - guifg = comment_fg, - guibg = background_color, + fg = comment_fg, + bg = background_color, }, buffer_visible = { - guifg = comment_fg, - guibg = visible_bg, + fg = comment_fg, + bg = visible_bg, }, buffer_selected = { - guifg = normal_fg, - guibg = normal_bg, - gui = "bold,italic", + fg = normal_fg, + bg = normal_bg, + bold = true, + italic = true, }, numbers = { - guifg = comment_fg, - guibg = background_color, + fg = comment_fg, + bg = background_color, }, numbers_selected = { - guifg = normal_fg, - guibg = normal_bg, - gui = "bold,italic", + fg = normal_fg, + bg = normal_bg, + bold = true, + italic = true, }, numbers_visible = { - guifg = comment_fg, - guibg = visible_bg, + fg = comment_fg, + bg = visible_bg, }, diagnostic = { - guifg = comment_diagnostic_fg, - guibg = background_color, + fg = comment_diagnostic_fg, + bg = background_color, }, diagnostic_visible = { - guifg = comment_diagnostic_fg, - guibg = visible_bg, + fg = comment_diagnostic_fg, + bg = visible_bg, }, diagnostic_selected = { - guifg = normal_diagnostic_fg, - guibg = normal_bg, - gui = "bold,italic", + fg = normal_diagnostic_fg, + bg = normal_bg, + bold = true, + italic = true, }, hint = { - guifg = comment_fg, - guisp = hint_fg, - guibg = background_color, + fg = comment_fg, + sp = hint_fg, + bg = background_color, }, hint_visible = { - guifg = comment_fg, - guibg = visible_bg, + fg = comment_fg, + bg = visible_bg, }, hint_selected = { - guifg = hint_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = hint_fg, + fg = hint_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = hint_fg, }, hint_diagnostic = { - guifg = comment_diagnostic_fg, - guisp = hint_diagnostic_fg, - guibg = background_color, + fg = comment_diagnostic_fg, + sp = hint_diagnostic_fg, + bg = background_color, }, hint_diagnostic_visible = { - guifg = comment_diagnostic_fg, - guibg = visible_bg, + fg = comment_diagnostic_fg, + bg = visible_bg, }, hint_diagnostic_selected = { - guifg = hint_diagnostic_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = hint_diagnostic_fg, + fg = hint_diagnostic_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = hint_diagnostic_fg, }, info = { - guifg = comment_fg, - guisp = info_fg, - guibg = background_color, + fg = comment_fg, + sp = info_fg, + bg = background_color, }, info_visible = { - guifg = comment_fg, - guibg = visible_bg, + fg = comment_fg, + bg = visible_bg, }, info_selected = { - guifg = info_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = info_fg, + fg = info_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = info_fg, }, info_diagnostic = { - guifg = comment_diagnostic_fg, - guisp = info_diagnostic_fg, - guibg = background_color, + fg = comment_diagnostic_fg, + sp = info_diagnostic_fg, + bg = background_color, }, info_diagnostic_visible = { - guifg = comment_diagnostic_fg, - guibg = visible_bg, + fg = comment_diagnostic_fg, + bg = visible_bg, }, info_diagnostic_selected = { - guifg = info_diagnostic_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = info_diagnostic_fg, + fg = info_diagnostic_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = info_diagnostic_fg, }, warning = { - guifg = comment_fg, - guisp = warning_fg, - guibg = background_color, + fg = comment_fg, + sp = warning_fg, + bg = background_color, }, warning_visible = { - guifg = comment_fg, - guibg = visible_bg, + fg = comment_fg, + bg = visible_bg, }, warning_selected = { - guifg = warning_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = warning_fg, + fg = warning_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = warning_fg, }, warning_diagnostic = { - guifg = comment_diagnostic_fg, - guisp = warning_diagnostic_fg, - guibg = background_color, + fg = comment_diagnostic_fg, + sp = warning_diagnostic_fg, + bg = background_color, }, warning_diagnostic_visible = { - guifg = comment_diagnostic_fg, - guibg = visible_bg, + fg = comment_diagnostic_fg, + bg = visible_bg, }, warning_diagnostic_selected = { - guifg = warning_diagnostic_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = warning_diagnostic_fg, + fg = warning_diagnostic_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = warning_diagnostic_fg, }, error = { - guifg = comment_fg, - guibg = background_color, - guisp = error_fg, + fg = comment_fg, + bg = background_color, + sp = error_fg, }, error_visible = { - guifg = comment_fg, - guibg = visible_bg, + fg = comment_fg, + bg = visible_bg, }, error_selected = { - guifg = error_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = error_fg, + fg = error_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = error_fg, }, error_diagnostic = { - guifg = comment_diagnostic_fg, - guibg = background_color, - guisp = error_diagnostic_fg, + fg = comment_diagnostic_fg, + bg = background_color, + sp = error_diagnostic_fg, }, error_diagnostic_visible = { - guifg = comment_diagnostic_fg, - guibg = visible_bg, + fg = comment_diagnostic_fg, + bg = visible_bg, }, error_diagnostic_selected = { - guifg = error_diagnostic_fg, - guibg = normal_bg, - gui = "bold,italic", - guisp = error_diagnostic_fg, + fg = error_diagnostic_fg, + bg = normal_bg, + bold = true, + italic = true, + sp = error_diagnostic_fg, }, modified = { - guifg = string_fg, - guibg = background_color, + fg = string_fg, + bg = background_color, }, modified_visible = { - guifg = string_fg, - guibg = visible_bg, + fg = string_fg, + bg = visible_bg, }, modified_selected = { - guifg = string_fg, - guibg = normal_bg, + fg = string_fg, + bg = normal_bg, }, duplicate_selected = { - guifg = duplicate_color, - gui = "italic", - guibg = normal_bg, + fg = duplicate_color, + italic = true, + bg = normal_bg, }, duplicate_visible = { - guifg = duplicate_color, - gui = "italic", - guibg = visible_bg, + fg = duplicate_color, + italic = true, + bg = visible_bg, }, duplicate = { - guifg = duplicate_color, - gui = "italic", - guibg = background_color, + fg = duplicate_color, + italic = true, + bg = background_color, }, separator_selected = { - guifg = separator_background_color, - guibg = normal_bg, + fg = separator_background_color, + bg = normal_bg, }, separator_visible = { - guifg = separator_background_color, - guibg = visible_bg, + fg = separator_background_color, + bg = visible_bg, }, separator = { - guifg = separator_background_color, - guibg = background_color, + fg = separator_background_color, + bg = background_color, }, indicator_selected = { - guifg = tabline_sel_bg, - guibg = normal_bg, + fg = tabline_sel_bg, + bg = normal_bg, }, indicator_visible = { - guifg = visible_bg, - guibg = visible_bg, + fg = visible_bg, + bg = visible_bg, }, pick_selected = { - guifg = error_fg, - guibg = normal_bg, - gui = "bold,italic", + fg = error_fg, + bg = normal_bg, + bold = true, + italic = true, }, pick_visible = { - guifg = error_fg, - guibg = visible_bg, - gui = "bold,italic", + fg = error_fg, + bg = visible_bg, + bold = true, + italic = true, }, pick = { - guifg = error_fg, - guibg = background_color, - gui = "bold,italic", + fg = error_fg, + bg = background_color, + bold = true, + italic = true, }, } end @@ -556,7 +621,7 @@ local function get_defaults() ---@type BufferlineOptions options = { mode = "buffers", - themable = true, -- whether or not bufferline highlights can be overriden externally + themable = true, -- whether or not bufferline highlights can be overridden externally numbers = "none", buffer_close_icon = "", modified_icon = "●", @@ -604,73 +669,97 @@ local function get_defaults() } end ---- Resolve/change any incompatible options based on the values of other options +--- Resolve (and update) any incompatible options based on the values of other options --- e.g. in tabline only certain values are valid/certain options no longer make sense. function Config:resolve(defaults) - if self.highlights and type(self.highlights) == "function" then - local resolved = self.highlights(defaults) - self.highlights, self.original.highlights = resolved, resolved - end + local user, hl = self.user.highlights, self.highlights + if type(user) == "function" then hl = user(defaults) end + + self.highlights = utils.fold(function(accum, opts, hl_name) + accum[hl_name] = highlights.translate_legacy_options(opts) + return accum + end, hl_table_to_color(hl)) + if self:is_tabline() then local opts = defaults.options -- If the sort by mechanism is "tabs" but the user is in tabline mode -- then the id will be that of the tabs so sort by should be id i.e. "tabs" sort -- is redundant in tabs mode if opts.sort_by == "tabs" then opts.sort_by = "id" end - - -- Don't show tab indicators in tabline mode if opts.show_tab_indicators then opts.show_tab_indicators = false end - opts.close_command = utils.close_tab opts.right_mouse_command = "tabclose %d" opts.left_mouse_command = api.nvim_set_current_tabpage end + return hl end ---Generate highlight groups from user ---@param map table --- TODO: can this become part of a metatable for each highlight group so it is done at the point ---of usage -local function add_highlight_groups(map) - for name, tbl in pairs(map) do - highlights.add_group(name, tbl) +local function set_highlight_groups(map) + for name, opts in pairs(map) do + opts.hl_group = highlights.generate_name(name) end end -function Config:resolve_highlights(defaults) end +---Add highlight groups for a group +---@param hls BufferlineHighlights +local function set_group_highlights(hls) + for _, group in pairs(groups.get_all()) do + local group_hl, name = group.highlight, group.name + if group_hl and type(group_hl) == "table" then + group_hl = highlights.translate_legacy_options(group_hl) + local sep_name = fmt("%s_separator", name) + local label_name = fmt("%s_label", name) + local selected_name = fmt("%s_selected", name) + local visible_name = fmt("%s_visible", name) + hls[sep_name] = { + fg = group_hl.fg or group_hl.sp or hls.group_separator.fg, + bg = hls.fill.bg, + } + hls[label_name] = { + fg = hls.fill.bg, + bg = group_hl.fg or group_hl.sp or hls.group_separator.fg, + } + hls[selected_name] = vim.tbl_extend("keep", group_hl, hls.buffer_selected) + hls[visible_name] = vim.tbl_extend("keep", group_hl, hls.buffer_visible) + hls[name] = vim.tbl_extend("keep", group_hl, hls.buffer) + + hls[sep_name].hl_group = highlights.generate_name(sep_name) + hls[label_name].hl_group = highlights.generate_name(label_name) + end + end +end --- Merge user config with defaults +--- @param quiet boolean? whether or not to validate the configuration --- @return BufferlineConfig -function M.apply() +function M.apply(quiet) local defaults = get_defaults() - config:resolve(defaults) - config:validate(defaults) + local resolved = config:resolve(defaults) + if not quiet then config:validate(defaults, resolved) end config:merge(defaults) - -- TODO: Can setting up of group highlights be constrained to the config module - groups.setup(config) - add_highlight_groups(config.highlights) + set_highlight_groups(config.highlights) + set_group_highlights(config.highlights) return config end ---Keep track of a users config for use throughout the plugin as well as ensuring ---defaults are set. This is also so we can diff what the user set this is useful ---for setting the highlight groups etc. once this has been merged with the defaults ----@param conf BufferlineConfig +---@param conf BufferlineConfig? function M.set(conf) config = Config:new(conf or {}) end ---Update highlight colours when the colour scheme changes -function M.update_highlights() - config:merge({ highlights = derive_colors() }) - groups.reset_highlights(config.highlights) - add_highlight_groups(config.highlights) - return config -end +function M.update_highlights() return M.apply(true) end ---Get the user's configuration or a key from it ---@param key string? ---@return BufferlineConfig? ----@overload fun(key: '"options"'): BufferlineOptions ----@overload fun(key: '"highlights"'): BufferlineHighlights +---@overload fun(key: "options"): BufferlineOptions +---@overload fun(key: "highlights"): BufferlineHighlights function M.get(key) if not config then return end return config[key] or config diff --git a/lua/bufferline/constants.lua b/lua/bufferline/constants.lua index 4a61bae0..a436ea84 100644 --- a/lua/bufferline/constants.lua +++ b/lua/bufferline/constants.lua @@ -22,9 +22,9 @@ M.sep_chars = { M.positions_key = "BufferlinePositions" M.visibility = { - INACTIVE = 1, - SELECTED = 2, - NONE = 3, + SELECTED = 3, + INACTIVE = 2, + NONE = 1, } M.FOLDER_ICON = "" diff --git a/lua/bufferline/custom_area.lua b/lua/bufferline/custom_area.lua index ec52fc69..2098ad63 100644 --- a/lua/bufferline/custom_area.lua +++ b/lua/bufferline/custom_area.lua @@ -1,5 +1,10 @@ -local config = require("bufferline.config") -local utils = require("bufferline.utils") +local lazy = require("bufferline.lazy") +---@module "bufferline.config" +local config = lazy.require("bufferline.config") +---@module "bufferline.utils" +local utils = lazy.require("bufferline.utils") +---@module "bufferline.highlights" +local highlights = lazy.require("bufferline.highlights") local M = {} @@ -10,17 +15,15 @@ local fmt = string.format ---@param index integer ---@param side string ---@param section table ----@param guibg string? -local function create_hl(index, side, section, guibg) +---@param bg string? +local function create_hl(index, side, section, bg) local name = fmt("BufferLine%sCustomAreaText%d", side:gsub("^%l", string.upper), index) - local H = require("bufferline.highlights") - H.set_one(name, { - guifg = section.guifg, - guibg = section.guibg or guibg, - gui = section.gui, - default = true, -- We need to be able to constantly override these highlights so they should always be default - }) - return H.hl(name) + local opts = highlights.translate_legacy_options(section) + opts.bg = opts.bg or bg + -- We need to be able to constantly override these highlights so they should always be default + opts.default = true + highlights.set_one(name, opts) + return highlights.hl(name) end ---@param text string diff --git a/lua/bufferline/duplicates.lua b/lua/bufferline/duplicates.lua index 99d894d9..23eeb0f6 100644 --- a/lua/bufferline/duplicates.lua +++ b/lua/bufferline/duplicates.lua @@ -8,55 +8,62 @@ local utils = require("bufferline.utils") local duplicates = {} +local api = vim.api + function M.reset() duplicates = {} end +local function is_same_path(a, b, depth) + local a_path = vim.split(a, utils.path_sep) + local b_path = vim.split(b, utils.path_sep) + local a_index = depth <= #a_path and (#a_path - depth) + 1 or 1 + local b_index = depth <= #b_path and (#b_path - depth) + 1 or 1 + return b_path[b_index] == a_path[a_index] +end --- This function marks any duplicate buffers granted --- the buffer names have changes ----@param buffers Buffer[] ----@return Buffer[] -function M.mark(buffers) - return vim.tbl_map(function(current) - -- Do not attempt to mark unnamed files +---@param elements TabElement[] +---@return TabElement[] +function M.mark(elements) + return utils.map(function(current) if current.path == "" then return current end local duplicate = duplicates[current.name] if not duplicate then duplicates[current.name] = { current } else - local depth = 1 - local limit = 10 - for _, buf in ipairs(duplicate) do - local buf_depth = 1 - while current:ancestor(buf_depth) == buf:ancestor(buf_depth) do - -- short circuit if we have gone up 10 directories, we don't expect to have - -- to look that far to find a non-matching ancestor and we might be looping - -- endlessly - if buf_depth >= limit then return end - - buf_depth = buf_depth + 1 + local depth, limit, is_same_buffer = 1, 10, false + for _, element in ipairs(duplicate) do + local element_depth = 1 + is_same_buffer = current.path == element.path + while is_same_path(current.path, element.path, element_depth) and not is_same_buffer do + if element_depth >= limit then break end + element_depth = element_depth + 1 end - if buf_depth > depth then depth = buf_depth end - buf.duplicated = true - buf.prefix_count = buf_depth - buffers[buf.ordinal] = buf + if element_depth > depth then depth = element_depth end + elements[element.ordinal].prefix_count = element_depth + elements[element.ordinal].duplicated = is_same_buffer and "element" or "path" end - current.duplicated = true current.prefix_count = depth - table.insert(duplicate, current) + current.duplicated = is_same_buffer and "element" or "path" + duplicate[#duplicate + 1] = current end return current - end, buffers) + end, elements) end --- @param dir string --- @param depth number --- @param max_size number local function truncate(dir, depth, max_size) - if #dir <= max_size then return dir end + if api.nvim_strwidth(dir) <= max_size then return dir end -- we truncate any section of the ancestor which is too long -- by dividing the allotted space for each section by the depth i.e. -- the amount of ancestors which will be prefixed - local allowed_size = math.ceil(max_size / depth) - return utils.truncate_name(dir, allowed_size + 1) + local allowed_size = math.floor(max_size / depth) + 1 -- Add one to account for the path separator + local truncated = utils.map( + function(part) return utils.truncate_name(part, allowed_size) end, + vim.split(dir, utils.path_sep) + ) + return table.concat(truncated, utils.path_sep) end --- @param context RenderContext diff --git a/lua/bufferline/groups.lua b/lua/bufferline/groups.lua index 3dd42970..003fcac0 100644 --- a/lua/bufferline/groups.lua +++ b/lua/bufferline/groups.lua @@ -27,7 +27,7 @@ local fn = vim.fn ---@alias GroupSeparator fun(group:Group, hls: BufferlineHLGroup, count_item: string?): Separators ---@alias GroupSeparators table ----@alias grouper fun(b: Buffer): boolean +---@alias grouper fun(b: NvimBuffer): boolean ---@class Group ---@field public id string used for identifying the group in the tabline @@ -70,18 +70,20 @@ local function format_name(name) return name:gsub("[^%w]+", "_") end ---------------------------------------------------------------------------------------------------- local separator = {} -local function space_end(hl_groups) return { { highlight = hl_groups.fill.hl, text = padding } } end +local function space_end(hl_groups) + return { { highlight = hl_groups.fill.hl_group, text = padding } } +end ---@param group Group, ---@param hls table> ---@param count string ---@return Separators function separator.pill(group, hls, count) - local bg_hl = hls.fill.hl + local bg_hl = hls.fill.hl_group local name, display_name = group.name, group.display_name local sep_grp, label_grp = hls[fmt("%s_separator", name)], hls[fmt("%s_label", name)] - local sep_hl = sep_grp and sep_grp.hl or hls.group_separator.hl - local label_hl = label_grp and label_grp.hl or hls.group_label.hl + local sep_hl = sep_grp and sep_grp.hl_group or hls.group_separator.hl_group + local label_hl = label_grp and label_grp.hl_group or hls.group_label.hl_group local left, right = "█", "█" local indicator = { { text = padding, highlight = bg_hl }, @@ -99,8 +101,8 @@ end ---@return Separators ---@type GroupSeparator function separator.tab(group, hls, count) - local hl = hls.fill.hl - local indicator_hl = hls.buffer.hl + local hl = hls.fill.hl_group + local indicator_hl = hls.buffer.hl_group local indicator = { { higlight = hl, text = padding }, { highlight = indicator_hl, text = padding .. group.name .. count .. padding }, @@ -204,7 +206,7 @@ end ---Group buffers based on user criteria ---buffers only carry a copy of the group ID which is then used to retrieve the correct group ----@param buffer Buffer +---@param buffer NvimBuffer ---@return string? function M.set_id(buffer) if vim.tbl_isempty(state.user_groups) then return end @@ -237,7 +239,7 @@ function M.component(ctx) local group = state.user_groups[element.group] if not group then return end local group_hl = hls[group.name] - local hl = group_hl or hls.buffer.hl + local hl = group_hl or hls.buffer if not group.icon then return nil end local extends = { { id = ui.components.id.name } } if group_hl then extends[#extends + 1] = { id = ui.components.id.duplicates } end @@ -248,33 +250,6 @@ function M.component(ctx) } end ----Add highlight groups for a group ----@param group Group ----@param hls BufferlineHighlights -local function set_group_highlights(group, hls) - local hl = group.highlight - local name = group.name - if not hl or type(hl) ~= "table" then return end - hls[fmt("%s_separator", name)] = { - guifg = hl.guifg or hl.guisp or hls.group_separator.guifg, - guibg = hls.fill.guibg, - } - hls[fmt("%s_label", name)] = { - guifg = hls.fill.guibg, - guibg = hl.guifg or hl.guisp or hls.group_separator.guifg, - } - hls[fmt("%s_selected", name)] = vim.tbl_extend("keep", hl, hls.buffer_selected) - hls[fmt("%s_visible", name)] = vim.tbl_extend("keep", hl, hls.buffer_visible) - hls[name] = vim.tbl_extend("keep", hl, hls.buffer) -end - ----@param highlights BufferlineHighlights -function M.reset_highlights(highlights) - for _, group in pairs(state.user_groups) do - set_group_highlights(group, highlights) - end -end - --- Pull pinned buffers saved in a vim.g global variable and restore them --- to the manual_groupings table. local function restore_pinned_buffers() @@ -293,8 +268,8 @@ end ---@param config BufferlineConfig function M.setup(config) if not config then return end - - local groups = config.options.groups.items or {} + ---@type Group[] + local groups = vim.tbl_get(config, "options", "groups", "items") or {} -- NOTE: if the user has already set the pinned builtin themselves -- then we want each group to have a priority based on it's position in the list @@ -314,42 +289,19 @@ function M.setup(config) priority = vim.tbl_count(state.user_groups) + 1, }) end - for _, group in pairs(state.user_groups) do - set_group_highlights(group, config.highlights) - end - -- Restore pinned buffer from the previous session api.nvim_create_autocmd("SessionLoadPost", { once = true, callback = restore_pinned_buffers }) end ---- Add the current highlight for a specific buffer ---- NOTE: this function mutates the current highlights. ----@param buffer TabElement ----@param highlights table> ----@param current_hl table -function M.set_current_hl(buffer, highlights, current_hl) - local group = state.user_groups[buffer.group] - if not group or not group.name or not group.highlight then return end - local name = group.name - local hl_name = buffer:current() and fmt("%s_selected", name) - or buffer:visible() and fmt("%s_visible", name) - or name - if highlights[hl_name] then - current_hl[name] = highlights[hl_name].hl - else - utils.log.debug(fmt("%s group highlight not found", name)) - end -end - ---Execute a command on each buffer of a group ---@param group_name string ----@param callback fun(b: Buffer) +---@param callback fun(b: NvimBuffer) function M.command(group_name, callback) local group = utils.find( - state.components_by_group, - function(list) return list.name == group_name end + function(list) return list.name == group_name end, + state.components_by_group ) - utils.for_each(group, callback) + utils.for_each(callback, group) end ---@generic T @@ -498,6 +450,8 @@ local function sort_by_groups(components) return sorted, clustered end +function M.get_all() return state.user_groups end + -- FIXME: -- 1. this function does a lot of looping that can maybe be consolidated ---@param components Component[] diff --git a/lua/bufferline/highlights.lua b/lua/bufferline/highlights.lua index 667bdc9b..13b3083b 100644 --- a/lua/bufferline/highlights.lua +++ b/lua/bufferline/highlights.lua @@ -8,8 +8,11 @@ local constants = lazy.require("bufferline.constants") local config = lazy.require("bufferline.config") --- @module "bufferline.groups" local groups = lazy.require("bufferline.groups") +--- @module "bufferline.utils.log" +local log = lazy.require("bufferline.utils.log") local api = vim.api +local V = constants.visibility ---------------------------------------------------------------------------// -- Highlights ---------------------------------------------------------------------------// @@ -17,12 +20,6 @@ local M = {} local PREFIX = "BufferLine" -local visibility_suffix = { - [constants.visibility.INACTIVE] = "Inactive", - [constants.visibility.SELECTED] = "Selected", - [constants.visibility.NONE] = "", -} - --- @class NameGenerationArgs --- @field visibility number @@ -31,14 +28,26 @@ local visibility_suffix = { ---@param name string ---@param opts NameGenerationArgs ---@return string -function M.generate_name(name, opts) +function M.generate_name_for_state(name, opts) opts = opts or {} - return fmt("%s%s%s", PREFIX, name, visibility_suffix[opts.visibility]) + local visibility_suffix = ({ + [V.INACTIVE] = "Inactive", + [V.SELECTED] = "Selected", + [V.NONE] = "", + })[opts.visibility] + return fmt("%s%s%s", PREFIX, name, visibility_suffix) +end + +--- Generate highlight groups names i.e +--- convert 'bufferline_value' to 'BufferlineValue' -> snake to pascal +---@param name string +function M.generate_name(name) + return PREFIX .. name:gsub("_(.)", name.upper):gsub("^%l", string.upper) end function M.hl(item) if not item then return "" end - return "%#" .. item .. "#" + return fmt("%%#%s#", item) end function M.hl_exists(name) return vim.fn.hlexists(name) > 0 end @@ -55,11 +64,14 @@ end local keys = { guisp = "sp", - guibg = "background", - guifg = "foreground", + guibg = "bg", + guifg = "fg", default = "default", - foreground = "foreground", - background = "background", + foreground = "fg", + background = "fg", + fg = "fg", + bg = "bg", + sp = "special", italic = "italic", bold = "bold", underline = "underline", @@ -77,121 +89,102 @@ end --- Transform legacy highlight keys to new nvim_set_hl api keys ---@param opts table ---@return table -local function convert_hl_keys(opts) +function M.translate_legacy_options(opts) + assert(opts, '"opts" must be passed for conversion') local hls = {} for key, value in pairs(opts) do if keys[key] then hls[keys[key]] = value end end if opts.gui then hls = vim.tbl_extend("force", hls, convert_gui(opts.gui)) end - hls.default = vim.F.if_nil(opts.default, config.options.themable) - ---@diagnostic disable-next-line: return-type-mismatch + hls.default = opts.default or (config.options and config.options.themable) return hls end +local function filter_invalid_keys(hl) + return utils.fold(function(accum, item, key) + if keys[key] then accum[key] = item end + return accum + end, hl) +end + ---Apply a single highlight ---@param name string ---@param opts table function M.set_one(name, opts) if opts and not vim.tbl_isempty(opts) then - local hls = convert_hl_keys(opts) - local ok, msg = pcall(api.nvim_set_hl, 0, name, hls) + local hl = filter_invalid_keys(opts) + local ok, msg = pcall(api.nvim_set_hl, 0, name, hl) if not ok then utils.notify( - fmt("Failed setting %s highlight, something isn't configured correctly: %s", name, msg), + fmt("Failed setting %s highlight, something isn't configured correctly: %s", name, msg), utils.E ) end end end ----Generate highlight groups from user ----@param highlight table -function M.add_group(name, highlight) - -- convert 'bufferline_value' to 'BufferlineValue' -> snake to pascal - local formatted = PREFIX .. name:gsub("_(.)", name.upper):gsub("^%l", string.upper) - highlight.hl = formatted -end - --- Map through user colors and convert the keys to highlight names --- by changing the strings to pascal case and using those for highlight name --- @param conf BufferlineConfig function M.set_all(conf) - for name, tbl in pairs(conf.highlights) do - if not tbl or not tbl.hl then - utils.notify( - fmt("Error setting highlight group: no name for %s - %s", name, vim.inspect(tbl), utils.E) - ) + local msgs = {} + for name, opts in pairs(conf.highlights) do + if not opts or not opts.hl_group then + msgs[#msgs + 1] = fmt("* %s - %s", name, vim.inspect(opts)) else - M.set_one(tbl.hl, tbl) + M.set_one(opts.hl_group, opts) end end + if next(msgs) then + utils.notify(fmt("Error setting highlight group(s) for: \n", table.concat(msgs, "\n")), utils.E) + end end ----@param element Buffer | Tabpage ----@return table +---@param vis Visibility +---@param hls BufferlineHighlights +---@param name string +---@param base string? +---@return string +local function get_hl_group_for_state(vis, hls, name, base) + if not base then base = name end + local state = ({ [V.INACTIVE] = "visible", [V.SELECTED] = "selected" })[vis] + local hl_name = state and fmt("%s_%s", name, state) or base + if hls[hl_name].hl_group then return hls[hl_name].hl_group end + log.debug(fmt("%s highlight not found", name)) + return "" +end + +---@param element NvimBuffer | NvimTab +---@return table function M.for_element(element) local hl = {} - local h = config.get("highlights") - if not h then return hl end - --- TODO: find a tidier way to do this if possible - if element:current() then - hl.background = h.buffer_selected.hl - hl.modified = h.modified_selected.hl - hl.duplicate = h.duplicate_selected.hl - hl.pick = h.pick_selected.hl - hl.separator = h.separator_selected.hl - hl.buffer = h.buffer_selected - hl.diagnostic = h.diagnostic_selected.hl - hl.error = h.error_selected.hl - hl.error_diagnostic = h.error_diagnostic_selected.hl - hl.warning = h.warning_selected.hl - hl.warning_diagnostic = h.warning_diagnostic_selected.hl - hl.info = h.info_selected.hl - hl.info_diagnostic = h.info_diagnostic_selected.hl - hl.hint = h.hint_selected.hl - hl.hint_diagnostic = h.hint_diagnostic_selected.hl - hl.close_button = h.close_button_selected.hl - hl.numbers = h.numbers_selected.hl - elseif element:visible() then - hl.background = h.buffer_visible.hl - hl.modified = h.modified_visible.hl - hl.duplicate = h.duplicate_visible.hl - hl.pick = h.pick_visible.hl - hl.separator = h.separator_visible.hl - hl.buffer = h.buffer_visible - hl.diagnostic = h.diagnostic_visible.hl - hl.error = h.error_visible.hl - hl.error_diagnostic = h.error_diagnostic_visible.hl - hl.warning = h.warning_visible.hl - hl.warning_diagnostic = h.warning_diagnostic_visible.hl - hl.info = h.info_visible.hl - hl.info_diagnostic = h.info_diagnostic_visible.hl - hl.hint = h.hint_visible.hl - hl.hint_diagnostic = h.hint_diagnostic_visible.hl - hl.close_button = h.close_button_visible.hl - hl.numbers = h.numbers_visible.hl - else - hl.background = h.background.hl - hl.modified = h.modified.hl - hl.duplicate = h.duplicate.hl - hl.pick = h.pick.hl - hl.separator = h.separator.hl - hl.buffer = h.background - hl.diagnostic = h.diagnostic.hl - hl.error = h.error.hl - hl.error_diagnostic = h.error_diagnostic.hl - hl.warning = h.warning.hl - hl.warning_diagnostic = h.warning_diagnostic.hl - hl.info = h.info.hl - hl.info_diagnostic = h.info_diagnostic.hl - hl.hint = h.hint.hl - hl.hint_diagnostic = h.hint_diagnostic.hl - hl.close_button = h.close_button.hl - hl.numbers = h.numbers.hl - end - if element.group then groups.set_current_hl(element, h, hl) end + local function hl_group(name, fallback) + return get_hl_group_for_state(element:visibility(), config.highlights, name, fallback) + end + hl.modified = hl_group("modified") + hl.duplicate = hl_group("duplicate") + hl.pick = hl_group("pick") + hl.separator = hl_group("separator") + hl.diagnostic = hl_group("diagnostic") + hl.error = hl_group("error") + hl.error_diagnostic = hl_group("error_diagnostic") + hl.warning = hl_group("warning") + hl.warning_diagnostic = hl_group("warning_diagnostic") + hl.info = hl_group("info") + hl.info_diagnostic = hl_group("info_diagnostic") + hl.hint = hl_group("hint") + hl.hint_diagnostic = hl_group("hint_diagnostic") + hl.close_button = hl_group("close_button") + hl.numbers = hl_group("numbers") + hl.buffer = hl_group("buffer", "background") + hl.background = hl.buffer + + if element.group then + local group = groups.get_all()[element.group] + if group and group.name and group.highlight then hl[group.name] = hl_group(group.name) end + end return hl end diff --git a/lua/bufferline/models.lua b/lua/bufferline/models.lua index fff875d5..52623fb1 100644 --- a/lua/bufferline/models.lua +++ b/lua/bufferline/models.lua @@ -1,6 +1,8 @@ local lazy = require("bufferline.lazy") --- @module "bufferline.utils" local utils = lazy.require("bufferline.utils") +--- @module "bufferline.utils.log" +local log = lazy.require("bufferline.utils.log") --- @module "bufferline.constants" local constants = lazy.require("bufferline.constants") @@ -9,7 +11,6 @@ local M = {} local api = vim.api local fn = vim.fn local fmt = string.format -local log = utils.log local visibility = constants.visibility --[[ @@ -28,16 +29,21 @@ i.e. * this list is not exhaustive --]] +--- @alias Visibility 1 | 2 | 3 +--- @alias Duplicate "path" | "element" | nil + --- The base class that represents a visual tab in the tabline --- i.e. not necessarily representative of a vim tab or buffer ---@class Component ---@field name string? ---@field id integer +---@field path string? ---@field length integer ---@field component fun(BufferlineState): string ---@field hidden boolean ---@field focusable boolean ---@field type 'group_end' | 'group_start' | 'buffer' | 'tabpage' +---@field __ancestor fun(self: Component, depth: integer, formatter: (fun(string, integer): string)?): string local Component = {} ---@param field string @@ -46,6 +52,9 @@ local function not_implemented(field) error(fmt("%s is not implemented yet", field)) end +---@generic T +---@param t table +---@return T function Component:new(t) assert(t.type, "all components must have a type") self.length = t.length or 0 @@ -72,6 +81,20 @@ function Component:as_element() if vim.tbl_contains({ "buffer", "tab" }, self.type) then return self end end +---Find the directory prefix of an element up to a certain depth +---@param depth integer +---@param formatter (fun(path: string, depth: integer): string)? +---@return string +function Component:__ancestor(depth, formatter) + if self.type ~= "buffer" and self.type ~= "tab" then return "" end + local parts = vim.split(self.path, utils.path_sep, { trimempty = true }) + local index = (depth and depth > #parts) and 1 or (#parts - depth) + 1 + local dir = table.concat(parts, utils.path_sep, index, #parts - 1) .. utils.path_sep + if dir == "" then return "" end + if formatter then dir = formatter(dir, depth) end + return dir +end + local GroupView = Component:new({ type = "group", focusable = false }) function GroupView:new(group) @@ -85,9 +108,9 @@ end function GroupView:current() return false end ----@alias TabElement Tabpage|Buffer +---@alias TabElement NvimTab|NvimBuffer ----@class Tabpage +---@class NvimTab ---@field public id integer ---@field public buf integer ---@field public icon string @@ -96,9 +119,10 @@ function GroupView:current() return false end ---@field public letter string ---@field public modified boolean ---@field public modifiable boolean ----@field public duplicated boolean +---@field public duplicated Duplicate ---@field public extension string the file extension ---@field public path string the full path to the file +---@field __ancestor fun(self: Component, depth: integer, formatter: (fun(string, integer): string)?): string local Tabpage = Component:new({ type = "tab" }) function Tabpage:new(tab) @@ -122,10 +146,11 @@ function Tabpage:new(tab) return tab end +--- @return Visibility function Tabpage:visibility() - return self:current() and visibility.SELECTED - or self:visible() and visibility.INACTIVE - or visibility.NONE + if self:current() then return visibility.SELECTED end + if self:visible() then return visibility.INACTIVE end + return visibility.NONE end function Tabpage:current() return api.nvim_get_current_tabpage() == self.id end @@ -137,24 +162,15 @@ function Tabpage:visible() return api.nvim_get_current_tabpage() == self.id end --- @param formatter function(string, number) --- @returns string function Tabpage:ancestor(depth, formatter) - depth = (depth and depth > 1) and depth or 1 - local ancestor = "" - for index = 1, depth do - local modifier = string.rep(":h", index) - local dir = fn.fnamemodify(self.path, ":p" .. modifier .. ":t") - if dir == "" then break end - if formatter then dir = formatter(dir, depth) end - - ancestor = dir .. require("bufferline.utils").path_sep .. ancestor - end - return ancestor + if self.duplicated == "element" then return "(duplicated) " end + return self:__ancestor(depth, formatter) end ---@alias BufferComponent fun(index: integer, buf_count: integer): string -- A single buffer class -- this extends the [Component] class ----@class Buffer +---@class NvimBuffer ---@field public extension string the file extension ---@field public path string the full path to the file ---@field public name_formatter function? dictates how the name should be shown @@ -168,8 +184,8 @@ end ---@field public buftype string ---@field public letter string? ---@field public ordinal integer ----@field public duplicated boolean ----@field public prefix_count boolean +---@field public duplicated Duplicate +---@field public prefix_count integer ---@field public component BufferComponent ---@field public group string? ---@field public group_fn string @@ -177,15 +193,16 @@ end ---@field public visibility fun(): integer ---@field public current fun(): boolean ---@field public visible fun(): boolean +---@field private ancestor fun(self: NvimBuffer, formatter: fun(string): string, depth: integer): string +---@field private __ancestor fun(self: Component, depth: integer, formatter: (fun(string, integer): string)?): string ---@field public find_index fun(Buffer, BufferlineState): integer ---@field public is_new fun(Buffer, BufferlineState): boolean ---@field public is_existing fun(Buffer, BufferlineState): boolean ----@deprecated public filename string the visible name for the file local Buffer = Component:new({ type = "buffer" }) ---create a new buffer class ----@param buf Buffer ----@return Buffer +---@param buf NvimBuffer +---@return NvimBuffer function Buffer:new(buf) assert(buf, "A buffer must be passed to create a buffer class") buf.modifiable = vim.bo[buf.id].modifiable @@ -214,10 +231,11 @@ function Buffer:new(buf) return buf end +---@return Visibility function Buffer:visibility() - return self:current() and visibility.SELECTED - or self:visible() and visibility.INACTIVE - or visibility.NONE + if self:current() then return visibility.SELECTED end + if self:visible() then return visibility.INACTIVE end + return visibility.NONE end function Buffer:current() return api.nvim_get_current_buf() == self.id end @@ -227,7 +245,7 @@ function Buffer:current() return api.nvim_get_current_buf() == self.id end ---@param state BufferlineState ---@return boolean function Buffer:is_existing(state) - return utils.find(state.components, function(component) return component.id == self.id end) ~= nil + return utils.find(function(component) return component.id == self.id end, state.components) ~= nil end -- Find and return the index of the matching buffer (by id) in the list in state @@ -246,19 +264,7 @@ function Buffer:visible() return fn.bufwinnr(self.id) > 0 end --- @param depth integer --- @param formatter function(string, integer) --- @returns string -function Buffer:ancestor(depth, formatter) - depth = (depth and depth > 1) and depth or 1 - local ancestor = "" - for index = 1, depth do - local modifier = string.rep(":h", index) - local dir = fn.fnamemodify(self.path, ":p" .. modifier .. ":t") - if dir == "" then break end - if formatter then dir = formatter(dir, depth) end - - ancestor = dir .. require("bufferline.utils").path_sep .. ancestor - end - return ancestor -end +function Buffer:ancestor(depth, formatter) return self:__ancestor(depth, formatter) end ---@class Section ---@field items Component[] diff --git a/lua/bufferline/offset.lua b/lua/bufferline/offset.lua index c19c9625..5d5cae87 100644 --- a/lua/bufferline/offset.lua +++ b/lua/bufferline/offset.lua @@ -134,7 +134,7 @@ function M.get() local hl_name = offset.highlight or guess_window_highlight(win_id) - or config.highlights.fill.hl + or config.highlights.fill.hl_group local hl = require("bufferline.highlights").hl(hl_name) local component = get_section_text(width, hl, offset) diff --git a/lua/bufferline/pick.lua b/lua/bufferline/pick.lua index 3a21ea4a..4c6367dc 100644 --- a/lua/bufferline/pick.lua +++ b/lua/bufferline/pick.lua @@ -3,6 +3,8 @@ local lazy = require("bufferline.lazy") local state = lazy.require("bufferline.state") ---@module "bufferline.ui" local ui = lazy.require("bufferline.ui") +---@module "bufferline.config" +local config = lazy.require("bufferline.config") local M = {} @@ -33,7 +35,7 @@ function M.choose_then(func) ui.refresh() end ----@param element Tabpage|Buffer +---@param element NvimTab|NvimBuffer ---@return string? function M.get(element) local first_letter = element.name:sub(1, 1) @@ -56,13 +58,12 @@ end ---@return Segment? function M.component(ctx) local padding = require("bufferline.constants").padding - local options = require("bufferline.config").get("options") local element = ctx.tab local hl = ctx.current_highlights local letter = element.letter - if options.show_buffer_icons and element.icon then + if config.options.show_buffer_icons and element.icon then local right = string.rep(padding, math.ceil((strwidth(element.icon) - 1) / 2)) local left = string.rep(padding, math.floor((strwidth(element.icon) - 1) / 2)) letter = left .. element.letter .. right diff --git a/lua/bufferline/sorters.lua b/lua/bufferline/sorters.lua index 2180c780..ca347fc3 100644 --- a/lua/bufferline/sorters.lua +++ b/lua/bufferline/sorters.lua @@ -12,14 +12,14 @@ local function full_path(path) return fnamemodify(path, ":p") end -- @param path string local function is_relative_path(path) return full_path(path) ~= path end ---- @param buf_a Buffer ---- @param buf_b Buffer +--- @param buf_a NvimBuffer +--- @param buf_b NvimBuffer local function sort_by_extension(buf_a, buf_b) return fnamemodify(buf_a.name, ":e") < fnamemodify(buf_b.name, ":e") end ---- @param buf_a Buffer ---- @param buf_b Buffer +--- @param buf_a NvimBuffer +--- @param buf_b NvimBuffer local function sort_by_relative_directory(buf_a, buf_b) local ra = is_relative_path(buf_a.path) local rb = is_relative_path(buf_b.path) @@ -28,12 +28,12 @@ local function sort_by_relative_directory(buf_a, buf_b) return buf_a.path < buf_b.path end ---- @param buf_a Buffer ---- @param buf_b Buffer +--- @param buf_a NvimBuffer +--- @param buf_b NvimBuffer local function sort_by_directory(buf_a, buf_b) return full_path(buf_a.path) < full_path(buf_b.path) end ---- @param buf_a Buffer ---- @param buf_b Buffer +--- @param buf_a NvimBuffer +--- @param buf_b NvimBuffer local function sort_by_id(buf_a, buf_b) if not buf_a and buf_b then return true @@ -43,7 +43,7 @@ local function sort_by_id(buf_a, buf_b) return buf_a.id < buf_b.id end ---- @param buf Buffer +--- @param buf NvimBuffer local function init_buffer_tabnr(buf) local maxinteger = 1000000000 -- If the buffer is visible, then its initial value shouldn't be @@ -55,8 +55,8 @@ local function init_buffer_tabnr(buf) return maxinteger end ---- @param buf_a Buffer ---- @param buf_b Buffer +--- @param buf_a NvimBuffer +--- @param buf_b NvimBuffer local function sort_by_tabs(buf_a, buf_b) local buf_a_tabnr = init_buffer_tabnr(buf_a) local buf_b_tabnr = init_buffer_tabnr(buf_b) @@ -80,8 +80,8 @@ end --- @param state BufferlineState local sort_by_new_after_existing = function(state) - --- @param item_a Buffer - --- @param item_b Buffer + --- @param item_a NvimBuffer + --- @param item_b NvimBuffer return function(item_a, item_b) if item_a:is_new(state) and item_b:is_existing(state) then return false @@ -94,8 +94,8 @@ end --- @param state BufferlineState local sort_by_new_after_current = function(state) - --- @param item_a Buffer - --- @param item_b Buffer + --- @param item_a NvimBuffer + --- @param item_b NvimBuffer return function(item_a, item_b) local a_index = item_a:find_index(state) local a_is_new = item_a:is_new(state) diff --git a/lua/bufferline/tabpages.lua b/lua/bufferline/tabpages.lua index b66e1f61..1605b241 100644 --- a/lua/bufferline/tabpages.lua +++ b/lua/bufferline/tabpages.lua @@ -13,6 +13,8 @@ local duplicates = lazy.require("bufferline.duplicates") local diagnostics = lazy.require("bufferline.diagnostics") ---@module "bufferline.utils" local utils = lazy.require("bufferline.utils") +---@module "bufferline.models" +local models = lazy.require("bufferline.models") local api = vim.api @@ -24,8 +26,8 @@ local function tab_click_component(num) return "%" .. num .. "T" end local function render(tabpage, is_active, style, highlights) local h = highlights - local hl = is_active and h.tab_selected.hl or h.tab.hl - local separator_hl = is_active and h.separator_selected.hl or h.separator.hl + local hl = is_active and h.tab_selected.hl_group or h.tab.hl_group + local separator_hl = is_active and h.separator_selected.hl_group or h.separator.hl_group local separator_component = style == "thick" and "▐" or "▕" local name = padding .. padding .. tabpage.tabnr .. padding return { @@ -96,13 +98,13 @@ local function get_tab_buffers(tab_num) end ---@param state BufferlineState ----@return Tabpage[] +---@return NvimTab[] function M.get_components(state) local options = config.options local tabs = get_valid_tabs() - local Tabpage = require("bufferline.models").Tabpage - ---@type Tabpage[] + local Tabpage = models.Tabpage + ---@type NvimTab[] local components = {} pick.reset() duplicates.reset() diff --git a/lua/bufferline/ui.lua b/lua/bufferline/ui.lua index b25c6a94..647673ca 100644 --- a/lua/bufferline/ui.lua +++ b/lua/bufferline/ui.lua @@ -69,8 +69,8 @@ local function get_id(component) return component and component.attr and compone ---@class RenderContext ---@field preferences BufferlineConfig ----@field current_highlights table> ----@field tab Tabpage | Buffer +---@field current_highlights table +---@field tab NvimTab | NvimBuffer ---@field is_picking boolean ---@type RenderContext local Context = {} @@ -170,7 +170,7 @@ local function get_tab_close_button(options, hls) return { { text = padding .. options.close_icon .. padding, - highlight = hls.tab_close.hl, + highlight = hls.tab_close.hl_group, attr = { prefix = "%999X" }, }, } @@ -221,7 +221,7 @@ local function add_space(ctx, length) left_size = left_size + strwidth(icon) end return pad({ - left = { size = left_size, hl = curr_hl.buffer.hl }, + left = { size = left_size, hl = curr_hl.buffer }, right = { size = right_size }, }) end @@ -238,22 +238,22 @@ local function get_icon_with_highlight(buffer, color_icons, hl_defs) if not hl or hl == "" then return { text = icon } end local state = buffer:visibility() - local bg_hls = { - [visibility.INACTIVE] = hl_defs.buffer_visible.hl, - [visibility.SELECTED] = hl_defs.buffer_selected.hl, - [visibility.NONE] = hl_defs.background.hl, - } + local bg = ({ + [visibility.INACTIVE] = hl_defs.buffer_visible.hl_group, + [visibility.SELECTED] = hl_defs.buffer_selected.hl_group, + [visibility.NONE] = hl_defs.background.hl_group, + })[state] - local new_hl = highlights.generate_name(hl, { visibility = state }) + local new_hl = highlights.generate_name_for_state(hl, { visibility = state }) local hl_colors = { - guifg = not color_icons and "fg" or colors.get_color({ name = hl, attribute = "fg" }), - guibg = colors.get_color({ name = bg_hls[state], attribute = "bg" }), + fg = not color_icons and "fg" or colors.get_color({ name = hl, attribute = "fg" }), + bg = colors.get_color({ name = bg, attribute = "bg" }), ctermfg = not color_icons and "fg" or colors.get_color({ name = hl, attribute = "fg", cterm = true, }), - ctermbg = colors.get_color({ name = bg_hls[state], attribute = "bg", cterm = true }), + ctermbg = colors.get_color({ name = bg, attribute = "bg", cterm = true }), } highlights.set_one(new_hl, hl_colors) return { text = icon, highlight = new_hl, attr = { text = "%*" } } @@ -305,9 +305,9 @@ local function add_indicator(context) local is_current = element:current() symbol = is_current and options.indicator_icon or symbol - highlight = is_current and hl.indicator_selected.hl - or element:visible() and hl.indicator_visible.hl - or curr_hl.buffer.hl + highlight = is_current and hl.indicator_selected.hl_group + or element:visible() and hl.indicator_visible.hl_group + or curr_hl.buffer -- since all non-current buffers do not have an indicator they need -- to be padded to make up the difference in size @@ -355,7 +355,7 @@ local function add_separators(context) local style = options.separator_style local focused = context.tab:current() or context.tab:visible() local right_sep, left_sep = get_separator(focused, style) - local sep_hl = is_slant(style) and context.current_highlights.separator or hl.separator.hl + local sep_hl = is_slant(style) and context.current_highlights.separator or hl.separator.hl_group local left_separator = left_sep and { text = left_sep, highlight = sep_hl } or nil local right_separator = { text = right_sep, highlight = sep_hl } @@ -388,7 +388,7 @@ local function get_name(ctx) local name = utils.truncate_name(ctx.tab.name, max_length) -- escape filenames that contain "%" as this breaks in statusline patterns name = name:gsub("%%", "%%%1") - return { text = name, highlight = ctx.current_highlights.buffer.hl } + return { text = name, highlight = ctx.current_highlights.buffer } end ---Create the render function that components need to position their @@ -507,7 +507,7 @@ function M.element(state, element) spacing({ when = group_item }), set_id(duplicate_prefix, components.id.duplicates), set_id(name, components.id.name), - spacing({ when = name, highlight = curr_hl.buffer.hl }), + spacing({ when = name, highlight = curr_hl.buffer }), set_id(diagnostic, components.id.diagnostics), spacing({ when = diagnostic and #diagnostic.text > 0 }), right_space, @@ -585,9 +585,10 @@ end ---@param after Section ---@param available_width number ---@param marker table +---@param visible Component[] ---@return Segment[][] ---@return table ----@return Buffer[] +---@return NvimBuffer[] local function truncate(before, current, after, available_width, marker, visible) visible = visible or {} @@ -595,7 +596,6 @@ local function truncate(before, current, after, available_width, marker, visible local right_trunc_marker = get_marker_size(marker.right_count, marker.right_element_size) local markers_length = left_trunc_marker + right_trunc_marker - local total_length = before.length + current.length + after.length + markers_length if available_width >= total_length then @@ -646,7 +646,7 @@ end function M.tabline(items, tab_indicators) local options = config.options local hl = config.highlights - local right_align = { { highlight = hl.fill.hl, text = "%=" } } + local right_align = { { highlight = hl.fill.hl_group, text = "%=" } } local tab_close_button = get_tab_close_button(options, hl) local tab_close_button_length = get_component_size(tab_close_button) @@ -677,7 +677,7 @@ function M.tabline(items, tab_indicators) right_element_size = right_element_size, }) - local fill = hl.fill.hl + local fill = hl.fill.hl_group local left_marker = get_trunc_marker(left_trunc_icon, fill, fill, marker.left_count) local right_marker = get_trunc_marker(right_trunc_icon, fill, fill, marker.right_count) diff --git a/lua/bufferline/utils.lua b/lua/bufferline/utils/init.lua similarity index 87% rename from lua/bufferline/utils.lua rename to lua/bufferline/utils/init.lua index 704d98e6..477dafb1 100644 --- a/lua/bufferline/utils.lua +++ b/lua/bufferline/utils/init.lua @@ -7,9 +7,8 @@ local constants = lazy.require("bufferline.constants") --- @module "bufferline.config" local config = lazy.require("bufferline.config") -local M = { log = {} } +local M = {} -local fmt = string.format local fn = vim.fn local api = vim.api @@ -18,35 +17,22 @@ function M.is_test() return __TEST end ----@return boolean -local function check_logging() return config.options.debug.logging end - ----@param msg string -function M.log.debug(msg) - if check_logging() then - local info = debug.getinfo(2, "S") - vim.schedule( - function() - M.notify( - fmt("[bufferline]: %s\n%s:%s", msg, info.linedefined, info.short_src), - M.D, - { once = true } - ) - end - ) - end -end - ---Takes a list of items and runs the callback ---on each updating the initial value ---@generic T ---@param accum T ----@param callback fun(accum:T, item: T, index: number): T ----@param list T[] +---@param callback fun(accum:T, item: T, key: number|string): T +---@param list table ---@return T +---@overload fun(callback: fun(accum: any, item: any, key: (number|string)), list: any[]): any function M.fold(accum, callback, list) assert(accum and callback, "An initial value and callback must be passed to fold") - for i, v in ipairs(list) do + if type(accum) == "function" and type(callback) == "table" then + list = callback + callback = accum + accum = {} + end + for i, v in pairs(list) do accum = callback(accum, v, i) end return accum @@ -75,17 +61,18 @@ end ---@param list T[] ---@return T[] function M.map(callback, list) - return M.fold({}, function(accum, item, index) - table.insert(accum, callback(item, index)) - return accum - end, list) + local accum = {} + for index, item in ipairs(list) do + accum[index] = callback(item, index) + end + return accum end ---@generic T ---@param list T[] ---@param callback fun(item: T): boolean ---@return T? -function M.find(list, callback) +function M.find(callback, list) for _, v in ipairs(list) do if callback(v) then return v end end @@ -98,7 +85,7 @@ end -- https://stackoverflow.com/questions/1410862/concatenation-of-tables-in-lua --- @generic T --- @vararg T ---- @return T[] +--- @return T function M.merge_lists(...) local t = {} for n = 1, select("#", ...) do @@ -119,7 +106,7 @@ end ---@param list T[] ---@param callback fun(item: `T`) ---@param matcher (fun(item: `T`):boolean)? -function M.for_each(list, callback, matcher) +function M.for_each(callback, list, matcher) for _, item in ipairs(list) do if not matcher or matcher(item) then callback(item) end end @@ -171,8 +158,8 @@ M.D = vim.log.levels.DEBUG function M.notify(msg, level, opts) opts = opts or {} local nopts = { title = "Bufferline" } - if opts.once then return vim.notify_once(msg, level, nopts) end - vim.notify(msg, level, nopts) + if opts.once then return vim.schedule(function() vim.notify_once(msg, level, nopts) end) end + vim.schedule(function() vim.notify(msg, level, nopts) end) end ---@class GetIconOpts diff --git a/lua/bufferline/utils/log.lua b/lua/bufferline/utils/log.lua new file mode 100644 index 00000000..b1f9cd82 --- /dev/null +++ b/lua/bufferline/utils/log.lua @@ -0,0 +1,22 @@ +local lazy = require("bufferline.lazy") +--- @module "bufferline.config" +local config = lazy.require("bufferline.config") +--- @module "bufferline.config" +local utils = lazy.require("bufferline.utils") + +local M = {} + +local fmt = string.format + +---@return boolean +local function check_logging() return config.options.debug.logging end + +---@param msg string +function M.debug(msg) + if check_logging() then + local info = debug.getinfo(2, "S") + utils.notify(fmt("%s\n%s:%s", msg, info.linedefined, info.short_src), "debug", { once = true }) + end +end + +return M diff --git a/tests/config_spec.lua b/tests/config_spec.lua index b1e10bbc..1cee199e 100644 --- a/tests/config_spec.lua +++ b/tests/config_spec.lua @@ -29,15 +29,15 @@ describe("Config tests", function() }) local under_test = config.apply() - assert.equal(under_test.highlights.fill.guifg, "red") - assert.equal(under_test.highlights.fill.hl, "BufferLineFill") + assert.equal(under_test.highlights.fill.fg, "red") + assert.equal(under_test.highlights.fill.hl_group, "BufferLineFill") end) it("should derive colors from the existing highlights", function() vim.cmd(fmt("hi Comment guifg=%s", whitesmoke)) config.set({}) local under_test = config.apply() - assert.equal(under_test.highlights.info.guifg, whitesmoke:lower()) + assert.equal(whitesmoke:lower(), under_test.highlights.info.fg) end) it("should update highlights on colorscheme change", function() @@ -50,7 +50,7 @@ describe("Config tests", function() }) local conf = config.apply() conf = config.update_highlights() - assert.is_equal(conf.highlights.buffer_selected.guifg, "red") + assert.is_equal(conf.highlights.buffer_selected.fg, "red") end) end) end) diff --git a/tests/duplicates_spec.lua b/tests/duplicates_spec.lua new file mode 100644 index 00000000..70dbdc20 --- /dev/null +++ b/tests/duplicates_spec.lua @@ -0,0 +1,149 @@ +local Tabpage = require("bufferline.models").Tabpage + +describe("Duplicate Tests - ", function() + local duplicates + local config + + before_each(function() + package.loaded["bufferline.duplicates"] = nil + duplicates = require("bufferline.duplicates") + config = require("bufferline.config") + end) + + it("should mark duplicate files", function() + local result = duplicates.mark({ + { + path = "/test/dir_a/dir_b/file.txt", + name = "file.txt", + ordinal = 1, + }, + { + path = "/test/dir_a/dir_c/file.txt", + name = "file.txt", + ordinal = 2, + }, + { + path = "/test/dir_a/result.txt", + name = "result.txt", + ordinal = 3, + }, + }) + assert.is_equal(result[1].duplicated, "path") + assert.is_equal(result[2].duplicated, "path") + assert.falsy(result[3].duplicated) + assert.is_equal(#result, 3) + end) + + it("should return the correct prefix count", function() + local result = duplicates.mark({ + { + path = "/test/dir_a/dir_b/file.txt", + name = "file.txt", + ordinal = 1, + }, + { + path = "/test/dir_a/dir_c/file.txt", + name = "file.txt", + ordinal = 2, + }, + { + path = "/test/dir_a/result.txt", + name = "result.txt", + ordinal = 3, + }, + }) + assert.equal(result[1].prefix_count, 2) + assert.equal(result[2].prefix_count, 2) + assert.falsy(result[3].prefix_count) + end) + + it("should indicate if a buffer is exactly the same as another", function() + local result = duplicates.mark({ + { + path = "/test/dir_a/dir_b/file.txt", + name = "file.txt", + ordinal = 1, + }, + { + path = "/test/dir_a/dir_b/file.txt", + name = "file.txt", + ordinal = 2, + }, + { + path = "/test/dir_a/result.txt", + name = "result.txt", + ordinal = 3, + }, + }) + assert.equal(result[1].duplicated, "element") + assert.equal(result[2].duplicated, "element") + assert.falsy(result[3].prefix_count) + end) + + it("should return a prefixed element if duplicated", function() + config.set({ options = { enforce_regular_tabs = false } }) + config.apply() + + local component = duplicates.component({ + current_highlights = { duplicate = "TestHighlight" }, + tab = Tabpage:new({ + path = "very_long_directory_name/test/dir_a/result.txt", + buf = 1, + buffers = { 1 }, + id = 1, + ordinal = 1, + diagnostics = {}, + hidden = false, + focusable = true, + duplicated = true, + prefix_count = 2, + }), + }) + + assert.truthy(component.text) + assert.is_equal(component.text, "dir_a/") + + component = duplicates.component({ + current_highlights = { duplicate = "TestHighlight" }, + tab = Tabpage:new({ + path = "very_long_directory_name/test/dir_a/result.txt", + buf = 1, + buffers = { 1 }, + id = 1, + ordinal = 1, + diagnostics = {}, + hidden = false, + focusable = true, + duplicated = true, + prefix_count = 3, + }), + }) + + assert.truthy(component.text) + assert.is_equal(component.text, "test/dir_a/") + end) + + it("should truncate a very long directory name", function() + config.set({ options = { enforce_regular_tabs = false, max_prefix_length = 10 } }) + config.apply() + + local component = duplicates.component({ + current_highlights = { duplicate = "TestHighlight" }, + tab = Tabpage:new({ + path = "very_long_directory_name/dir_a/result.txt", + buf = 1, + buffers = { 1 }, + id = 1, + ordinal = 1, + diagnostics = {}, + hidden = false, + focusable = true, + duplicated = true, + prefix_count = 3, + }), + }) + + assert.is_true(vim.api.nvim_strwidth(component.text) <= 10) + assert.is_equal(component.text, "ver…/dir…/") + end) +end) diff --git a/tests/groups_spec.lua b/tests/groups_spec.lua index 1aeea857..f54dfd16 100644 --- a/tests/groups_spec.lua +++ b/tests/groups_spec.lua @@ -9,6 +9,8 @@ describe("Group tests - ", function() local groups --- @module "bufferline.state" local state + --- @module "bufferline.config" + local config --- @module "bufferline" local bufferline @@ -16,9 +18,11 @@ describe("Group tests - ", function() package.loaded["bufferline"] = nil package.loaded["bufferline.groups"] = nil package.loaded["bufferline.state"] = nil + package.loaded["bufferline.config"] = nil groups = require("bufferline.groups") bufferline = require("bufferline") state = require("bufferline.state") + config = require("bufferline.config") end) local function set_buf_group(buffer) @@ -60,7 +64,7 @@ describe("Group tests - ", function() end) it("should set highlights on setup", function() - local config = { + local c = { highlights = { buffer_selected = { guifg = "black", @@ -91,12 +95,15 @@ describe("Group tests - ", function() }, }, } - groups.setup(config) - assert.truthy(config.highlights.test_group_selected) - assert.truthy(config.highlights.test_group_visible) - assert.truthy(config.highlights.test_group) + groups.setup(c) + config.set(c) + local conf = config.apply() + local hls = conf.highlights + assert.truthy(hls.test_group_selected) + assert.truthy(hls.test_group_visible) + assert.truthy(hls.test_group) - assert.equal(config.highlights.test_group.guifg, "red") + assert.equal(hls.test_group.fg, "red") end) it("should sort components by groups", function() @@ -126,7 +133,7 @@ describe("Group tests - ", function() end) it("should add group markers", function() - local config = { + local conf = { highlights = {}, options = { groups = { @@ -139,7 +146,7 @@ describe("Group tests - ", function() }, }, } - bufferline.setup(config) + bufferline.setup(conf) local components = { Buffer:new({ name = "dummy-1.txt" }), Buffer:new({ name = "dummy-2.txt" }), @@ -157,7 +164,7 @@ describe("Group tests - ", function() end) it("should sort each group individually", function() - local config = { + local conf = { highlights = {}, options = { groups = { @@ -178,7 +185,7 @@ describe("Group tests - ", function() }, }, } - bufferline.setup(config) + bufferline.setup(conf) local components = { Buffer:new({ name = "a.txt" }), Buffer:new({ name = "b.txt" }),