diff --git a/doc/bufferline.txt b/doc/bufferline.txt index d498cc14..bc2b3ecf 100644 --- a/doc/bufferline.txt +++ b/doc/bufferline.txt @@ -75,7 +75,7 @@ The available configuration are: right_mouse_command = "bdelete! %d", -- can be a string | function | false, see "Mouse actions" left_mouse_command = "buffer %d", -- can be a string | function, | false see "Mouse actions" middle_mouse_command = nil, -- can be a string | function, | false see "Mouse actions" - indicator = { + indicator = { -- marker for the current active tab icon = '▎', -- this should be omitted if indicator style is not 'icon' style = 'icon' | 'underline' | 'none', }, @@ -157,7 +157,7 @@ The available configuration are: -- can also be a table containing 2 custom separators -- [focused and unfocused]. eg: { '|', '|' } separator_style = "slant" | "slope" | "thick" | "thin" | { 'any', 'any' }, - enforce_regular_tabs = false | true, + enforce_regular_tabs = false | true, -- Whether to enforce all tabs having uniform width or variable width always_show_bufferline = true | false, auto_toggle_bufferline = true | false, hover = { @@ -353,11 +353,17 @@ In order to group buffers specify a list of groups in your config e.g. groups = { options = { toggle_hidden_on_enter = true -- when you re-enter a hidden group this options re-opens that group so the buffer is visible + -- position separator at the end/start/both sides for buffers + separator_position = "both", -- start | end | both + separator_style = "thin" -- thin | thick | {leftIcon,rightIcon} }, items = { { name = "Tests", -- Mandatory highlight = {underline = true, sp = "blue"}, -- Optional + -- Additional Highlights can be configured by setting the hl groups + -- BufferLineTestLabel, BufferLineTest, BufferLineTestSelected, BufferLineTestVisible + priority = 2, -- determines where it will appear relative to other groups (Optional) icon = "", -- Optional matcher = function(buf) -- Mandatory @@ -595,7 +601,7 @@ for left or right aligned sidebar windows such as `NvimTree`, `NERDTree` or { filetype = "NvimTree", text = "File Explorer", - highlight = "Directory", + highlight = "Directory", -- Highlight group applied for the Explorer separator = true -- use a "true" to enable the default, or set your own character } } diff --git a/doc/tags b/doc/tags new file mode 100644 index 00000000..329a7871 --- /dev/null +++ b/doc/tags @@ -0,0 +1,36 @@ +NOT bufferline.txt /*NOT* +NOTE: bufferline.txt /*NOTE:* +bufferline bufferline.txt /*bufferline* +bufferline-colorscheme-development bufferline.txt /*bufferline-colorscheme-development* +bufferline-commands bufferline.txt /*bufferline-commands* +bufferline-configuration bufferline.txt /*bufferline-configuration* +bufferline-contents bufferline.txt /*bufferline-contents* +bufferline-custom-areas bufferline.txt /*bufferline-custom-areas* +bufferline-diagnostics bufferline.txt /*bufferline-diagnostics* +bufferline-filtering bufferline.txt /*bufferline-filtering* +bufferline-functions bufferline.txt /*bufferline-functions* +bufferline-group-commands bufferline.txt /*bufferline-group-commands* +bufferline-groups bufferline.txt /*bufferline-groups* +bufferline-highlights bufferline.txt /*bufferline-highlights* +bufferline-hover-events bufferline.txt /*bufferline-hover-events* +bufferline-introduction bufferline.txt /*bufferline-introduction* +bufferline-issues bufferline.txt /*bufferline-issues* +bufferline-mappings bufferline.txt /*bufferline-mappings* +bufferline-mouse-actions bufferline.txt /*bufferline-mouse-actions* +bufferline-numbers bufferline.txt /*bufferline-numbers* +bufferline-offset bufferline.txt /*bufferline-offset* +bufferline-ordering-groups bufferline.txt /*bufferline-ordering-groups* +bufferline-pick bufferline.txt /*bufferline-pick* +bufferline-pinning bufferline.txt /*bufferline-pinning* +bufferline-regular-tabs bufferline.txt /*bufferline-regular-tabs* +bufferline-sorting bufferline.txt /*bufferline-sorting* +bufferline-style-presets bufferline.txt /*bufferline-style-presets* +bufferline-styling bufferline.txt /*bufferline-styling* +bufferline-tabpages bufferline.txt /*bufferline-tabpages* +bufferline-usage bufferline.txt /*bufferline-usage* +bufferline-working-with-elements bufferline.txt /*bufferline-working-with-elements* +bufferline.nvim bufferline.txt /*bufferline.nvim* +optional bufferline.txt /*optional* +strongly bufferline.txt /*strongly* +vertical bufferline.txt /*vertical* +your bufferline.txt /*your* diff --git a/lua/bufferline.lua b/lua/bufferline.lua index 60bce661..3de00b99 100644 --- a/lua/bufferline.lua +++ b/lua/bufferline.lua @@ -11,6 +11,8 @@ local tabpages = lazy.require("bufferline.tabpages") ---@module "bufferline.tabp local highlights = lazy.require("bufferline.highlights") ---@module "bufferline.highlights" local hover = lazy.require("bufferline.hover") ---@module "bufferline.hover" +local get_tabline_text_and_highlights = require("bufferline.pr").get_tabline_text_and_highlights + -- @v:lua@ in the tabline only supports global functions, so this is -- the only way to add click handlers without autoloaded vimscript functions _G.___bufferline_private = _G.___bufferline_private or {} -- to guard against reloads @@ -50,6 +52,8 @@ local M = { } -----------------------------------------------------------------------------// +local capture_next_render = false + --- @return string, bufferline.Segment[][] local function bufferline() local is_tabline = config:is_tabline() @@ -83,6 +87,13 @@ local function bufferline() left_offset_size = tabline.left_offset_size, right_offset_size = tabline.right_offset_size, }) + + -- :BufferLineDebug -> prints the rendered tabline from the BufferLineDebug command + if capture_next_render then + print("Current Tabline and Highlights:\n" .. vim.inspect(get_tabline_text_and_highlights(tabline.str))) + capture_next_render = false + end + return tabline.str, tabline.segments end @@ -179,6 +190,22 @@ local function setup_commands() nargs = 1, complete = groups.complete, }) + --- New commands + --- 1. Print the current rendered tabline so users can easily find which Highlight group to change + command("BufferLineDebug", function() + debug_tabline = true + ui.refresh() + end, { nargs = 0 }) + -- 2. Remove Command to remove a single buffer from a Group instead of only allowing buffer naming to control groups + -- Moves the buf from the group to the ungrouped group + command("BufferLineRemove", function(opts) + local args = vim.split(opts.args, " ") + local buffer_id, group_id = tonumber(args[1]), args[2] + if buffer_id and group_id then + groups.remove_buf_from_group(buffer_id, group_id) + ui.refresh() + end + end, { nargs = "+", complete = function(ArgLead, CmdLine, CursorPos) return {} end }) end local function setup_diagnostic_handler(preferences) diff --git a/lua/bufferline/commands.lua b/lua/bufferline/commands.lua index 55a07152..a94b236b 100644 --- a/lua/bufferline/commands.lua +++ b/lua/bufferline/commands.lua @@ -198,7 +198,7 @@ function M.cycle(direction) if direction > 0 then vim.cmd("bnext") end if direction < 0 then vim.cmd("bprev") end end - local index = M.get_current_element_index(state) + local index = M.get_current_element_index(state) or groups.toggled_index() if not index then return end local length = #state.components local next_index = index + direction diff --git a/lua/bufferline/config.lua b/lua/bufferline/config.lua index e0def13d..8afd54f8 100644 --- a/lua/bufferline/config.lua +++ b/lua/bufferline/config.lua @@ -671,7 +671,10 @@ local function get_defaults() diagnostics_update_in_insert = true, diagnostics_update_on_event = true, offsets = {}, - groups = { items = {}, options = { toggle_hidden_on_enter = true } }, + groups = { + items = {}, + options = { toggle_hidden_on_enter = true, separator_position = "both", separator_style = "thin" }, + }, hover = { enabled = false, reveal = {}, delay = 200 }, debug = { logging = false }, } diff --git a/lua/bufferline/groups.lua b/lua/bufferline/groups.lua index a1aefc25..dbb1bd17 100644 --- a/lua/bufferline/groups.lua +++ b/lua/bufferline/groups.lua @@ -9,6 +9,19 @@ local C = lazy.require("bufferline.constants") ---@module "bufferline.constants" local fn = vim.fn +-- contains info about the changes +-- local pr = require('bufferline.pr').get_instance() + +--- @Group PR +-- 1. Made type on_close optional in bufferline.Group type [types.lua:193] +-- 2. in Group Setup - added group specific separator options - such as placing the sep at start/end [config.lua:678] +-- 3. Added functionality to Add/Remove groups on their own, for flexibility [groups:286] +-- 4. Fixed the render() function and kept the old one for reference [groups:913] +-- 5. Added a BufferLineDebug user command to print the rendered tabline with HL's and Text + Padding [bufferline:207] +-- 6. Updated doc to provide more info on how to use options [doc/bufferline.txt] +-- 7. Added a set_bufferline_hls function for the user to directly specify all the styles required for Group Labels and Buffers in one go [pr:38] +-- 8. Fixed the error of BufferLineCyclePrev/Next not working when current buffer is toggled and in a group [commands.lua:204 and groups:67] + ---------------------------------------------------------------------------------------------------- -- CONSTANTS ---------------------------------------------------------------------------------------------------- @@ -35,9 +48,17 @@ local function format_name(name) return name:gsub("[^%w]+", "_") end ---------------------------------------------------------------------------------------------------- -- SEPARATORS ---------------------------------------------------------------------------------------------------- + +-- initialized in setup() by reading config +local group_sep_position = "both" -- "start" , "end" , "both" +local group_sep_left, group_sep_right = "▎", "▎" -- thin/thick/custom + local separator = {} -local function space_end(hl_groups) return { { highlight = hl_groups.fill.hl_group, text = C.padding } } end +------------------------------------------------------------ +--- @Change : Fixed logic for Group Tabs + +function M.set_group_hls(group_name, opts) require("bufferline.pr").set_group_hls(group_name, opts) end ---@param group bufferline.Group, ---@param hls table> @@ -50,14 +71,95 @@ function separator.pill(group, hls, count) 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 start_group_sep = group_sep_left + local end_group_sep = group_sep_right + + if group_sep_position == "start" then + end_group_sep = "" + elseif group_sep_position == "end" then + start_group_sep = "" + end + local end_tab_sep = { highlight = sep_hl, text = end_group_sep } + local indicator = { - { text = C.padding, highlight = bg_hl }, + { text = start_group_sep, highlight = bg_hl }, { text = left, highlight = sep_hl }, { text = display_name .. count, highlight = label_hl }, { text = right, highlight = sep_hl }, { text = C.padding, highlight = bg_hl }, } - return { sep_start = indicator, sep_end = space_end(hls) } + + -- old pill creation + -- local indicator = { -- old + -- { text = C.padding, highlight = bg_hl }, + -- { text = left, highlight = sep_hl }, + -- { text = display_name .. count, highlight = label_hl }, + -- { text = right, highlight = sep_hl }, + -- { text = C.padding, highlight = bg_hl }, + -- } + -- return { sep_start = indicator, sep_end = space_end(hls) } + + return { sep_start = indicator, sep_end = { end_tab_sep } } +end + +--- Util function to get label sep highlight groups +--- @param group bufferline.Group, +--- @param hls table> +--- @return string +--- @return string +local function get_label_sep_hls(group, hls) + local name = group.name + local label_grp = hls[fmt("%s_label", name)] + local label_hl = label_grp and label_grp.hl_group or hls.group_label.hl_group + local sep_grp = hls[fmt("%s_separator", name)] + local sep_hl = sep_grp and sep_grp.hl_group or hls.group_separator.hl_group + return label_hl, sep_hl +end + +--- @param group bufferline.Group, +--- @param count string +--- @return string +local function get_tab_label(group, count) + local group_name = group.display_name or group.name + local count_text = count and #count > 0 and " " .. count or "" + return fmt(" %s%s ", group_name, count_text) +end + +--- @param hl string +--- @param text string +--- @return bufferline.Segment +local function create_style(hl, text) return { highlight = hl, text = text } end + +--- Creates the tab for the group +---@param group bufferline.Group, +---@param hls table> +---@param count string +---@param group_sep_pos string +---@param groupsep_left string +---@param groupsep_right string +---@return bufferline.Separators +local function create_tab(group, hls, count, group_sep_pos, groupsep_left, groupsep_right) + local hl = hls.fill.hl_group + local label_hl, sep_hl = get_label_sep_hls(group, hls) + + local tab_label_text = get_tab_label(group, count) + + local tab_left, tab_label = create_style(hl, ""), create_style(label_hl, tab_label_text) + + local start_group_sep, end_group_sep = groupsep_left, groupsep_right + + if group_sep_pos == "start" then + end_group_sep = "" + elseif group_sep_pos == "end" then + start_group_sep = "" + end + + local start_tab_sep, end_tab_sep = create_style(sep_hl, start_group_sep), create_style(sep_hl, end_group_sep) + + local indicator = { tab_left, tab_label, start_tab_sep } + + return { sep_start = indicator, sep_end = { end_tab_sep } } end ---@param group bufferline.Group, @@ -66,16 +168,28 @@ end ---@return bufferline.Separators ---@type GroupSeparator function separator.tab(group, hls, count) - local hl = hls.fill.hl_group - local indicator_hl = hls.buffer.hl_group - local indicator = { - { highlight = hl, text = C.padding }, - { highlight = indicator_hl, text = C.padding .. group.name .. count .. C.padding }, - { highlight = hl, text = C.padding }, - } - return { sep_start = indicator, sep_end = space_end(hls) } + local tab_gen = create_tab(group, hls, count, group_sep_position, group_sep_left, group_sep_right) + return tab_gen end +--[[ Old Separator creation - +---@param group bufferline.Group, +---@param hls table> +---@param count string +---@return bufferline.Separators +---@type GroupSeparator +function separator.tab_old(group, hls, count) + local hl = hls.fill.hl_group + local indicator_hl = hls.buffer.hl_group + local indicator = { + { highlight = hl, text = C.padding }, + { highlight = indicator_hl, text = C.padding .. group.name .. count .. C.padding }, + { highlight = hl, text = C.padding }, + } + return { sep_start = indicator, sep_end = space_end(hls) } +end +--]] + ---@type GroupSeparator function separator.none() return { sep_start = {}, sep_end = {} } end @@ -91,6 +205,7 @@ local Group = {} ---@param index number? ---@return bufferline.Group function Group:new(o, index) + --------- o = o or { priority = index } self.__index = self local name = format_name(o.name) @@ -98,8 +213,9 @@ function Group:new(o, index) id = o.id or name, hidden = o.hidden == nil and false or o.hidden, name = name, - display_name = o.name, - priority = o.priority or index, + -- display_name = o.name, priority to display name here + display_name = o.display_name or o.name, + priority = o.priority or index, -- ungrouped has no priority set , pinned has it by default }) return setmetatable(o, self) end @@ -133,6 +249,8 @@ builtin.pinned = Group:new({ -- STATE ---------------------------------------------------------------------------------------------------- +-- user_groups = Map[group_id | group_name, group] + --- @type bufferline.GroupState local group_state = { -- A table of buffers mapped to a specific group by a user @@ -145,6 +263,24 @@ local group_state = { components_by_group = {}, } +-- Maintain count of toggled groups - to fix cycling not working if the user toggles an active tab +local toggled_groups = 0 + +--- This solves the issue of cycling when the user is on a toggled group +--- We keep a count of how many groups are currently toggled off (minimized) +--- thus we can get the next index by passing the count of toggled groups as the index (1 for example) +function M.toggled_index() + if toggled_groups >= 1 then return toggled_groups end +end + +--- @param count 1 | -1 +local function update_toggled(count) + assert(count == 1 or count == -1, "count must be either 1 or -1") + assert(toggled_groups + count <= vim.tbl_count(group_state.user_groups), "count cannot exceed number of user groups") + assert(toggled_groups + count >= 0, "count cannot be lower than 0 " .. toggled_groups) + toggled_groups = toggled_groups + count +end + --- Store a list of pinned buffer as a string of ids e.g. "12,10,5" --- in a vim.g global variable that can be persisted across vim sessions local function persist_pinned_buffers() @@ -164,6 +300,40 @@ end ---@return string local function get_manual_group(element) return group_state.manual_groupings[element.id] end +-------------------------------- +--- @Remove Group feature added, simply moves a buffer from the group to the ungrouped group + +--- @param buffer_id integer +local function move_buffer_to_ungrouped(buffer_id, from_group_id) + local ungrouped_index + local buffer_to_move + for i, group in ipairs(group_state.components_by_group) do + if group.id == from_group_id then + for j, buf in ipairs(group) do + if buf.id == buffer_id then + buffer_to_move = table.remove(group, j) + break + end + end + elseif group.id == "ungrouped" then + ungrouped_index = i + end + end + if buffer_to_move and ungrouped_index then + table.insert(group_state.components_by_group[ungrouped_index], buffer_to_move) + end + + -- Update manual_groupings if it exists + if group_state.manual_groupings then group_state.manual_groupings[buffer_id] = nil end +end + +--- Remove a buffer from the group - public function +--- @param buf_id integer +--- @param group_id string +function M.remove_buf_from_group(buf_id, group_id) move_buffer_to_ungrouped(buf_id, group_id) end + +-------------------------------- + --- Wrapper to abstract interacting directly with manual groups as the access mechanism -- can vary i.e. buffer id or path and this should be changed in a centralised way. ---@param id number @@ -211,6 +381,10 @@ end ---@return bufferline.Group function M.get_by_id(id) return group_state.user_groups[id] end +--- Create an empty list of size n to store sorted buffers for each Group +--- This will store the buffers for the groups according to priority +--- @param size integer +--- @return table local function generate_sublists(size) local list = {} for i = 1, size do @@ -219,6 +393,7 @@ local function generate_sublists(size) return list end +--- Converts the component to a Segment with group styles applied ---Add group styling to the buffer component ---@param ctx bufferline.RenderContext ---@return bufferline.Segment? @@ -232,11 +407,7 @@ function M.component(ctx) 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 - return { - text = group.icon, - highlight = hl, - attr = { extends = extends }, - } + return { text = group.icon, highlight = hl, attr = { extends = extends } } end --- Pull pinned buffers saved in a vim.g global variable and restore them @@ -255,23 +426,47 @@ local function restore_pinned_buffers() ui.refresh() end +--- Initialize the group sep settings from the config +---@param conf bufferline.UserConfig +local function initialize_group_separators(conf) + local group_sep_type = vim.tbl_get(conf, "options", "groups", "options", "separator_position") or "" + if group_sep_type then group_sep_position = group_sep_type end + + local separator_style = vim.tbl_get(conf, "options", "groups", "options", "separator_style") or "none" + + if separator_style == "thick" then + group_sep_left = "▎" + group_sep_right = "▎" + elseif separator_style == "thin" then + group_sep_left = "▏" + group_sep_right = "▏" + elseif type(separator_style) == "table" and #separator_style == 2 then + group_sep_left = separator_style[1] + group_sep_right = separator_style[2] + end +end + --- NOTE: this function mutates the user's configuration by adding group highlights to the user highlights table. ---- ---@param conf bufferline.UserConfig function M.setup(conf) if not conf then return end local groups = vim.tbl_get(conf, "options", "groups", "items") or {} ---@type bufferline.Group[] - -- 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 -- otherwise we want to shift the priorities of their groups by 1 to accommodate the pinned group local has_set_pinned = not vim.tbl_isempty(vim.tbl_filter(function(group) return group.id == PINNED_ID end, groups)) + -- new: check if the user wants the sep to appear before the group bufs or after or both + initialize_group_separators(conf) + + -- if pinned is set - pinned priority is always 1 + -- hence group priorities start from 1..n , n <- number of groups defined for index, current in ipairs(groups) do local priority = has_set_pinned and index or index + 1 local group = Group:new(current, priority) group_state.user_groups[group.id] = group end + -- We only set the builtin groups after we know what the user has configured if not group_state.user_groups[PINNED_ID] then group_state.user_groups[PINNED_ID] = builtin.pinned end if not group_state.user_groups[UNGROUPED_ID] then @@ -288,9 +483,7 @@ end ---@param callback fun(b: bufferline.Buffer) local function command(group_name, callback) local group = utils.find(function(list) return list.name == group_name end, group_state.components_by_group) - if not group then return end - utils.for_each(callback, group) end @@ -344,11 +537,19 @@ function M.set_hidden(id, value) if group then group.hidden = value end end +--- Keeping track of minimized buffer groups solves the BufferLineCycle bug when active tab is minimized ---@param priority number? ---@param name string? function M.toggle_hidden(priority, name) local group = priority and group_by_priority(priority) or group_by_name(name) - if group then group.hidden = not group.hidden end + if group then + if not group.hidden then + update_toggled(1) + else + update_toggled(-1) + end + group.hidden = not group.hidden + end end ---Get the names for all bufferline groups @@ -377,14 +578,20 @@ function M.complete(arg_lead, cmd_line, cursor_pos) return names() end ---@return bufferline.Separators local function create_indicator(group, hls, count) hls = hls or {} + local count_item = group.hidden and fmt("(%s)", count) or "" local seps = group.separator.style(group, hls, count_item) + if seps.sep_start then table.insert(seps.sep_start, ui.make_clickable("handle_group_click", group.priority, { attr = { global = true } })) end + return seps end +--- Once sorting is done and we have the components and clustered by group +--- Create the start/end visual indicators for each group + ---Create the visual indicators bookending buffer groups ---@param group_id string ---@param components bufferline.Component[] @@ -432,11 +639,14 @@ end local function sort_by_groups(components) local sorted = {} local clustered = generate_sublists(vim.tbl_count(group_state.user_groups)) + -- for each buffer - get it's group id for index, tab in ipairs(components) do local buf = tab:as_element() if buf then + -- get sublist the buf is supposed to be in local group = group_state.user_groups[buf.group] local sublist = clustered[group.priority] + if not sublist.name then sublist.id = group.id sublist.name = group.name @@ -448,6 +658,8 @@ local function sort_by_groups(components) table.insert(sorted, buf) end end + + -- sorted is all components ordered return sorted, clustered end @@ -462,7 +674,6 @@ function M.action(name, action) if action == "close" then command(name, function(b) api.nvim_buf_delete(b.id, { force = true }) end) ui.refresh() - if name == PINNED_NAME then vim.g[PINNED_KEY] = {} end for buf, group_id in pairs(group_state.manual_groupings) do if group_id == name then group_state.manual_groupings[buf] = nil end @@ -500,31 +711,124 @@ function M.handle_group_enter() end, state.components) end +--- @class UserGroup +--- @field id string +--- @field name string +--- @field priority integer +--- @field hidden boolean +--- @field display_name string + +---@class BufferInfo +---@field id number +---@field index number + +--- @class GroupBuffers +--- @field id string +--- @field name string +--- @field priority integer +--- @field hidden boolean +--- @field display_name string +--- @field [integer] BufferInfo + +--- @alias UserGroups table + +--- Get the buffer group from the tab/buf - and return the priority as we use Priority to index our user groups. +--- using priority gives us the index where the buffers will be placed +--- @param buf bufferline.Buffer|bufferline.Tab +local function get_buf_group_and_priority(buf) + local buf_group = group_state.user_groups[buf.group] + return buf_group, buf_group.priority +end + +--- If the user group containing buffers is fresh (has no name,display name..) - set the fields from the child buffer +--- @param usergroup GroupBuffers +--- @param buf bufferline.Buffer|bufferline.Tab +local function set_usergroup_fields(usergroup, buf) + if not usergroup.name then + local buf_group = group_state.user_groups[buf.group] + usergroup.id = buf_group.id + usergroup.name = buf_group.name + usergroup.priority = buf_group.priority + usergroup.hidden = buf_group.hidden + usergroup.display_name = buf_group.display_name + end +end + +--- Creates a UserGroups container to store each Group info and a List of the buffers +--- @return UserGroups +local function create_user_groups_list() + local user_groups = {} + local size = vim.tbl_count(group_state.user_groups) + for i = 1, size do + user_groups[i] = {} + end + return user_groups +end + +--- @param usergroup UserGroup +--- @param result bufferline.Component[] +local function insert_group_with_start_end(usergroup, result) + if #usergroup > 0 then + local group_start, group_end = get_group_marker(usergroup.id, usergroup) + if group_start then table.insert(result, group_start) end + for _, tab in ipairs(usergroup) do + table.insert(result, tab) + end + if group_end then table.insert(result, group_end) end + end +end + -- FIXME: this function does a lot of looping that can maybe be consolidated --- +--- The function as it is - called in render() - with redundancy and indirection ---@param components bufferline.Component[] ----@param sorter fun(list: bufferline.Component[]):bufferline.Component[] ---@return bufferline.Component[] -function M.render(components, sorter) - components, group_state.components_by_group = sort_by_groups(components) - if vim.tbl_isempty(group_state.components_by_group) then return components end +local function render_old(components, sorter) + local sorted = {} + + local clustered = {} + + local size = vim.tbl_count(group_state.user_groups) + for i = 1, size do + clustered[i] = {} + end + + for index, tab in ipairs(components) do + local buf = tab:as_element() + if buf then + local buf_group = group_state.user_groups[buf.group] + local buf_container = clustered[buf_group.priority] + if not buf_container.name then + buf_container.id = buf_group.id + buf_container.name = buf_group.name + buf_container.priority = buf_group.priority + buf_container.hidden = buf_group.hidden + buf_container.display_name = buf_group.display_name + end + table.insert(buf_container, { id = buf.id, index = index }) + table.insert(sorted, buf) + end + end + + group_state.components_by_group = clustered + + if vim.tbl_isempty(clustered) then return sorted end local result = {} ---@type bufferline.Component[] - for _, sublist in ipairs(group_state.components_by_group) do - local buf_group_id = sublist.id + for _, group_buf_infos in ipairs(clustered) do + local buf_group_id = group_buf_infos.id local buf_group = group_state.user_groups[buf_group_id] - --- convert our components by group which is essentially and index of tab positions and ids - --- to the actual tab by pulling the full value out of the tab map - local items = utils.map(function(map) - local t = components[map.index] - --- filter out tab views that are hidden + + local items = {} + + for index, item in ipairs(group_buf_infos) do + local t = components[item.index] t.hidden = buf_group and buf_group.hidden - return t - end, sublist) - --- Sort *each* group, TODO: in the future each group should be able to have it's own sorter + items[index] = t + end + items = sorter(items) - if #sublist > 0 then - local group_start, group_end = get_group_marker(buf_group_id, sublist) + if #group_buf_infos > 0 then + local group_start, group_end = get_group_marker(group_buf_infos.id, group_buf_infos) if group_start then table.insert(items, 1, group_start) items[#items + 1] = group_end @@ -532,9 +836,103 @@ function M.render(components, sorter) end result = utils.merge_lists(result, items) end + + return result +end + +--- Revamped logic without redundant looping +---@param components bufferline.Component[] +---@return bufferline.Component[] +local function render_new(components, sorter) + local clustered = {} + local size = vim.tbl_count(group_state.user_groups) + for i = 1, size do + clustered[i] = {} + end + for i, tab in ipairs(components) do + local buf = tab:as_element() + if buf then + local buf_group = group_state.user_groups[buf.group] + local buf_container = clustered[buf_group.priority] + if not buf_container.name then + buf_container.id = buf_group.id + buf_container.name = buf_group.name + buf_container.priority = buf_group.priority + buf_container.hidden = buf_group.hidden + buf_container.display_name = buf_group.display_name + end + tab.hidden = buf_group.hidden + table.insert(buf_container, tab) + -- table.insert(buf_container, { id = buf.id, index = i}) (only stores bufid , index} + end + end + + -- how relevant is doing this once we update the user groups + -- I am assuming we only want to store the index and id here - in the below version + -- that is what I added , this function just has all the logic laid out + group_state.components_by_group = clustered + + if vim.tbl_isempty(clustered) then return components end + + local result = {} ---@type bufferline.Component[] + + for _, group_buf_infos in ipairs(clustered) do + group_buf_infos = sorter(group_buf_infos) + + if #group_buf_infos > 0 then + local group_start, group_end = get_group_marker(group_buf_infos.id, group_buf_infos) + if group_start then table.insert(result, group_start) end + for _, tab in ipairs(group_buf_infos) do + table.insert(result, tab) + end + if group_end then table.insert(result, group_end) end + end + end + return result +end + +--- Revamped logic without redundant looping , using functions (same as render_new) +---@param components bufferline.Component[] +---@return bufferline.Component[] +local function render_clean(components, sorter) + local user_groups = create_user_groups_list() + + -- since we store only the id and name in the persistent component state, use this for the local state + local user_groups_minimal = create_user_groups_list() + + for index, tab in ipairs(components) do + local buf = tab:as_element() + if buf then + local buf_group, priority = get_buf_group_and_priority(buf) + local user_group, minimal = user_groups[priority], user_groups_minimal[priority] + if not user_group.name then + set_usergroup_fields(user_group, buf) + set_usergroup_fields(minimal, buf) + end + tab.hidden = buf_group.hidden -- if the group is hidden - set tab to be hidden too + table.insert(user_group, tab) + table.insert(minimal, { id = buf.id, index = index }) + end + end + + -- Set Group State with the minimal table that only has id and index for buffers + group_state.components_by_group = user_groups_minimal + + if vim.tbl_isempty(user_groups) then return components end + local result = {} ---@type bufferline.Component[] + for _, usergroup in ipairs(user_groups) do + usergroup = sorter(usergroup) -- No Op + if #usergroup > 0 then insert_group_with_start_end(usergroup, result) end + end return result end +-- v1 original - uses sort_by_groups_v1 +---@param components bufferline.Component[] +---@param sorter fun(list: bufferline.Component[]):bufferline.Component[] +---@return bufferline.Component[] +function M.render(components, sorter) return render_clean(components, sorter) end + M.builtin = builtin M.separator = separator diff --git a/lua/bufferline/pr.lua b/lua/bufferline/pr.lua new file mode 100644 index 00000000..1b89ebb9 --- /dev/null +++ b/lua/bufferline/pr.lua @@ -0,0 +1,395 @@ +local fmt = string.format + +------------- +--- @Utils + +--- @class PR +local P = {} + +--- @Group PR +-- 1. Made type on_close optional in bufferline.Group type [types.lua:193] +-- 2. in Group Setup - added group specific separator options - such as placing the sep at start/end [config.lua:678] +-- 3. Added functionality to Add/Remove groups on their own, for flexibility [groups:286] +-- 4. Fixed the render() function and kept the old one for reference [groups:913] +-- 5. Added a BufferLineDebug user command to print the rendered tabline with HL's and Text + Padding [bufferline:207] +-- 6. Updated doc to provide more info on how to use options [doc/bufferline.txt] +-- 7. Added a set_bufferline_hls function for the user to directly specify all the styles required for Group Labels and Buffers in one go [pr:38] +-- 8. Fixed the error of BufferLineCyclePrev/Next not working when current buffer is toggled and in a group [commands.lua:203 and groups:67] + +-- The functions used from this file are - +-- 1. set_group_hls -> Not sure if this should go in config or groups +-- 2. get_tabline_text_and_highlights -> fn that prints the rendered data with applied hl's +---------------------------------------------------------------------------------------------- +--- Function to set all group related highlights - for the Group Label, and the Group Buffers +--- @class HighlightOpts +--- @field active_fg string? +--- @field active_bg string? +--- @field inactive_fg string? +--- @field inactive_bg string? +--- @field bold boolean? +--- @field italic boolean? +--- @field label_fg string? +--- @field label_bg string? + +--- @alias BufferLineHighlights HighlightOpts + +--- @param group string +--- @param highlight vim.api.keyset.highlight +local function set_hl(group, highlight) vim.api.nvim_set_hl(0, group, highlight) end + +local inactive = "#7a8aaa" + +--- Set required highlight groups for Group Buffers and Group Label +--- @Usage set_group_hls("A",{active_fg=C.blue,label_fg=C.teal,inactive_fg=C.comment...}) +--- @param group_name string +--- @param opts BufferLineHighlights +local function set_group_hls(group_name, opts) + if not opts or (not opts.active_bg and not opts.active_fg) then return end + + local function set_style(style, fg, bg, bold, italic) + if fg then style.fg = fg end + if bg then style.bg = bg end + if bold then style.bold = true end + if italic then style.italic = true end + end + + local active_style = {} + local inactive_style = {} + + set_style(active_style, opts.active_fg, opts.active_bg, opts.bold, opts.italic) + set_style(inactive_style, opts.inactive_fg, opts.inactive_bg) + set_hl(fmt("BufferLine%sSelected", group_name), active_style) + + if opts.inactive_bg or opts.inactive_fg then + set_hl(fmt("BufferLine%s", group_name), inactive_style) + set_hl(fmt("BufferLine%sVisible", group_name), inactive_style) + else + set_hl(fmt("BufferLine%s", group_name), active_style) + set_hl(fmt("BufferLine%sVisible", group_name), { fg = inactive }) + end + + if opts.label_bg or opts.label_fg then + local label_style = {} + set_style(label_style, opts.label_fg, opts.label_bg) + set_hl(fmt("BufferLine%sLabel", group_name), label_style) + end +end + +---------------------------------------------------------------------------------------------- +---@Group Functionality + +-- Maintain count of toggled groups - to fix cycling not working if the user toggles an active tab +P.toggled_groups = 0 + +--- This solves the issue of cycling when the user is on a toggled group +--- We keep a count of how many groups are currently toggled off (minimized) +--- thus we can get the next index by passing the count of toggled groups as the index (1 for example) +function P:toggled_index() + if self.toggled_groups >= 1 then return self.toggled_groups end +end + +--- @Group PR + +--[[ old +local C = lazy.require("bufferline.constants") ---@module "bufferline.constants" +local function space_end(hl_groups) return { { highlight = hl_groups.fill.hl_group, text = C.padding } } end +local function tab_old(group, hls, count) + -- On Toggle - the tab_sep is not hiding + -- '▎' "▏" + local hl = hls.fill.hl_group + local indicator_hl = hls.buffer.hl_group + local indicator = { + { highlight = hl, text = C.padding }, + { highlight = indicator_hl, text = C.padding .. group.name .. count .. C.padding }, + { highlight = hl, text = C.padding }, + } + return { sep_start = indicator, sep_end = space_end(hls) } +end +--]] + +-- these are defined in groups.lua +--- @param count 1 | -1 +local function update_toggled(count) P.toggled_groups = P.toggled_groups + count end + +--- just updated toggle function to increment/decrement the toggled counter at [groups:463] +--- @param group_by_callback function +local function toggle_hidden(group_by_callback) + local group = group_by_callback() + if group then + if not group.hidden then + update_toggled(1) + else + update_toggled(-1) + end + group.hidden = not group.hidden + end +end + +---@class ActiveBuffers +---@field length integer +---@field id integer +---@field filename string +---@field name string +---@field highlights ActiveHighlights +---@field modified boolean + +---@class ActiveHighlights +---@field text string +---@field highlight string +---@field config_hl string +---@field id integer + +--- Convert highlight name to readable form +--- @param highlight string +--- @return string +local function convert_highlight_name(highlight) + local name = highlight:gsub("^BufferLine", "") + name = name:gsub("(%u)", function(c) return "_" .. c:lower() end) + name = name:gsub("^_", "") + return name +end + +--- Get the current segments with their text and HL groups +--- @return ActiveBuffers[], string +--- @param tabline_data BufferlineTablineData +function P:get_highlight_groups(tabline_data) + local active_bufs = {} + local buf_str = "" + + for i, component in ipairs(tabline_data.visible_components) do + if component.component then + local name = component.name + local segments = tabline_data.segments[i] -- Directly use segments + local component_segments = {} + for _, seg in ipairs(segments) do + local visible_component = {} + local visible_text, visible_hl = seg.text, seg.highlight + if visible_text then visible_component.text = visible_text end + + if visible_hl then + visible_component.highlight = visible_hl + visible_component.config_hl = convert_highlight_name(visible_hl) + if visible_text and visible_text == name then + buf_str = buf_str .. "\n" .. name .. " : " .. visible_component.config_hl + end + end + table.insert(component_segments, visible_component) + end + local active_component = { + length = component.length, + id = component.id, + name = component.name, + filename = component.filename, + highlights = component_segments, + modified = component.modified, + } + + table.insert(active_bufs, active_component) + end + end + + return active_bufs, self:compact(active_bufs) +end + +-- Returns a compact table with no spacing +local function compact_print(t, indent, printed) + indent = indent or "" + printed = printed or {} + if printed[t] then return "..." end + printed[t] = true + local result = "{" + local first = true + for k, v in pairs(t) do + if not first then + result = result .. "," + else + first = false + end + result = result .. k .. "=" + if type(v) == "table" then + if next(v) == nil then + result = result .. "{}" + else + result = result .. compact_print(v, indent .. " ", printed) + end + elseif type(v) == "string" then + result = result .. '"' .. v .. '"' + elseif type(v) == "function" then + result = result .. "" + else + result = result .. tostring(v) + end + end + return result .. "}" +end + +function P:compact(data) return compact_print(data) end + +local function rgb_to_hex(rgb) + local hex = string.format("#%06x", rgb) + return hex +end + +local function parse_to_objects(parsed_tabline) + local objects = {} + for _, line in ipairs(parsed_tabline) do + local highlight, text = line:match("^#(.-)#(.*)$") + if highlight and text then + table.insert(objects, { highlight = { hl = highlight }, text = text }) + else + table.insert(objects, { text = line }) + end + end + + return objects +end + +local function get_highlight_attributes(group) + local hl = vim.api.nvim_get_hl(0, { name = group }) + if hl then + if hl.fg then hl.fg = rgb_to_hex(hl.fg) end + if hl.bg then hl.bg = rgb_to_hex(hl.bg) end + return hl + else + return nil + end +end + +local function enhance_with_highlight(objects) + for _, obj in ipairs(objects) do + if obj.highlight and obj.highlight.hl then + local hl = get_highlight_attributes(obj.highlight.hl) + if hl then + for k, v in pairs(hl) do + obj.highlight[k] = v + end + else + obj.highlight.hl_attributes = "Highlight group not found" + end + end + end + return objects +end + +--- Get all of the Highlight groups and Text (including spaces) to get a complete picture of how the tabline is rendered +--- @param tabline string +local function get_tabline_text_and_highlights(tabline) + local result = {} + for part in tabline:gmatch("([^%%]*)") do + if part ~= "" then table.insert(result, part) end + end + local split = parse_to_objects(result) + local whl = enhance_with_highlight(split) + + return whl +end + +------------------------------------------------------------------------------------- +--- private utils for debugging, not relevant +------------------------------------------------------------------------------------- +local delim = "-------------------" +P.delimn = "\n" .. delim .. "\n" +P.nl = "\n\n" +P.override = false + +local instance -- Singleton + +function P.new() + local self = setmetatable({}, { __index = P }) + self.i = 0 + self.debug = false + return self +end + +P.files = {} + +--- @param msg string +--- @param data any +--- @param override boolean? +function P:log(msg, data, override) + if self.debug == true or override then self:write(msg, data) end +end + +--- @param msg string +--- @param data any +function P:write(msg, data) + if self.file then + self.file:write("\n" .. msg .. self.delimn) + if data then self.file:write(vim.inspect(data) .. self.delimn) end + else + if type(data) == "string" then + print(data) + else + print(vim.inspect(data) .. self.delimn) + end + end +end + +--- @param data string +function P:writestr(data) + if self.debug == true and data then self.file:write("\n" .. data .. self.delimn) end +end + +function P:set_logfile(path) + if path then self.logfile = path end + local file = io.open(self.logfile, "a") -- Open the file in append mode + if file then + self.file = file + self:log("Logger Initialization") + else + print("Error: Could not open file for writing") + end +end + +--- @class InitOptions +--- @field debug boolean +--- @field logfile string? + +--- @param opts InitOptions +function P:init(opts) + self.i = 0 + if opts.debug == false then + self.debug = false + return + end + self.debug = true + if opts.logfile then self:set_logfile(opts.logfile) end +end + +--- @param msg string +--- @param data any +function P:logf(key, msg, data) + if self.debug == true and self.files[key] then self:writef(key, msg, data) end +end + +function P:writef(key, msg, data) + if self.files[key] then + self.files[key]:write("\n" .. msg .. "\n") + if data then + local compact_data = self:compact(data) + self.files[key]:write(compact_data .. self.delimn) + end + end +end + +function P:add_logfile(path, key) + local file = io.open(path, "a") -- Open the file in append mode + if file then + self.files[key] = file + self:logf(key, "Logger Initialization") + else + print("Error: Could not open file for writing") + end +end + +--- @return PR +local function get_instance() + if not instance then instance = P.new() end + return instance +end + +return { + get_instance = get_instance, + set_group_hls = set_group_hls, + get_tabline_text_and_highlights = get_tabline_text_and_highlights, +} diff --git a/lua/bufferline/types.lua b/lua/bufferline/types.lua index 2ad54149..46a4103c 100644 --- a/lua/bufferline/types.lua +++ b/lua/bufferline/types.lua @@ -5,6 +5,8 @@ ---@class bufferline.GroupOptions ---@field toggle_hidden_on_enter? boolean re-open hidden groups on bufenter +---@field separator_position? "start" | "end" | "both" -- new option +---@field separator_style? "thin" | "thick" | {[1]: string, [2]: string} ---@class bufferline.GroupOpts ---@field options? bufferline.GroupOptions @@ -188,7 +190,7 @@ ---@field public icon? string ---@field public hidden? boolean ---@field public with? fun(Group, Group): bufferline.Group ----@field auto_close boolean when leaving the group automatically close it +---@field auto_close boolean? when leaving the group automatically close it ---@class bufferline.RenderContext ---@field preferences bufferline.Config diff --git a/lua/bufferline/ui.lua b/lua/bufferline/ui.lua index 440b203c..bd0644ea 100644 --- a/lua/bufferline/ui.lua +++ b/lua/bufferline/ui.lua @@ -590,9 +590,9 @@ local function truncate(before, current, after, available_width, marker, visible table.insert(items, item.component(visible[index + 1])) end return items, marker, visible - -- if we aren't even able to fit the current buffer into the - -- available space that means the window is really narrow - -- so don't show anything + -- if we aren't even able to fit the current buffer into the + -- available space that means the window is really narrow + -- so don't show anything elseif available_width < current.length then return {}, marker, visible else diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index f65787c5..1a613015 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,5 +1,8 @@ local M = {} +-- Run tests using +-- nvim --headless --noplugin -u tests/minimal_init.lua -c "lua require('plenary.test_harness').test_directory('/path/bufferline.nvim/tests', {minimal_init = 'tests/minimal_init.lua'})" + function M.root(root) local f = debug.getinfo(1, "S").source:sub(2) return vim.fn.fnamemodify(f, ":p:h:h") .. "/" .. (root or "")