diff --git a/README.md b/README.md index 5611c97..09c1528 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ that information can be useful. For example, in statuslines and the tabline. - [Using mini.deps and mini.statusline](#using-minideps-and-ministatusline) - [Configuration](#configuration) - [Formatters](#formatters) - - [The "extended" built-in](#the-extended-built-in) + - [The "default" built-in](#the-default-built-in) - [The "short" built-in](#the-short-built-in) - [Customize a built-in](#customize-a-built-in) - [Use a custom formatter](#use-a-custom-formatter) @@ -39,6 +39,8 @@ that information can be useful. For example, in statuslines and the tabline. ![1710925071](https://github.com/abeldekat/harpoonline/assets/58370433/4b911ed1-428d-4a64-ba9d-f67ba6438ce7) *Custom statusline in NvChad v2.5* +*Note*: The video demonstrates the first release and will become outdated. + ## Features - Supports multiple [harpoon2] lists. @@ -101,7 +103,7 @@ local function config() if MiniStatusline.is_truncated(args.trunc_width) or isnt_normal_buffer() then return "" end - return Harpoonline.format() ----> produce the info + return Harpoonline.format() end local function active() -- Hook, see mini.statusline setup -- copy any lines from mini.statusline, H.default_content_active: @@ -140,42 +142,37 @@ The following configuration is implied when calling `setup` without arguments: ```lua ---@class HarpoonLineConfig Harpoonline.config = { - -- other nice icons: "󰀱", "", "󱡅" + -- other nice icons: "󰀱", "", "󱡅", "󰛢" ---@type string icon = '󰀱', -- An empty string disables showing the icon - -- Harpoon:list(), without a name, retrieves the default list: + -- Harpoon:list(), when name is nil, retrieves the default list: -- default_list_name: Configures the display name for the default list. ---@type string default_list_name = '', - ---@type "extended" | "short" - formatter = 'extended', -- use a built-in formatter + ---@type "default" | "short" + formatter = 'default', -- use a built-in formatter formatter_opts = { - extended = { - -- An indicator corresponds to a position in the harpoon list - -- Suggestion: Add an indicator for each configured "select" keybinding - indicators = { ' 1 ', ' 2 ', ' 3 ', ' 4 ' }, - active_indicators = { '[1]', '[2]', '[3]', '[4]' }, - - -- 1 More indicators than items in the harpoon list: - empty_slot = '', -- ' · ', -- middledot. Disable using empty string - - -- 2 Less indicators than items in the harpoon list - more_marks_indicator = ' … ', -- horizontal elipsis. Disable using empty string - more_marks_active_indicator = '[…]', -- Disable using empty string + default = { + inactive = ' %s ', -- including spaces + active = '[%s]', + -- Max number of slots to display: + max_slots = 4, -- Suggestion: as many as there are "select" keybindings + -- The number of items in the harpoon list exceeds max_slots: + more = '…', -- horizontal elipsis. Disable using empty string }, short = { inner_separator = '|', }, }, - ---@type fun():string|nil + ---@type HarpoonlineFormatter custom_formatter = nil, -- use this formatter when configured ---@type fun()|nil - on_update = nil, -- optional action to perform after update + on_update = nil, -- Recommended: trigger the client when the line has been rebuild. } ``` @@ -188,9 +185,9 @@ Scenario's: - A: 3 marks, the current buffer is not harpooned - B: 3 marks, the current buffer is harpooned on mark 2 -#### The "extended" built-in +#### The "default" built-in -This is the default formatter. Default options: `config.formatter_opts.extended` +Default options: `config.formatter_opts.default` Output A: :anchor: ` 1 2 3 ` @@ -198,7 +195,7 @@ Output B: :anchor: ` 1 [2] 3 ` **Note**: Five marks, the fifth mark is the active buffer: -Output B: 󰛢 ` 1 2 3 4 […] ` +Output B: :anchor: ` 1 2 3 4 […] ` #### The "short" built-in @@ -212,66 +209,90 @@ Output B: :anchor: `[2|3]` ```lua Harpoonline.setup({ - -- formatter = "extended", -- configure the default formatter + -- config formatter_opts = { - extended = { -- remove all spaces... - indicators = { "1", "2", "3", "4" }, - empty_slot = "·", - more_marks_indicator = "…", -- horizontal elipsis. Disable with empty string - more_marks_active_indicator = "[…]", -- Disable with empty string + default = { -- remove all spaces... + inactive = "%s", + active = "[%s]", }, }, - on_update = on_update, + -- more config }) ``` -Output A: :anchor: `123·` +Output A: :anchor: `123` -Output B: :anchor: `1[2]3·` +Output B: :anchor: `1[2]3` #### Use a custom formatter The following data is kept up-to-date internally, to be processed by formatters: ```lua ----@class HarpoonLineData -H.data = { - -- Harpoon's default list is in use when list_name = nil - --- @type string|nil - list_name = nil, -- the name of the current list - --- @type number - list_length = 0, -- the length of the current list - --- @type number|nil - buffer_idx = nil, -- the mark of the current buffer if harpooned -} +---@class HarpoonlineData +---@field list_name string|nil -- the name of the current list +---@field items HarpoonItem[] -- the items of the current list +---@field active_idx number|nil -- the harpoon index of the current buffer ``` -Example: +Example "very short": ```lua -local Harpoonline = require("harpoonline") Harpoonline.setup({ - custom_formatter = Harpoonline.gen_formatter( - ---@param data HarpoonLineData - ---@return string - function(data) - return string.format( -- very short, without the length of the harpoon list - "%s%s%s", - "➡️ ", - data.list_name and string.format("%s ", data.list_name) or "", - data.buffer_idx and string.format("%d", data.buffer_idx) or "-" - ) - end - ), + -- config + ---@param data HarpoonlineData + ---@param opts HarpoonLineConfig + ---@return string + custom_formatter = function(data,opts) + return string.format( -- very short, without the length of the harpoon list + "%s%s%s", + opts.icon .. " ", + data.list_name and string.format("%s ", data.list_name) or "", + data.active_idx and string.format("%d", data.active_idx) or "-" + ) + end + -- more config }) ``` -Output A: :arrow_right: `-` +Output A: :anchor: `-` -Output B: :arrow_right: `2` +Output B: :anchor: `2` + +Example "letters": + +```lua +Harpoonline.setup({ + -- config + ---@param data HarpoonlineData + ---@param opts HarpoonLineConfig + ---@return string + custom_formatter = function(data, opts) + local letters = { "j", "k", "l", "h" } + local idx = data.active_idx + local slot = 0 + local slots = vim.tbl_map(function(letter) + slot = slot + 1 + return idx and idx == slot and string.upper(letter) or letter + end, vim.list_slice(letters, 1, math.min(#letters, #data.items))) + + local name = data.list_name and data.list_name or opts.default_list_name + local header = string.format("%s%s%s", opts.icon, name == "" and "" or " ", name) + return header .. " " .. table.concat(slots) + end, + -- more config +}) +``` + +Output A: :anchor: `jkl` + +Output B: :anchor: `jKl` + +*Note*: -*Note*: You can also use inner highlights in the formatter function. +- You can also use inner highlights in the formatter function. See the example recipe for NvChad. +- You can use the `harpoon` information inside each `data.items` ## Harpoon lists @@ -413,7 +434,6 @@ return M - @theprimeagen: Harpoon is the most important part of my workflow. - @echasnovski: The structure of this plugin is heavily based on [mini.nvim] -- @letieu: The `extended` formatter is inspired by plugin [harpoon-lualine] [harpoon2]: https://github.com/ThePrimeagen/harpoon/tree/harpoon2 [mini.statusline]: https://github.com/echasnovski/mini.statusline diff --git a/lua/harpoonline/init.lua b/lua/harpoonline/init.lua index 846ac0f..6daccb1 100644 --- a/lua/harpoonline/init.lua +++ b/lua/harpoonline/init.lua @@ -1,4 +1,12 @@ --- Module definition ========================================================== +-- Moddefinition ========================================================== + +---@class HarpoonlineData +---@field list_name string|nil -- the name of the current list +---@field items HarpoonItem[] -- the items of the current list +---@field active_idx number|nil -- the harpoon index of the current buffer + +--The signature of a formatter function: +---@alias HarpoonlineFormatter fun(data: HarpoonlineData, opts: HarpoonLineConfig): string ---@class HarpoonLine local Harpoonline = {} @@ -10,13 +18,19 @@ Harpoonline.setup = function(config) if not has_harpoon then return end H.harpoon_plugin = Harpoon + H.apply_config(H.setup_config(config)) + + H.produce() -- initialize the line + if H.get_config().on_update then + local produce = H.produce + H.produce = function() -- composition: add on_update + produce() + H.get_config().on_update() -- notify clients + end + end - config = H.setup_config(config) - H.apply_config(config) H.create_autocommands() - H.create_extensions(require('harpoon.extensions')) - H.initialize() end ---@class HarpoonLineConfig @@ -25,91 +39,68 @@ Harpoonline.config = { ---@type string icon = '󰀱', -- An empty string disables showing the icon - -- Harpoon:list(), without a name, retrieves the default list: + -- Harpoon:list(), when name is nil, retrieves the default list: -- default_list_name: Configures the display name for the default list. ---@type string default_list_name = '', - ---@type "extended" | "short" - formatter = 'extended', -- use a builtin formatter + ---@type "default" | "short" + formatter = 'default', -- use a builtin formatter formatter_opts = { - extended = { - -- An indicator corresponds to a position in the harpoon list - -- Suggestion: Add an indicator for each configured "select" keybinding - indicators = { ' 1 ', ' 2 ', ' 3 ', ' 4 ' }, - active_indicators = { '[1]', '[2]', '[3]', '[4]' }, - - -- 1 More indicators than items in the harpoon list: - empty_slot = '', -- ' · ', -- middledot. Disable using empty string - - -- 2 Less indicators than items in the harpoon list - more_marks_indicator = ' … ', -- horizontal elipsis. Disable using empty string - more_marks_active_indicator = '[…]', -- Disable using empty string + default = { + inactive = ' %s ', -- including spaces + active = '[%s]', + -- Number of slots to display: + max_slots = 4, -- Suggestion: as many as there are "select" keybindings + -- The number of items in the harpoon list exceeds max_slots: + more = '…', -- horizontal elipsis. Disable using empty string }, short = { inner_separator = '|', }, }, - ---@type fun():string|nil + ---@type HarpoonlineFormatter custom_formatter = nil, -- use this formatter when configured ---@type fun()|nil - on_update = nil, -- optional action to perform after update + on_update = nil, -- Recommended: trigger the client when the line has been rebuild. } +-- Module functionality ======================================================= + ---@class HarpoonlineFormatterConfig Harpoonline.formatters = { - extended = function() return H.builtin_extended end, + default = function() return H.builtin_default end, short = function() return H.builtin_short end, } --- Module functionality ======================================================= - --- Given a formatter function, return a wrapper function that can be invoked --- by consumers. ----@param formatter fun(data: HarpoonLineData):string ----@return function -Harpoonline.gen_formatter = function(formatter) - return function() return formatter(H.data) end -end - -- Return true is the current buffer is harpooned, false otherwise -- Useful for extra highlighting ---@return boolean -Harpoonline.is_buffer_harpooned = function() return H.data.buffer_idx ~= nil end +Harpoonline.is_buffer_harpooned = function() return H.active_idx ~= nil end -- The function to be used by consumers ---@return string -Harpoonline.format = function() - if not H.cached_result then H.cached_result = H.formatter and H.formatter() or '' end - return H.cached_result -end +Harpoonline.format = function() return H.cached_line end -- Helper data ================================================================ H.harpoon_plugin = nil +---@type HarpoonlineFormatter +H.formatter = nil + ---@type HarpoonLineConfig H.default_config = vim.deepcopy(Harpoonline.config) ----@class HarpoonLineData -H.data = { - -- Harpoon's default list is in use when list_name = nil - --- @type string|nil - list_name = nil, -- the name of the current list - --- @type number - list_length = 0, -- the length of the current list - --- @type number|nil - buffer_idx = nil, -- the mark of the current buffer if harpooned -} - --- @type string|nil -H.cached_result = nil - ----@type fun():string|nil -H.formatter = nil +---@type string +H.cached_line = '' +---@type string | nil +H.list_name = nil +---@type number | nil +H.active_idx = nil -- Helper functionality ======================================================= @@ -131,17 +122,17 @@ end -- Sets the final config to use. -- If config.custom_formatter is configured, this will be the final formatter. -- Otherwise, use builtin config.formatter if its valid. --- Otherwise, fallback to the "extended" builint formatter +-- Otherwise, fallback to the "extended" builtin formatter ---@param config HarpoonLineConfig H.apply_config = function(config) + Harpoonline.config = config + if config.custom_formatter then H.formatter = config.custom_formatter else - local is_valid = vim.tbl_contains(vim.tbl_keys(Harpoonline.formatters), config.formatter) - local key = is_valid and config.formatter or 'extended' - H.formatter = Harpoonline.gen_formatter(Harpoonline.formatters[key]()) + local builtin = Harpoonline.formatters[config.formatter] + H.formatter = builtin and builtin() or H.builtin_default end - Harpoonline.config = config end ---@return HarpoonLineConfig @@ -150,21 +141,21 @@ H.get_config = function() return Harpoonline.config end -- Update the data on each BufEnter event -- Update the name of the list on custom event HarpoonSwitchedList H.create_autocommands = function() - local augroup = vim.api.nvim_create_augroup('HarpoonLine', {}) + local augroup = vim.api.nvim_create_augroup('Harpoonline', {}) vim.api.nvim_create_autocmd('User', { group = augroup, pattern = 'HarpoonSwitchedList', callback = function(event) - H.data.list_name = event.data - H.update() + H.list_name = event.data + H.produce() end, }) vim.api.nvim_create_autocmd({ 'BufEnter' }, { group = augroup, pattern = '*', - callback = H.update, + callback = H.produce, }) end @@ -172,20 +163,19 @@ end -- Needed because those actions can be done without leaving the buffer. -- All other update scenarios are covered by listening to the BufEnter event. H.create_extensions = function(Extensions) - H.harpoon_plugin:extend({ [Extensions.event_names.ADD] = H.update }) - H.harpoon_plugin:extend({ [Extensions.event_names.REMOVE] = H.update }) + H.harpoon_plugin:extend({ [Extensions.event_names.ADD] = H.produce }) + H.harpoon_plugin:extend({ [Extensions.event_names.REMOVE] = H.produce }) end ----@return HarpoonList -H.get_list = function() return H.harpoon_plugin:list(H.data.list_name) end - -- If the current buffer is harpooned, return the index of the harpoon mark -- Otherwise, return nil ---@param list HarpoonList ---@return number|nil -H.buffer_idx = function(list) +H.find_active_idx = function(list) if vim.bo.buftype ~= '' then return end -- not a normal buffer - if list:length() == 0 then return end -- no items in the list + + -- if list:length() == 0 -- NOTE: Harpoon issue #555 + if #list.items == 0 then return end -- no items in the list local current_file = vim.fn.expand('%:p:.') for idx, item in ipairs(list.items) do @@ -193,23 +183,23 @@ H.buffer_idx = function(list) end end ----@param list HarpoonList -H.update_data = function(list) - H.data.list_length = list:length() - H.data.buffer_idx = H.buffer_idx(list) -end - -- To be invoked on any harpoon-related event -- Performs action on_update if present -H.update = function() - H.update_data(H.get_list()) - H.cached_result = nil -- the format function should recompute - - local on_update = H.get_config().on_update - if on_update then on_update() end +H.produce = function() + ---@type HarpoonList + local list = H.harpoon_plugin:list(H.list_name) + H.active_idx = H.find_active_idx(list) + + H.cached_line = H.formatter({ + list_name = H.list_name, + -- list_length = list:length(), -- NOTE: Harpoon issue #555 + items = list.items, + active_idx = H.active_idx, + }, H.get_config()) end -H.initialize = function() H.update_data(H.get_list()) end +-- Returns the name of the list, or the configured default_list_name +H.make_list_name = function(name) return name and name or H.get_config().default_list_name end -- Return either the icon or an empty string ---@return string @@ -218,61 +208,46 @@ H.make_icon = function() return icon ~= '' and icon or '' end ----@param data HarpoonLineData +---@param data HarpoonlineData +---@param opts HarpoonLineConfig ---@return string -H.builtin_short = function(data) - local opts = H.get_config().formatter_opts.short +H.builtin_short = function(data, opts) local icon = H.make_icon() - local list_name = data.list_name and data.list_name or H.get_config().default_list_name + local list_name = H.make_list_name(data.list_name) + + local o = opts.formatter_opts.short return string.format( '%s%s%s[%s%d]', icon, icon == '' and '' or ' ', list_name, -- no space after list name... - data.buffer_idx and string.format('%s%s', data.buffer_idx, opts.inner_separator) or '', - data.list_length + data.active_idx and string.format('%s%s', data.active_idx, o.inner_separator) or '', + #data.items ) end ----@param data HarpoonLineData +---@param data HarpoonlineData +---@param opts HarpoonLineConfig ---@return string -H.builtin_extended = function(data) - local opts = H.get_config().formatter_opts.extended - local show_empty_slots = opts.empty_slot and opts.empty_slot ~= '' - - -- build prefix - local show_prefix = true -- show_empty_slots or data.number_of_tags > 0 - local icon = H.make_icon() - local list_name = data.list_name and data.list_name or H.get_config().default_list_name - local prefix = not show_prefix and '' - or string.format( - '%s%s%s', -- - icon, - list_name == '' and '' or ' ', - list_name - ) - - -- build slots - local nr_of_slots = #opts.indicators - local status = {} - for i = 1, nr_of_slots do - if i > data.list_length then -- more slots then ... - if show_empty_slots then table.insert(status, opts.empty_slot) end - elseif i == data.buffer_idx then - table.insert(status, opts.active_indicators[i]) - else - table.insert(status, opts.indicators[i]) +H.builtin_default = function(data, opts) + local list_name = H.make_list_name(data.list_name) + local header = string.format('%s%s%s', H.make_icon(), list_name == '' and '' or ' ', list_name) + + local o = opts.formatter_opts.default + local idx = data.active_idx + local slot = 0 + local slots = vim.tbl_map(function() + slot = slot + 1 + return string.format(idx and idx == slot and o.active or o.inactive, slot) + end, vim.list_slice(data.items, 1, math.min(o.max_slots, #data.items))) + + if #data.items > o.max_slots then + if o.more and o.more ~= '' then + local fmt = idx and idx > o.max_slots and o.active or o.inactive + table.insert(slots, string.format(fmt, o.more)) end end - -- add more marks indicator - if data.list_length > nr_of_slots then -- more marks then... - local ind = opts.more_marks_indicator - if data.buffer_idx and data.buffer_idx > nr_of_slots then ind = opts.more_marks_active_indicator end - if ind and ind ~= '' then table.insert(status, ind) end - end - - prefix = prefix == '' and prefix or prefix .. ' ' - return prefix .. table.concat(status) + return header .. (header == '' and '' or ' ') .. table.concat(slots) end return Harpoonline diff --git a/tests/test_harpoonline.lua b/tests/test_harpoonline.lua index 77281ba..496dcd5 100644 --- a/tests/test_harpoonline.lua +++ b/tests/test_harpoonline.lua @@ -76,77 +76,56 @@ T['format()'] = new_set() -- ╭─────────────────────────────────────────────────────────╮ -- │ Default formatter │ -- ╰─────────────────────────────────────────────────────────╯ -T['format()']['extended'] = new_set() -T['format()']['extended']['one harpoon'] = function() +T['format()']['default'] = new_set() +T['format()']['default']['one harpoon'] = function() child.lua([[M.setup()]]) add_files_to_list({ '1' }) eq(child.lua_get([[ M.format() ]]), icon .. ' [1]') end -T['format()']['extended']['four harpoons'] = function() +T['format()']['default']['four harpoons'] = function() child.lua([[M.setup()]]) add_files_to_list({ '1', '2', '3', '4' }) eq(child.lua_get([[ M.format() ]]), icon .. ' 1 2 3 [4]') end -T['format()']['extended']['six harpoons'] = function() +T['format()']['default']['six harpoons'] = function() child.lua([[M.setup()]]) add_files_to_list({ '1', '2', '3', '4', '5', '6' }) eq(child.lua_get([[ M.format() ]]), icon .. ' 1 2 3 4 [' .. more .. ']') end -T['format()']['extended']['custom indicators'] = function() +T['format()']['default']['more marks'] = function() child.lua([[ M.setup({ - formatter_opts = { extended = { - indicators = {"A", "B"}, active_indicators = {"-A-", "-B-"} - }} - }) - ]]) - add_files_to_list({ '1', '2' }) - eq(child.lua_get([[ M.format() ]]), icon .. ' A-B-') -end -T['format()']['extended']['empty slots'] = function() - child.lua([[ - M.setup({ - formatter_opts = { extended = { - empty_slot = ' · ' - }} - }) - ]]) - add_files_to_list({ '1', '2' }) - eq(child.lua_get([[ M.format() ]]), icon .. ' 1 [2] · · ') -end -T['format()']['extended']['more marks'] = function() - child.lua([[ - M.setup({ - formatter_opts = { extended = { - more_marks_indicator = '', more_marks_active_indicator = '', + formatter_opts = { default = { + more = "" }} }) ]]) add_files_to_list({ '1', '2', '3', '4', '5', '6' }) eq(child.lua_get([[ M.format() ]]), icon .. ' 1 2 3 4 ') end -T['format()']['extended']['buffer not harpooned'] = function() +T['format()']['default']['buffer not harpooned'] = function() child.lua([[M.setup()]]) add_files_to_list({ '1', '2', '3', '4', '5' }) edit('9') eq(child.lua_get([[ M.format() ]]), icon .. ' 1 2 3 4 ' .. more .. ' ') end -T['format()']['extended']['remove item'] = function() +T['format()']['default']['remove item'] = function() child.lua([[M.setup()]]) add_files_to_list({ '1', '2', '3' }) child.lua([[ require("harpoon"):list():remove_at(3) ]]) eq(child.lua_get([[ M.format() ]]), icon .. ' 1 2 ') end -T['format()']['extended']['remove all items'] = function() +T['format()']['default']['remove all items'] = function() child.lua([[M.setup()]]) add_files_to_list({ '1', '2' }) child.lua([[ require("harpoon"):list():remove_at(2) ]]) child.lua([[ require("harpoon"):list():remove_at(1) ]]) - eq(child.lua_get([[ M.format() ]]), icon .. ' 1 ') -- should be empty + eq(child.lua_get([[ M.format() ]]), icon .. ' ') -- should be empty - MiniTest.add_note('Incorrect, not empty! See harpoon issue #555') + -- eq(child.lua_get([[ M.format() ]]), icon .. ' 1 ') -- should be empty + -- MiniTest.add_note('Incorrect, not empty! See harpoon issue #555') end -T['format()']['extended']['switch list'] = function() +T['format()']['default']['switch list'] = function() child.lua([[M.setup()]]) add_files_to_list({ '1', '2' }, 'dev') child.lua([[ @@ -156,7 +135,7 @@ T['format()']['extended']['switch list'] = function() ]]) eq(child.lua_get([[ M.format() ]]), icon .. ' dev 1 [2]') end -T['format()']['extended']['default_list_name'] = function() +T['format()']['default']['default_list_name'] = function() child.lua([[M.setup({default_list_name="mainlist"})]]) add_files_to_list({ '1', '2' }) eq(child.lua_get([[ M.format() ]]), icon .. ' mainlist 1 [2]') @@ -215,16 +194,14 @@ end T['format()']['custom'] = function() child.lua([[ M.setup({ - custom_formatter = M.gen_formatter( - function(data) - return string.format("%s%s%s%s", - "Harpoonline: ", - data.buffer_idx and "Buffer is harpooned " or "Buffer is not harpooned ", - "in list ", - data.list_name and data.list_name or "default" - ) - end - ) + custom_formatter = function(data, _) + return string.format("%s%s%s%s", + "Harpoonline: ", + data.active_idx and "Buffer is harpooned " or "Buffer is not harpooned ", + "in list ", + data.list_name and data.list_name or "default" + ) + end }) ]]) add_files_to_list({ '1', '2' })