From ccf5e1cc0ab0389f2de364669a5ade5dd50760a4 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 11:52:07 -0600 Subject: [PATCH 01/80] feat: the basic list for harpoons --- HARPOON2.md | 90 ++++++ lua/harpoon/cmd-ui.lua | 160 ---------- lua/harpoon/config.lua | 68 +++++ lua/harpoon/dev.lua | 48 --- lua/harpoon/init.lua | 281 ++---------------- lua/harpoon/list.lua | 111 +++++++ lua/harpoon/mark.lua | 420 --------------------------- lua/harpoon/term.lua | 146 ---------- lua/harpoon/test/list_spec.lua | 34 +++ lua/harpoon/test/manage-a-mark.lua | 3 - lua/harpoon/test/manage_cmd_spec.lua | 141 --------- lua/harpoon/tmux.lua | 230 --------------- lua/harpoon/ui.lua | 284 ------------------ lua/harpoon/utils.lua | 64 ---- 14 files changed, 326 insertions(+), 1754 deletions(-) create mode 100644 HARPOON2.md delete mode 100644 lua/harpoon/cmd-ui.lua create mode 100644 lua/harpoon/config.lua delete mode 100644 lua/harpoon/dev.lua create mode 100644 lua/harpoon/list.lua delete mode 100644 lua/harpoon/mark.lua delete mode 100644 lua/harpoon/term.lua create mode 100644 lua/harpoon/test/list_spec.lua delete mode 100644 lua/harpoon/test/manage-a-mark.lua delete mode 100644 lua/harpoon/test/manage_cmd_spec.lua delete mode 100644 lua/harpoon/tmux.lua delete mode 100644 lua/harpoon/ui.lua delete mode 100644 lua/harpoon/utils.lua diff --git a/HARPOON2.md b/HARPOON2.md new file mode 100644 index 00000000..b79b7ef7 --- /dev/null +++ b/HARPOON2.md @@ -0,0 +1,90 @@ +### Features +* select how to generate the list key + +#### was +list_key = [cwd [+ git branch]] +* files +* terminals +* tmux + +#### is +list_key = [key] + [list_name] + +nil = default +false = turn off + +listA = { + listLine({ ... }) + { ... } + { ... } + { ... } +} + +harpoon.setup({ + + settings = { + jumpToFileLocation: boolean => defaults true + } + + default = { + // defaults to json.parse + encode = function(obj) => string + decode = function(string) => object + key = function() ... end + display = function(listLine) => string + select = function(listLine) => void + equals = function(list_line_a, list_line_b) => boolean + + # question mark: what does it take to support custom things in here? + # potentially subject to change + add = function() => void + } + + frecency = { + ... a file list that is generated by harpoon ... + ... can be opened via viewer ... + } + + events = { + on_change = function(operation, list, value) + } + + project = { + //key = vim.loop.cwd + key = git origin? + } + + specifics = { + key = vim.loop.cwd + git_branch + } + + list_name = { + key = function() ... end + } + +}) + +### Functionality +select by index +prev/next +addToBack +addToFront +checking for deleted files? +- perhaps this could be part of the default select operation and use error +select +- default file select should come with options so you can open split/tab as + well + +harpoon.current = "default" +harpoon.current = "listName" + +harpoon.set_current(list_name) + +### LATER FEATUERS +frecency = later feature likely, but great idea +- https://github.com/agkozak/zsh-z +- https://en.wikipedia.org/wiki/Frecency + +// i don't understand this one +harpoon -> qfix : qfix -> harpoon + diff --git a/lua/harpoon/cmd-ui.lua b/lua/harpoon/cmd-ui.lua deleted file mode 100644 index 69caaf67..00000000 --- a/lua/harpoon/cmd-ui.lua +++ /dev/null @@ -1,160 +0,0 @@ -local harpoon = require("harpoon") -local popup = require("plenary.popup") -local utils = require("harpoon.utils") -local log = require("harpoon.dev").log -local term = require("harpoon.term") - -local M = {} - -Harpoon_cmd_win_id = nil -Harpoon_cmd_bufh = nil - -local function close_menu(force_save) - force_save = force_save or false - local global_config = harpoon.get_global_settings() - - if global_config.save_on_toggle or force_save then - require("harpoon.cmd-ui").on_menu_save() - end - - vim.api.nvim_win_close(Harpoon_cmd_win_id, true) - - Harpoon_cmd_win_id = nil - Harpoon_cmd_bufh = nil -end - -local function create_window() - log.trace("_create_window()") - local config = harpoon.get_menu_config() - local width = config.width or 60 - local height = config.height or 10 - local borderchars = config.borderchars - or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } - local bufnr = vim.api.nvim_create_buf(false, false) - - local Harpoon_cmd_win_id, win = popup.create(bufnr, { - title = "Harpoon Commands", - highlight = "HarpoonWindow", - line = math.floor(((vim.o.lines - height) / 2) - 1), - col = math.floor((vim.o.columns - width) / 2), - minwidth = width, - minheight = height, - borderchars = borderchars, - }) - - vim.api.nvim_win_set_option( - win.border.win_id, - "winhl", - "Normal:HarpoonBorder" - ) - - return { - bufnr = bufnr, - win_id = Harpoon_cmd_win_id, - } -end - -local function get_menu_items() - log.trace("_get_menu_items()") - local lines = vim.api.nvim_buf_get_lines(Harpoon_cmd_bufh, 0, -1, true) - local indices = {} - - for _, line in pairs(lines) do - if not utils.is_white_space(line) then - table.insert(indices, line) - end - end - - return indices -end - -function M.toggle_quick_menu() - log.trace("cmd-ui#toggle_quick_menu()") - if - Harpoon_cmd_win_id ~= nil - and vim.api.nvim_win_is_valid(Harpoon_cmd_win_id) - then - close_menu() - return - end - - local win_info = create_window() - local contents = {} - local global_config = harpoon.get_global_settings() - - Harpoon_cmd_win_id = win_info.win_id - Harpoon_cmd_bufh = win_info.bufnr - - for idx, cmd in pairs(harpoon.get_term_config().cmds) do - contents[idx] = cmd - end - - vim.api.nvim_win_set_option(Harpoon_cmd_win_id, "number", true) - vim.api.nvim_buf_set_name(Harpoon_cmd_bufh, "harpoon-cmd-menu") - vim.api.nvim_buf_set_lines(Harpoon_cmd_bufh, 0, #contents, false, contents) - vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "filetype", "harpoon") - vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "buftype", "acwrite") - vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "bufhidden", "delete") - vim.api.nvim_buf_set_keymap( - Harpoon_cmd_bufh, - "n", - "q", - "lua require('harpoon.cmd-ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_cmd_bufh, - "n", - "", - "lua require('harpoon.cmd-ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_cmd_bufh, - "n", - "", - "lua require('harpoon.cmd-ui').select_menu_item()", - {} - ) - vim.cmd( - string.format( - "autocmd BufWriteCmd lua require('harpoon.cmd-ui').on_menu_save()", - Harpoon_cmd_bufh - ) - ) - if global_config.save_on_change then - vim.cmd( - string.format( - "autocmd TextChanged,TextChangedI lua require('harpoon.cmd-ui').on_menu_save()", - Harpoon_cmd_bufh - ) - ) - end - vim.cmd( - string.format( - "autocmd BufModifiedSet set nomodified", - Harpoon_cmd_bufh - ) - ) -end - -function M.select_menu_item() - log.trace("cmd-ui#select_menu_item()") - local cmd = vim.fn.line(".") - close_menu(true) - local answer = vim.fn.input("Terminal index (default to 1): ") - if answer == "" then - answer = "1" - end - local idx = tonumber(answer) - if idx then - term.sendCommand(idx, cmd) - end -end - -function M.on_menu_save() - log.trace("cmd-ui#on_menu_save()") - term.set_cmd_list(get_menu_items()) -end - -return M diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua new file mode 100644 index 00000000..01eb6130 --- /dev/null +++ b/lua/harpoon/config.lua @@ -0,0 +1,68 @@ +local M = {} + +function M.get_config(config, name) + return vim.tbl_extend("force", {}, config.default, config[name] or {}) +end + +function M.get_default_config() + return { + settings = { + save_on_toggle = true, + jump_to_file_location = true, + }, + + default = { + ---@param obj HarpoonListItem + ---@return string + encode = function(obj) + return vim.json.encode(obj) + end, + + ---@param str string + ---@return HarpoonListItem + decode = function(str) + return vim.json.decode(str) + end, + + key = function() + return vim.loop.cwd() + end, + + ---@param list_item HarpoonListItem + display = function(list_item) + return list_item.value + end, + + ---@param list_item HarpoonListItem + select = function(list_item) + error("please implement select") + end, + + ---@param list_item_a HarpoonListItem + ---@param list_item_b HarpoonListItem + equals = function(list_item_a, list_item_b) + return list_item_a.value == list_item_b.value + end, + + add = function() + error("please implement add") + end, + } + } +end + +function M.merge_config(partial_config, latest_config) + local config = latest_config or M.get_default_config() + for k, v in pairs(partial_config) do + if k == "settings" then + config.settings = vim.tbl_extend("force", config.settings, v) + elseif k == "default" then + config.default = vim.tbl_extend("force", config.default, v) + else + config[k] = vim.tbl_extend("force", config[k] or {}, v) + end + end + return config +end + +return M diff --git a/lua/harpoon/dev.lua b/lua/harpoon/dev.lua deleted file mode 100644 index b0373eea..00000000 --- a/lua/harpoon/dev.lua +++ /dev/null @@ -1,48 +0,0 @@ --- Don't include this file, we should manually include it via --- require("harpoon.dev").reload(); --- --- A quick mapping can be setup using something like: --- :nmap rr :lua require("harpoon.dev").reload() -local M = {} - -function M.reload() - require("plenary.reload").reload_module("harpoon") -end - -local log_levels = { "trace", "debug", "info", "warn", "error", "fatal" } -local function set_log_level() - local log_level = vim.env.HARPOON_LOG or vim.g.harpoon_log_level - - for _, level in pairs(log_levels) do - if level == log_level then - return log_level - end - end - - return "warn" -- default, if user hasn't set to one from log_levels -end - -local log_level = set_log_level() -M.log = require("plenary.log").new({ - plugin = "harpoon", - level = log_level, -}) - -local log_key = os.time() - -local function override(key) - local fn = M.log[key] - M.log[key] = function(...) - fn(log_key, ...) - end -end - -for _, v in pairs(log_levels) do - override(v) -end - -function M.get_log_key() - return log_key -end - -return M diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 88aaa3d2..ed623024 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -1,268 +1,33 @@ -local Path = require("plenary.path") -local utils = require("harpoon.utils") -local Dev = require("harpoon.dev") -local log = Dev.log -local config_path = vim.fn.stdpath("config") -local data_path = vim.fn.stdpath("data") -local user_config = string.format("%s/harpoon.json", config_path) -local cache_config = string.format("%s/harpoon.json", data_path) +-- setup +-- read from a config file +-- -local M = {} - -local the_primeagen_harpoon = - vim.api.nvim_create_augroup("THE_PRIMEAGEN_HARPOON", { clear = true }) - -vim.api.nvim_create_autocmd({ "BufLeave, VimLeave" }, { - callback = function() - require("harpoon.mark").store_offset() - end, - group = the_primeagen_harpoon, -}) - ---[[ -{ - projects = { - ["/path/to/director"] = { - term = { - cmds = { - } - ... is there anything that could be options? - }, - mark = { - marks = { - } - ... is there anything that could be options? - } - } - }, - ... high level settings -} ---]] -HarpoonConfig = HarpoonConfig or {} - --- tbl_deep_extend does not work the way you would think -local function merge_table_impl(t1, t2) - for k, v in pairs(t2) do - if type(v) == "table" then - if type(t1[k]) == "table" then - merge_table_impl(t1[k], v) - else - t1[k] = v - end - else - t1[k] = v - end - end -end - -local function mark_config_key(global_settings) - global_settings = global_settings or M.get_global_settings() - if global_settings.mark_branch then - return utils.branch_key() - else - return utils.project_key() - end -end - -local function merge_tables(...) - log.trace("_merge_tables()") - local out = {} - for i = 1, select("#", ...) do - merge_table_impl(out, select(i, ...)) - end - return out -end - -local function ensure_correct_config(config) - log.trace("_ensure_correct_config()") - local projects = config.projects - local mark_key = mark_config_key(config.global_settings) - if projects[mark_key] == nil then - log.debug("ensure_correct_config(): No config found for:", mark_key) - projects[mark_key] = { - mark = { marks = {} }, - term = { - cmds = {}, - }, - } - end - - local proj = projects[mark_key] - if proj.mark == nil then - log.debug("ensure_correct_config(): No marks found for", mark_key) - proj.mark = { marks = {} } - end - - if proj.term == nil then - log.debug( - "ensure_correct_config(): No terminal commands found for", - mark_key - ) - proj.term = { cmds = {} } - end - - local marks = proj.mark.marks - - for idx, mark in pairs(marks) do - if type(mark) == "string" then - mark = { filename = mark } - marks[idx] = mark - end - - marks[idx].filename = utils.normalize_path(mark.filename) - end - - return config -end - -local function expand_dir(config) - log.trace("_expand_dir(): Config pre-expansion:", config) - - local projects = config.projects or {} - for k in pairs(projects) do - local expanded_path = Path.new(k):expand() - projects[expanded_path] = projects[k] - if expanded_path ~= k then - projects[k] = nil - end - end - - log.trace("_expand_dir(): Config post-expansion:", config) - return config -end - -function M.save() - -- first refresh from disk everything but our project - M.refresh_projects_b4update() - - log.trace("save(): Saving cache config to", cache_config) - Path:new(cache_config):write(vim.fn.json_encode(HarpoonConfig), "w") -end - -local function read_config(local_config) - log.trace("_read_config():", local_config) - return vim.json.decode(Path:new(local_config):read()) -end +---@alias HarpoonListItem {value: any, context: any} --- 1. saved. Where do we save? -function M.setup(config) - log.trace("setup(): Setting up...") +---@class HarpoonPartialConfigItem +---@field encode? (fun(list_item: HarpoonListItem): string) +---@field decode? (fun(obj: string): any) +---@field key? (fun(): string) +---@field display? (fun(list_item: HarpoonListItem): string) +---@field select? (fun(list_item: HarpoonListItem): nil) +---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) +---@field add? fun(): HarpoonListItem - if not config then - config = {} - end +---@class HarpoonSettings +---@field save_on_toggle boolean defaults to true +---@field jump_to_file_location boolean defaults to true - local ok, u_config = pcall(read_config, user_config) +---@class HarpoonConfig +---@field default HarpoonPartialConfigItem +---@field settings HarpoonSettings +---@field [string] HarpoonPartialConfigItem - if not ok then - log.debug("setup(): No user config present at", user_config) - u_config = {} - end - - local ok2, c_config = pcall(read_config, cache_config) - - if not ok2 then - log.debug("setup(): No cache config present at", cache_config) - c_config = {} - end - - local complete_config = merge_tables({ - projects = {}, - global_settings = { - ["save_on_toggle"] = false, - ["save_on_change"] = true, - ["enter_on_sendcmd"] = false, - ["tmux_autoclose_windows"] = false, - ["excluded_filetypes"] = { "harpoon" }, - ["mark_branch"] = false, - }, - }, expand_dir(c_config), expand_dir(u_config), expand_dir(config)) - - -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had - -- an object for vim.loop.cwd() - ensure_correct_config(complete_config) - - HarpoonConfig = complete_config - log.debug("setup(): Complete config", HarpoonConfig) - log.trace("setup(): log_key", Dev.get_log_key()) -end - -function M.get_global_settings() - log.trace("get_global_settings()") - return HarpoonConfig.global_settings -end - --- refresh all projects from disk, except our current one -function M.refresh_projects_b4update() - log.trace( - "refresh_projects_b4update(): refreshing other projects", - cache_config - ) - -- save current runtime version of our project config for merging back in later - local cwd = mark_config_key() - local current_p_config = { - projects = { - [cwd] = ensure_correct_config(HarpoonConfig).projects[cwd], - }, - } - - -- erase all projects from global config, will be loaded back from disk - HarpoonConfig.projects = nil - - -- this reads a stale version of our project but up-to-date versions - -- of all other projects - local ok2, c_config = pcall(read_config, cache_config) - - if not ok2 then - log.debug( - "refresh_projects_b4update(): No cache config present at", - cache_config - ) - c_config = { projects = {} } - end - -- don't override non-project config in HarpoonConfig later - c_config = { projects = c_config.projects } - - -- erase our own project, will be merged in from current_p_config later - c_config.projects[cwd] = nil - - local complete_config = merge_tables( - HarpoonConfig, - expand_dir(c_config), - expand_dir(current_p_config) - ) - - -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had - -- an object for vim.loop.cwd() - ensure_correct_config(complete_config) - - HarpoonConfig = complete_config - log.debug("refresh_projects_b4update(): Complete config", HarpoonConfig) - log.trace("refresh_projects_b4update(): log_key", Dev.get_log_key()) -end - -function M.get_term_config() - log.trace("get_term_config()") - return ensure_correct_config(HarpoonConfig).projects[utils.project_key()].term -end - -function M.get_mark_config() - log.trace("get_mark_config()") - return ensure_correct_config(HarpoonConfig).projects[mark_config_key()].mark -end - -function M.get_menu_config() - log.trace("get_menu_config()") - return HarpoonConfig.menu or {} -end +local M = {} --- should only be called for debug purposes -function M.print_config() - print(vim.inspect(HarpoonConfig)) +---@param c HarpoonConfig +function config(c) end --- Sets a default config with no values -M.setup() - return M + diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua new file mode 100644 index 00000000..3f0cc982 --- /dev/null +++ b/lua/harpoon/list.lua @@ -0,0 +1,111 @@ +local get_config = require "harpoon.config".get_config + +-- TODO: Define the config object + +--- @class Item +--- @field value string +--- @field context any + +--- create a table object to be new'd +--- @class List +--- @field config any +--- @field name string +--- @field items Item[] +local List = {} + +List.__index = List +function List:new(config, name, items) + return setmetatable({ + items = items, + config = config, + name = name, + }, self) +end + +function List:push(item) + table.insert(self.items, item) +end + +function List:addToFront(item) + table.insert(self.items, 1, item) +end + +function List:remove(item) + for i, v in ipairs(self.items) do + if get_config(self.config, self.name)(v, item) then + table.remove(self.items, i) + break + end + end +end + +function List:removeAt(index) + table.remove(self.items, index) +end + +function List:get(index) + return self.items[index] +end + +--- much inefficiencies. dun care +---@param displayed string[] +function List:resolve_displayed(displayed) + local not_found = {} + local config = get_config(self.config, self.name) + for _, v in ipairs(displayed) do + local found = false + for _, in_table in ipairs(self.items) do + found = config.display(in_table, v) + break + end + + if not found then + table.insert(not_found, v) + end + end + + for _, v in ipairs(not_found) do + self:remove(v) + end +end + +--- @return string[] +function List:display() + local out = {} + local config = get_config(self.config, self.name) + for _, v in ipairs(self.items) do + table.insert(out, config.display(v)) + end + + return out +end + +--- @return string[] +function List:encode() + local out = {} + local config = get_config(self.config, self.name) + for _, v in ipairs(self.items) do + table.insert(out, config.encode(v)) + end + + return out +end + +--- @return List +--- @param config HarpoonConfig +--- @param name string +--- @param items string[] +function List.decode(config, name, items) + local list_items = {} + local c = get_config(config, name) + + for _, item in ipairs(items) do + table.insert(list_items, c.decode(item)) + end + + return List:new(config, name, list_items) +end + + +return List + diff --git a/lua/harpoon/mark.lua b/lua/harpoon/mark.lua deleted file mode 100644 index 3f498d7c..00000000 --- a/lua/harpoon/mark.lua +++ /dev/null @@ -1,420 +0,0 @@ -local harpoon = require("harpoon") -local utils = require("harpoon.utils") -local log = require("harpoon.dev").log - --- I think that I may have to organize this better. I am not the biggest fan --- of procedural all the things -local M = {} -local callbacks = {} - --- I am trying to avoid over engineering the whole thing. We will likely only --- need one event emitted -local function emit_changed() - log.trace("_emit_changed()") - if harpoon.get_global_settings().save_on_change then - harpoon.save() - end - - if not callbacks["changed"] then - log.trace("_emit_changed(): no callbacks for 'changed', returning") - return - end - - for idx, cb in pairs(callbacks["changed"]) do - log.trace( - string.format( - "_emit_changed(): Running callback #%d for 'changed'", - idx - ) - ) - cb() - end -end - -local function filter_empty_string(list) - log.trace("_filter_empty_string()") - local next = {} - for idx = 1, #list do - if list[idx] ~= "" then - table.insert(next, list[idx].filename) - end - end - - return next -end - -local function get_first_empty_slot() - log.trace("_get_first_empty_slot()") - for idx = 1, M.get_length() do - local filename = M.get_marked_file_name(idx) - if filename == "" then - return idx - end - end - - return M.get_length() + 1 -end - -local function get_buf_name(id) - log.trace("_get_buf_name():", id) - if id == nil then - return utils.normalize_path(vim.api.nvim_buf_get_name(0)) - elseif type(id) == "string" then - return utils.normalize_path(id) - end - - local idx = M.get_index_of(id) - if M.valid_index(idx) then - return M.get_marked_file_name(idx) - end - -- - -- not sure what to do here... - -- - return "" -end - -local function create_mark(filename) - local cursor_pos = vim.api.nvim_win_get_cursor(0) - log.trace( - string.format( - "_create_mark(): Creating mark at row: %d, col: %d for %s", - cursor_pos[1], - cursor_pos[2], - filename - ) - ) - return { - filename = filename, - row = cursor_pos[1], - col = cursor_pos[2], - } -end - -local function mark_exists(buf_name) - log.trace("_mark_exists()") - for idx = 1, M.get_length() do - if M.get_marked_file_name(idx) == buf_name then - log.debug("_mark_exists(): Mark exists", buf_name) - return true - end - end - - log.debug("_mark_exists(): Mark doesn't exist", buf_name) - return false -end - -local function validate_buf_name(buf_name) - log.trace("_validate_buf_name():", buf_name) - if buf_name == "" or buf_name == nil then - log.error( - "_validate_buf_name(): Not a valid name for a mark,", - buf_name - ) - error("Couldn't find a valid file name to mark, sorry.") - return - end -end - -local function filter_filetype() - local current_filetype = vim.bo.filetype - local excluded_filetypes = harpoon.get_global_settings().excluded_filetypes - - if current_filetype == "harpoon" then - log.error("filter_filetype(): You can't add harpoon to the harpoon") - error("You can't add harpoon to the harpoon") - return - end - - if vim.tbl_contains(excluded_filetypes, current_filetype) then - log.error( - 'filter_filetype(): This filetype cannot be added or is included in the "excluded_filetypes" option' - ) - error( - 'This filetype cannot be added or is included in the "excluded_filetypes" option' - ) - return - end -end - -function M.get_index_of(item, marks) - log.trace("get_index_of():", item) - if item == nil then - log.error( - "get_index_of(): Function has been supplied with a nil value." - ) - error( - "You have provided a nil value to Harpoon, please provide a string rep of the file or the file idx." - ) - return - end - - if type(item) == "string" then - local relative_item = utils.normalize_path(item) - if marks == nil then - marks = harpoon.get_mark_config().marks - end - for idx = 1, M.get_length(marks) do - if marks[idx] and marks[idx].filename == relative_item then - return idx - end - end - - return nil - end - - -- TODO move this to a "harpoon_" prefix or global config? - if vim.g.manage_a_mark_zero_index then - item = item + 1 - end - - if item <= M.get_length() and item >= 1 then - return item - end - - log.debug("get_index_of(): No item found,", item) - return nil -end - -function M.status(bufnr) - log.trace("status()") - local buf_name - if bufnr then - buf_name = vim.api.nvim_buf_get_name(bufnr) - else - buf_name = vim.api.nvim_buf_get_name(0) - end - - local norm_name = utils.normalize_path(buf_name) - local idx = M.get_index_of(norm_name) - - if M.valid_index(idx) then - return "M" .. idx - end - return "" -end - -function M.valid_index(idx, marks) - log.trace("valid_index():", idx) - if idx == nil then - return false - end - - local file_name = M.get_marked_file_name(idx, marks) - return file_name ~= nil and file_name ~= "" -end - -function M.add_file(file_name_or_buf_id) - filter_filetype() - local buf_name = get_buf_name(file_name_or_buf_id) - log.trace("add_file():", buf_name) - - if M.valid_index(M.get_index_of(buf_name)) then - -- we don't alter file layout. - return - end - - validate_buf_name(buf_name) - - local found_idx = get_first_empty_slot() - harpoon.get_mark_config().marks[found_idx] = create_mark(buf_name) - M.remove_empty_tail(false) - emit_changed() -end - --- _emit_on_changed == false should only be used internally -function M.remove_empty_tail(_emit_on_changed) - log.trace("remove_empty_tail()") - _emit_on_changed = _emit_on_changed == nil or _emit_on_changed - local config = harpoon.get_mark_config() - local found = false - - for i = M.get_length(), 1, -1 do - local filename = M.get_marked_file_name(i) - if filename ~= "" then - return - end - - if filename == "" then - table.remove(config.marks, i) - found = found or _emit_on_changed - end - end - - if found then - emit_changed() - end -end - -function M.store_offset() - log.trace("store_offset()") - local ok, res = pcall(function() - local marks = harpoon.get_mark_config().marks - local buf_name = get_buf_name() - local idx = M.get_index_of(buf_name, marks) - if not M.valid_index(idx, marks) then - return - end - - local cursor_pos = vim.api.nvim_win_get_cursor(0) - log.debug( - string.format( - "store_offset(): Stored row: %d, col: %d", - cursor_pos[1], - cursor_pos[2] - ) - ) - marks[idx].row = cursor_pos[1] - marks[idx].col = cursor_pos[2] - end) - - if not ok then - log.warn("store_offset(): Could not store offset:", res) - end - - emit_changed() -end - -function M.rm_file(file_name_or_buf_id) - local buf_name = get_buf_name(file_name_or_buf_id) - local idx = M.get_index_of(buf_name) - log.trace("rm_file(): Removing mark at id", idx) - - if not M.valid_index(idx) then - log.debug("rm_file(): No mark exists for id", file_name_or_buf_id) - return - end - - harpoon.get_mark_config().marks[idx] = create_mark("") - M.remove_empty_tail(false) - emit_changed() -end - -function M.clear_all() - harpoon.get_mark_config().marks = {} - log.trace("clear_all(): Clearing all marks.") - emit_changed() -end - ---- ENTERPRISE PROGRAMMING -function M.get_marked_file(idxOrName) - log.trace("get_marked_file():", idxOrName) - if type(idxOrName) == "string" then - idxOrName = M.get_index_of(idxOrName) - end - return harpoon.get_mark_config().marks[idxOrName] -end - -function M.get_marked_file_name(idx, marks) - local mark - if marks ~= nil then - mark = marks[idx] - else - mark = harpoon.get_mark_config().marks[idx] - end - log.trace("get_marked_file_name():", mark and mark.filename) - return mark and mark.filename -end - -function M.get_length(marks) - if marks == nil then - marks = harpoon.get_mark_config().marks - end - log.trace("get_length()") - return table.maxn(marks) -end - -function M.set_current_at(idx) - filter_filetype() - local buf_name = get_buf_name() - log.trace("set_current_at(): Setting id", idx, buf_name) - local config = harpoon.get_mark_config() - local current_idx = M.get_index_of(buf_name) - - -- Remove it if it already exists - if M.valid_index(current_idx) then - config.marks[current_idx] = create_mark("") - end - - config.marks[idx] = create_mark(buf_name) - - for i = 1, M.get_length() do - if not config.marks[i] then - config.marks[i] = create_mark("") - end - end - - emit_changed() -end - -function M.to_quickfix_list() - log.trace("to_quickfix_list(): Sending marks to quickfix list.") - local config = harpoon.get_mark_config() - local file_list = filter_empty_string(config.marks) - local qf_list = {} - for idx = 1, #file_list do - local mark = M.get_marked_file(idx) - qf_list[idx] = { - text = string.format("%d: %s", idx, file_list[idx]), - filename = mark.filename, - row = mark.row, - col = mark.col, - } - end - log.debug("to_quickfix_list(): qf_list:", qf_list) - vim.fn.setqflist(qf_list) -end - -function M.set_mark_list(new_list) - log.trace("set_mark_list(): New list:", new_list) - - local config = harpoon.get_mark_config() - - for k, v in pairs(new_list) do - if type(v) == "string" then - local mark = M.get_marked_file(v) - if not mark then - mark = create_mark(v) - end - - new_list[k] = mark - end - end - - config.marks = new_list - emit_changed() -end - -function M.toggle_file(file_name_or_buf_id) - local buf_name = get_buf_name(file_name_or_buf_id) - log.trace("toggle_file():", buf_name) - - validate_buf_name(buf_name) - - if mark_exists(buf_name) then - M.rm_file(buf_name) - print("Mark removed") - log.debug("toggle_file(): Mark removed") - else - M.add_file(buf_name) - print("Mark added") - log.debug("toggle_file(): Mark added") - end -end - -function M.get_current_index() - log.trace("get_current_index()") - return M.get_index_of(vim.api.nvim_buf_get_name(0)) -end - -function M.on(event, cb) - log.trace("on():", event) - if not callbacks[event] then - log.debug("on(): no callbacks yet for", event) - callbacks[event] = {} - end - - table.insert(callbacks[event], cb) - log.debug("on(): All callbacks:", callbacks) -end - -return M diff --git a/lua/harpoon/term.lua b/lua/harpoon/term.lua deleted file mode 100644 index 3c429a29..00000000 --- a/lua/harpoon/term.lua +++ /dev/null @@ -1,146 +0,0 @@ -local harpoon = require("harpoon") -local log = require("harpoon.dev").log -local global_config = harpoon.get_global_settings() - -local M = {} -local terminals = {} - -local function create_terminal(create_with) - if not create_with then - create_with = ":terminal" - end - log.trace("term: _create_terminal(): Init:", create_with) - local current_id = vim.api.nvim_get_current_buf() - - vim.cmd(create_with) - local buf_id = vim.api.nvim_get_current_buf() - local term_id = vim.b.terminal_job_id - - if term_id == nil then - log.error("_create_terminal(): term_id is nil") - -- TODO: Throw an error? - return nil - end - - -- Make sure the term buffer has "hidden" set so it doesn't get thrown - -- away and cause an error - vim.api.nvim_buf_set_option(buf_id, "bufhidden", "hide") - - -- Resets the buffer back to the old one - vim.api.nvim_set_current_buf(current_id) - return buf_id, term_id -end - -local function find_terminal(args) - log.trace("term: _find_terminal(): Terminal:", args) - if type(args) == "number" then - args = { idx = args } - end - local term_handle = terminals[args.idx] - if not term_handle or not vim.api.nvim_buf_is_valid(term_handle.buf_id) then - local buf_id, term_id = create_terminal(args.create_with) - if buf_id == nil then - error("Failed to find and create terminal.") - return - end - - term_handle = { - buf_id = buf_id, - term_id = term_id, - } - terminals[args.idx] = term_handle - end - return term_handle -end - -local function get_first_empty_slot() - log.trace("_get_first_empty_slot()") - for idx, cmd in pairs(harpoon.get_term_config().cmds) do - if cmd == "" then - return idx - end - end - return M.get_length() + 1 -end - -function M.gotoTerminal(idx) - log.trace("term: gotoTerminal(): Terminal:", idx) - local term_handle = find_terminal(idx) - - vim.api.nvim_set_current_buf(term_handle.buf_id) -end - -function M.sendCommand(idx, cmd, ...) - log.trace("term: sendCommand(): Terminal:", idx) - local term_handle = find_terminal(idx) - - if type(cmd) == "number" then - cmd = harpoon.get_term_config().cmds[cmd] - end - - if global_config.enter_on_sendcmd then - cmd = cmd .. "\n" - end - - if cmd then - log.debug("sendCommand:", cmd) - vim.api.nvim_chan_send(term_handle.term_id, string.format(cmd, ...)) - end -end - -function M.clear_all() - log.trace("term: clear_all(): Clearing all terminals.") - for _, term in ipairs(terminals) do - vim.api.nvim_buf_delete(term.buf_id, { force = true }) - end - terminals = {} -end - -function M.get_length() - log.trace("_get_length()") - return table.maxn(harpoon.get_term_config().cmds) -end - -function M.valid_index(idx) - if idx == nil or idx > M.get_length() or idx <= 0 then - return false - end - return true -end - -function M.emit_changed() - log.trace("_emit_changed()") - if harpoon.get_global_settings().save_on_change then - harpoon.save() - end -end - -function M.add_cmd(cmd) - log.trace("add_cmd()") - local found_idx = get_first_empty_slot() - harpoon.get_term_config().cmds[found_idx] = cmd - M.emit_changed() -end - -function M.rm_cmd(idx) - log.trace("rm_cmd()") - if not M.valid_index(idx) then - log.debug("rm_cmd(): no cmd exists for index", idx) - return - end - table.remove(harpoon.get_term_config().cmds, idx) - M.emit_changed() -end - -function M.set_cmd_list(new_list) - log.trace("set_cmd_list(): New list:", new_list) - for k in pairs(harpoon.get_term_config().cmds) do - harpoon.get_term_config().cmds[k] = nil - end - for k, v in pairs(new_list) do - harpoon.get_term_config().cmds[k] = v - end - M.emit_changed() -end - -return M diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua new file mode 100644 index 00000000..ef6ef7b5 --- /dev/null +++ b/lua/harpoon/test/list_spec.lua @@ -0,0 +1,34 @@ +local List = require("harpoon.list") +local Config = require("harpoon.config") +local eq = assert.are.same + +describe("list", function() + it("decode", function() + + local config = Config.merge_config({ + foo = { + decode = function(item) + -- split item on : + local parts = vim.split(item, ":") + return { + value = parts, + context = nil, + } + end, + + display = function(item) + return table.concat(item.value, "---") + end + } + }) + + local list = List.decode(config, "foo", {"foo:bar", "baz:qux"}) + local displayed = list:display() + + eq(displayed, { + "foo---bar", + "baz---qux", + }) + end) +end) + diff --git a/lua/harpoon/test/manage-a-mark.lua b/lua/harpoon/test/manage-a-mark.lua deleted file mode 100644 index df1969dc..00000000 --- a/lua/harpoon/test/manage-a-mark.lua +++ /dev/null @@ -1,3 +0,0 @@ --- TODO: Harpooned --- local Marker = require('harpoon.mark') --- local eq = assert.are.same diff --git a/lua/harpoon/test/manage_cmd_spec.lua b/lua/harpoon/test/manage_cmd_spec.lua deleted file mode 100644 index f63ad515..00000000 --- a/lua/harpoon/test/manage_cmd_spec.lua +++ /dev/null @@ -1,141 +0,0 @@ -local harpoon = require("harpoon") -local term = require("harpoon.term") - -local function assert_table_equals(tbl1, tbl2) - if #tbl1 ~= #tbl2 then - assert(false, "" .. #tbl1 .. " != " .. #tbl2) - end - for i = 1, #tbl1 do - if tbl1[i] ~= tbl2[i] then - assert.equals(tbl1[i], tbl2[i]) - end - end -end - -describe("basic functionalities", function() - local emitted - local cmds - - before_each(function() - emitted = false - cmds = {} - harpoon.get_term_config = function() - return { - cmds = cmds, - } - end - term.emit_changed = function() - emitted = true - end - end) - - it("add_cmd for empty", function() - term.add_cmd("cmake ..") - local expected_result = { - "cmake ..", - } - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - assert.equals(emitted, true) - end) - - it("add_cmd for non_empty", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - local expected_result = { - "cmake ..", - "make", - "ninja", - } - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - assert.equals(emitted, true) - end) - - it("rm_cmd: removing a valid element", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - term.rm_cmd(2) - local expected_result = { - "cmake ..", - "ninja", - } - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - assert.equals(emitted, true) - end) - - it("rm_cmd: remove first element", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - term.rm_cmd(1) - local expected_result = { - "make", - "ninja", - } - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - assert.equals(emitted, true) - end) - - it("rm_cmd: remove last element", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - term.rm_cmd(3) - local expected_result = { - "cmake ..", - "make", - } - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - assert.equals(emitted, true) - end) - - it("rm_cmd: trying to remove invalid element", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - term.rm_cmd(5) - local expected_result = { - "cmake ..", - "make", - "ninja", - } - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - assert.equals(emitted, true) - term.rm_cmd(0) - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - term.rm_cmd(-1) - assert_table_equals(harpoon.get_term_config().cmds, expected_result) - end) - - it("get_length", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - assert.equals(term.get_length(), 3) - end) - - it("valid_index", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - assert(term.valid_index(1)) - assert(term.valid_index(2)) - assert(term.valid_index(3)) - assert(not term.valid_index(0)) - assert(not term.valid_index(-1)) - assert(not term.valid_index(4)) - end) - - it("set_cmd_list", function() - term.add_cmd("cmake ..") - term.add_cmd("make") - term.add_cmd("ninja") - term.set_cmd_list({ "make uninstall", "make install" }) - local expected_result = { - "make uninstall", - "make install", - } - assert_table_equals(expected_result, harpoon.get_term_config().cmds) - end) -end) diff --git a/lua/harpoon/tmux.lua b/lua/harpoon/tmux.lua deleted file mode 100644 index 2c04c80b..00000000 --- a/lua/harpoon/tmux.lua +++ /dev/null @@ -1,230 +0,0 @@ -local harpoon = require("harpoon") -local log = require("harpoon.dev").log -local global_config = harpoon.get_global_settings() -local utils = require("harpoon.utils") - -local M = {} -local tmux_windows = {} - -if global_config.tmux_autoclose_windows then - local harpoon_tmux_group = - vim.api.nvim_create_augroup("HARPOON_TMUX", { clear = true }) - - vim.api.nvim_create_autocmd("VimLeave", { - callback = function() - require("harpoon.tmux").clear_all() - end, - group = harpoon_tmux_group, - }) -end - -local function create_terminal() - log.trace("tmux: _create_terminal())") - - local window_id - - -- Create a new tmux window and store the window id - local out, ret, _ = utils.get_os_command_output({ - "tmux", - "new-window", - "-P", - "-F", - "#{pane_id}", - }, vim.loop.cwd()) - - if ret == 0 then - window_id = out[1]:sub(2) - end - - if window_id == nil then - log.error("tmux: _create_terminal(): window_id is nil") - return nil - end - - return window_id -end - --- Checks if the tmux window with the given window id exists -local function terminal_exists(window_id) - log.trace("_terminal_exists(): Window:", window_id) - - local exists = false - - local window_list, _, _ = utils.get_os_command_output({ - "tmux", - "list-windows", - }, vim.loop.cwd()) - - -- This has to be done this way because tmux has-session does not give - -- updated results - for _, line in pairs(window_list) do - local window_info = utils.split_string(line, "@")[2] - - if string.find(window_info, string.sub(window_id, 2)) then - exists = true - end - end - - return exists -end - -local function find_terminal(args) - log.trace("tmux: _find_terminal(): Window:", args) - - if type(args) == "string" then - -- assume args is a valid tmux target identifier - -- if invalid, the error returned by tmux will be thrown - return { - window_id = args, - pane = true, - } - end - - if type(args) == "number" then - args = { idx = args } - end - - local window_handle = tmux_windows[args.idx] - local window_exists - - if window_handle then - window_exists = terminal_exists(window_handle.window_id) - end - - if not window_handle or not window_exists then - local window_id = create_terminal() - - if window_id == nil then - error("Failed to find and create tmux window.") - return - end - - window_handle = { - window_id = "%" .. window_id, - } - - tmux_windows[args.idx] = window_handle - end - - return window_handle -end - -local function get_first_empty_slot() - log.trace("_get_first_empty_slot()") - for idx, cmd in pairs(harpoon.get_term_config().cmds) do - if cmd == "" then - return idx - end - end - return M.get_length() + 1 -end - -function M.gotoTerminal(idx) - log.trace("tmux: gotoTerminal(): Window:", idx) - local window_handle = find_terminal(idx) - - local _, ret, stderr = utils.get_os_command_output({ - "tmux", - window_handle.pane and "select-pane" or "select-window", - "-t", - window_handle.window_id, - }, vim.loop.cwd()) - - if ret ~= 0 then - error("Failed to go to terminal." .. stderr[1]) - end -end - -function M.sendCommand(idx, cmd, ...) - log.trace("tmux: sendCommand(): Window:", idx) - local window_handle = find_terminal(idx) - - if type(cmd) == "number" then - cmd = harpoon.get_term_config().cmds[cmd] - end - - if global_config.enter_on_sendcmd then - cmd = cmd .. "\n" - end - - if cmd then - log.debug("sendCommand:", cmd) - - local _, ret, stderr = utils.get_os_command_output({ - "tmux", - "send-keys", - "-t", - window_handle.window_id, - string.format(cmd, ...), - }, vim.loop.cwd()) - - if ret ~= 0 then - error("Failed to send command. " .. stderr[1]) - end - end -end - -function M.clear_all() - log.trace("tmux: clear_all(): Clearing all tmux windows.") - - for _, window in pairs(tmux_windows) do - -- Delete the current tmux window - utils.get_os_command_output({ - "tmux", - "kill-window", - "-t", - window.window_id, - }, vim.loop.cwd()) - end - - tmux_windows = {} -end - -function M.get_length() - log.trace("_get_length()") - return table.maxn(harpoon.get_term_config().cmds) -end - -function M.valid_index(idx) - if idx == nil or idx > M.get_length() or idx <= 0 then - return false - end - return true -end - -function M.emit_changed() - log.trace("_emit_changed()") - if harpoon.get_global_settings().save_on_change then - harpoon.save() - end -end - -function M.add_cmd(cmd) - log.trace("add_cmd()") - local found_idx = get_first_empty_slot() - harpoon.get_term_config().cmds[found_idx] = cmd - M.emit_changed() -end - -function M.rm_cmd(idx) - log.trace("rm_cmd()") - if not M.valid_index(idx) then - log.debug("rm_cmd(): no cmd exists for index", idx) - return - end - table.remove(harpoon.get_term_config().cmds, idx) - M.emit_changed() -end - -function M.set_cmd_list(new_list) - log.trace("set_cmd_list(): New list:", new_list) - for k in pairs(harpoon.get_term_config().cmds) do - harpoon.get_term_config().cmds[k] = nil - end - for k, v in pairs(new_list) do - harpoon.get_term_config().cmds[k] = v - end - M.emit_changed() -end - -return M diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua deleted file mode 100644 index fcb310b0..00000000 --- a/lua/harpoon/ui.lua +++ /dev/null @@ -1,284 +0,0 @@ -local harpoon = require("harpoon") -local popup = require("plenary.popup") -local Marked = require("harpoon.mark") -local utils = require("harpoon.utils") -local log = require("harpoon.dev").log - -local M = {} - -Harpoon_win_id = nil -Harpoon_bufh = nil - --- We save before we close because we use the state of the buffer as the list --- of items. -local function close_menu(force_save) - force_save = force_save or false - local global_config = harpoon.get_global_settings() - - if global_config.save_on_toggle or force_save then - require("harpoon.ui").on_menu_save() - end - - vim.api.nvim_win_close(Harpoon_win_id, true) - - Harpoon_win_id = nil - Harpoon_bufh = nil -end - -local function create_window() - log.trace("_create_window()") - local config = harpoon.get_menu_config() - local width = config.width or 60 - local height = config.height or 10 - local borderchars = config.borderchars - or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } - local bufnr = vim.api.nvim_create_buf(false, false) - - local Harpoon_win_id, win = popup.create(bufnr, { - title = "Harpoon", - highlight = "HarpoonWindow", - line = math.floor(((vim.o.lines - height) / 2) - 1), - col = math.floor((vim.o.columns - width) / 2), - minwidth = width, - minheight = height, - borderchars = borderchars, - }) - - vim.api.nvim_win_set_option( - win.border.win_id, - "winhl", - "Normal:HarpoonBorder" - ) - - return { - bufnr = bufnr, - win_id = Harpoon_win_id, - } -end - -local function get_menu_items() - log.trace("_get_menu_items()") - local lines = vim.api.nvim_buf_get_lines(Harpoon_bufh, 0, -1, true) - local indices = {} - - for _, line in pairs(lines) do - if not utils.is_white_space(line) then - table.insert(indices, line) - end - end - - return indices -end - -function M.toggle_quick_menu() - log.trace("toggle_quick_menu()") - if Harpoon_win_id ~= nil and vim.api.nvim_win_is_valid(Harpoon_win_id) then - close_menu() - return - end - - local win_info = create_window() - local contents = {} - local global_config = harpoon.get_global_settings() - - Harpoon_win_id = win_info.win_id - Harpoon_bufh = win_info.bufnr - - for idx = 1, Marked.get_length() do - local file = Marked.get_marked_file_name(idx) - if file == "" then - file = "(empty)" - end - contents[idx] = string.format("%s", file) - end - - vim.api.nvim_win_set_option(Harpoon_win_id, "number", true) - vim.api.nvim_buf_set_name(Harpoon_bufh, "harpoon-menu") - vim.api.nvim_buf_set_lines(Harpoon_bufh, 0, #contents, false, contents) - vim.api.nvim_buf_set_option(Harpoon_bufh, "filetype", "harpoon") - vim.api.nvim_buf_set_option(Harpoon_bufh, "buftype", "acwrite") - vim.api.nvim_buf_set_option(Harpoon_bufh, "bufhidden", "delete") - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "q", - "lua require('harpoon.ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "", - "lua require('harpoon.ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "", - "lua require('harpoon.ui').select_menu_item()", - {} - ) - vim.cmd( - string.format( - "autocmd BufWriteCmd lua require('harpoon.ui').on_menu_save()", - Harpoon_bufh - ) - ) - if global_config.save_on_change then - vim.cmd( - string.format( - "autocmd TextChanged,TextChangedI lua require('harpoon.ui').on_menu_save()", - Harpoon_bufh - ) - ) - end - vim.cmd( - string.format( - "autocmd BufModifiedSet set nomodified", - Harpoon_bufh - ) - ) - vim.cmd( - "autocmd BufLeave ++nested ++once silent lua require('harpoon.ui').toggle_quick_menu()" - ) -end - -function M.select_menu_item() - local idx = vim.fn.line(".") - close_menu(true) - M.nav_file(idx) -end - -function M.on_menu_save() - log.trace("on_menu_save()") - Marked.set_mark_list(get_menu_items()) -end - -local function get_or_create_buffer(filename) - local buf_exists = vim.fn.bufexists(filename) ~= 0 - if buf_exists then - return vim.fn.bufnr(filename) - end - - return vim.fn.bufadd(filename) -end - -function M.nav_file(id) - log.trace("nav_file(): Navigating to", id) - local idx = Marked.get_index_of(id) - if not Marked.valid_index(idx) then - log.debug("nav_file(): No mark exists for id", id) - return - end - - local mark = Marked.get_marked_file(idx) - local filename = mark.filename - if filename:sub(1, 1) ~= "/" then - filename = vim.loop.cwd() .. "/" .. mark.filename - end - local buf_id = get_or_create_buffer(filename) - local set_row = not vim.api.nvim_buf_is_loaded(buf_id) - - vim.api.nvim_set_current_buf(buf_id) - vim.api.nvim_buf_set_option(buf_id, "buflisted", true) - if set_row and mark.row and mark.col then - vim.cmd(string.format(":call cursor(%d, %d)", mark.row, mark.col)) - log.debug( - string.format( - "nav_file(): Setting cursor to row: %d, col: %d", - mark.row, - mark.col - ) - ) - end -end - -function M.location_window(options) - local default_options = { - relative = "editor", - style = "minimal", - width = 30, - height = 15, - row = 2, - col = 2, - } - options = vim.tbl_extend("keep", options, default_options) - - local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true) - local win_id = vim.api.nvim_open_win(bufnr, true, options) - - return { - bufnr = bufnr, - win_id = win_id, - } -end - -function M.notification(text) - local win_stats = vim.api.nvim_list_uis()[1] - local win_width = win_stats.width - - local prev_win = vim.api.nvim_get_current_win() - - local info = M.location_window({ - width = 20, - height = 2, - row = 1, - col = win_width - 21, - }) - - vim.api.nvim_buf_set_lines( - info.bufnr, - 0, - 5, - false, - { "!!! Notification", text } - ) - vim.api.nvim_set_current_win(prev_win) - - return { - bufnr = info.bufnr, - win_id = info.win_id, - } -end - -function M.close_notification(bufnr) - vim.api.nvim_buf_delete(bufnr) -end - -function M.nav_next() - log.trace("nav_next()") - local current_index = Marked.get_current_index() - local number_of_items = Marked.get_length() - - if current_index == nil then - current_index = 1 - else - current_index = current_index + 1 - end - - if current_index > number_of_items then - current_index = 1 - end - M.nav_file(current_index) -end - -function M.nav_prev() - log.trace("nav_prev()") - local current_index = Marked.get_current_index() - local number_of_items = Marked.get_length() - - if current_index == nil then - current_index = number_of_items - else - current_index = current_index - 1 - end - - if current_index < 1 then - current_index = number_of_items - end - - M.nav_file(current_index) -end - -return M diff --git a/lua/harpoon/utils.lua b/lua/harpoon/utils.lua deleted file mode 100644 index 0f600e64..00000000 --- a/lua/harpoon/utils.lua +++ /dev/null @@ -1,64 +0,0 @@ -local Path = require("plenary.path") -local data_path = vim.fn.stdpath("data") -local Job = require("plenary.job") - -local M = {} - -M.data_path = data_path - -function M.project_key() - return vim.loop.cwd() -end - -function M.branch_key() - -- `git branch --show-current` requires Git v2.22.0+ so going with more - -- widely available command - local branch = M.get_os_command_output({ - "git", - "rev-parse", - "--abbrev-ref", - "HEAD", - })[1] - - if branch then - return vim.loop.cwd() .. "-" .. branch - else - return M.project_key() - end -end - -function M.normalize_path(item) - return Path:new(item):make_relative(M.project_key()) -end - -function M.get_os_command_output(cmd, cwd) - if type(cmd) ~= "table" then - print("Harpoon: [get_os_command_output]: cmd has to be a table") - return {} - end - local command = table.remove(cmd, 1) - local stderr = {} - local stdout, ret = Job:new({ - command = command, - args = cmd, - cwd = cwd, - on_stderr = function(_, data) - table.insert(stderr, data) - end, - }):sync() - return stdout, ret, stderr -end - -function M.split_string(str, delimiter) - local result = {} - for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do - table.insert(result, match) - end - return result -end - -function M.is_white_space(str) - return str:gsub("%s", "") == "" -end - -return M From 2aa20a84da76dd5f0d7a9a39644e3d2c05bdd143 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 11:57:35 -0600 Subject: [PATCH 02/80] chore: moved types around --- lua/harpoon/config.lua | 25 +++++++++++++++++++++++++ lua/harpoon/init.lua | 20 -------------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 01eb6130..717ba74d 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -1,9 +1,31 @@ local M = {} +---@alias HarpoonListItem {value: any, context: any} + +---@class HarpoonPartialConfigItem +---@field encode? (fun(list_item: HarpoonListItem): string) +---@field decode? (fun(obj: string): any) +---@field key? (fun(): string) +---@field display? (fun(list_item: HarpoonListItem): string) +---@field select? (fun(list_item: HarpoonListItem): nil) +---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) +---@field add? fun(): HarpoonListItem + +---@class HarpoonSettings +---@field save_on_toggle boolean defaults to true +---@field jump_to_file_location boolean defaults to true + +---@class HarpoonConfig +---@field default HarpoonPartialConfigItem +---@field settings HarpoonSettings +---@field [string] HarpoonPartialConfigItem + +---@return HarpoonConfig function M.get_config(config, name) return vim.tbl_extend("force", {}, config.default, config[name] or {}) end +---@return HarpoonConfig function M.get_default_config() return { settings = { @@ -51,6 +73,9 @@ function M.get_default_config() } end +---@param partial_config HarpoonConfig +---@param latest_config HarpoonConfig? +---@return HarpoonConfig function M.merge_config(partial_config, latest_config) local config = latest_config or M.get_default_config() for k, v in pairs(partial_config) do diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index ed623024..d7de3c89 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -3,26 +3,6 @@ -- read from a config file -- ----@alias HarpoonListItem {value: any, context: any} - ----@class HarpoonPartialConfigItem ----@field encode? (fun(list_item: HarpoonListItem): string) ----@field decode? (fun(obj: string): any) ----@field key? (fun(): string) ----@field display? (fun(list_item: HarpoonListItem): string) ----@field select? (fun(list_item: HarpoonListItem): nil) ----@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ----@field add? fun(): HarpoonListItem - ----@class HarpoonSettings ----@field save_on_toggle boolean defaults to true ----@field jump_to_file_location boolean defaults to true - ----@class HarpoonConfig ----@field default HarpoonPartialConfigItem ----@field settings HarpoonSettings ----@field [string] HarpoonPartialConfigItem - local M = {} ---@param c HarpoonConfig From 9e60c390570faabdd705fd976074af8447bd996f Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 13:34:08 -0600 Subject: [PATCH 03/80] feat: config add now produces a correct HarpoonFileListItem --- HARPOON2.md | 2 +- lua/harpoon/config.lua | 45 +++++++++++++++++++++++++++----- lua/harpoon/list.lua | 36 ++++++++++++------------- lua/harpoon/test/config_spec.lua | 33 +++++++++++++++++++++++ 4 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 lua/harpoon/test/config_spec.lua diff --git a/HARPOON2.md b/HARPOON2.md index b79b7ef7..e9df0422 100644 --- a/HARPOON2.md +++ b/HARPOON2.md @@ -37,7 +37,7 @@ harpoon.setup({ # question mark: what does it take to support custom things in here? # potentially subject to change - add = function() => void + add = function() HarpoonListItem } frecency = { diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 717ba74d..c10a6643 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -1,6 +1,7 @@ local M = {} ---@alias HarpoonListItem {value: any, context: any} +---@alias HarpoonListFileItem {value: string, context: {row: number, col: number}} ---@class HarpoonPartialConfigItem ---@field encode? (fun(list_item: HarpoonListItem): string) @@ -20,7 +21,13 @@ local M = {} ---@field settings HarpoonSettings ---@field [string] HarpoonPartialConfigItem ----@return HarpoonConfig +---@class HarpoonPartialConfig +---@field default HarpoonPartialConfigItem? +---@field settings HarpoonSettings? +---@field [string] HarpoonPartialConfigItem + + +---@return HarpoonPartialConfigItem function M.get_config(config, name) return vim.tbl_extend("force", {}, config.default, config[name] or {}) end @@ -55,9 +62,26 @@ function M.get_default_config() return list_item.value end, - ---@param list_item HarpoonListItem - select = function(list_item) - error("please implement select") + ---@param file_item HarpoonListFileItem + select = function(file_item) + if file_item == nil then + return + end + + local bufnr = vim.fn.bufnr(file_item.value) + local set_position = false + if bufnr == -1 then + set_position = true + bufnr = vim.fn.bufnr(file_item.value, true) + end + + vim.api.nvim_set_current_buf(bufnr) + if set_position then + vim.api.nvim_win_set_cursor(0, { + file_item.context.row or 1, + file_item.context.col or 0 + }) + end end, ---@param list_item_a HarpoonListItem @@ -66,14 +90,23 @@ function M.get_default_config() return list_item_a.value == list_item_b.value end, + ---@return HarpoonListItem add = function() - error("please implement add") + local name = vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + local pos = vim.api.nvim_win_get_cursor(0) + return { + value = name, + context = { + row = pos[1], + col = pos[2], + } + } end, } } end ----@param partial_config HarpoonConfig +---@param partial_config HarpoonPartialConfig ---@param latest_config HarpoonConfig? ---@return HarpoonConfig function M.merge_config(partial_config, latest_config) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 3f0cc982..6ba17003 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -2,19 +2,19 @@ local get_config = require "harpoon.config".get_config -- TODO: Define the config object ---- @class Item +--- @class HarpoonItem --- @field value string --- @field context any --- create a table object to be new'd ---- @class List +--- @class HarpoonList --- @field config any --- @field name string ---- @field items Item[] -local List = {} +--- @field items HarpoonItem[] +local HarpoonList = {} -List.__index = List -function List:new(config, name, items) +HarpoonList.__index = HarpoonList +function HarpoonList:new(config, name, items) return setmetatable({ items = items, config = config, @@ -22,15 +22,15 @@ function List:new(config, name, items) }, self) end -function List:push(item) +function HarpoonList:push(item) table.insert(self.items, item) end -function List:addToFront(item) +function HarpoonList:addToFront(item) table.insert(self.items, 1, item) end -function List:remove(item) +function HarpoonList:remove(item) for i, v in ipairs(self.items) do if get_config(self.config, self.name)(v, item) then table.remove(self.items, i) @@ -39,17 +39,17 @@ function List:remove(item) end end -function List:removeAt(index) +function HarpoonList:removeAt(index) table.remove(self.items, index) end -function List:get(index) +function HarpoonList:get(index) return self.items[index] end --- much inefficiencies. dun care ---@param displayed string[] -function List:resolve_displayed(displayed) +function HarpoonList:resolve_displayed(displayed) local not_found = {} local config = get_config(self.config, self.name) for _, v in ipairs(displayed) do @@ -70,7 +70,7 @@ function List:resolve_displayed(displayed) end --- @return string[] -function List:display() +function HarpoonList:display() local out = {} local config = get_config(self.config, self.name) for _, v in ipairs(self.items) do @@ -81,7 +81,7 @@ function List:display() end --- @return string[] -function List:encode() +function HarpoonList:encode() local out = {} local config = get_config(self.config, self.name) for _, v in ipairs(self.items) do @@ -91,11 +91,11 @@ function List:encode() return out end ---- @return List +--- @return HarpoonList --- @param config HarpoonConfig --- @param name string --- @param items string[] -function List.decode(config, name, items) +function HarpoonList.decode(config, name, items) local list_items = {} local c = get_config(config, name) @@ -103,9 +103,9 @@ function List.decode(config, name, items) table.insert(list_items, c.decode(item)) end - return List:new(config, name, list_items) + return HarpoonList:new(config, name, list_items) end -return List +return HarpoonList diff --git a/lua/harpoon/test/config_spec.lua b/lua/harpoon/test/config_spec.lua new file mode 100644 index 00000000..b0e9e149 --- /dev/null +++ b/lua/harpoon/test/config_spec.lua @@ -0,0 +1,33 @@ +local List = require("harpoon.list") +local Config = require("harpoon.config") +local eq = assert.are.same + +describe("config", function() + it("default.add", function() + local config = Config.get_default_config() + local config_item = Config.get_config(config, "foo") + + local bufnr = vim.fn.bufnr("/tmp/harpoon-test", true) + + vim.api.nvim_set_current_buf(bufnr) + + vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { + "foo", + "bar", + "baz", + "qux" + }) + vim.api.nvim_win_set_cursor(0, {3, 1}) + + local item = config_item.add() + eq(item, { + value = "/tmp/harpoon-test", + context = { + row = 3, + col = 1, + } + }) + end) +end) + + From 543f3a16e143db22204934a415e242770da815e3 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 15:18:57 -0600 Subject: [PATCH 04/80] feat: I am going to marry that data and that list --- HARPOON2.md | 4 + lua/harpoon/data.lua | 100 ++++++++++++++++++ lua/harpoon/init.lua | 36 ++++++- lua/harpoon/list.lua | 7 +- lua/harpoon/some.json | 228 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 6 deletions(-) create mode 100644 lua/harpoon/data.lua create mode 100644 lua/harpoon/some.json diff --git a/HARPOON2.md b/HARPOON2.md index e9df0422..d2820693 100644 --- a/HARPOON2.md +++ b/HARPOON2.md @@ -88,3 +88,7 @@ frecency = later feature likely, but great idea // i don't understand this one harpoon -> qfix : qfix -> harpoon + +harpoon.list("notehu") -> HarpoonList +harpoon.list().add() + diff --git a/lua/harpoon/data.lua b/lua/harpoon/data.lua new file mode 100644 index 00000000..2d07d1e7 --- /dev/null +++ b/lua/harpoon/data.lua @@ -0,0 +1,100 @@ +local Path = require("plenary.path") + +local data_path = vim.fn.stdpath("data") +local full_data_path = string.format("%s/harpoon2.json", data_path) + +local M = {} + +function M.set_data_path(path) + full_data_path = path +end + +local function has_keys(t) + for _ in pairs(t) do + return true + end + return false +end + +--- @alias RawData {[string]: string[]} + +--- @class Data +--- @field seen {[string]: boolean} +--- @field _data RawData +--- @field has_error boolean + + +-- 1. load the data +-- 2. keep track of the lists requested +-- 3. sync save + +local Data = {} + +Data.__index = Data + +---@param data any +local function write_data(data) + Path:new(full_data_path):write_data(vim.json.encode(data)) +end + +---@return RawData +local function read_data() + return vim.json.decode(Path:new(full_data_path):read()) +end + +---@return Harpoon +function Data:new() + local ok, data = pcall(read_data) + + return setmetatable({ + _data = data, + has_error = not ok, + seen = {} + }, self) +end + +---@param name string +---@return string[] +function Data:data(name) + if self.has_error then + error("Harpoon: there was an error reading the data file, cannot read data") + end + return self._data[name] or {} +end + +---@param name string +---@param values string[] +function Data:update(name, values) + if self.has_error then + error("Harpoon: there was an error reading the data file, cannot update") + end + self.seen[name] = true + self._data[name] = values +end + +function Data:sync() + if self.has_error then + return + end + + if not has_keys(self.seen) then + return + end + + local ok, data = pcall(read_data) + if not ok then + error("Harpoon: unable to sync data, error reading data file") + end + + for k, v in pairs(self._data) do + data[k] = v + end + + pcall(write_data, data) +end + + +M.Data = Data + +return M + diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index d7de3c89..dc6e7b74 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -1,13 +1,41 @@ +local Data = require("harpoon.data") +local Config = require("harpoon.config") -- setup -- read from a config file -- -local M = {} +local DEFAULT_LIST = "__harpoon_files" ----@param c HarpoonConfig -function config(c) +---@class Harpoon +---@field config HarpoonConfig +---@field data Data +local Harpoon = {} + +Harpoon.__index = Harpoon + +---@return Harpoon +function Harpoon:new() + local config = Config.get_default_config() + + return setmetatable({ + config = config, + data = Data:new() + }, self) +end + +---@param partial_config HarpoonPartialConfig +---@return Harpoon +function Harpoon:setup(partial_config) + self.config = Config.merge_config(partial_config, self.config) + return self +end + +---@param list string? +---@return HarpoonList +function Harpoon:list(name) + name = name or DEFAULT_LIST end -return M +return Harpoon:new() diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 6ba17003..7874dd77 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -52,11 +52,14 @@ end function HarpoonList:resolve_displayed(displayed) local not_found = {} local config = get_config(self.config, self.name) + for _, v in ipairs(displayed) do local found = false for _, in_table in ipairs(self.items) do - found = config.display(in_table, v) - break + if config.display(in_table) == v then + found = true + break + end end if not found then diff --git a/lua/harpoon/some.json b/lua/harpoon/some.json new file mode 100644 index 00000000..e9fa5201 --- /dev/null +++ b/lua/harpoon/some.json @@ -0,0 +1,228 @@ +{ + "key": [ + + nothuntoehuntoehuntoehuntoehuntoehun + oentuhnoteuhntoehuouenhtuoenhtuonhtuoenhtuoenht 0,nthouenhtouenthoue + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + + 1, + 3, + 5, + 7, + 9, + 11, + 13, + 15, + 17, + 19, + 21, + 23, + + 2, + 5, + 8, + 11, + 14, + 17, + 20, + 23, + 26, + 29, + 32, + 35, + + 3, + 7, + 11, + 15, + 19, + 23, + 27, + 31, + 35, + 39, + 43, + 47, + + 4, + 9, + 14, + 19, + 24, + 29, + 34, + 39, + 44, + 49, + 54, + 59, + + 5, + 11, + 17, + 23, + 29, + 35, + 41, + 47, + 53, + 59, + 65, + 71, + + 6, + 13, + 20, + 27, + 34, + 41, + 48, + 55, + 62, + 69, + 76, + 83, + + 7, + 15, + 23, + 31, + 39, + 47, + 55, + 63, + 71, + 79, + 87, + 95, + + 8, + 17, + 26, + 35, + 44, + 53, + 62, + 71, + 80, + 89, + 98, + 107, + + 9, + 19, + 29, + 39, + 49, + 59, + 69, + 79, + 89, + 99, + 109, + 119, + + 10, + 21, + 32, + 43, + 54, + 65, + 76, + 87, + 98, + 109, + 120, + 131, + + 11, + 23, + 35, + 47, + 59, + 71, + 83, + 95, + 107, + 119, + 131, + 143, + + 12, + 25, + 38, + 51, + 64, + 77, + 90, + 103, + 116, + 129, + 142, + 155, + + 13, + 27, + 41, + 55, + 69, + 83, + 97, + 111, + 125, + 139, + 153, + 167, + + 14, + 29, + 44, + 59, + 74, + 89, + 104, + 119, + 134, + 149, + 164, + 179, + + 15, + 31, + 47, + 63, + 79, + 95, + 111, + 127, + 143, + 159, + 175, + 191, + + 16, + 33, + 50, + 67, + 84, + 101, + 118, + 135, + 152, + 169, + 186, + 203, + + ] +} + From c1f49fef43e32af011fb49838a4eb6306250f37c Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 16:57:38 -0600 Subject: [PATCH 05/80] feat: list adding now works but duplicates are a problem --- lua/harpoon/init.lua | 41 --------- lua/{harpoon => harpoon2}/config.lua | 0 lua/{harpoon => harpoon2}/data.lua | 52 +++++++---- lua/harpoon2/init.lua | 86 +++++++++++++++++++ lua/{harpoon => harpoon2}/list.lua | 34 ++++---- lua/{harpoon => harpoon2}/some.json | 0 .../test/config_spec.lua | 5 +- lua/harpoon2/test/harpoon_spec.lua | 41 +++++++++ lua/{harpoon => harpoon2}/test/list_spec.lua | 4 +- 9 files changed, 186 insertions(+), 77 deletions(-) delete mode 100644 lua/harpoon/init.lua rename lua/{harpoon => harpoon2}/config.lua (100%) rename lua/{harpoon => harpoon2}/data.lua (71%) create mode 100644 lua/harpoon2/init.lua rename lua/{harpoon => harpoon2}/list.lua (72%) rename lua/{harpoon => harpoon2}/some.json (100%) rename lua/{harpoon => harpoon2}/test/config_spec.lua (89%) create mode 100644 lua/harpoon2/test/harpoon_spec.lua rename lua/{harpoon => harpoon2}/test/list_spec.lua (90%) diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua deleted file mode 100644 index dc6e7b74..00000000 --- a/lua/harpoon/init.lua +++ /dev/null @@ -1,41 +0,0 @@ -local Data = require("harpoon.data") -local Config = require("harpoon.config") - --- setup --- read from a config file --- - -local DEFAULT_LIST = "__harpoon_files" - ----@class Harpoon ----@field config HarpoonConfig ----@field data Data -local Harpoon = {} - -Harpoon.__index = Harpoon - ----@return Harpoon -function Harpoon:new() - local config = Config.get_default_config() - - return setmetatable({ - config = config, - data = Data:new() - }, self) -end - ----@param partial_config HarpoonPartialConfig ----@return Harpoon -function Harpoon:setup(partial_config) - self.config = Config.merge_config(partial_config, self.config) - return self -end - ----@param list string? ----@return HarpoonList -function Harpoon:list(name) - name = name or DEFAULT_LIST -end - -return Harpoon:new() - diff --git a/lua/harpoon/config.lua b/lua/harpoon2/config.lua similarity index 100% rename from lua/harpoon/config.lua rename to lua/harpoon2/config.lua diff --git a/lua/harpoon/data.lua b/lua/harpoon2/data.lua similarity index 71% rename from lua/harpoon/data.lua rename to lua/harpoon2/data.lua index 2d07d1e7..6b4f04dd 100644 --- a/lua/harpoon/data.lua +++ b/lua/harpoon2/data.lua @@ -3,8 +3,24 @@ local Path = require("plenary.path") local data_path = vim.fn.stdpath("data") local full_data_path = string.format("%s/harpoon2.json", data_path) +---@param data any +local function write_data(data) + Path:new(full_data_path):write(vim.json.encode(data), "w") +end + local M = {} +function M.__dangerously_clear_data() + write_data({}) +end + +function M.info() + return { + data_path = data_path, + full_data_path = full_data_path, + } +end + function M.set_data_path(path) full_data_path = path end @@ -16,33 +32,34 @@ local function has_keys(t) return false end ---- @alias RawData {[string]: string[]} +--- @alias HarpoonRawData {[string]: string[]} ---- @class Data +--- @class HarpoonData --- @field seen {[string]: boolean} ---- @field _data RawData +--- @field _data HarpoonRawData --- @field has_error boolean +local Data = {} -- 1. load the data -- 2. keep track of the lists requested -- 3. sync save -local Data = {} - Data.__index = Data ----@param data any -local function write_data(data) - Path:new(full_data_path):write_data(vim.json.encode(data)) -end - ----@return RawData +---@return HarpoonRawData local function read_data() - return vim.json.decode(Path:new(full_data_path):read()) + local path = Path:new(full_data_path) + local exists = path:exists() + if not exists then + write_data({}) + end + + local data = vim.json.decode(path:read()) + return data end ----@return Harpoon +---@return HarpoonData function Data:new() local ok, data = pcall(read_data) @@ -59,6 +76,7 @@ function Data:data(name) if self.has_error then error("Harpoon: there was an error reading the data file, cannot read data") end + self.seen[name] = true return self._data[name] or {} end @@ -68,7 +86,6 @@ function Data:update(name, values) if self.has_error then error("Harpoon: there was an error reading the data file, cannot update") end - self.seen[name] = true self._data[name] = values end @@ -90,11 +107,14 @@ function Data:sync() data[k] = v end - pcall(write_data, data) + ok = pcall(write_data, data) + + if ok then + self.seen = {} + end end M.Data = Data return M - diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua new file mode 100644 index 00000000..83443a08 --- /dev/null +++ b/lua/harpoon2/init.lua @@ -0,0 +1,86 @@ +local Data = require("harpoon2.data") +local Config = require("harpoon2.config") +local List = require("harpoon2.list") + +-- setup +-- read from a config file +-- + +local DEFAULT_LIST = "__harpoon_files" + +---@class Harpoon +---@field config HarpoonConfig +---@field data HarpoonData +---@field lists HarpoonList[] +local Harpoon = {} + +Harpoon.__index = Harpoon + +---@return Harpoon +function Harpoon:new() + local config = Config.get_default_config() + + return setmetatable({ + config = config, + data = Data.Data:new(), + lists = {}, + }, self) +end + +---@param partial_config HarpoonPartialConfig +---@return Harpoon +function Harpoon:setup(partial_config) + self.config = Config.merge_config(partial_config, self.config) + return self +end + +---@param name string? +---@return HarpoonList +function Harpoon:list(name) + name = name or DEFAULT_LIST + + local existing_list = self.lists[name] + + if existing_list then + return self.lists[name] + end + + local data = self.data:data(name) + local list_config = Config.get_config(self.config, name) + + local list = List.decode(list_config, name, data) + self.lists[name] = list + + return list +end + +function Harpoon:sync() + for k, _ in pairs(self.data.seen) do + local encoded = self.lists[k]:encode() + self.data:update(k, encoded) + end + self.data:sync() +end + +function Harpoon:setup_hooks() + -- setup the autocommands + -- vim exits sync data + -- buf exit setup the cursor location + error("I haven't implemented this yet") +end + +function Harpoon:info() + return { + paths = Data.info(), + default_key = DEFAULT_LIST, + } +end + +--- PLEASE DONT USE THIS OR YOU WILL BE FIRED +function Harpoon:dump() + return self.data._data +end + +return Harpoon:new() + + diff --git a/lua/harpoon/list.lua b/lua/harpoon2/list.lua similarity index 72% rename from lua/harpoon/list.lua rename to lua/harpoon2/list.lua index 7874dd77..3c22198a 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon2/list.lua @@ -1,5 +1,3 @@ -local get_config = require "harpoon.config".get_config - -- TODO: Define the config object --- @class HarpoonItem @@ -8,7 +6,7 @@ local get_config = require "harpoon.config".get_config --- create a table object to be new'd --- @class HarpoonList ---- @field config any +--- @field config HarpoonPartialConfigItem --- @field name string --- @field items HarpoonItem[] local HarpoonList = {} @@ -22,25 +20,35 @@ function HarpoonList:new(config, name, items) }, self) end +---@return HarpoonList function HarpoonList:push(item) + item = item or self.config.add() table.insert(self.items, item) + return self end +---@return HarpoonList function HarpoonList:addToFront(item) + item = item or self.config.add() table.insert(self.items, 1, item) + return self end +---@return HarpoonList function HarpoonList:remove(item) for i, v in ipairs(self.items) do - if get_config(self.config, self.name)(v, item) then + if self.config.equals(v, item) then table.remove(self.items, i) break end end + return self end +---@return HarpoonList function HarpoonList:removeAt(index) table.remove(self.items, index) + return self end function HarpoonList:get(index) @@ -51,12 +59,11 @@ end ---@param displayed string[] function HarpoonList:resolve_displayed(displayed) local not_found = {} - local config = get_config(self.config, self.name) for _, v in ipairs(displayed) do local found = false for _, in_table in ipairs(self.items) do - if config.display(in_table) == v then + if self.config.display(in_table) == v then found = true break end @@ -75,9 +82,8 @@ end --- @return string[] function HarpoonList:display() local out = {} - local config = get_config(self.config, self.name) for _, v in ipairs(self.items) do - table.insert(out, config.display(v)) + table.insert(out, self.config.display(v)) end return out @@ -86,27 +92,25 @@ end --- @return string[] function HarpoonList:encode() local out = {} - local config = get_config(self.config, self.name) for _, v in ipairs(self.items) do - table.insert(out, config.encode(v)) + table.insert(out, self.config.encode(v)) end return out end --- @return HarpoonList ---- @param config HarpoonConfig +--- @param list_config HarpoonPartialConfigItem --- @param name string --- @param items string[] -function HarpoonList.decode(config, name, items) +function HarpoonList.decode(list_config, name, items) local list_items = {} - local c = get_config(config, name) for _, item in ipairs(items) do - table.insert(list_items, c.decode(item)) + table.insert(list_items, list_config.decode(item)) end - return HarpoonList:new(config, name, list_items) + return HarpoonList:new(list_config, name, list_items) end diff --git a/lua/harpoon/some.json b/lua/harpoon2/some.json similarity index 100% rename from lua/harpoon/some.json rename to lua/harpoon2/some.json diff --git a/lua/harpoon/test/config_spec.lua b/lua/harpoon2/test/config_spec.lua similarity index 89% rename from lua/harpoon/test/config_spec.lua rename to lua/harpoon2/test/config_spec.lua index b0e9e149..c81f99e1 100644 --- a/lua/harpoon/test/config_spec.lua +++ b/lua/harpoon2/test/config_spec.lua @@ -1,5 +1,5 @@ -local List = require("harpoon.list") -local Config = require("harpoon.config") +local List = require("harpoon2.list") +local Config = require("harpoon2.config") local eq = assert.are.same describe("config", function() @@ -10,7 +10,6 @@ describe("config", function() local bufnr = vim.fn.bufnr("/tmp/harpoon-test", true) vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { "foo", "bar", diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua new file mode 100644 index 00000000..e409a252 --- /dev/null +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -0,0 +1,41 @@ +local Data = require("harpoon2.data") +local harpoon = require("harpoon2") + +local eq = assert.are.same + +describe("harpoon", function() + + before_each(function() + Data.set_data_path("/tmp/harpoon2.json") + Data.__dangerously_clear_data() + require("plenary.reload").reload_module("harpoon2") + Data = require("harpoon2.data") + Data.set_data_path("/tmp/harpoon2.json") + harpoon = require("harpoon2") + end) + + it("full harpoon add sync cycle", function() + local file_name = "/tmp/harpoon-test" + local row = 3 + local col = 1 + local bufnr = vim.fn.bufnr(file_name, true) + local default_key = harpoon:info().default_key + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { + "foo", + "bar", + "baz", + "qux" + }) + vim.api.nvim_win_set_cursor(0, {row, col}) + + local list = harpoon:list():push() + harpoon:sync() + + eq(harpoon:dump(), { + [default_key] = list:encode() + }) + end) +end) + + diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon2/test/list_spec.lua similarity index 90% rename from lua/harpoon/test/list_spec.lua rename to lua/harpoon2/test/list_spec.lua index ef6ef7b5..4ef86275 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon2/test/list_spec.lua @@ -1,5 +1,5 @@ -local List = require("harpoon.list") -local Config = require("harpoon.config") +local List = require("harpoon2.list") +local Config = require("harpoon2.config") local eq = assert.are.same describe("list", function() From 734d10bc6d48fee6fd908b0e5f9bd85c953ff080 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 18:09:20 -0600 Subject: [PATCH 06/80] fix: rename and small lint/bug fix and dedupe add --- lua/harpoon2/list.lua | 24 +++++++++++++++++++----- lua/harpoon2/test/harpoon_spec.lua | 2 +- lua/harpoon2/test/list_spec.lua | 3 ++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index 3c22198a..269ee35f 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -1,10 +1,19 @@ --- TODO: Define the config object +local function index_of(config, items, element) + local index = -1 + for i, item in ipairs(items) do + if config.equals(element, item) then + index = i + break + end + end + + return index +end --- @class HarpoonItem --- @field value string --- @field context any ---- create a table object to be new'd --- @class HarpoonList --- @field config HarpoonPartialConfigItem --- @field name string @@ -21,14 +30,19 @@ function HarpoonList:new(config, name, items) end ---@return HarpoonList -function HarpoonList:push(item) +function HarpoonList:append(item) item = item or self.config.add() - table.insert(self.items, item) + + local index = index_of(self.config, self.items, item) + if index == -1 then + table.insert(self.items, item) + end + return self end ---@return HarpoonList -function HarpoonList:addToFront(item) +function HarpoonList:prepend(item) item = item or self.config.add() table.insert(self.items, 1, item) return self diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index e409a252..3c807115 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -29,7 +29,7 @@ describe("harpoon", function() }) vim.api.nvim_win_set_cursor(0, {row, col}) - local list = harpoon:list():push() + local list = harpoon:list():append() harpoon:sync() eq(harpoon:dump(), { diff --git a/lua/harpoon2/test/list_spec.lua b/lua/harpoon2/test/list_spec.lua index 4ef86275..bf99690a 100644 --- a/lua/harpoon2/test/list_spec.lua +++ b/lua/harpoon2/test/list_spec.lua @@ -21,8 +21,9 @@ describe("list", function() end } }) + local list_config = Config.get_config(config, "foo") - local list = List.decode(config, "foo", {"foo:bar", "baz:qux"}) + local list = List.decode(list_config, "foo", {"foo:bar", "baz:qux"}) local displayed = list:display() eq(displayed, { From 65d652dccd67394182c3d2206c5ba8f67d590666 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 18:13:46 -0600 Subject: [PATCH 07/80] fix: dedupe prepend --- lua/harpoon2/list.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index 269ee35f..fe7727d0 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -44,7 +44,11 @@ end ---@return HarpoonList function HarpoonList:prepend(item) item = item or self.config.add() - table.insert(self.items, 1, item) + local index = index_of(self.config, self.items, item) + if index == -1 then + table.insert(self.items, 1, item) + end + return self end From ab45ad29825a73fbb1d6358294816dc0e4583045 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 18:30:35 -0600 Subject: [PATCH 08/80] feat: good ass testing going on here --- lua/harpoon2/test/harpoon_spec.lua | 46 +++++++++++++++++++++++++++--- lua/harpoon2/test/utils.lua | 17 +++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 lua/harpoon2/test/utils.lua diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index 3c807115..e37427b3 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -1,3 +1,4 @@ +local utils = require("harpoon2.test.utils") local Data = require("harpoon2.data") local harpoon = require("harpoon2") @@ -18,23 +19,60 @@ describe("harpoon", function() local file_name = "/tmp/harpoon-test" local row = 3 local col = 1 - local bufnr = vim.fn.bufnr(file_name, true) local default_key = harpoon:info().default_key - vim.api.nvim_set_current_buf(bufnr) - vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, { + local bufnr = utils.create_file(file_name, { "foo", "bar", "baz", "qux" + }, row, col) + + local list = harpoon:list():append() + harpoon:sync() + + eq(harpoon:dump(), { + [default_key] = list:encode() }) - vim.api.nvim_win_set_cursor(0, {row, col}) + end) + + it("prepend/append double add", function() + local default_key = harpoon:info().default_key + local file_name_1 = "/tmp/harpoon-test" + local row_1 = 3 + local col_1 = 1 + + local file_name_2 = "/tmp/harpoon-test-2" + local row_2 = 1 + local col_2 = 2 + local contents = { "foo", "bar", "baz", "qux" } + + local bufnr_1 = utils.create_file(file_name_1, contents, row_1, col_1) local list = harpoon:list():append() + + utils.create_file(file_name_2, contents, row_2, col_2) + harpoon:list():prepend() + harpoon:sync() eq(harpoon:dump(), { [default_key] = list:encode() }) + + eq(list.items, { + {value = file_name_2, context = {row = row_2, col = col_2}}, + {value = file_name_1, context = {row = row_1, col = col_1}}, + }) + + harpoon:list():append() + vim.api.nvim_set_current_buf(bufnr_1) + harpoon:list():prepend() + + eq(list.items, { + {value = file_name_2, context = {row = row_2, col = col_2}}, + {value = file_name_1, context = {row = row_1, col = col_1}}, + }) + end) end) diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua new file mode 100644 index 00000000..e40b3bf1 --- /dev/null +++ b/lua/harpoon2/test/utils.lua @@ -0,0 +1,17 @@ + +local M = {} + +---@param name string +---@param contents string[] +function M.create_file(name, contents, row, col) + local bufnr = vim.fn.bufnr(name, true) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, contents) + if row then + vim.api.nvim_win_set_cursor(0, {row, col}) + end + + return bufnr +end + +return M From 362b79fe78ecc779954ea913ad2b69ab64feb55a Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 19:28:27 -0600 Subject: [PATCH 09/80] feat: resolve_display now works --- lua/harpoon2/config.lua | 15 +++-- lua/harpoon2/list.lua | 35 ++++++----- lua/harpoon2/test/harpoon_spec.lua | 97 ++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 22 deletions(-) diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index c10a6643..6bd375ef 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -10,7 +10,7 @@ local M = {} ---@field display? (fun(list_item: HarpoonListItem): string) ---@field select? (fun(list_item: HarpoonListItem): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ----@field add? fun(): HarpoonListItem +---@field add? fun(item: any?): HarpoonListItem ---@class HarpoonSettings ---@field save_on_toggle boolean defaults to true @@ -90,10 +90,17 @@ function M.get_default_config() return list_item_a.value == list_item_b.value end, + ---@param value any ---@return HarpoonListItem - add = function() - local name = vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) - local pos = vim.api.nvim_win_get_cursor(0) + add = function(name) + name = name or vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + local bufnr = vim.fn.bufnr(name, false) + + local pos = {1, 0} + if bufnr ~= -1 then + pos = vim.api.nvim_win_get_cursor(0) + end + return { value = name, context = { diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index fe7727d0..d7a2adc2 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -1,7 +1,8 @@ -local function index_of(config, items, element) +local function index_of(items, element, config) + local equals = config and config.equals or function(a, b) return a == b end local index = -1 for i, item in ipairs(items) do - if config.equals(element, item) then + if equals(element, item) then index = i break end @@ -33,7 +34,7 @@ end function HarpoonList:append(item) item = item or self.config.add() - local index = index_of(self.config, self.items, item) + local index = index_of(self.items, item, self.config) if index == -1 then table.insert(self.items, item) end @@ -44,7 +45,7 @@ end ---@return HarpoonList function HarpoonList:prepend(item) item = item or self.config.add() - local index = index_of(self.config, self.items, item) + local index = index_of(self.items, item, self.config) if index == -1 then table.insert(self.items, 1, item) end @@ -76,25 +77,23 @@ end --- much inefficiencies. dun care ---@param displayed string[] function HarpoonList:resolve_displayed(displayed) - local not_found = {} - - for _, v in ipairs(displayed) do - local found = false - for _, in_table in ipairs(self.items) do - if self.config.display(in_table) == v then - found = true - break + local new_list = {} + + local list_displayed = self:display() + for i, v in ipairs(displayed) do + local index = index_of(list_displayed, v) + if index == -1 then + table.insert(new_list, self.config.add(v)) + else + local index_in_new_list = index_of(new_list, self.items[index], self.config) + if index_in_new_list == -1 then + table.insert(new_list, self.items[index]) end end - if not found then - table.insert(not_found, v) - end end - for _, v in ipairs(not_found) do - self:remove(v) - end + self.items = new_list end --- @return string[] diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index e37427b3..60f1cb9e 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -74,6 +74,103 @@ describe("harpoon", function() }) end) + + it("ui - display resolve", function() + harpoon:setup({ + default = { + display = function(item) + -- split string on / + local parts = vim.split(item.value, "/") + return parts[#parts] + end + } + }) + + local file_names = { + "/tmp/harpoon-test-1", + "/tmp/harpoon-test-2", + "/tmp/harpoon-test-3", + "/tmp/harpoon-test-4", + } + + local contents = { "foo", "bar", "baz", "qux" } + + local bufnrs = {} + local list = harpoon:list() + for _, v in ipairs(file_names) do + table.insert(bufnrs, utils.create_file(v, contents)) + harpoon:list():append() + end + + local displayed = list:display() + eq(displayed, { + "harpoon-test-1", + "harpoon-test-2", + "harpoon-test-3", + "harpoon-test-4", + }) + + table.remove(displayed, 3) + table.remove(displayed, 2) + + list:resolve_displayed(displayed) + + eq(list.items, { + {value = file_names[1], context = {row = 4, col = 2}}, + {value = file_names[4], context = {row = 4, col = 2}}, + }) + end) + + it("ui - display resolve", function() + local file_names = { + "/tmp/harpoon-test-1", + "/tmp/harpoon-test-2", + "/tmp/harpoon-test-3", + "/tmp/harpoon-test-4", + } + + local contents = { "foo", "bar", "baz", "qux" } + + local bufnrs = {} + local list = harpoon:list() + for _, v in ipairs(file_names) do + table.insert(bufnrs, utils.create_file(v, contents)) + harpoon:list():append() + end + + local displayed = list:display() + eq(displayed, { + "/tmp/harpoon-test-1", + "/tmp/harpoon-test-2", + "/tmp/harpoon-test-3", + "/tmp/harpoon-test-4", + }) + + table.remove(displayed, 3) + table.remove(displayed, 2) + + table.insert(displayed, "/tmp/harpoon-test-other-file-1") + table.insert(displayed, "/tmp/harpoon-test-other-file-2") + + list:resolve_displayed(displayed) + + eq(list.items, { + {value = file_names[1], context = {row = 4, col = 2}}, + {value = file_names[4], context = {row = 4, col = 2}}, + {value = "/tmp/harpoon-test-other-file-1", context = {row = 1, col = 0}}, + {value = "/tmp/harpoon-test-other-file-2", context = {row = 1, col = 0}}, + }) + + table.remove(displayed, 3) + table.insert(displayed, "/tmp/harpoon-test-4") + list:resolve_displayed(displayed) + + eq(list.items, { + {value = file_names[1], context = {row = 4, col = 2}}, + {value = file_names[4], context = {row = 4, col = 2}}, + {value = "/tmp/harpoon-test-other-file-2", context = {row = 1, col = 0}}, + }) + end) end) From e08020477a53aa3a6e5e193d77eba1e5e7a99dac Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 3 Nov 2023 19:43:44 -0600 Subject: [PATCH 10/80] partial: i don't know if the splits ackshually work --- lua/harpoon2/config.lua | 15 ++++++++++++--- lua/harpoon2/list.lua | 7 +++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index 6bd375ef..8fc16597 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -8,7 +8,7 @@ local M = {} ---@field decode? (fun(obj: string): any) ---@field key? (fun(): string) ---@field display? (fun(list_item: HarpoonListItem): string) ----@field select? (fun(list_item: HarpoonListItem): nil) +---@field select? (fun(list_item: HarpoonListItem, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ---@field add? fun(item: any?): HarpoonListItem @@ -63,7 +63,7 @@ function M.get_default_config() end, ---@param file_item HarpoonListFileItem - select = function(file_item) + select = function(file_item, options) if file_item == nil then return end @@ -75,7 +75,16 @@ function M.get_default_config() bufnr = vim.fn.bufnr(file_item.value, true) end - vim.api.nvim_set_current_buf(bufnr) + if not options or not options.vsplit or not options.split then + vim.api.nvim_set_current_buf(bufnr) + elseif options.vsplit then + vim.cmd("vsplit") + vim.api.nvim_set_current_buf(bufnr) + elseif options.split then + vim.cmd("split") + vim.api.nvim_set_current_buf(bufnr) + end + if set_position then vim.api.nvim_win_set_cursor(0, { file_item.context.row or 1, diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index d7a2adc2..c9eada4e 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -96,6 +96,13 @@ function HarpoonList:resolve_displayed(displayed) self.items = new_list end +function HarpoonList:select(index, options) + local item = self.items[index] + if item then + self.config.select(item, options) + end +end + --- @return string[] function HarpoonList:display() local out = {} From eafaec8a2ebd6620824dca3e712cd6225444eee4 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 9 Nov 2023 08:46:05 -0700 Subject: [PATCH 11/80] feat: key for indexing --- HARPOON2.md | 86 +---------- lua/harpoon2/config.lua | 22 ++- lua/harpoon2/data.lua | 35 ++++- lua/harpoon2/init.lua | 31 ++-- lua/harpoon2/some.json | 228 ----------------------------- lua/harpoon2/test/harpoon_spec.lua | 33 +++-- lua/harpoon2/test/utils.lua | 11 ++ 7 files changed, 103 insertions(+), 343 deletions(-) delete mode 100644 lua/harpoon2/some.json diff --git a/HARPOON2.md b/HARPOON2.md index d2820693..ed90f431 100644 --- a/HARPOON2.md +++ b/HARPOON2.md @@ -1,84 +1,8 @@ -### Features -* select how to generate the list key - -#### was -list_key = [cwd [+ git branch]] -* files -* terminals -* tmux - -#### is -list_key = [key] + [list_name] - -nil = default -false = turn off - -listA = { - listLine({ ... }) - { ... } - { ... } - { ... } -} - -harpoon.setup({ - - settings = { - jumpToFileLocation: boolean => defaults true - } - - default = { - // defaults to json.parse - encode = function(obj) => string - decode = function(string) => object - key = function() ... end - display = function(listLine) => string - select = function(listLine) => void - equals = function(list_line_a, list_line_b) => boolean - - # question mark: what does it take to support custom things in here? - # potentially subject to change - add = function() HarpoonListItem - } - - frecency = { - ... a file list that is generated by harpoon ... - ... can be opened via viewer ... - } - - events = { - on_change = function(operation, list, value) - } - - project = { - //key = vim.loop.cwd - key = git origin? - } - - specifics = { - key = vim.loop.cwd + git_branch - } - - list_name = { - key = function() ... end - } - -}) - -### Functionality -select by index -prev/next -addToBack -addToFront -checking for deleted files? -- perhaps this could be part of the default select operation and use error -select -- default file select should come with options so you can open split/tab as - well - -harpoon.current = "default" -harpoon.current = "listName" - -harpoon.set_current(list_name) +### TODO +* encode being false means no writing to disk +* bring over the ui from harpoon 1.0 +* autocmds for leaving buffer and quitteriousing vim +* write some tests around file moving within the display ### LATER FEATUERS frecency = later feature likely, but great idea diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index 8fc16597..b8f21a49 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -6,7 +6,6 @@ local M = {} ---@class HarpoonPartialConfigItem ---@field encode? (fun(list_item: HarpoonListItem): string) ---@field decode? (fun(obj: string): any) ----@field key? (fun(): string) ---@field display? (fun(list_item: HarpoonListItem): string) ---@field select? (fun(list_item: HarpoonListItem, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) @@ -15,6 +14,13 @@ local M = {} ---@class HarpoonSettings ---@field save_on_toggle boolean defaults to true ---@field jump_to_file_location boolean defaults to true +---@field key (fun(): string) + +---@class HarpoonPartialSettings +---@field save_on_toggle? boolean +---@field jump_to_file_location? boolean +---@field key? (fun(): string) + ---@class HarpoonConfig ---@field default HarpoonPartialConfigItem @@ -22,8 +28,8 @@ local M = {} ---@field [string] HarpoonPartialConfigItem ---@class HarpoonPartialConfig ----@field default HarpoonPartialConfigItem? ----@field settings HarpoonSettings? +---@field default? HarpoonPartialConfigItem +---@field settings? HarpoonPartialSettings ---@field [string] HarpoonPartialConfigItem @@ -35,9 +41,13 @@ end ---@return HarpoonConfig function M.get_default_config() return { + settings = { save_on_toggle = true, jump_to_file_location = true, + key = function() + return vim.loop.cwd() + end, }, default = { @@ -53,10 +63,6 @@ function M.get_default_config() return vim.json.decode(str) end, - key = function() - return vim.loop.cwd() - end, - ---@param list_item HarpoonListItem display = function(list_item) return list_item.value @@ -99,7 +105,7 @@ function M.get_default_config() return list_item_a.value == list_item_b.value end, - ---@param value any + ---@param name any ---@return HarpoonListItem add = function(name) name = name or vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) diff --git a/lua/harpoon2/data.lua b/lua/harpoon2/data.lua index 6b4f04dd..7050b89e 100644 --- a/lua/harpoon2/data.lua +++ b/lua/harpoon2/data.lua @@ -32,10 +32,10 @@ local function has_keys(t) return false end ---- @alias HarpoonRawData {[string]: string[]} +--- @alias HarpoonRawData {[string]: {[string]: string[]}} --- @class HarpoonData ---- @field seen {[string]: boolean} +--- @field seen {[string]: {[string]: boolean}} --- @field _data HarpoonRawData --- @field has_error boolean local Data = {} @@ -51,6 +51,7 @@ Data.__index = Data local function read_data() local path = Path:new(full_data_path) local exists = path:exists() + if not exists then write_data({}) end @@ -68,25 +69,45 @@ function Data:new() has_error = not ok, seen = {} }, self) + +end + +---@param key string +---@param name string +---@return string[] +function Data:_get_data(key, name) + if not self._data[key] then + self._data[key] = {} + end + + return self._data[key][name] or {} end +---@param key string ---@param name string ---@return string[] -function Data:data(name) +function Data:data(key, name) if self.has_error then error("Harpoon: there was an error reading the data file, cannot read data") end - self.seen[name] = true - return self._data[name] or {} + + if not self.seen[key] then + self.seen[key] = {} + end + + self.seen[key][name] = true + + return self:_get_data(key, name) end ---@param name string ---@param values string[] -function Data:update(name, values) +function Data:update(key, name, values) if self.has_error then error("Harpoon: there was an error reading the data file, cannot update") end - self._data[name] = values + self:_get_data(key, name) + self._data[key][name] = values end function Data:sync() diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index 83443a08..e700cbec 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -6,12 +6,14 @@ local List = require("harpoon2.list") -- read from a config file -- +-- TODO: rename lists into something better... + local DEFAULT_LIST = "__harpoon_files" ---@class Harpoon ---@field config HarpoonConfig ---@field data HarpoonData ----@field lists HarpoonList[] +---@field lists {[string]: {[string]: HarpoonList}} local Harpoon = {} Harpoon.__index = Harpoon @@ -39,25 +41,36 @@ end function Harpoon:list(name) name = name or DEFAULT_LIST - local existing_list = self.lists[name] + local key = self.config.settings.key() + local lists = self.lists[key] + + if not lists then + lists = {} + self.lists[key] = lists + end + + local existing_list = lists[name] if existing_list then - return self.lists[name] + return existing_list end - local data = self.data:data(name) + local data = self.data:data(key, name) local list_config = Config.get_config(self.config, name) local list = List.decode(list_config, name, data) - self.lists[name] = list + lists[name] = list return list end function Harpoon:sync() - for k, _ in pairs(self.data.seen) do - local encoded = self.lists[k]:encode() - self.data:update(k, encoded) + local key = self.config.settings.key() + local seen = self.data.seen[key] + local lists = self.lists[key] + for list_name, _ in pairs(seen) do + local encoded = lists[list_name]:encode() + self.data:update(key, list_name, encoded) end self.data:sync() end @@ -72,7 +85,7 @@ end function Harpoon:info() return { paths = Data.info(), - default_key = DEFAULT_LIST, + default_list_name = DEFAULT_LIST, } end diff --git a/lua/harpoon2/some.json b/lua/harpoon2/some.json deleted file mode 100644 index e9fa5201..00000000 --- a/lua/harpoon2/some.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "key": [ - - nothuntoehuntoehuntoehuntoehuntoehun - oentuhnoteuhntoehuouenhtuoenhtuonhtuoenhtuoenht 0,nthouenhtouenthoue - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - - 1, - 3, - 5, - 7, - 9, - 11, - 13, - 15, - 17, - 19, - 21, - 23, - - 2, - 5, - 8, - 11, - 14, - 17, - 20, - 23, - 26, - 29, - 32, - 35, - - 3, - 7, - 11, - 15, - 19, - 23, - 27, - 31, - 35, - 39, - 43, - 47, - - 4, - 9, - 14, - 19, - 24, - 29, - 34, - 39, - 44, - 49, - 54, - 59, - - 5, - 11, - 17, - 23, - 29, - 35, - 41, - 47, - 53, - 59, - 65, - 71, - - 6, - 13, - 20, - 27, - 34, - 41, - 48, - 55, - 62, - 69, - 76, - 83, - - 7, - 15, - 23, - 31, - 39, - 47, - 55, - 63, - 71, - 79, - 87, - 95, - - 8, - 17, - 26, - 35, - 44, - 53, - 62, - 71, - 80, - 89, - 98, - 107, - - 9, - 19, - 29, - 39, - 49, - 59, - 69, - 79, - 89, - 99, - 109, - 119, - - 10, - 21, - 32, - 43, - 54, - 65, - 76, - 87, - 98, - 109, - 120, - 131, - - 11, - 23, - 35, - 47, - 59, - 71, - 83, - 95, - 107, - 119, - 131, - 143, - - 12, - 25, - 38, - 51, - 64, - 77, - 90, - 103, - 116, - 129, - 142, - 155, - - 13, - 27, - 41, - 55, - 69, - 83, - 97, - 111, - 125, - 139, - 153, - 167, - - 14, - 29, - 44, - 59, - 74, - 89, - 104, - 119, - 134, - 149, - 164, - 179, - - 15, - 31, - 47, - 63, - 79, - 95, - 111, - 127, - 143, - 159, - 175, - 191, - - 16, - 33, - 50, - 67, - 84, - 101, - 118, - 135, - 152, - 169, - 186, - 203, - - ] -} - diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index 60f1cb9e..02670b30 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -13,14 +13,24 @@ describe("harpoon", function() Data = require("harpoon2.data") Data.set_data_path("/tmp/harpoon2.json") harpoon = require("harpoon2") + utils.clean_files() + + harpoon:setup({ + settings = { + key = function() + return "testies" + end + } + }) + end) it("full harpoon add sync cycle", function() local file_name = "/tmp/harpoon-test" local row = 3 local col = 1 - local default_key = harpoon:info().default_key - local bufnr = utils.create_file(file_name, { + local default_list_name = harpoon:info().default_list_name + utils.create_file(file_name, { "foo", "bar", "baz", @@ -31,12 +41,14 @@ describe("harpoon", function() harpoon:sync() eq(harpoon:dump(), { - [default_key] = list:encode() + testies = { + [default_list_name] = list:encode() + } }) end) it("prepend/append double add", function() - local default_key = harpoon:info().default_key + local default_list_name = harpoon:info().default_list_name local file_name_1 = "/tmp/harpoon-test" local row_1 = 3 local col_1 = 1 @@ -56,7 +68,9 @@ describe("harpoon", function() harpoon:sync() eq(harpoon:dump(), { - [default_key] = list:encode() + testies = { + [default_list_name] = list:encode() + } }) eq(list.items, { @@ -154,23 +168,22 @@ describe("harpoon", function() list:resolve_displayed(displayed) - eq(list.items, { + eq({ {value = file_names[1], context = {row = 4, col = 2}}, {value = file_names[4], context = {row = 4, col = 2}}, {value = "/tmp/harpoon-test-other-file-1", context = {row = 1, col = 0}}, {value = "/tmp/harpoon-test-other-file-2", context = {row = 1, col = 0}}, - }) + }, list.items) table.remove(displayed, 3) table.insert(displayed, "/tmp/harpoon-test-4") list:resolve_displayed(displayed) - eq(list.items, { + eq({ {value = file_names[1], context = {row = 4, col = 2}}, {value = file_names[4], context = {row = 4, col = 2}}, {value = "/tmp/harpoon-test-other-file-2", context = {row = 1, col = 0}}, - }) + }, list.items) end) end) - diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua index e40b3bf1..ecee9c18 100644 --- a/lua/harpoon2/test/utils.lua +++ b/lua/harpoon2/test/utils.lua @@ -1,6 +1,16 @@ local M = {} +M.created_files = {} + +function M.clean_files() + for _, bufnr in ipairs(M.created_files) do + vim.api.nvim_buf_delete(bufnr, {force = true}) + end + + M.created_files = {} +end + ---@param name string ---@param contents string[] function M.create_file(name, contents, row, col) @@ -11,6 +21,7 @@ function M.create_file(name, contents, row, col) vim.api.nvim_win_set_cursor(0, {row, col}) end + table.insert(M.created_files, bufnr) return bufnr end From 90bcfebca901015dbe8686ef82f322b5f86dcf8f Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 20 Nov 2023 21:01:15 -0700 Subject: [PATCH 12/80] feat: changing buffers updates list positions --- lua/harpoon2/config.lua | 17 ++++++++++ lua/harpoon2/init.lua | 53 +++++++++++++++++++++++++----- lua/harpoon2/list.lua | 10 ++++++ lua/harpoon2/test/harpoon_spec.lua | 32 ++++++++++++++++++ 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index b8f21a49..1365cb9b 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -10,7 +10,10 @@ local M = {} ---@field select? (fun(list_item: HarpoonListItem, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ---@field add? fun(item: any?): HarpoonListItem +---@field BufLeave? fun(evt: any, list: HarpoonList): nil +---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil +---notehunthoeunthoeunthoeunthoeunthoeunth ---@class HarpoonSettings ---@field save_on_toggle boolean defaults to true ---@field jump_to_file_location boolean defaults to true @@ -124,6 +127,20 @@ function M.get_default_config() } } end, + + BufLeave = function(arg, list) + local bufnr = arg.buf; + local bufname = vim.api.nvim_buf_get_name(bufnr); + local item = list:get_by_display(bufname) + + if item then + local pos = vim.api.nvim_win_get_cursor(0) + item.context.row = pos[1] + item.context.col = pos[2] + end + end, + + autocmds = {"BufLeave"}, } } end diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index e700cbec..517a886a 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -14,6 +14,7 @@ local DEFAULT_LIST = "__harpoon_files" ---@field config HarpoonConfig ---@field data HarpoonData ---@field lists {[string]: {[string]: HarpoonList}} +---@field hooks_setup boolean local Harpoon = {} Harpoon.__index = Harpoon @@ -26,6 +27,7 @@ function Harpoon:new() config = config, data = Data.Data:new(), lists = {}, + hooks_setup = false, }, self) end @@ -33,6 +35,32 @@ end ---@return Harpoon function Harpoon:setup(partial_config) self.config = Config.merge_config(partial_config, self.config) + + if self.hooks_setup == false then + local augroup = vim.api.nvim_create_augroup + local HarpoonGroup = augroup('Harpoon', {}) + + vim.api.nvim_create_autocmd({"BufLeave", "VimLeavePre"}, { + group = HarpoonGroup, + pattern = '*', + callback = function(ev) + self:_for_each_list(function(list, config) + + local fn = config[ev.event] + if fn ~= nil then + fn(ev, list) + end + + if ev.event == "VimLeavePre" then + self:sync() + end + end) + end, + }) + + self.hooks_setup = true + end + return self end @@ -64,22 +92,29 @@ function Harpoon:list(name) return list end -function Harpoon:sync() +---@param cb fun(list: HarpoonList, config: HarpoonPartialConfigItem, name: string) +function Harpoon:_for_each_list(cb) local key = self.config.settings.key() local seen = self.data.seen[key] local lists = self.lists[key] + + if not seen then + return + end + for list_name, _ in pairs(seen) do - local encoded = lists[list_name]:encode() - self.data:update(key, list_name, encoded) + local list_config = Config.get_config(self.config, list_name) + cb(lists[list_name], list_config, list_name) end - self.data:sync() end -function Harpoon:setup_hooks() - -- setup the autocommands - -- vim exits sync data - -- buf exit setup the cursor location - error("I haven't implemented this yet") +function Harpoon:sync() + local key = self.config.settings.key() + self:_for_each_list(function(list, _, list_name) + local encoded = list:encode() + self.data:update(key, list_name, encoded) + end) + self.data:sync() end function Harpoon:info() diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index c9eada4e..8bb588c5 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -74,6 +74,16 @@ function HarpoonList:get(index) return self.items[index] end +function HarpoonList:get_by_display(name) + local displayed = self:display() + local index = index_of(displayed, name) + if index == -1 then + return nil + end + return self.items[index] +end + + --- much inefficiencies. dun care ---@param displayed string[] function HarpoonList:resolve_displayed(displayed) diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index 02670b30..7d72779f 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -6,6 +6,7 @@ local eq = assert.are.same describe("harpoon", function() + before_each(function() Data.set_data_path("/tmp/harpoon2.json") Data.__dangerously_clear_data() @@ -25,6 +26,37 @@ describe("harpoon", function() end) + it("when we change buffers we update the row and column", function() + local file_name = "/tmp/harpoon-test" + local row = 1 + local col = 0 + local target_buf = utils.create_file(file_name, { + "foo", + "bar", + "baz", + "qux" + }, row, col) + + local list = harpoon:list():append() + local other_buf = utils.create_file("other-file", { + "foo", + "bar", + "baz", + "qux" + }, row, col) + + vim.api.nvim_set_current_buf(target_buf) + vim.api.nvim_win_set_cursor(0, {row + 1, col}) + vim.api.nvim_set_current_buf(other_buf) + + local expected = { + {value = file_name, context = {row = row + 1, col = col}}, + } + + eq(list.items, expected) + + end) + it("full harpoon add sync cycle", function() local file_name = "/tmp/harpoon-test" local row = 3 From 2fdac4f4df57001a509b14d7431438e7fd1d2fc0 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 20 Nov 2023 21:02:33 -0700 Subject: [PATCH 13/80] feat: brought over the old ui. need to get this upgrade to new api --- lua/harpoon2/ui.lua | 310 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 lua/harpoon2/ui.lua diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua new file mode 100644 index 00000000..a59651ed --- /dev/null +++ b/lua/harpoon2/ui.lua @@ -0,0 +1,310 @@ +-- TODO: This is just the UI from the previous harpoon. +-- it needs to be cleaned up and converted to the new api +local harpoon = require("harpoon") +local popup = require("plenary.popup") +local Marked = require("harpoon.mark") +local utils = require("harpoon.utils") +local log = require("harpoon.dev").log + +local M = {} + +Harpoon_win_id = nil +Harpoon_bufh = nil + +-- We save before we close because we use the state of the buffer as the list +-- of items. +local function close_menu(force_save) + force_save = force_save or false + local global_config = harpoon.get_global_settings() + + if global_config.save_on_toggle or force_save then + require("harpoon.ui").on_menu_save() + end + + vim.api.nvim_win_close(Harpoon_win_id, true) + + Harpoon_win_id = nil + Harpoon_bufh = nil +end + +local function create_window() + log.trace("_create_window()") + local config = harpoon.get_menu_config() + local width = config.width or 60 + local height = config.height or 10 + local borderchars = config.borderchars + or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } + local bufnr = vim.api.nvim_create_buf(false, false) + + local Harpoon_win_id, win = popup.create(bufnr, { + title = "Harpoon", + highlight = "HarpoonWindow", + line = math.floor(((vim.o.lines - height) / 2) - 1), + col = math.floor((vim.o.columns - width) / 2), + minwidth = width, + minheight = height, + borderchars = borderchars, + }) + + vim.api.nvim_win_set_option( + win.border.win_id, + "winhl", + "Normal:HarpoonBorder" + ) + + return { + bufnr = bufnr, + win_id = Harpoon_win_id, + } +end + +local function get_menu_items() + log.trace("_get_menu_items()") + local lines = vim.api.nvim_buf_get_lines(Harpoon_bufh, 0, -1, true) + local indices = {} + + for _, line in pairs(lines) do + if not utils.is_white_space(line) then + table.insert(indices, line) + end + end + + return indices +end + +function M.toggle_quick_menu() + log.trace("toggle_quick_menu()") + if Harpoon_win_id ~= nil and vim.api.nvim_win_is_valid(Harpoon_win_id) then + close_menu() + return + end + + local curr_file = utils.normalize_path(vim.api.nvim_buf_get_name(0)) + vim.cmd( + string.format( + "autocmd Filetype harpoon " + .. "let path = '%s' | call clearmatches() | " + -- move the cursor to the line containing the current filename + .. "call search('\\V'.path.'\\$') | " + -- add a hl group to that line + .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", + curr_file:gsub("\\", "\\\\") + ) + ) + + local win_info = create_window() + local contents = {} + local global_config = harpoon.get_global_settings() + + Harpoon_win_id = win_info.win_id + Harpoon_bufh = win_info.bufnr + + for idx = 1, Marked.get_length() do + local file = Marked.get_marked_file_name(idx) + if file == "" then + file = "(empty)" + end + contents[idx] = string.format("%s", file) + end + + vim.api.nvim_win_set_option(Harpoon_win_id, "number", true) + vim.api.nvim_buf_set_name(Harpoon_bufh, "harpoon-menu") + vim.api.nvim_buf_set_lines(Harpoon_bufh, 0, #contents, false, contents) + vim.api.nvim_buf_set_option(Harpoon_bufh, "filetype", "harpoon") + vim.api.nvim_buf_set_option(Harpoon_bufh, "buftype", "acwrite") + vim.api.nvim_buf_set_option(Harpoon_bufh, "bufhidden", "delete") + vim.api.nvim_buf_set_keymap( + Harpoon_bufh, + "n", + "q", + "lua require('harpoon.ui').toggle_quick_menu()", + { silent = true } + ) + vim.api.nvim_buf_set_keymap( + Harpoon_bufh, + "n", + "", + "lua require('harpoon.ui').toggle_quick_menu()", + { silent = true } + ) + vim.api.nvim_buf_set_keymap( + Harpoon_bufh, + "n", + "", + "lua require('harpoon.ui').select_menu_item()", + {} + ) + vim.cmd( + string.format( + "autocmd BufWriteCmd lua require('harpoon.ui').on_menu_save()", + Harpoon_bufh + ) + ) + if global_config.save_on_change then + vim.cmd( + string.format( + "autocmd TextChanged,TextChangedI lua require('harpoon.ui').on_menu_save()", + Harpoon_bufh + ) + ) + end + vim.cmd( + string.format( + "autocmd BufModifiedSet set nomodified", + Harpoon_bufh + ) + ) + vim.cmd( + "autocmd BufLeave ++nested ++once silent lua require('harpoon.ui').toggle_quick_menu()" + ) +end + +function M.select_menu_item() + local idx = vim.fn.line(".") + close_menu(true) + M.nav_file(idx) +end + +function M.on_menu_save() + log.trace("on_menu_save()") + Marked.set_mark_list(get_menu_items()) +end + +local function get_or_create_buffer(filename) + local buf_exists = vim.fn.bufexists(filename) ~= 0 + if buf_exists then + return vim.fn.bufnr(filename) + end + + return vim.fn.bufadd(filename) +end + +function M.nav_file(id) + log.trace("nav_file(): Navigating to", id) + local idx = Marked.get_index_of(id) + if not Marked.valid_index(idx) then + log.debug("nav_file(): No mark exists for id", id) + return + end + + local mark = Marked.get_marked_file(idx) + local filename = vim.fs.normalize(mark.filename) + local buf_id = get_or_create_buffer(filename) + local set_row = not vim.api.nvim_buf_is_loaded(buf_id) + + local old_bufnr = vim.api.nvim_get_current_buf() + + vim.api.nvim_set_current_buf(buf_id) + vim.api.nvim_buf_set_option(buf_id, "buflisted", true) + if set_row and mark.row and mark.col then + vim.cmd(string.format(":call cursor(%d, %d)", mark.row, mark.col)) + log.debug( + string.format( + "nav_file(): Setting cursor to row: %d, col: %d", + mark.row, + mark.col + ) + ) + end + + local old_bufinfo = vim.fn.getbufinfo(old_bufnr) + if type(old_bufinfo) == "table" and #old_bufinfo >= 1 then + old_bufinfo = old_bufinfo[1] + local no_name = old_bufinfo.name == "" + local one_line = old_bufinfo.linecount == 1 + local unchanged = old_bufinfo.changed == 0 + if no_name and one_line and unchanged then + vim.api.nvim_buf_delete(old_bufnr, {}) + end + end +end + +function M.location_window(options) + local default_options = { + relative = "editor", + style = "minimal", + width = 30, + height = 15, + row = 2, + col = 2, + } + options = vim.tbl_extend("keep", options, default_options) + + local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true) + local win_id = vim.api.nvim_open_win(bufnr, true, options) + + return { + bufnr = bufnr, + win_id = win_id, + } +end + +function M.notification(text) + local win_stats = vim.api.nvim_list_uis()[1] + local win_width = win_stats.width + + local prev_win = vim.api.nvim_get_current_win() + + local info = M.location_window({ + width = 20, + height = 2, + row = 1, + col = win_width - 21, + }) + + vim.api.nvim_buf_set_lines( + info.bufnr, + 0, + 5, + false, + { "!!! Notification", text } + ) + vim.api.nvim_set_current_win(prev_win) + + return { + bufnr = info.bufnr, + win_id = info.win_id, + } +end + +function M.close_notification(bufnr) + vim.api.nvim_buf_delete(bufnr) +end + +function M.nav_next() + log.trace("nav_next()") + local current_index = Marked.get_current_index() + local number_of_items = Marked.get_length() + + if current_index == nil then + current_index = 1 + else + current_index = current_index + 1 + end + + if current_index > number_of_items then + current_index = 1 + end + M.nav_file(current_index) +end + +function M.nav_prev() + log.trace("nav_prev()") + local current_index = Marked.get_current_index() + local number_of_items = Marked.get_length() + + if current_index == nil then + current_index = number_of_items + else + current_index = current_index - 1 + end + + if current_index < 1 then + current_index = number_of_items + end + + M.nav_file(current_index) +end + +return M + From 5d7ee0d89449e77c9318af60d72b3835595c11c6 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Sun, 26 Nov 2023 21:04:54 -0700 Subject: [PATCH 14/80] checkpoint: i must sleep with my beautiful wife, but f... its borked --- lua/harpoon2/buffer.lua | 121 ++++++++++++++ lua/harpoon2/config.lua | 20 ++- lua/harpoon2/init.lua | 24 ++- lua/harpoon2/list.lua | 25 +++ lua/harpoon2/test/ui_spec.lua | 18 ++ lua/harpoon2/test/utils.lua | 19 +++ lua/harpoon2/ui.lua | 299 +++++++++------------------------- lua/harpoon2/utils.lua | 12 ++ 8 files changed, 317 insertions(+), 221 deletions(-) create mode 100644 lua/harpoon2/buffer.lua create mode 100644 lua/harpoon2/test/ui_spec.lua create mode 100644 lua/harpoon2/utils.lua diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua new file mode 100644 index 00000000..4ba6d79a --- /dev/null +++ b/lua/harpoon2/buffer.lua @@ -0,0 +1,121 @@ +local utils = require("harpoon2.utils") +local M = {} + +local HARPOON_MENU = "__harpoon-menu__" + +-- simple reason here is that if we are deving harpoon, we will create several +-- ui objects, each with their own buffer, which will cause the name to be duplicated and then we will get a vim error on nvim_buf_set_name +local harpoon_menu_id = 0 + +local function get_harpoon_menu_name() + harpoon_menu_id = harpoon_menu_id + 1 + return HARPOON_MENU .. harpoon_menu_id +end + +---TODO: I don't know how to do what i want to do, but i want to be able to +---make this so we use callbacks for these buffer actions instead of using +---strings back into the ui. it feels gross and it puts odd coupling +---@param bufnr number +function M.setup_autocmds_and_keymaps(bufnr) + --[[ + -- TODO: Do the highlighting better + local curr_file = vim.api.nvim_buf_get_name(0) + local cmd = + string.format( + "autocmd Filetype harpoon " + .. "let path = '%s' | call clearmatches() | " + -- move the cursor to the line containing the current filename + .. "call search('\\V'.path.'\\$') | " + -- add a hl group to that line + .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", + curr_file:gsub("\\", "\\\\") + ) + print(cmd) + vim.cmd(cmd) + --]] + + if vim.api.nvim_buf_get_name(bufnr) == "" then + vim.api.nvim_buf_set_name(bufnr, get_harpoon_menu_name()) + end + + vim.api.nvim_buf_set_option(bufnr, "filetype", "harpoon") + vim.api.nvim_buf_set_option(bufnr, "buftype", "acwrite") + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "delete") + + --[[ + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "z", + "lua print('WTF')", + { silent = true } + ) + --]] + + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "q", + "lua require('harpoon2').ui:toggle_quick_menu()", + { silent = true } + ) + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "", + "lua require('harpoon2').ui:toggle_quick_menu()", + { silent = true } + ) + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "", + "lua require('harpoon2').ui:select_menu_item()", + {} + ) + -- TODO: Update these to use the new autocmd api + vim.cmd( + string.format( + "autocmd BufWriteCmd lua require('harpoon2').ui:on_menu_save()", + bufnr + ) + ) + -- TODO: Do we want this? is this a thing? + -- its odd... why save on text change? shouldn't we wait until close / w / esc? + --[[ + if global_config.save_on_change then + vim.cmd( + string.format( + "autocmd TextChanged,TextChangedI lua require('harpoon2').ui:on_menu_save()", + bufnr + ) + ) + end + --]] + vim.cmd( + string.format( + "autocmd BufModifiedSet set nomodified", + bufnr + ) + ) + vim.cmd( + "autocmd BufLeave ++nested ++once silent lua require('harpoon2').ui:toggle_quick_menu()" + ) + +end + +---@param bufnr number +function M.get_contents(bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) + local indices = {} + + for _, line in pairs(lines) do + if not utils.is_white_space(line) then + table.insert(indices, line) + end + end + + return indices +end + +return M diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index 1365cb9b..f3ec2da3 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -1,3 +1,4 @@ +local utils = require("harpoon2.utils") local M = {} ---@alias HarpoonListItem {value: any, context: any} @@ -12,6 +13,12 @@ local M = {} ---@field add? fun(item: any?): HarpoonListItem ---@field BufLeave? fun(evt: any, list: HarpoonList): nil ---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil +---@field get_root_dir? fun(): string + +---@class HarpoonWindowSettings +---@field width number +---@field height number + ---notehunthoeunthoeunthoeunthoeunthoeunth ---@class HarpoonSettings @@ -108,10 +115,21 @@ function M.get_default_config() return list_item_a.value == list_item_b.value end, + get_root_dir = function() + return vim.loop.cwd() + end, + ---@param name any ---@return HarpoonListItem add = function(name) - name = name or vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + name = name or + -- TODO: should we do path normalization??? + -- i know i have seen sometimes it becoming an absolute + -- path, if that is the case we can use the context to + -- store the bufname and then have value be the normalized + -- value + vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + local bufnr = vim.fn.bufnr(name, false) local pos = {1, 0} diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index 517a886a..593b8b5c 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -1,3 +1,4 @@ +local Ui = require("harpoon2.ui") local Data = require("harpoon2.data") local Config = require("harpoon2.config") local List = require("harpoon2.list") @@ -12,6 +13,7 @@ local DEFAULT_LIST = "__harpoon_files" ---@class Harpoon ---@field config HarpoonConfig +---@field ui HarpoonUI ---@field data HarpoonData ---@field lists {[string]: {[string]: HarpoonList}} ---@field hooks_setup boolean @@ -23,18 +25,24 @@ Harpoon.__index = Harpoon function Harpoon:new() local config = Config.get_default_config() - return setmetatable({ + local harpoon = setmetatable({ config = config, data = Data.Data:new(), + ui = Ui:new(config.settings), lists = {}, hooks_setup = false, }, self) + + return harpoon end ---@param partial_config HarpoonPartialConfig ---@return Harpoon function Harpoon:setup(partial_config) self.config = Config.merge_config(partial_config, self.config) + self.ui:configure(self.config.settings) + + ---TODO: should we go through every seen list and update its config? if self.hooks_setup == false then local augroup = vim.api.nvim_create_augroup @@ -44,6 +52,7 @@ function Harpoon:setup(partial_config) group = HarpoonGroup, pattern = '*', callback = function(ev) + --[[ self:_for_each_list(function(list, config) local fn = config[ev.event] @@ -55,6 +64,7 @@ function Harpoon:setup(partial_config) self:sync() end end) + --]] end, }) @@ -129,6 +139,16 @@ function Harpoon:dump() return self.data._data end -return Harpoon:new() +function Harpoon:__debug_reset() + require("plenary.reload").reload_module("harpoon2") +end +local harpoon = Harpoon:new() +HARPOON_DEBUG_VAR = HARPOON_DEBUG_VAR or 0 +if HARPOON_DEBUG_VAR == 0 then + harpoon.ui:toggle_quick_menu(harpoon:list()) + HARPOON_DEBUG_VAR = 1 +end +-- leave this undone, i sometimes use this for debugging +return harpoon diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index 8bb588c5..21d86810 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -18,6 +18,7 @@ end --- @class HarpoonList --- @field config HarpoonPartialConfigItem --- @field name string +--- @field _index number --- @field items HarpoonItem[] local HarpoonList = {} @@ -27,9 +28,15 @@ function HarpoonList:new(config, name, items) items = items, config = config, name = name, + _index = 1, }, self) end +---@return number +function HarpoonList:length() + return #self.items +end + ---@return HarpoonList function HarpoonList:append(item) item = item or self.config.add() @@ -113,6 +120,24 @@ function HarpoonList:select(index, options) end end +function HarpoonList:next() + self._index = self._index + 1 + if self._index > #self.items then + self._index = 1 + end + + self:select(self._index) +end + +function HarpoonList:prev() + self._index = self._index - 1 + if self._index < 1 then + self._index = #self.items + end + + self:select(self._index) +end + --- @return string[] function HarpoonList:display() local out = {} diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua new file mode 100644 index 00000000..5ec055fa --- /dev/null +++ b/lua/harpoon2/test/ui_spec.lua @@ -0,0 +1,18 @@ +local utils = require("harpoon2.test.utils") + +local eq = assert.are.same + +describe("harpoon", function() + + before_each(utils.before_each) + + it("open the ui without any items in the list", function() + local harpoon = require("harpoon2") + harpoon.ui:toggle_quick_menu(harpoon:list()) + + -- no test, just wanted it to run without error'ing + end) + +end) + + diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua index ecee9c18..25e06683 100644 --- a/lua/harpoon2/test/utils.lua +++ b/lua/harpoon2/test/utils.lua @@ -1,8 +1,27 @@ +local Data = require("harpoon2.data") local M = {} M.created_files = {} +function M.before_each() + Data.set_data_path("/tmp/harpoon2.json") + Data.__dangerously_clear_data() + require("plenary.reload").reload_module("harpoon2") + Data = require("harpoon2.data") + Data.set_data_path("/tmp/harpoon2.json") + local harpoon = require("harpoon2") + M.clean_files() + + harpoon:setup({ + settings = { + key = function() + return "testies" + end + } + }) +end + function M.clean_files() for _, bufnr in ipairs(M.created_files) do vim.api.nvim_buf_delete(bufnr, {force = true}) diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index a59651ed..3b525ada 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -1,42 +1,57 @@ --- TODO: This is just the UI from the previous harpoon. --- it needs to be cleaned up and converted to the new api -local harpoon = require("harpoon") -local popup = require("plenary.popup") -local Marked = require("harpoon.mark") -local utils = require("harpoon.utils") -local log = require("harpoon.dev").log - -local M = {} - -Harpoon_win_id = nil -Harpoon_bufh = nil - --- We save before we close because we use the state of the buffer as the list --- of items. -local function close_menu(force_save) - force_save = force_save or false - local global_config = harpoon.get_global_settings() +local popup = require("plenary").popup +local Buffer = require("harpoon2.buffer") +local DEFAULT_WINDOW_WIDTH = 69 -- nice + +---@class HarpoonUI +---@field win_id number +---@field bufnr number +---@field settings HarpoonSettings +---@field active_list HarpoonList +local HarpoonUI = {} + +HarpoonUI.__index = HarpoonUI + +---@param settings HarpoonSettings +---@return HarpoonUI +function HarpoonUI:new(settings) + return setmetatable({ + win_id = nil, + bufnr = nil, + active_list = nil, + settings = settings, + }, self) +end - if global_config.save_on_toggle or force_save then - require("harpoon.ui").on_menu_save() +function HarpoonUI:close_menu() + print("CLOSING MENU") + if self.win_id ~= nil and vim.api.nvim_win_is_valid(self.win_id) then + vim.api.nvim_win_close(self.win_id, true) end - vim.api.nvim_win_close(Harpoon_win_id, true) + if self.bufnr ~= nil and vim.api.nvim_buf_is_valid(self.bufnr) then + vim.api.nvim_buf_delete(self.bufnr, { force = true }) + end - Harpoon_win_id = nil - Harpoon_bufh = nil + self.active_list = nil + self.win_id = nil + self.bufnr = nil end -local function create_window() - log.trace("_create_window()") - local config = harpoon.get_menu_config() - local width = config.width or 60 - local height = config.height or 10 - local borderchars = config.borderchars - or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } - local bufnr = vim.api.nvim_create_buf(false, false) +---@return number,number +function HarpoonUI:_create_window() + local win = vim.api.nvim_list_uis() - local Harpoon_win_id, win = popup.create(bufnr, { + local width = DEFAULT_WINDOW_WIDTH + if #win > 0 then + -- no ackshual reason for 0.62569, just looks complicated, and i want + -- to make my boss think i am smart + width = math.floor(win[1].width * 0.62569) + end + + local height = 8 + local borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } + local bufnr = vim.api.nvim_create_buf(false, false) + local win_id, _ = popup.create(bufnr, { title = "Harpoon", highlight = "HarpoonWindow", line = math.floor(((vim.o.lines - height) / 2) - 1), @@ -45,180 +60,62 @@ local function create_window() minheight = height, borderchars = borderchars, }) + Buffer.setup_autocmds_and_keymaps(bufnr) + self.win_id = win_id + vim.api.nvim_win_set_option(self.win_id, "number", true) vim.api.nvim_win_set_option( - win.border.win_id, + win_id, "winhl", "Normal:HarpoonBorder" ) - return { - bufnr = bufnr, - win_id = Harpoon_win_id, - } + return win_id, bufnr end -local function get_menu_items() - log.trace("_get_menu_items()") - local lines = vim.api.nvim_buf_get_lines(Harpoon_bufh, 0, -1, true) - local indices = {} +local count = 0 - for _, line in pairs(lines) do - if not utils.is_white_space(line) then - table.insert(indices, line) - end - end +---@param list HarpoonList +function HarpoonUI:toggle_quick_menu(list) - return indices -end + count = count + 1 + print("toggle?", self.win_id, self.bufnr, count) -function M.toggle_quick_menu() - log.trace("toggle_quick_menu()") - if Harpoon_win_id ~= nil and vim.api.nvim_win_is_valid(Harpoon_win_id) then - close_menu() + if list == nil or self.win_id ~= nil then + self:close_menu() return end - local curr_file = utils.normalize_path(vim.api.nvim_buf_get_name(0)) - vim.cmd( - string.format( - "autocmd Filetype harpoon " - .. "let path = '%s' | call clearmatches() | " - -- move the cursor to the line containing the current filename - .. "call search('\\V'.path.'\\$') | " - -- add a hl group to that line - .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", - curr_file:gsub("\\", "\\\\") - ) - ) - - local win_info = create_window() - local contents = {} - local global_config = harpoon.get_global_settings() + local win_id, bufnr = self:_create_window() - Harpoon_win_id = win_info.win_id - Harpoon_bufh = win_info.bufnr + print("_create_window_results", win_id, bufnr, count) + self.win_id = win_id + self.bufnr = bufnr + self.active_list = list - for idx = 1, Marked.get_length() do - local file = Marked.get_marked_file_name(idx) - if file == "" then - file = "(empty)" - end - contents[idx] = string.format("%s", file) - end - - vim.api.nvim_win_set_option(Harpoon_win_id, "number", true) - vim.api.nvim_buf_set_name(Harpoon_bufh, "harpoon-menu") - vim.api.nvim_buf_set_lines(Harpoon_bufh, 0, #contents, false, contents) - vim.api.nvim_buf_set_option(Harpoon_bufh, "filetype", "harpoon") - vim.api.nvim_buf_set_option(Harpoon_bufh, "buftype", "acwrite") - vim.api.nvim_buf_set_option(Harpoon_bufh, "bufhidden", "delete") - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "q", - "lua require('harpoon.ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "", - "lua require('harpoon.ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "", - "lua require('harpoon.ui').select_menu_item()", - {} - ) - vim.cmd( - string.format( - "autocmd BufWriteCmd lua require('harpoon.ui').on_menu_save()", - Harpoon_bufh - ) - ) - if global_config.save_on_change then - vim.cmd( - string.format( - "autocmd TextChanged,TextChangedI lua require('harpoon.ui').on_menu_save()", - Harpoon_bufh - ) - ) - end - vim.cmd( - string.format( - "autocmd BufModifiedSet set nomodified", - Harpoon_bufh - ) - ) - vim.cmd( - "autocmd BufLeave ++nested ++once silent lua require('harpoon.ui').toggle_quick_menu()" - ) + local contents = self.active_list:display() + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, contents) end -function M.select_menu_item() +function HarpoonUI:select_menu_item() + error("select_menu_item...?") local idx = vim.fn.line(".") - close_menu(true) - M.nav_file(idx) + self.active_list:select(idx) + self:close_menu() end -function M.on_menu_save() - log.trace("on_menu_save()") - Marked.set_mark_list(get_menu_items()) +function HarpoonUI:on_menu_save() + error("saving...?") + local list = Buffer.get_contents(self.bufnr) + self.active_list:resolve_displayed(list) end -local function get_or_create_buffer(filename) - local buf_exists = vim.fn.bufexists(filename) ~= 0 - if buf_exists then - return vim.fn.bufnr(filename) - end - - return vim.fn.bufadd(filename) -end - -function M.nav_file(id) - log.trace("nav_file(): Navigating to", id) - local idx = Marked.get_index_of(id) - if not Marked.valid_index(idx) then - log.debug("nav_file(): No mark exists for id", id) - return - end - - local mark = Marked.get_marked_file(idx) - local filename = vim.fs.normalize(mark.filename) - local buf_id = get_or_create_buffer(filename) - local set_row = not vim.api.nvim_buf_is_loaded(buf_id) - - local old_bufnr = vim.api.nvim_get_current_buf() - - vim.api.nvim_set_current_buf(buf_id) - vim.api.nvim_buf_set_option(buf_id, "buflisted", true) - if set_row and mark.row and mark.col then - vim.cmd(string.format(":call cursor(%d, %d)", mark.row, mark.col)) - log.debug( - string.format( - "nav_file(): Setting cursor to row: %d, col: %d", - mark.row, - mark.col - ) - ) - end - - local old_bufinfo = vim.fn.getbufinfo(old_bufnr) - if type(old_bufinfo) == "table" and #old_bufinfo >= 1 then - old_bufinfo = old_bufinfo[1] - local no_name = old_bufinfo.name == "" - local one_line = old_bufinfo.linecount == 1 - local unchanged = old_bufinfo.changed == 0 - if no_name and one_line and unchanged then - vim.api.nvim_buf_delete(old_bufnr, {}) - end - end +---@param settings HarpoonSettings +function HarpoonUI:configure(settings) + self.settings = settings end +--[[ function M.location_window(options) local default_options = { relative = "editor", @@ -239,6 +136,7 @@ function M.location_window(options) } end +-- TODO: What is this used for? function M.notification(text) local win_stats = vim.api.nvim_list_uis()[1] local win_width = win_stats.width @@ -270,41 +168,6 @@ end function M.close_notification(bufnr) vim.api.nvim_buf_delete(bufnr) end +--]] -function M.nav_next() - log.trace("nav_next()") - local current_index = Marked.get_current_index() - local number_of_items = Marked.get_length() - - if current_index == nil then - current_index = 1 - else - current_index = current_index + 1 - end - - if current_index > number_of_items then - current_index = 1 - end - M.nav_file(current_index) -end - -function M.nav_prev() - log.trace("nav_prev()") - local current_index = Marked.get_current_index() - local number_of_items = Marked.get_length() - - if current_index == nil then - current_index = number_of_items - else - current_index = current_index - 1 - end - - if current_index < 1 then - current_index = number_of_items - end - - M.nav_file(current_index) -end - -return M - +return HarpoonUI diff --git a/lua/harpoon2/utils.lua b/lua/harpoon2/utils.lua new file mode 100644 index 00000000..8dfa86a8 --- /dev/null +++ b/lua/harpoon2/utils.lua @@ -0,0 +1,12 @@ +local Path = require("plenary.path") + +local M = {} +function M.normalize_path(item) + return Path:new(item):make_relative(M.project_key()) +end + +function M.is_white_space(str) + return str:gsub("%s", "") == "" +end + +return M From b22cb4a873a583a9acfe8d118b0aeba62270564a Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 27 Nov 2023 21:06:26 -0700 Subject: [PATCH 15/80] feat: toggling menu now works! --- lua/harpoon2/buffer.lua | 10 -------- lua/harpoon2/data.lua | 3 ++- lua/harpoon2/init.lua | 12 +--------- lua/harpoon2/scratch/toggle.lua | 6 +++++ lua/harpoon2/test/harpoon_spec.lua | 25 +++++--------------- lua/harpoon2/test/ui_spec.lua | 12 ++++++++-- lua/harpoon2/test/utils.lua | 37 +++++++++++++++++------------- lua/harpoon2/ui.lua | 23 +++++++++++-------- 8 files changed, 60 insertions(+), 68 deletions(-) create mode 100644 lua/harpoon2/scratch/toggle.lua diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua index 4ba6d79a..6df7c6a7 100644 --- a/lua/harpoon2/buffer.lua +++ b/lua/harpoon2/buffer.lua @@ -42,16 +42,6 @@ function M.setup_autocmds_and_keymaps(bufnr) vim.api.nvim_buf_set_option(bufnr, "buftype", "acwrite") vim.api.nvim_buf_set_option(bufnr, "bufhidden", "delete") - --[[ - vim.api.nvim_buf_set_keymap( - bufnr, - "n", - "z", - "lua print('WTF')", - { silent = true } - ) - --]] - vim.api.nvim_buf_set_keymap( bufnr, "n", diff --git a/lua/harpoon2/data.lua b/lua/harpoon2/data.lua index 7050b89e..577b6ff1 100644 --- a/lua/harpoon2/data.lua +++ b/lua/harpoon2/data.lua @@ -56,7 +56,8 @@ local function read_data() write_data({}) end - local data = vim.json.decode(path:read()) + local out_data = path:read() + local data = vim.json.decode(out_data) return data end diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index 593b8b5c..25829d9d 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -52,7 +52,6 @@ function Harpoon:setup(partial_config) group = HarpoonGroup, pattern = '*', callback = function(ev) - --[[ self:_for_each_list(function(list, config) local fn = config[ev.event] @@ -64,7 +63,6 @@ function Harpoon:setup(partial_config) self:sync() end end) - --]] end, }) @@ -143,12 +141,4 @@ function Harpoon:__debug_reset() require("plenary.reload").reload_module("harpoon2") end -local harpoon = Harpoon:new() -HARPOON_DEBUG_VAR = HARPOON_DEBUG_VAR or 0 -if HARPOON_DEBUG_VAR == 0 then - harpoon.ui:toggle_quick_menu(harpoon:list()) - HARPOON_DEBUG_VAR = 1 -end --- leave this undone, i sometimes use this for debugging - -return harpoon +return Harpoon:new() diff --git a/lua/harpoon2/scratch/toggle.lua b/lua/harpoon2/scratch/toggle.lua new file mode 100644 index 00000000..93c25c37 --- /dev/null +++ b/lua/harpoon2/scratch/toggle.lua @@ -0,0 +1,6 @@ + +local harpoon = require("harpoon2") + +harpoon.ui:toggle_quick_menu(harpoon:list()) + + diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index 7d72779f..66889d4e 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -1,29 +1,15 @@ local utils = require("harpoon2.test.utils") -local Data = require("harpoon2.data") local harpoon = require("harpoon2") local eq = assert.are.same -describe("harpoon", function() +local be = utils.before_each(os.tmpname()) +describe("harpoon", function() before_each(function() - Data.set_data_path("/tmp/harpoon2.json") - Data.__dangerously_clear_data() - require("plenary.reload").reload_module("harpoon2") - Data = require("harpoon2.data") - Data.set_data_path("/tmp/harpoon2.json") + be() harpoon = require("harpoon2") - utils.clean_files() - - harpoon:setup({ - settings = { - key = function() - return "testies" - end - } - }) - end) it("when we change buffers we update the row and column", function() @@ -53,7 +39,7 @@ describe("harpoon", function() {value = file_name, context = {row = row + 1, col = col}}, } - eq(list.items, expected) + eq(expected, list.items) end) @@ -69,7 +55,8 @@ describe("harpoon", function() "qux" }, row, col) - local list = harpoon:list():append() + local list = harpoon:list() + list:append() harpoon:sync() eq(harpoon:dump(), { diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index 5ec055fa..3c0cb2dd 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -4,13 +4,21 @@ local eq = assert.are.same describe("harpoon", function() - before_each(utils.before_each) + before_each(utils.before_each(os.tmpname())) it("open the ui without any items in the list", function() local harpoon = require("harpoon2") harpoon.ui:toggle_quick_menu(harpoon:list()) - -- no test, just wanted it to run without error'ing + local bufnr = harpoon.ui.bufnr + local win_id = harpoon.ui.win_id + + harpoon.ui:toggle_quick_menu() + + eq(vim.api.nvim_buf_is_valid(bufnr), false) + eq(vim.api.nvim_win_is_valid(win_id), false) + eq(harpoon.ui.bufnr, nil) + eq(harpoon.ui.win_id, nil) end) end) diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua index 25e06683..d7cfc165 100644 --- a/lua/harpoon2/test/utils.lua +++ b/lua/harpoon2/test/utils.lua @@ -4,22 +4,27 @@ local M = {} M.created_files = {} -function M.before_each() - Data.set_data_path("/tmp/harpoon2.json") - Data.__dangerously_clear_data() - require("plenary.reload").reload_module("harpoon2") - Data = require("harpoon2.data") - Data.set_data_path("/tmp/harpoon2.json") - local harpoon = require("harpoon2") - M.clean_files() - - harpoon:setup({ - settings = { - key = function() - return "testies" - end - } - }) +---@param name string +function M.before_each(name) + return function() + Data.set_data_path(name) + Data.__dangerously_clear_data() + + require("plenary.reload").reload_module("harpoon2") + Data = require("harpoon2.data") + Data.set_data_path(name) + local harpoon = require("harpoon2") + + M.clean_files() + + harpoon:setup({ + settings = { + key = function() + return "testies" + end + } + }) + end end function M.clean_files() diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index 3b525ada..9ea84f9f 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -23,18 +23,25 @@ function HarpoonUI:new(settings) end function HarpoonUI:close_menu() - print("CLOSING MENU") - if self.win_id ~= nil and vim.api.nvim_win_is_valid(self.win_id) then - vim.api.nvim_win_close(self.win_id, true) + if self.closing then + return end + self.closing = true + if self.bufnr ~= nil and vim.api.nvim_buf_is_valid(self.bufnr) then vim.api.nvim_buf_delete(self.bufnr, { force = true }) end + if self.win_id ~= nil and vim.api.nvim_win_is_valid(self.win_id) then + vim.api.nvim_win_close(self.win_id, true) + end + self.active_list = nil self.win_id = nil self.bufnr = nil + + self.closing = false end ---@return number,number @@ -51,7 +58,7 @@ function HarpoonUI:_create_window() local height = 8 local borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } local bufnr = vim.api.nvim_create_buf(false, false) - local win_id, _ = popup.create(bufnr, { + local _, popup_info = popup.create(bufnr, { title = "Harpoon", highlight = "HarpoonWindow", line = math.floor(((vim.o.lines - height) / 2) - 1), @@ -60,6 +67,8 @@ function HarpoonUI:_create_window() minheight = height, borderchars = borderchars, }) + local win_id = popup_info.win_id + Buffer.setup_autocmds_and_keymaps(bufnr) self.win_id = win_id @@ -75,11 +84,10 @@ end local count = 0 ----@param list HarpoonList +---@param list? HarpoonList function HarpoonUI:toggle_quick_menu(list) count = count + 1 - print("toggle?", self.win_id, self.bufnr, count) if list == nil or self.win_id ~= nil then self:close_menu() @@ -88,7 +96,6 @@ function HarpoonUI:toggle_quick_menu(list) local win_id, bufnr = self:_create_window() - print("_create_window_results", win_id, bufnr, count) self.win_id = win_id self.bufnr = bufnr self.active_list = list @@ -98,14 +105,12 @@ function HarpoonUI:toggle_quick_menu(list) end function HarpoonUI:select_menu_item() - error("select_menu_item...?") local idx = vim.fn.line(".") self.active_list:select(idx) self:close_menu() end function HarpoonUI:on_menu_save() - error("saving...?") local list = Buffer.get_contents(self.bufnr) self.active_list:resolve_displayed(list) end From 6fdff8bc4121ad9261f3ac13609ead3e643b92ec Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 28 Nov 2023 20:34:00 -0700 Subject: [PATCH 16/80] feat: listeners for list actions one thing to consider is if we want to add filtering to the listeners by list? or should we move the listeners to the list? --- lua/harpoon2/config.lua | 6 ++-- lua/harpoon2/init.lua | 3 ++ lua/harpoon2/list.lua | 21 +++++++++++-- lua/harpoon2/listeners.lua | 57 +++++++++++++++++++++++++++++++++++ lua/harpoon2/test/ui_spec.lua | 4 ++- 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 lua/harpoon2/listeners.lua diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index f3ec2da3..d05c98dd 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -1,4 +1,6 @@ -local utils = require("harpoon2.utils") +local Listeners = require("harpoon2.listeners") +local listeners = Listeners.listeners + local M = {} ---@alias HarpoonListItem {value: any, context: any} @@ -19,7 +21,6 @@ local M = {} ---@field width number ---@field height number - ---notehunthoeunthoeunthoeunthoeunthoeunth ---@class HarpoonSettings ---@field save_on_toggle boolean defaults to true @@ -31,7 +32,6 @@ local M = {} ---@field jump_to_file_location? boolean ---@field key? (fun(): string) - ---@class HarpoonConfig ---@field default HarpoonPartialConfigItem ---@field settings HarpoonSettings diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index 25829d9d..60246ed2 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -2,6 +2,7 @@ local Ui = require("harpoon2.ui") local Data = require("harpoon2.data") local Config = require("harpoon2.config") local List = require("harpoon2.list") +local Listeners = require("harpoon2.listeners") -- setup -- read from a config file @@ -14,6 +15,7 @@ local DEFAULT_LIST = "__harpoon_files" ---@class Harpoon ---@field config HarpoonConfig ---@field ui HarpoonUI +---@field listeners HarpoonListeners ---@field data HarpoonData ---@field lists {[string]: {[string]: HarpoonList}} ---@field hooks_setup boolean @@ -29,6 +31,7 @@ function Harpoon:new() config = config, data = Data.Data:new(), ui = Ui:new(config.settings), + listeners = Listeners.listeners, lists = {}, hooks_setup = false, }, self) diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index 21d86810..1e60cc4c 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -1,3 +1,5 @@ +local Listeners = require("harpoon2.listeners") + local function index_of(items, element, config) local equals = config and config.equals or function(a, b) return a == b end local index = -1 @@ -43,6 +45,7 @@ function HarpoonList:append(item) local index = index_of(self.items, item, self.config) if index == -1 then + Listeners.listeners:emit(Listeners.event_names.ADD, {list = self, item = item, idx = #self.items + 1}) table.insert(self.items, item) end @@ -54,6 +57,7 @@ function HarpoonList:prepend(item) item = item or self.config.add() local index = index_of(self.items, item, self.config) if index == -1 then + Listeners.listeners:emit(Listeners.event_names.ADD, {list = self, item = item, idx = 1}) table.insert(self.items, 1, item) end @@ -64,6 +68,7 @@ end function HarpoonList:remove(item) for i, v in ipairs(self.items) do if self.config.equals(v, item) then + Listeners.listeners:emit(Listeners.event_names.REMOVE, {list = self, item = item, idx = i}) table.remove(self.items, i) break end @@ -73,6 +78,7 @@ end ---@return HarpoonList function HarpoonList:removeAt(index) + Listeners.listeners:emit(Listeners.event_names.REMOVE, {list = self, item = self.items[index], idx = index}) table.remove(self.items, index) return self end @@ -97,17 +103,25 @@ function HarpoonList:resolve_displayed(displayed) local new_list = {} local list_displayed = self:display() + + for i, v in ipairs(list_displayed) do + local index = index_of(list_displayed, v) + if index == -1 then + Listeners.listeners:emit(Listeners.event_names.REMOVE, {list = self, item = v, idx = i}) + end + end + for i, v in ipairs(displayed) do local index = index_of(list_displayed, v) if index == -1 then - table.insert(new_list, self.config.add(v)) + Listeners.listeners:emit(Listeners.event_names.ADD, {list = self, item = v, idx = i}) + new_list[i] = self.config.add(v) else local index_in_new_list = index_of(new_list, self.items[index], self.config) if index_in_new_list == -1 then - table.insert(new_list, self.items[index]) + new_list[i] = self.items[index] end end - end self.items = new_list @@ -116,6 +130,7 @@ end function HarpoonList:select(index, options) local item = self.items[index] if item then + Listeners.listeners:emit(Listeners.event_names.SELECT, {list = self, item = item, idx = index}) self.config.select(item, options) end end diff --git a/lua/harpoon2/listeners.lua b/lua/harpoon2/listeners.lua new file mode 100644 index 00000000..0a0a08f2 --- /dev/null +++ b/lua/harpoon2/listeners.lua @@ -0,0 +1,57 @@ + +---@alias HarpoonListener fun(type: string, args: any[] | any | nil): nil + +---@class HarpoonListeners +---@field listeners (HarpoonListener)[] +---@field listenersByType (table)[] +local HarpoonListeners = {} + +HarpoonListeners.__index = HarpoonListeners + +function HarpoonListeners:new() + return setmetatable({ + listeners = {}, + listenersByType = {} + }, self) +end + +---@param cbOrType HarpoonListener | string +---@param cbOrNil HarpoonListener | string +function HarpoonListeners:add_listener(cbOrType, cbOrNil) + if (type(cbOrType) == "string") then + if not self.listenersByType[cbOrType] then + self.listenersByType[cbOrType] = {} + end + table.insert(self.listenersByType[cbOrType], cbOrNil) + else + table.insert(self.listeners, cbOrType) + end +end + +function HarpoonListeners:clear_listeners() + self.listeners = {} +end + +---@param type string +---@param args any[] | any | nil +function HarpoonListeners:emit(type, args) + for _, cb in ipairs(self.listeners) do + cb(type, args) + end + + local listeners = self.listenersByType[type] + if listeners ~= nil then + for _, cb in ipairs(listeners) do + cb(type, args) + end + end +end + +return { + listeners = HarpoonListeners:new(), + event_names = { + ADD = "ADD", + SELECT = "SELECT", + REMOVE = "REMOVE", + }, +} diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index 3c0cb2dd..de21eb86 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -13,6 +13,9 @@ describe("harpoon", function() local bufnr = harpoon.ui.bufnr local win_id = harpoon.ui.win_id + eq(vim.api.nvim_buf_is_valid(bufnr), true) + eq(vim.api.nvim_win_is_valid(win_id), true) + harpoon.ui:toggle_quick_menu() eq(vim.api.nvim_buf_is_valid(bufnr), false) @@ -20,7 +23,6 @@ describe("harpoon", function() eq(harpoon.ui.bufnr, nil) eq(harpoon.ui.win_id, nil) end) - end) From b58e3db559d1679079b949704f5cb89f84e6cc04 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 28 Nov 2023 20:36:20 -0700 Subject: [PATCH 17/80] chore: format --- lua/harpoon2/buffer.lua | 1 - lua/harpoon2/config.lua | 19 +++++----- lua/harpoon2/data.lua | 15 ++++---- lua/harpoon2/init.lua | 7 ++-- lua/harpoon2/list.lua | 46 ++++++++++++++++------ lua/harpoon2/listeners.lua | 5 +-- lua/harpoon2/scratch/toggle.lua | 3 -- lua/harpoon2/test/config_spec.lua | 8 ++-- lua/harpoon2/test/harpoon_spec.lua | 61 ++++++++++++++++-------------- lua/harpoon2/test/list_spec.lua | 8 ++-- lua/harpoon2/test/ui_spec.lua | 3 -- lua/harpoon2/test/utils.lua | 8 ++-- lua/harpoon2/ui.lua | 10 ++--- 13 files changed, 102 insertions(+), 92 deletions(-) diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua index 6df7c6a7..b09d34e3 100644 --- a/lua/harpoon2/buffer.lua +++ b/lua/harpoon2/buffer.lua @@ -91,7 +91,6 @@ function M.setup_autocmds_and_keymaps(bufnr) vim.cmd( "autocmd BufLeave ++nested ++once silent lua require('harpoon2').ui:toggle_quick_menu()" ) - end ---@param bufnr number diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index d05c98dd..3355cbca 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -42,7 +42,6 @@ local M = {} ---@field settings? HarpoonPartialSettings ---@field [string] HarpoonPartialConfigItem - ---@return HarpoonPartialConfigItem function M.get_config(config, name) return vim.tbl_extend("force", {}, config.default, config[name] or {}) @@ -104,7 +103,7 @@ function M.get_default_config() if set_position then vim.api.nvim_win_set_cursor(0, { file_item.context.row or 1, - file_item.context.col or 0 + file_item.context.col or 0, }) end end, @@ -122,17 +121,17 @@ function M.get_default_config() ---@param name any ---@return HarpoonListItem add = function(name) - name = name or + name = name -- TODO: should we do path normalization??? -- i know i have seen sometimes it becoming an absolute -- path, if that is the case we can use the context to -- store the bufname and then have value be the normalized -- value - vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + or vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) local bufnr = vim.fn.bufnr(name, false) - local pos = {1, 0} + local pos = { 1, 0 } if bufnr ~= -1 then pos = vim.api.nvim_win_get_cursor(0) end @@ -142,13 +141,13 @@ function M.get_default_config() context = { row = pos[1], col = pos[2], - } + }, } end, BufLeave = function(arg, list) - local bufnr = arg.buf; - local bufname = vim.api.nvim_buf_get_name(bufnr); + local bufnr = arg.buf + local bufname = vim.api.nvim_buf_get_name(bufnr) local item = list:get_by_display(bufname) if item then @@ -158,8 +157,8 @@ function M.get_default_config() end end, - autocmds = {"BufLeave"}, - } + autocmds = { "BufLeave" }, + }, } end diff --git a/lua/harpoon2/data.lua b/lua/harpoon2/data.lua index 577b6ff1..2bf5e453 100644 --- a/lua/harpoon2/data.lua +++ b/lua/harpoon2/data.lua @@ -40,7 +40,6 @@ end --- @field has_error boolean local Data = {} - -- 1. load the data -- 2. keep track of the lists requested -- 3. sync save @@ -49,7 +48,7 @@ Data.__index = Data ---@return HarpoonRawData local function read_data() - local path = Path:new(full_data_path) + local path = Path:new(full_data_path) local exists = path:exists() if not exists then @@ -68,9 +67,8 @@ function Data:new() return setmetatable({ _data = data, has_error = not ok, - seen = {} + seen = {}, }, self) - end ---@param key string @@ -89,7 +87,9 @@ end ---@return string[] function Data:data(key, name) if self.has_error then - error("Harpoon: there was an error reading the data file, cannot read data") + error( + "Harpoon: there was an error reading the data file, cannot read data" + ) end if not self.seen[key] then @@ -105,7 +105,9 @@ end ---@param values string[] function Data:update(key, name, values) if self.has_error then - error("Harpoon: there was an error reading the data file, cannot update") + error( + "Harpoon: there was an error reading the data file, cannot update" + ) end self:_get_data(key, name) self._data[key][name] = values @@ -136,7 +138,6 @@ function Data:sync() end end - M.Data = Data return M diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index 60246ed2..c2e8c40f 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -49,14 +49,13 @@ function Harpoon:setup(partial_config) if self.hooks_setup == false then local augroup = vim.api.nvim_create_augroup - local HarpoonGroup = augroup('Harpoon', {}) + local HarpoonGroup = augroup("Harpoon", {}) - vim.api.nvim_create_autocmd({"BufLeave", "VimLeavePre"}, { + vim.api.nvim_create_autocmd({ "BufLeave", "VimLeavePre" }, { group = HarpoonGroup, - pattern = '*', + pattern = "*", callback = function(ev) self:_for_each_list(function(list, config) - local fn = config[ev.event] if fn ~= nil then fn(ev, list) diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index 1e60cc4c..6c527067 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -1,7 +1,10 @@ local Listeners = require("harpoon2.listeners") local function index_of(items, element, config) - local equals = config and config.equals or function(a, b) return a == b end + local equals = config and config.equals + or function(a, b) + return a == b + end local index = -1 for i, item in ipairs(items) do if equals(element, item) then @@ -45,7 +48,10 @@ function HarpoonList:append(item) local index = index_of(self.items, item, self.config) if index == -1 then - Listeners.listeners:emit(Listeners.event_names.ADD, {list = self, item = item, idx = #self.items + 1}) + Listeners.listeners:emit( + Listeners.event_names.ADD, + { list = self, item = item, idx = #self.items + 1 } + ) table.insert(self.items, item) end @@ -57,7 +63,10 @@ function HarpoonList:prepend(item) item = item or self.config.add() local index = index_of(self.items, item, self.config) if index == -1 then - Listeners.listeners:emit(Listeners.event_names.ADD, {list = self, item = item, idx = 1}) + Listeners.listeners:emit( + Listeners.event_names.ADD, + { list = self, item = item, idx = 1 } + ) table.insert(self.items, 1, item) end @@ -68,7 +77,10 @@ end function HarpoonList:remove(item) for i, v in ipairs(self.items) do if self.config.equals(v, item) then - Listeners.listeners:emit(Listeners.event_names.REMOVE, {list = self, item = item, idx = i}) + Listeners.listeners:emit( + Listeners.event_names.REMOVE, + { list = self, item = item, idx = i } + ) table.remove(self.items, i) break end @@ -78,7 +90,10 @@ end ---@return HarpoonList function HarpoonList:removeAt(index) - Listeners.listeners:emit(Listeners.event_names.REMOVE, {list = self, item = self.items[index], idx = index}) + Listeners.listeners:emit( + Listeners.event_names.REMOVE, + { list = self, item = self.items[index], idx = index } + ) table.remove(self.items, index) return self end @@ -96,7 +111,6 @@ function HarpoonList:get_by_display(name) return self.items[index] end - --- much inefficiencies. dun care ---@param displayed string[] function HarpoonList:resolve_displayed(displayed) @@ -107,17 +121,24 @@ function HarpoonList:resolve_displayed(displayed) for i, v in ipairs(list_displayed) do local index = index_of(list_displayed, v) if index == -1 then - Listeners.listeners:emit(Listeners.event_names.REMOVE, {list = self, item = v, idx = i}) + Listeners.listeners:emit( + Listeners.event_names.REMOVE, + { list = self, item = v, idx = i } + ) end end for i, v in ipairs(displayed) do local index = index_of(list_displayed, v) if index == -1 then - Listeners.listeners:emit(Listeners.event_names.ADD, {list = self, item = v, idx = i}) + Listeners.listeners:emit( + Listeners.event_names.ADD, + { list = self, item = v, idx = i } + ) new_list[i] = self.config.add(v) else - local index_in_new_list = index_of(new_list, self.items[index], self.config) + local index_in_new_list = + index_of(new_list, self.items[index], self.config) if index_in_new_list == -1 then new_list[i] = self.items[index] end @@ -130,7 +151,10 @@ end function HarpoonList:select(index, options) local item = self.items[index] if item then - Listeners.listeners:emit(Listeners.event_names.SELECT, {list = self, item = item, idx = index}) + Listeners.listeners:emit( + Listeners.event_names.SELECT, + { list = self, item = item, idx = index } + ) self.config.select(item, options) end end @@ -187,6 +211,4 @@ function HarpoonList.decode(list_config, name, items) return HarpoonList:new(list_config, name, list_items) end - return HarpoonList - diff --git a/lua/harpoon2/listeners.lua b/lua/harpoon2/listeners.lua index 0a0a08f2..c079b633 100644 --- a/lua/harpoon2/listeners.lua +++ b/lua/harpoon2/listeners.lua @@ -1,4 +1,3 @@ - ---@alias HarpoonListener fun(type: string, args: any[] | any | nil): nil ---@class HarpoonListeners @@ -11,14 +10,14 @@ HarpoonListeners.__index = HarpoonListeners function HarpoonListeners:new() return setmetatable({ listeners = {}, - listenersByType = {} + listenersByType = {}, }, self) end ---@param cbOrType HarpoonListener | string ---@param cbOrNil HarpoonListener | string function HarpoonListeners:add_listener(cbOrType, cbOrNil) - if (type(cbOrType) == "string") then + if type(cbOrType) == "string" then if not self.listenersByType[cbOrType] then self.listenersByType[cbOrType] = {} end diff --git a/lua/harpoon2/scratch/toggle.lua b/lua/harpoon2/scratch/toggle.lua index 93c25c37..74859844 100644 --- a/lua/harpoon2/scratch/toggle.lua +++ b/lua/harpoon2/scratch/toggle.lua @@ -1,6 +1,3 @@ - local harpoon = require("harpoon2") harpoon.ui:toggle_quick_menu(harpoon:list()) - - diff --git a/lua/harpoon2/test/config_spec.lua b/lua/harpoon2/test/config_spec.lua index c81f99e1..b28a4a0c 100644 --- a/lua/harpoon2/test/config_spec.lua +++ b/lua/harpoon2/test/config_spec.lua @@ -14,9 +14,9 @@ describe("config", function() "foo", "bar", "baz", - "qux" + "qux", }) - vim.api.nvim_win_set_cursor(0, {3, 1}) + vim.api.nvim_win_set_cursor(0, { 3, 1 }) local item = config_item.add() eq(item, { @@ -24,9 +24,7 @@ describe("config", function() context = { row = 3, col = 1, - } + }, }) end) end) - - diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index 66889d4e..dca68f65 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -6,7 +6,6 @@ local eq = assert.are.same local be = utils.before_each(os.tmpname()) describe("harpoon", function() - before_each(function() be() harpoon = require("harpoon2") @@ -20,7 +19,7 @@ describe("harpoon", function() "foo", "bar", "baz", - "qux" + "qux", }, row, col) local list = harpoon:list():append() @@ -28,19 +27,18 @@ describe("harpoon", function() "foo", "bar", "baz", - "qux" + "qux", }, row, col) vim.api.nvim_set_current_buf(target_buf) - vim.api.nvim_win_set_cursor(0, {row + 1, col}) + vim.api.nvim_win_set_cursor(0, { row + 1, col }) vim.api.nvim_set_current_buf(other_buf) local expected = { - {value = file_name, context = {row = row + 1, col = col}}, + { value = file_name, context = { row = row + 1, col = col } }, } eq(expected, list.items) - end) it("full harpoon add sync cycle", function() @@ -52,7 +50,7 @@ describe("harpoon", function() "foo", "bar", "baz", - "qux" + "qux", }, row, col) local list = harpoon:list() @@ -61,8 +59,8 @@ describe("harpoon", function() eq(harpoon:dump(), { testies = { - [default_list_name] = list:encode() - } + [default_list_name] = list:encode(), + }, }) end) @@ -88,13 +86,13 @@ describe("harpoon", function() eq(harpoon:dump(), { testies = { - [default_list_name] = list:encode() - } + [default_list_name] = list:encode(), + }, }) eq(list.items, { - {value = file_name_2, context = {row = row_2, col = col_2}}, - {value = file_name_1, context = {row = row_1, col = col_1}}, + { value = file_name_2, context = { row = row_2, col = col_2 } }, + { value = file_name_1, context = { row = row_1, col = col_1 } }, }) harpoon:list():append() @@ -102,10 +100,9 @@ describe("harpoon", function() harpoon:list():prepend() eq(list.items, { - {value = file_name_2, context = {row = row_2, col = col_2}}, - {value = file_name_1, context = {row = row_1, col = col_1}}, + { value = file_name_2, context = { row = row_2, col = col_2 } }, + { value = file_name_1, context = { row = row_1, col = col_1 } }, }) - end) it("ui - display resolve", function() @@ -115,8 +112,8 @@ describe("harpoon", function() -- split string on / local parts = vim.split(item.value, "/") return parts[#parts] - end - } + end, + }, }) local file_names = { @@ -149,8 +146,8 @@ describe("harpoon", function() list:resolve_displayed(displayed) eq(list.items, { - {value = file_names[1], context = {row = 4, col = 2}}, - {value = file_names[4], context = {row = 4, col = 2}}, + { value = file_names[1], context = { row = 4, col = 2 } }, + { value = file_names[4], context = { row = 4, col = 2 } }, }) end) @@ -188,10 +185,16 @@ describe("harpoon", function() list:resolve_displayed(displayed) eq({ - {value = file_names[1], context = {row = 4, col = 2}}, - {value = file_names[4], context = {row = 4, col = 2}}, - {value = "/tmp/harpoon-test-other-file-1", context = {row = 1, col = 0}}, - {value = "/tmp/harpoon-test-other-file-2", context = {row = 1, col = 0}}, + { value = file_names[1], context = { row = 4, col = 2 } }, + { value = file_names[4], context = { row = 4, col = 2 } }, + { + value = "/tmp/harpoon-test-other-file-1", + context = { row = 1, col = 0 }, + }, + { + value = "/tmp/harpoon-test-other-file-2", + context = { row = 1, col = 0 }, + }, }, list.items) table.remove(displayed, 3) @@ -199,10 +202,12 @@ describe("harpoon", function() list:resolve_displayed(displayed) eq({ - {value = file_names[1], context = {row = 4, col = 2}}, - {value = file_names[4], context = {row = 4, col = 2}}, - {value = "/tmp/harpoon-test-other-file-2", context = {row = 1, col = 0}}, + { value = file_names[1], context = { row = 4, col = 2 } }, + { value = file_names[4], context = { row = 4, col = 2 } }, + { + value = "/tmp/harpoon-test-other-file-2", + context = { row = 1, col = 0 }, + }, }, list.items) end) end) - diff --git a/lua/harpoon2/test/list_spec.lua b/lua/harpoon2/test/list_spec.lua index bf99690a..8407d3e1 100644 --- a/lua/harpoon2/test/list_spec.lua +++ b/lua/harpoon2/test/list_spec.lua @@ -4,7 +4,6 @@ local eq = assert.are.same describe("list", function() it("decode", function() - local config = Config.merge_config({ foo = { decode = function(item) @@ -18,12 +17,12 @@ describe("list", function() display = function(item) return table.concat(item.value, "---") - end - } + end, + }, }) local list_config = Config.get_config(config, "foo") - local list = List.decode(list_config, "foo", {"foo:bar", "baz:qux"}) + local list = List.decode(list_config, "foo", { "foo:bar", "baz:qux" }) local displayed = list:display() eq(displayed, { @@ -32,4 +31,3 @@ describe("list", function() }) end) end) - diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index de21eb86..bc2dd0c3 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -3,7 +3,6 @@ local utils = require("harpoon2.test.utils") local eq = assert.are.same describe("harpoon", function() - before_each(utils.before_each(os.tmpname())) it("open the ui without any items in the list", function() @@ -24,5 +23,3 @@ describe("harpoon", function() eq(harpoon.ui.win_id, nil) end) end) - - diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua index d7cfc165..59268539 100644 --- a/lua/harpoon2/test/utils.lua +++ b/lua/harpoon2/test/utils.lua @@ -21,15 +21,15 @@ function M.before_each(name) settings = { key = function() return "testies" - end - } + end, + }, }) end end function M.clean_files() for _, bufnr in ipairs(M.created_files) do - vim.api.nvim_buf_delete(bufnr, {force = true}) + vim.api.nvim_buf_delete(bufnr, { force = true }) end M.created_files = {} @@ -42,7 +42,7 @@ function M.create_file(name, contents, row, col) vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, contents) if row then - vim.api.nvim_win_set_cursor(0, {row, col}) + vim.api.nvim_win_set_cursor(0, { row, col }) end table.insert(M.created_files, bufnr) diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index 9ea84f9f..98cc4260 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -56,7 +56,8 @@ function HarpoonUI:_create_window() end local height = 8 - local borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } + local borderchars = + { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } local bufnr = vim.api.nvim_create_buf(false, false) local _, popup_info = popup.create(bufnr, { title = "Harpoon", @@ -73,11 +74,7 @@ function HarpoonUI:_create_window() self.win_id = win_id vim.api.nvim_win_set_option(self.win_id, "number", true) - vim.api.nvim_win_set_option( - win_id, - "winhl", - "Normal:HarpoonBorder" - ) + vim.api.nvim_win_set_option(win_id, "winhl", "Normal:HarpoonBorder") return win_id, bufnr end @@ -86,7 +83,6 @@ local count = 0 ---@param list? HarpoonList function HarpoonUI:toggle_quick_menu(list) - count = count + 1 if list == nil or self.win_id ~= nil then From 5d6a39c945f76d056c2748a6739a5690e3d04587 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 28 Nov 2023 20:40:54 -0700 Subject: [PATCH 18/80] fix: lint --- lua/harpoon2/buffer.lua | 3 ++- lua/harpoon2/config.lua | 3 --- lua/harpoon2/data.lua | 3 +++ lua/harpoon2/init.lua | 1 + lua/harpoon2/test/config_spec.lua | 1 - 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua index b09d34e3..bc82316f 100644 --- a/lua/harpoon2/buffer.lua +++ b/lua/harpoon2/buffer.lua @@ -4,7 +4,8 @@ local M = {} local HARPOON_MENU = "__harpoon-menu__" -- simple reason here is that if we are deving harpoon, we will create several --- ui objects, each with their own buffer, which will cause the name to be duplicated and then we will get a vim error on nvim_buf_set_name +-- ui objects, each with their own buffer, which will cause the name to be +-- duplicated and then we will get a vim error on nvim_buf_set_name local harpoon_menu_id = 0 local function get_harpoon_menu_name() diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index 3355cbca..816eb2de 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -1,6 +1,3 @@ -local Listeners = require("harpoon2.listeners") -local listeners = Listeners.listeners - local M = {} ---@alias HarpoonListItem {value: any, context: any} diff --git a/lua/harpoon2/data.lua b/lua/harpoon2/data.lua index 2bf5e453..b2cd42c8 100644 --- a/lua/harpoon2/data.lua +++ b/lua/harpoon2/data.lua @@ -26,9 +26,12 @@ function M.set_data_path(path) end local function has_keys(t) + + -- luacheck: ignore 512 for _ in pairs(t) do return true end + return false end diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index c2e8c40f..1da18dd6 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -127,6 +127,7 @@ function Harpoon:sync() self.data:sync() end +--luacheck: ignore 212/self function Harpoon:info() return { paths = Data.info(), diff --git a/lua/harpoon2/test/config_spec.lua b/lua/harpoon2/test/config_spec.lua index b28a4a0c..4df93405 100644 --- a/lua/harpoon2/test/config_spec.lua +++ b/lua/harpoon2/test/config_spec.lua @@ -1,4 +1,3 @@ -local List = require("harpoon2.list") local Config = require("harpoon2.config") local eq = assert.are.same From b6800cb0ed4b0afd0400228f598fc183fc30afe5 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 28 Nov 2023 20:59:24 -0700 Subject: [PATCH 19/80] checkpoint: working through a ui test for removing from display --- lua/harpoon2/buffer.lua | 4 ++++ lua/harpoon2/test/ui_spec.lua | 22 ++++++++++++++++++++-- lua/harpoon2/test/utils.lua | 17 ++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua index bc82316f..c6ccc892 100644 --- a/lua/harpoon2/buffer.lua +++ b/lua/harpoon2/buffer.lua @@ -108,4 +108,8 @@ function M.get_contents(bufnr) return indices end +function M.set_contents(bufnr, contents) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, contents) +end + return M diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index bc2dd0c3..ee95aca7 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -1,12 +1,17 @@ local utils = require("harpoon2.test.utils") +local Buffer = require("harpoon2.buffer") +local harpoon = require("harpoon2") local eq = assert.are.same +local be = utils.before_each(os.tmpname()) describe("harpoon", function() - before_each(utils.before_each(os.tmpname())) + before_each(function() + be() + harpoon = require("harpoon2") + end) it("open the ui without any items in the list", function() - local harpoon = require("harpoon2") harpoon.ui:toggle_quick_menu(harpoon:list()) local bufnr = harpoon.ui.bufnr @@ -22,4 +27,17 @@ describe("harpoon", function() eq(harpoon.ui.bufnr, nil) eq(harpoon.ui.win_id, nil) end) + + it("delete file from list via ui", function() + local created_files = utils.fill_list_with_files(3, harpoon:list()) + eq(harpoon:list():length(), 3) + + harpoon.ui:toggle_quick_menu(harpoon:list()) + table.remove(created_files, 2) + Buffer.set_contents(harpoon.ui.bufnr, created_files) + harpoon.ui:toggle_quick_menu() + + eq(harpoon:list():length(), 2) + eq(harpoon:list():display(), created_files) + end) end) diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua index 59268539..a348171c 100644 --- a/lua/harpoon2/test/utils.lua +++ b/lua/harpoon2/test/utils.lua @@ -42,11 +42,26 @@ function M.create_file(name, contents, row, col) vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, contents) if row then - vim.api.nvim_win_set_cursor(0, { row, col }) + vim.api.nvim_win_set_cursor(0, { row or 1, col or 0 }) end table.insert(M.created_files, bufnr) return bufnr end +---@param count number +---@param list HarpoonList +function M.fill_list_with_files(count, list) + local files = {} + + for _ = 1, count do + local name = os.tmpname() + table.insert(files, name) + M.create_file(name, { "test" }) + list:append() + end + + return files +end + return M From d98c514359fc52b956f493abeca5f9d7fb23e5eb Mon Sep 17 00:00:00 2001 From: mpaulson Date: Wed, 29 Nov 2023 07:18:08 -0700 Subject: [PATCH 20/80] fix: use save instead of toggle --- lua/harpoon2/buffer.lua | 4 ++-- lua/harpoon2/test/ui_spec.lua | 2 +- lua/harpoon2/ui.lua | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua index c6ccc892..bad8558b 100644 --- a/lua/harpoon2/buffer.lua +++ b/lua/harpoon2/buffer.lua @@ -67,7 +67,7 @@ function M.setup_autocmds_and_keymaps(bufnr) -- TODO: Update these to use the new autocmd api vim.cmd( string.format( - "autocmd BufWriteCmd lua require('harpoon2').ui:on_menu_save()", + "autocmd BufWriteCmd lua require('harpoon2').ui:save()", bufnr ) ) @@ -77,7 +77,7 @@ function M.setup_autocmds_and_keymaps(bufnr) if global_config.save_on_change then vim.cmd( string.format( - "autocmd TextChanged,TextChangedI lua require('harpoon2').ui:on_menu_save()", + "autocmd TextChanged,TextChangedI lua require('harpoon2').ui:save()", bufnr ) ) diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index ee95aca7..4051b891 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -35,7 +35,7 @@ describe("harpoon", function() harpoon.ui:toggle_quick_menu(harpoon:list()) table.remove(created_files, 2) Buffer.set_contents(harpoon.ui.bufnr, created_files) - harpoon.ui:toggle_quick_menu() + harpoon.ui:save() eq(harpoon:list():length(), 2) eq(harpoon:list():display(), created_files) diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index 98cc4260..2f652b8d 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -106,7 +106,7 @@ function HarpoonUI:select_menu_item() self:close_menu() end -function HarpoonUI:on_menu_save() +function HarpoonUI:save() local list = Buffer.get_contents(self.bufnr) self.active_list:resolve_displayed(list) end From 5ec7f9200e1fa402266b9cb3ef5332d3170b3f17 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Wed, 29 Nov 2023 14:50:13 -0700 Subject: [PATCH 21/80] feat: adding and removing buffers from list --- lua/harpoon2/test/ui_spec.lua | 17 ++++++++++++++++- lua/harpoon2/ui.lua | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index 4051b891..c84f6759 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -28,7 +28,7 @@ describe("harpoon", function() eq(harpoon.ui.win_id, nil) end) - it("delete file from list via ui", function() + it("delete file from ui contents and save", function() local created_files = utils.fill_list_with_files(3, harpoon:list()) eq(harpoon:list():length(), 3) @@ -40,4 +40,19 @@ describe("harpoon", function() eq(harpoon:list():length(), 2) eq(harpoon:list():display(), created_files) end) + + it("add file from ui contents and save", function() + local list = harpoon:list() + local created_files = utils.fill_list_with_files(3, list) + table.insert(created_files, os.tmpname()) + + eq(list:length(), 3) + + harpoon.ui:toggle_quick_menu(list) + Buffer.set_contents(harpoon.ui.bufnr, created_files) + harpoon.ui:save() + + eq(list:length(), 4) + eq(list:display(), created_files) + end) end) diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index 2f652b8d..de16f8af 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -109,6 +109,7 @@ end function HarpoonUI:save() local list = Buffer.get_contents(self.bufnr) self.active_list:resolve_displayed(list) + self:close_menu() end ---@param settings HarpoonSettings From 547158830039c98f7f942fd4c24aadbd9eac94b9 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Wed, 29 Nov 2023 15:05:59 -0700 Subject: [PATCH 22/80] feat: select navigation --- lua/harpoon2/list.lua | 1 + lua/harpoon2/ui.lua | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index 6c527067..bb79ceda 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -1,4 +1,5 @@ local Listeners = require("harpoon2.listeners") +local Buffer = require("harpoon2.buffer") local function index_of(items, element, config) local equals = config and config.equals diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index de16f8af..2b7690cc 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -102,6 +102,12 @@ end function HarpoonUI:select_menu_item() local idx = vim.fn.line(".") + + -- must first save any updates potentially made to the list before + -- navigating + local list = Buffer.get_contents(self.bufnr) + self.active_list:resolve_displayed(list) + self.active_list:select(idx) self:close_menu() end From f84bf553e02ddaac4a61611c34af7e542053d4d2 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 12:17:39 -0700 Subject: [PATCH 23/80] feat: more tests, slowly updating readme --- README.md | 194 ++++------------------------------ lua/harpoon2/test/ui_spec.lua | 15 +++ 2 files changed, 33 insertions(+), 176 deletions(-) diff --git a/README.md b/README.md index 0392010f..6618fb75 100644 --- a/README.md +++ b/README.md @@ -10,27 +10,32 @@ ![Harpoon](harpoon.png) -- image provided by **Bob Rust** - -## ⇁ WIP -This is not fully baked, though used by several people. If you experience any -issues, see some improvement you think would be amazing, or just have some -feedback for harpoon (or me), make an issue! - - -## ⇁ The Problems: +## ⇁ TOC +[Note to legacy Harpoon 1 users](/#Note%20to%20legacy%20Harpoon%201%20users) +[The Problems:](/#The%20Problems:) +[ The Solutions:](/#%20The%20Solutions:) +[Installation](/#Installation) +[Getting Started](/#Getting%20Started) +[Social](/#Social) + +## ⇁ Note to legacy Harpoon 1 users +Original Harpoon will remain in a frozen state and i will merge PRs in with _no +code review_ for those that wish to remain on that. Harpoon 2 is significantly +better and allows for MUCH greater control. Please migrate to that (will +become `master` within the next few months). + +## ⇁ The Problems 1. You're working on a codebase. medium, large, tiny, whatever. You find yourself frequenting a small set of files and you are tired of using a fuzzy finder, `:bnext` & `:bprev` are getting too repetitive, alternate file doesn't quite cut it, etc etc. 1. You want to execute some project specific commands or have any number of persistent terminals that can be easily navigated to. - -## ⇁ The Solutions: +## ⇁ The Solutions 1. The ability to specify, or on the fly, mark and create persisting key strokes to go to the files you want. 1. Unlimited terminals and navigation. - ## ⇁ Installation * neovim 0.5.0+ required * install using your favorite plugin manager (`vim-plug` in this example) @@ -39,173 +44,10 @@ Plug 'nvim-lua/plenary.nvim' " don't forget to add this one if you don't have it Plug 'ThePrimeagen/harpoon' ``` -## ⇁ Harpooning -here we'll explain how to wield the power of the harpoon: - - -### Marks -you mark files you want to revisit later on -```lua -:lua require("harpoon.mark").add_file() -``` - -### File Navigation -view all project marks with: -```lua -:lua require("harpoon.ui").toggle_quick_menu() -``` -you can go up and down the list, enter, delete or reorder. `q` and `` exit and save the menu - -you also can switch to any mark without bringing up the menu, use the below with the desired mark index -```lua -:lua require("harpoon.ui").nav_file(3) -- navigates to file 3 -``` -you can also cycle the list in both directions -```lua -:lua require("harpoon.ui").nav_next() -- navigates to next mark -:lua require("harpoon.ui").nav_prev() -- navigates to previous mark -``` - -### Terminal Navigation -this works like file navigation except that if there is no terminal at the specified index -a new terminal is created. -```lua -lua require("harpoon.term").gotoTerminal(1) -- navigates to term 1 -``` - -### Commands to Terminals -commands can be sent to any terminal -```lua -lua require("harpoon.term").sendCommand(1, "ls -La") -- sends ls -La to tmux window 1 -``` -further more commands can be stored for later quick -```lua -lua require('harpoon.cmd-ui').toggle_quick_menu() -- shows the commands menu -lua require("harpoon.term").sendCommand(1, 1) -- sends command 1 to term 1 -``` - -### Tmux Support -tmux is supported out of the box and can be used as a drop-in replacement to normal terminals -by simply switching `'term' with 'tmux'` like so - -```lua -lua require("harpoon.tmux").gotoTerminal(1) -- goes to the first tmux window -lua require("harpoon.tmux").sendCommand(1, "ls -La") -- sends ls -La to tmux window 1 -lua require("harpoon.tmux").sendCommand(1, 1) -- sends command 1 to tmux window 1 -``` - -`sendCommand` and `goToTerminal` also accept any valid [tmux pane identifier](https://man7.org/linux/man-pages/man1/tmux.1.html#COMMANDS). -```lua -lua require("harpoon.tmux").gotoTerminal("{down-of}") -- focus the pane directly below -lua require("harpoon.tmux").sendCommand("%3", "ls") -- send a command to the pane with id '%3' -``` - -Once you switch to a tmux window you can always switch back to neovim, this is a -little bash script that will switch to the window which is running neovim. - -In your `tmux.conf` (or anywhere you have keybinds), add this -```bash -bind-key -r G run-shell "path-to-harpoon/harpoon/scripts/tmux/switch-back-to-nvim" -``` - -### Telescope Support -1st register harpoon as a telescope extension -```lua -require("telescope").load_extension('harpoon') -``` -currently only marks are supported in telescope -``` -:Telescope harpoon marks -``` - -## ⇁ Configuration -if configuring harpoon is desired it must be done through harpoons setup function -```lua -require("harpoon").setup({ ... }) -``` - -### Global Settings -here are all the available global settings with their default values -```lua -global_settings = { - -- sets the marks upon calling `toggle` on the ui, instead of require `:w`. - save_on_toggle = false, - - -- saves the harpoon file upon every change. disabling is unrecommended. - save_on_change = true, - - -- sets harpoon to run the command immediately as it's passed to the terminal when calling `sendCommand`. - enter_on_sendcmd = false, - - -- closes any tmux windows harpoon that harpoon creates when you close Neovim. - tmux_autoclose_windows = false, - - -- filetypes that you want to prevent from adding to the harpoon list menu. - excluded_filetypes = { "harpoon" }, - - -- set marks specific to each git branch inside git repository - mark_branch = false, -} -``` - - -### Preconfigured Terminal Commands -to preconfigure terminal commands for later use -```lua -projects = { - -- Yes $HOME works - ["$HOME/personal/vim-with-me/server"] = { - term = { - cmds = { - "./env && npx ts-node src/index.ts" - } - } - } -} -``` - -## ⇁ Logging -- logs are written to `harpoon.log` within the nvim cache path (`:echo stdpath("cache")`) -- available log levels are `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. `warn` is default -- log level can be set with `vim.g.harpoon_log_level` (must be **before** `setup()`) -- launching nvim with `HARPOON_LOG=debug nvim` takes precedence over `vim.g.harpoon_log_level`. -- invalid values default back to `warn`. - -## ⇁ Others -#### How do Harpoon marks differ from vim global marks -they serve a similar purpose however harpoon marks differ in a few key ways: -1. They auto update their position within the file -1. They are saved _per project_. -1. They can be hand edited vs replaced (swapping is easier) - -#### The Motivation behind Harpoon terminals -1. I want to use the terminal since I can gF and gF to any errors arising -from execution that are within the terminal that are not appropriate for -something like dispatch. (not just running tests but perhaps a server that runs -for X amount of time before crashing). -1. I want the terminal to be persistent and I can return to one of many terminals -with some finger wizardry and reparse any of the execution information that was -not necessarily error related. -1. I would like to have commands that can be tied to terminals and sent them -without much thinking. Some sort of middle ground between vim-test and just -typing them into a terminal (configuring netflix's television project isn't -quite building and there are tons of ways to configure). - -#### Use a dynamic width for the Harpoon popup menu -Sometimes the default width of `60` is not wide enough. -The following example demonstrates how to configure a custom width by setting -the menu's width relative to the current window's width. - -```lua -require("harpoon").setup({ - menu = { - width = vim.api.nvim_win_get_width(0) - 4, - } -}) -``` +## ⇁ Getting Started ## ⇁ Social -For questions about Harpoon, there's a #harpoon channel on [the Primagen's Discord](https://discord.gg/theprimeagen) server. +For questions about Harpoon, there's a #harpoon channel on [the Primagen's Discord](https://discord.gg/theprimeagen) server. * [Discord](https://discord.gg/theprimeagen) * [Twitch](https://www.twitch.tv/theprimeagen) * [Twitter](https://twitter.com/ThePrimeagen) diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index c84f6759..9c314436 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -55,4 +55,19 @@ describe("harpoon", function() eq(list:length(), 4) eq(list:display(), created_files) end) + + it("edit ui but toggle should not save", function() + local list = harpoon:list() + local created_files = utils.fill_list_with_files(3, list) + + eq(list:length(), 3) + + harpoon.ui:toggle_quick_menu(list) + Buffer.set_contents(harpoon.ui.bufnr, {}) + harpoon.ui:toggle_quick_menu() + + eq(list:length(), 3) + eq(created_files, list:display()) + + end) end) From 0bb8e5e2ea9a8dff04ad87ae3476228c1357228a Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 12:20:11 -0700 Subject: [PATCH 24/80] did i get the style right? --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 6618fb75..809c9ada 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,7 @@ -- image provided by **Bob Rust** ## ⇁ TOC -[Note to legacy Harpoon 1 users](/#Note%20to%20legacy%20Harpoon%201%20users) -[The Problems:](/#The%20Problems:) -[ The Solutions:](/#%20The%20Solutions:) -[Installation](/#Installation) -[Getting Started](/#Getting%20Started) -[Social](/#Social) +[Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) ## ⇁ Note to legacy Harpoon 1 users Original Harpoon will remain in a frozen state and i will merge PRs in with _no From c69292565b7117364076377d136b7622bfdbb507 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 13:37:10 -0700 Subject: [PATCH 25/80] TOC --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 809c9ada..27965f09 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,11 @@ ## ⇁ TOC [Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) +[The Problems](#-The-Problems) +[The Solutions](#-The-Solutions) +[Installation](#-Installation) +[Getting Started](#-Getting-Started) +[Social](#-Social) ## ⇁ Note to legacy Harpoon 1 users Original Harpoon will remain in a frozen state and i will merge PRs in with _no From fc35ba1831dae9944314d618b188434542809d91 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 13:40:36 -0700 Subject: [PATCH 26/80] TOC --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 27965f09..8e1ac920 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ -- image provided by **Bob Rust** ## ⇁ TOC -[Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) -[The Problems](#-The-Problems) -[The Solutions](#-The-Solutions) -[Installation](#-Installation) -[Getting Started](#-Getting-Started) -[Social](#-Social) +* [Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) +* [The Problems](#-The-Problems) +* [The Solutions](#-The-Solutions) +* [Installation](#-Installation) +* [Getting Started](#-Getting-Started) +* [Social](#-Social) ## ⇁ Note to legacy Harpoon 1 users Original Harpoon will remain in a frozen state and i will merge PRs in with _no From ac9cbed9ed15b90ef25a398da425bcacea19ad0e Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 14:03:51 -0700 Subject: [PATCH 27/80] feat: harpoon2 -> harpoon --- README.md | 42 +++++++++++++++++++++--------- lua/harpoon2/buffer.lua | 14 +++++----- lua/harpoon2/data.lua | 2 +- lua/harpoon2/init.lua | 12 ++++----- lua/harpoon2/list.lua | 4 +-- lua/harpoon2/scratch/toggle.lua | 2 +- lua/harpoon2/test/config_spec.lua | 2 +- lua/harpoon2/test/harpoon_spec.lua | 6 ++--- lua/harpoon2/test/list_spec.lua | 4 +-- lua/harpoon2/test/ui_spec.lua | 8 +++--- lua/harpoon2/test/utils.lua | 8 +++--- lua/harpoon2/ui.lua | 2 +- 12 files changed, 61 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 8e1ac920..94d10194 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,12 @@ -- image provided by **Bob Rust** ## ⇁ TOC -* [Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) * [The Problems](#-The-Problems) * [The Solutions](#-The-Solutions) * [Installation](#-Installation) * [Getting Started](#-Getting-Started) * [Social](#-Social) - -## ⇁ Note to legacy Harpoon 1 users -Original Harpoon will remain in a frozen state and i will merge PRs in with _no -code review_ for those that wish to remain on that. Harpoon 2 is significantly -better and allows for MUCH greater control. Please migrate to that (will -become `master` within the next few months). +* [Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) ## ⇁ The Problems 1. You're working on a codebase. medium, large, tiny, whatever. You find @@ -37,17 +31,39 @@ to go to the files you want. 1. Unlimited terminals and navigation. ## ⇁ Installation -* neovim 0.5.0+ required -* install using your favorite plugin manager (`vim-plug` in this example) -```vim -Plug 'nvim-lua/plenary.nvim' " don't forget to add this one if you don't have it yet! -Plug 'ThePrimeagen/harpoon' +* neovim 0.8.0+ required +* install using your favorite plugin manager (i am using `packer` in this case) +```lua +use "nvim-lua/plenary.nvim" -- don't forget to add this one if you don't have it yet! +use { + "ThePrimeagen/harpoon", + branch = "0.1.x", + requires = { {"nvim-lua/plenary.nvim"} } +} ``` ## ⇁ Getting Started +### Quick Note +You will want to add your style of remaps and such to your neovim dotfiles with +the shortcuts you like. My shortcuts are for me. Me alone. Which also means +they are designed with dvorak in mind (My layout btw, I use dvorak btw). + +```lua +local harpoon = require("harpoon") + +harpoon.ui:toggle_quick_menu(harpoon:list()) +``` + ## ⇁ Social -For questions about Harpoon, there's a #harpoon channel on [the Primagen's Discord](https://discord.gg/theprimeagen) server. +For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. * [Discord](https://discord.gg/theprimeagen) * [Twitch](https://www.twitch.tv/theprimeagen) * [Twitter](https://twitter.com/ThePrimeagen) + +## ⇁ Note to legacy Harpoon 1 users +Original Harpoon will remain in a frozen state and i will merge PRs in with _no +code review_ for those that wish to remain on that. Harpoon 2 is significantly +better and allows for MUCH greater control. Please migrate to that (will +become `master` within the next few months). + diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua index bad8558b..e0bdc9fb 100644 --- a/lua/harpoon2/buffer.lua +++ b/lua/harpoon2/buffer.lua @@ -1,4 +1,4 @@ -local utils = require("harpoon2.utils") +local utils = require("harpoon.utils") local M = {} local HARPOON_MENU = "__harpoon-menu__" @@ -47,27 +47,27 @@ function M.setup_autocmds_and_keymaps(bufnr) bufnr, "n", "q", - "lua require('harpoon2').ui:toggle_quick_menu()", + "lua require('harpoon').ui:toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( bufnr, "n", "", - "lua require('harpoon2').ui:toggle_quick_menu()", + "lua require('harpoon').ui:toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( bufnr, "n", "", - "lua require('harpoon2').ui:select_menu_item()", + "lua require('harpoon').ui:select_menu_item()", {} ) -- TODO: Update these to use the new autocmd api vim.cmd( string.format( - "autocmd BufWriteCmd lua require('harpoon2').ui:save()", + "autocmd BufWriteCmd lua require('harpoon').ui:save()", bufnr ) ) @@ -77,7 +77,7 @@ function M.setup_autocmds_and_keymaps(bufnr) if global_config.save_on_change then vim.cmd( string.format( - "autocmd TextChanged,TextChangedI lua require('harpoon2').ui:save()", + "autocmd TextChanged,TextChangedI lua require('harpoon').ui:save()", bufnr ) ) @@ -90,7 +90,7 @@ function M.setup_autocmds_and_keymaps(bufnr) ) ) vim.cmd( - "autocmd BufLeave ++nested ++once silent lua require('harpoon2').ui:toggle_quick_menu()" + "autocmd BufLeave ++nested ++once silent lua require('harpoon').ui:toggle_quick_menu()" ) end diff --git a/lua/harpoon2/data.lua b/lua/harpoon2/data.lua index b2cd42c8..a031b7e6 100644 --- a/lua/harpoon2/data.lua +++ b/lua/harpoon2/data.lua @@ -1,7 +1,7 @@ local Path = require("plenary.path") local data_path = vim.fn.stdpath("data") -local full_data_path = string.format("%s/harpoon2.json", data_path) +local full_data_path = string.format("%s/harpoon.json", data_path) ---@param data any local function write_data(data) diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index 1da18dd6..45857c69 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -1,8 +1,8 @@ -local Ui = require("harpoon2.ui") -local Data = require("harpoon2.data") -local Config = require("harpoon2.config") -local List = require("harpoon2.list") -local Listeners = require("harpoon2.listeners") +local Ui = require("harpoon.ui") +local Data = require("harpoon.data") +local Config = require("harpoon.config") +local List = require("harpoon.list") +local Listeners = require("harpoon.listeners") -- setup -- read from a config file @@ -141,7 +141,7 @@ function Harpoon:dump() end function Harpoon:__debug_reset() - require("plenary.reload").reload_module("harpoon2") + require("plenary.reload").reload_module("harpoon") end return Harpoon:new() diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index bb79ceda..a0e77712 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -1,5 +1,5 @@ -local Listeners = require("harpoon2.listeners") -local Buffer = require("harpoon2.buffer") +local Listeners = require("harpoon.listeners") +local Buffer = require("harpoon.buffer") local function index_of(items, element, config) local equals = config and config.equals diff --git a/lua/harpoon2/scratch/toggle.lua b/lua/harpoon2/scratch/toggle.lua index 74859844..2b8857b1 100644 --- a/lua/harpoon2/scratch/toggle.lua +++ b/lua/harpoon2/scratch/toggle.lua @@ -1,3 +1,3 @@ -local harpoon = require("harpoon2") +local harpoon = require("harpoon") harpoon.ui:toggle_quick_menu(harpoon:list()) diff --git a/lua/harpoon2/test/config_spec.lua b/lua/harpoon2/test/config_spec.lua index 4df93405..b0b66d31 100644 --- a/lua/harpoon2/test/config_spec.lua +++ b/lua/harpoon2/test/config_spec.lua @@ -1,4 +1,4 @@ -local Config = require("harpoon2.config") +local Config = require("harpoon.config") local eq = assert.are.same describe("config", function() diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon2/test/harpoon_spec.lua index dca68f65..af71a035 100644 --- a/lua/harpoon2/test/harpoon_spec.lua +++ b/lua/harpoon2/test/harpoon_spec.lua @@ -1,5 +1,5 @@ -local utils = require("harpoon2.test.utils") -local harpoon = require("harpoon2") +local utils = require("harpoon.test.utils") +local harpoon = require("harpoon") local eq = assert.are.same @@ -8,7 +8,7 @@ local be = utils.before_each(os.tmpname()) describe("harpoon", function() before_each(function() be() - harpoon = require("harpoon2") + harpoon = require("harpoon") end) it("when we change buffers we update the row and column", function() diff --git a/lua/harpoon2/test/list_spec.lua b/lua/harpoon2/test/list_spec.lua index 8407d3e1..476ad316 100644 --- a/lua/harpoon2/test/list_spec.lua +++ b/lua/harpoon2/test/list_spec.lua @@ -1,5 +1,5 @@ -local List = require("harpoon2.list") -local Config = require("harpoon2.config") +local List = require("harpoon.list") +local Config = require("harpoon.config") local eq = assert.are.same describe("list", function() diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua index 9c314436..2d5adeee 100644 --- a/lua/harpoon2/test/ui_spec.lua +++ b/lua/harpoon2/test/ui_spec.lua @@ -1,6 +1,6 @@ -local utils = require("harpoon2.test.utils") -local Buffer = require("harpoon2.buffer") -local harpoon = require("harpoon2") +local utils = require("harpoon.test.utils") +local Buffer = require("harpoon.buffer") +local harpoon = require("harpoon") local eq = assert.are.same local be = utils.before_each(os.tmpname()) @@ -8,7 +8,7 @@ local be = utils.before_each(os.tmpname()) describe("harpoon", function() before_each(function() be() - harpoon = require("harpoon2") + harpoon = require("harpoon") end) it("open the ui without any items in the list", function() diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua index a348171c..0cf284ad 100644 --- a/lua/harpoon2/test/utils.lua +++ b/lua/harpoon2/test/utils.lua @@ -1,4 +1,4 @@ -local Data = require("harpoon2.data") +local Data = require("harpoon.data") local M = {} @@ -10,10 +10,10 @@ function M.before_each(name) Data.set_data_path(name) Data.__dangerously_clear_data() - require("plenary.reload").reload_module("harpoon2") - Data = require("harpoon2.data") + require("plenary.reload").reload_module("harpoon") + Data = require("harpoon.data") Data.set_data_path(name) - local harpoon = require("harpoon2") + local harpoon = require("harpoon") M.clean_files() diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index 2b7690cc..66814cc1 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -1,5 +1,5 @@ local popup = require("plenary").popup -local Buffer = require("harpoon2.buffer") +local Buffer = require("harpoon.buffer") local DEFAULT_WINDOW_WIDTH = 69 -- nice ---@class HarpoonUI From 1d7ea5757589714db56d0fd7dc050ce0d77606d3 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 14:24:43 -0700 Subject: [PATCH 28/80] fix: vertical and horizontal split --- lua/harpoon2/config.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index 816eb2de..daaf028c 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -76,6 +76,7 @@ function M.get_default_config() ---@param file_item HarpoonListFileItem select = function(file_item, options) + options = options or {} if file_item == nil then return end @@ -87,14 +88,14 @@ function M.get_default_config() bufnr = vim.fn.bufnr(file_item.value, true) end - if not options or not options.vsplit or not options.split then - vim.api.nvim_set_current_buf(bufnr) - elseif options.vsplit then + if options.vsplit then vim.cmd("vsplit") vim.api.nvim_set_current_buf(bufnr) elseif options.split then vim.cmd("split") vim.api.nvim_set_current_buf(bufnr) + else + vim.api.nvim_set_current_buf(bufnr) end if set_position then From 0904ae057f97fcfcd86e51bcaf8542f781e451ea Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 14:28:53 -0700 Subject: [PATCH 29/80] fix: harpoon2 -> harpoon --- lua/{harpoon2 => harpoon}/buffer.lua | 0 lua/{harpoon2 => harpoon}/config.lua | 0 lua/{harpoon2 => harpoon}/data.lua | 0 lua/{harpoon2 => harpoon}/init.lua | 0 lua/{harpoon2 => harpoon}/list.lua | 0 lua/{harpoon2 => harpoon}/listeners.lua | 0 lua/{harpoon2 => harpoon}/scratch/toggle.lua | 0 lua/{harpoon2 => harpoon}/test/config_spec.lua | 0 lua/{harpoon2 => harpoon}/test/harpoon_spec.lua | 0 lua/{harpoon2 => harpoon}/test/list_spec.lua | 0 lua/{harpoon2 => harpoon}/test/ui_spec.lua | 0 lua/{harpoon2 => harpoon}/test/utils.lua | 0 lua/{harpoon2 => harpoon}/ui.lua | 0 lua/{harpoon2 => harpoon}/utils.lua | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename lua/{harpoon2 => harpoon}/buffer.lua (100%) rename lua/{harpoon2 => harpoon}/config.lua (100%) rename lua/{harpoon2 => harpoon}/data.lua (100%) rename lua/{harpoon2 => harpoon}/init.lua (100%) rename lua/{harpoon2 => harpoon}/list.lua (100%) rename lua/{harpoon2 => harpoon}/listeners.lua (100%) rename lua/{harpoon2 => harpoon}/scratch/toggle.lua (100%) rename lua/{harpoon2 => harpoon}/test/config_spec.lua (100%) rename lua/{harpoon2 => harpoon}/test/harpoon_spec.lua (100%) rename lua/{harpoon2 => harpoon}/test/list_spec.lua (100%) rename lua/{harpoon2 => harpoon}/test/ui_spec.lua (100%) rename lua/{harpoon2 => harpoon}/test/utils.lua (100%) rename lua/{harpoon2 => harpoon}/ui.lua (100%) rename lua/{harpoon2 => harpoon}/utils.lua (100%) diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon/buffer.lua similarity index 100% rename from lua/harpoon2/buffer.lua rename to lua/harpoon/buffer.lua diff --git a/lua/harpoon2/config.lua b/lua/harpoon/config.lua similarity index 100% rename from lua/harpoon2/config.lua rename to lua/harpoon/config.lua diff --git a/lua/harpoon2/data.lua b/lua/harpoon/data.lua similarity index 100% rename from lua/harpoon2/data.lua rename to lua/harpoon/data.lua diff --git a/lua/harpoon2/init.lua b/lua/harpoon/init.lua similarity index 100% rename from lua/harpoon2/init.lua rename to lua/harpoon/init.lua diff --git a/lua/harpoon2/list.lua b/lua/harpoon/list.lua similarity index 100% rename from lua/harpoon2/list.lua rename to lua/harpoon/list.lua diff --git a/lua/harpoon2/listeners.lua b/lua/harpoon/listeners.lua similarity index 100% rename from lua/harpoon2/listeners.lua rename to lua/harpoon/listeners.lua diff --git a/lua/harpoon2/scratch/toggle.lua b/lua/harpoon/scratch/toggle.lua similarity index 100% rename from lua/harpoon2/scratch/toggle.lua rename to lua/harpoon/scratch/toggle.lua diff --git a/lua/harpoon2/test/config_spec.lua b/lua/harpoon/test/config_spec.lua similarity index 100% rename from lua/harpoon2/test/config_spec.lua rename to lua/harpoon/test/config_spec.lua diff --git a/lua/harpoon2/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua similarity index 100% rename from lua/harpoon2/test/harpoon_spec.lua rename to lua/harpoon/test/harpoon_spec.lua diff --git a/lua/harpoon2/test/list_spec.lua b/lua/harpoon/test/list_spec.lua similarity index 100% rename from lua/harpoon2/test/list_spec.lua rename to lua/harpoon/test/list_spec.lua diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua similarity index 100% rename from lua/harpoon2/test/ui_spec.lua rename to lua/harpoon/test/ui_spec.lua diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon/test/utils.lua similarity index 100% rename from lua/harpoon2/test/utils.lua rename to lua/harpoon/test/utils.lua diff --git a/lua/harpoon2/ui.lua b/lua/harpoon/ui.lua similarity index 100% rename from lua/harpoon2/ui.lua rename to lua/harpoon/ui.lua diff --git a/lua/harpoon2/utils.lua b/lua/harpoon/utils.lua similarity index 100% rename from lua/harpoon2/utils.lua rename to lua/harpoon/utils.lua From 7d5d1415c200915955b689f8e980e5eb12e5d637 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 15:44:07 -0700 Subject: [PATCH 30/80] fix: leaving the window now destroys both border and content win --- lua/harpoon/autocmd.lua | 4 ++++ lua/harpoon/buffer.lua | 18 +++++++++++------- lua/harpoon/init.lua | 3 +-- lua/harpoon/test/ui_spec.lua | 19 +++++++++++++++++++ lua/harpoon/ui.lua | 10 +++++++++- 5 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 lua/harpoon/autocmd.lua diff --git a/lua/harpoon/autocmd.lua b/lua/harpoon/autocmd.lua new file mode 100644 index 00000000..de76e217 --- /dev/null +++ b/lua/harpoon/autocmd.lua @@ -0,0 +1,4 @@ +local augroup = vim.api.nvim_create_augroup +return augroup("Harpoon", {}) + + diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index e0bdc9fb..b08b72b9 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -1,4 +1,6 @@ local utils = require("harpoon.utils") +local HarpoonGroup = require("harpoon.autocmd") + local M = {} local HARPOON_MENU = "__harpoon-menu__" @@ -18,8 +20,6 @@ end ---strings back into the ui. it feels gross and it puts odd coupling ---@param bufnr number function M.setup_autocmds_and_keymaps(bufnr) - --[[ - -- TODO: Do the highlighting better local curr_file = vim.api.nvim_buf_get_name(0) local cmd = string.format( @@ -31,9 +31,7 @@ function M.setup_autocmds_and_keymaps(bufnr) .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", curr_file:gsub("\\", "\\\\") ) - print(cmd) vim.cmd(cmd) - --]] if vim.api.nvim_buf_get_name(bufnr) == "" then vim.api.nvim_buf_set_name(bufnr, get_harpoon_menu_name()) @@ -71,6 +69,7 @@ function M.setup_autocmds_and_keymaps(bufnr) bufnr ) ) + -- TODO: Do we want this? is this a thing? -- its odd... why save on text change? shouldn't we wait until close / w / esc? --[[ @@ -89,9 +88,14 @@ function M.setup_autocmds_and_keymaps(bufnr) bufnr ) ) - vim.cmd( - "autocmd BufLeave ++nested ++once silent lua require('harpoon').ui:toggle_quick_menu()" - ) + + vim.api.nvim_create_autocmd({ "BufLeave" }, { + group = HarpoonGroup, + pattern = "__harpoon*", + callback = function() + require("harpoon").ui:toggle_quick_menu() + end + }) end ---@param bufnr number diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 45857c69..20530dfc 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -3,6 +3,7 @@ local Data = require("harpoon.data") local Config = require("harpoon.config") local List = require("harpoon.list") local Listeners = require("harpoon.listeners") +local HarpoonGroup = require("harpoon.autocmd") -- setup -- read from a config file @@ -48,8 +49,6 @@ function Harpoon:setup(partial_config) ---TODO: should we go through every seen list and update its config? if self.hooks_setup == false then - local augroup = vim.api.nvim_create_augroup - local HarpoonGroup = augroup("Harpoon", {}) vim.api.nvim_create_autocmd({ "BufLeave", "VimLeavePre" }, { group = HarpoonGroup, diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index 2d5adeee..69122031 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -70,4 +70,23 @@ describe("harpoon", function() eq(created_files, list:display()) end) + + it("using :q to leave harpoon should quit everything", function() + harpoon.ui:toggle_quick_menu(harpoon:list()) + + local bufnr = harpoon.ui.bufnr + local win_id = harpoon.ui.win_id + + eq(vim.api.nvim_buf_is_valid(bufnr), true) + eq(vim.api.nvim_win_is_valid(win_id), true) + eq(vim.api.nvim_get_current_buf(), bufnr) + + vim.cmd [[ q! ]] -- TODO: I shouldn't need q! here + + eq(vim.api.nvim_buf_is_valid(bufnr), false) + eq(vim.api.nvim_win_is_valid(win_id), false) + eq(harpoon.ui.bufnr, nil) + eq(harpoon.ui.win_id, nil) + end) + end) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 66814cc1..20cc5462 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -4,6 +4,7 @@ local DEFAULT_WINDOW_WIDTH = 69 -- nice ---@class HarpoonUI ---@field win_id number +---@field border_win_id number ---@field bufnr number ---@field settings HarpoonSettings ---@field active_list HarpoonList @@ -16,6 +17,7 @@ HarpoonUI.__index = HarpoonUI function HarpoonUI:new(settings) return setmetatable({ win_id = nil, + border_win_id = nil, bufnr = nil, active_list = nil, settings = settings, @@ -37,8 +39,13 @@ function HarpoonUI:close_menu() vim.api.nvim_win_close(self.win_id, true) end + if self.border_win_id ~= nil and vim.api.nvim_win_is_valid(self.border_win_id) then + vim.api.nvim_win_close(self.border_win_id, true) + end + self.active_list = nil self.win_id = nil + self.border_win_id = nil self.bufnr = nil self.closing = false @@ -73,7 +80,8 @@ function HarpoonUI:_create_window() Buffer.setup_autocmds_and_keymaps(bufnr) self.win_id = win_id - vim.api.nvim_win_set_option(self.win_id, "number", true) + self.border_win_id = popup_info.border.win_id + vim.api.nvim_win_set_option(win_id, "number", true) vim.api.nvim_win_set_option(win_id, "winhl", "Normal:HarpoonBorder") return win_id, bufnr From d044315c89ae0cca92dfe53270443fc6a5fdaf37 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 15:45:25 -0700 Subject: [PATCH 31/80] fix: closing window --- lua/harpoon/test/ui_spec.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index 69122031..ed9ba761 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -16,16 +16,20 @@ describe("harpoon", function() local bufnr = harpoon.ui.bufnr local win_id = harpoon.ui.win_id + local border_win_id = harpoon.ui.border_win_id eq(vim.api.nvim_buf_is_valid(bufnr), true) eq(vim.api.nvim_win_is_valid(win_id), true) + eq(vim.api.nvim_win_is_valid(border_win_id), true) harpoon.ui:toggle_quick_menu() eq(vim.api.nvim_buf_is_valid(bufnr), false) eq(vim.api.nvim_win_is_valid(win_id), false) + eq(vim.api.nvim_win_is_valid(border_win_id), false) eq(harpoon.ui.bufnr, nil) eq(harpoon.ui.win_id, nil) + eq(harpoon.ui.border_win_id, nil) end) it("delete file from ui contents and save", function() From 6522b48a4e58fe08e9ebe6b939b9cfc38420b695 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 16:06:15 -0700 Subject: [PATCH 32/80] fix: wq would cause the editor to exit --- lua/harpoon/buffer.lua | 18 +++++++++++------- lua/harpoon/config.lua | 1 + lua/harpoon/init.lua | 3 +++ lua/harpoon/ui.lua | 1 - 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index b08b72b9..1c6f6232 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -62,13 +62,6 @@ function M.setup_autocmds_and_keymaps(bufnr) "lua require('harpoon').ui:select_menu_item()", {} ) - -- TODO: Update these to use the new autocmd api - vim.cmd( - string.format( - "autocmd BufWriteCmd lua require('harpoon').ui:save()", - bufnr - ) - ) -- TODO: Do we want this? is this a thing? -- its odd... why save on text change? shouldn't we wait until close / w / esc? @@ -89,6 +82,17 @@ function M.setup_autocmds_and_keymaps(bufnr) ) ) + vim.api.nvim_create_autocmd({ "BufWriteCmd" }, { + group = HarpoonGroup, + pattern = "__harpoon*", + callback = function() + require("harpoon").ui:save() + vim.schedule(function() + require("harpoon").ui:toggle_quick_menu() + end) + end + }) + vim.api.nvim_create_autocmd({ "BufLeave" }, { group = HarpoonGroup, pattern = "__harpoon*", diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index daaf028c..54d7a771 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -164,6 +164,7 @@ end ---@param latest_config HarpoonConfig? ---@return HarpoonConfig function M.merge_config(partial_config, latest_config) + partial_config = partial_config or {} local config = latest_config or M.get_default_config() for k, v in pairs(partial_config) do if k == "settings" then diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 20530dfc..12ed8104 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -1,3 +1,4 @@ +local Path = require("plenary.path") local Ui = require("harpoon.ui") local Data = require("harpoon.data") local Config = require("harpoon.config") @@ -144,3 +145,5 @@ function Harpoon:__debug_reset() end return Harpoon:new() + + diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 20cc5462..1543d3f2 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -123,7 +123,6 @@ end function HarpoonUI:save() local list = Buffer.get_contents(self.bufnr) self.active_list:resolve_displayed(list) - self:close_menu() end ---@param settings HarpoonSettings From d867a33b8e41ea9fb3003a13bc1c411731e47b1a Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 16:16:32 -0700 Subject: [PATCH 33/80] feat: README.md update --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 94d10194..b835a15c 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,25 @@ You will want to add your style of remaps and such to your neovim dotfiles with the shortcuts you like. My shortcuts are for me. Me alone. Which also means they are designed with dvorak in mind (My layout btw, I use dvorak btw). +### harpoon:setup +it is a requirement to call `harpoon:setup()`. This is required due to +autocmds setup. + +### Basic Setup +Here is my basic setup + ```lua local harpoon = require("harpoon") -harpoon.ui:toggle_quick_menu(harpoon:list()) +harpoon:setup() + +vim.keymap.set("n", "a", function() harpoon:list():append() end) +vim.keymap.set("n", "", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end) + +vim.keymap.set("n", "", function() harpoon:list():select(1) end) +vim.keymap.set("n", "", function() harpoon.ui:select(2) end) +vim.keymap.set("n", "", function() harpoon.ui:select(3) end) +vim.keymap.set("n", "", function() harpoon.ui:select(4) end) ``` ## ⇁ Social From 8501bfe8cb8ddd22b4efbfc4f50f25f57c09c13d Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 16:19:19 -0700 Subject: [PATCH 34/80] fix: nice branch name jerk --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b835a15c..62831f93 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ to go to the files you want. use "nvim-lua/plenary.nvim" -- don't forget to add this one if you don't have it yet! use { "ThePrimeagen/harpoon", - branch = "0.1.x", + branch = "harpoon2", requires = { {"nvim-lua/plenary.nvim"} } } ``` From d4edc91a1b60bf8c719a8c6e9a2d28840fe2f4b3 Mon Sep 17 00:00:00 2001 From: Jeffry Degrande Date: Thu, 30 Nov 2023 21:06:37 -0300 Subject: [PATCH 35/80] This fixes selecting file 2, 3, 4 for me --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 62831f93..47ffeb6f 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,9 @@ vim.keymap.set("n", "a", function() harpoon:list():append() end) vim.keymap.set("n", "", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end) vim.keymap.set("n", "", function() harpoon:list():select(1) end) -vim.keymap.set("n", "", function() harpoon.ui:select(2) end) -vim.keymap.set("n", "", function() harpoon.ui:select(3) end) -vim.keymap.set("n", "", function() harpoon.ui:select(4) end) +vim.keymap.set("n", "", function() harpoon:list():select(2) end) +vim.keymap.set("n", "", function() harpoon:list():select(3) end) +vim.keymap.set("n", "", function() harpoon:list():select(4) end) ``` ## ⇁ Social From 574adeb96d1972fbd1b6fe0011ba84d2a06f8da6 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 20:07:28 -0700 Subject: [PATCH 36/80] fix: small fixes --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++ lua/harpoon/config.lua | 16 ++++++------ lua/harpoon/ui.lua | 5 ++-- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 47ffeb6f..7111271b 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,62 @@ vim.keymap.set("n", "", function() harpoon:list():select(3) end) vim.keymap.set("n", "", function() harpoon:list():select(4) end) ``` +### Custom Lists +You can define custom behavior of a harpoon list by providing your own calls. + +Here is a simple example where i create a list named `cmd` that takes the +current line in the editor and adds it to harpoon menu. When +`list:select(...)` is called, we take the contents of the line and execute it +as a vim command + +I don't think this is a great use of harpoon, but its meant to show how to add +your own custom lists. You could imagine that a terminal list would be just as +easy to create. + +```lua +local harpoon = require("harpoon") + +harpoon:setup({ + -- Setting up custom behavior for a list named "cmd" + "cmd" = { + + -- When you call list:append() this function is called and the return + -- value will be put in the list at the end. + -- + -- which means same behavior for prepend except where in the list the + -- return value is added + -- + -- @param possible_value string only passed in when you alter the ui manual + add = function(possible_value) + -- get the current line idx + local idx = vim.fn.line(".") + + -- read the current line + local cmd = vim.api.nvim_buf_get_lines(0, idx - 1, idx, false)[1] + if cmd == nil then + return nil + end + + return { + value = cmd, + context = { ... any data you want ... }, + } + end, + + --- This function gets invoked with the options being passed in from + --- list:select(index, <...options...>) + --- @param list_item {value: any, context: any} + --- @param option any + select = function(list_item, option) + -- WOAH, IS THIS HTMX LEVEL XSS ATTACK?? + vim.cmd(list_item.value) + end + + } +}) + +``` + ## ⇁ Social For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. * [Discord](https://discord.gg/theprimeagen) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 54d7a771..8254ff9e 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -2,6 +2,7 @@ local M = {} ---@alias HarpoonListItem {value: any, context: any} ---@alias HarpoonListFileItem {value: string, context: {row: number, col: number}} +---@alias HarpoonListFileOptions {split: boolean, vsplit: boolean} ---@class HarpoonPartialConfigItem ---@field encode? (fun(list_item: HarpoonListItem): string) @@ -74,18 +75,19 @@ function M.get_default_config() return list_item.value end, - ---@param file_item HarpoonListFileItem - select = function(file_item, options) + ---@param list_item HarpoonListFileItem + ---@param options HarpoonListFileOptions + select = function(list_item, options) options = options or {} - if file_item == nil then + if list_item == nil then return end - local bufnr = vim.fn.bufnr(file_item.value) + local bufnr = vim.fn.bufnr(list_item.value) local set_position = false if bufnr == -1 then set_position = true - bufnr = vim.fn.bufnr(file_item.value, true) + bufnr = vim.fn.bufnr(list_item.value, true) end if options.vsplit then @@ -100,8 +102,8 @@ function M.get_default_config() if set_position then vim.api.nvim_win_set_cursor(0, { - file_item.context.row or 1, - file_item.context.col or 0, + list_item.context.row or 1, + list_item.context.col or 0, }) end end, diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 1543d3f2..6e427096 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -108,7 +108,8 @@ function HarpoonUI:toggle_quick_menu(list) vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, contents) end -function HarpoonUI:select_menu_item() +---@param options? any +function HarpoonUI:select_menu_item(options) local idx = vim.fn.line(".") -- must first save any updates potentially made to the list before @@ -116,7 +117,7 @@ function HarpoonUI:select_menu_item() local list = Buffer.get_contents(self.bufnr) self.active_list:resolve_displayed(list) - self.active_list:select(idx) + self.active_list:select(idx, options) self:close_menu() end From 047cd66d6f6df343ae26e303b2d42f6f87428e82 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 20:12:04 -0700 Subject: [PATCH 37/80] fix: addresses #346 and #345 --- lua/harpoon/config.lua | 8 +++++++- lua/harpoon/list.lua | 4 ++++ lua/harpoon/utils.lua | 4 ---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 8254ff9e..09f744bf 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -1,3 +1,9 @@ +local Path = require("plenary.path") +local function normalize_path(buf_name) + return Path:new(buf_name):make_relative(vim.loop.cwd()) +end + + local M = {} ---@alias HarpoonListItem {value: any, context: any} @@ -127,7 +133,7 @@ function M.get_default_config() -- path, if that is the case we can use the context to -- store the bufname and then have value be the normalized -- value - or vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + or normalize_path(vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf())) local bufnr = vim.fn.bufnr(name, false) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index a0e77712..c760a10f 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -43,6 +43,10 @@ function HarpoonList:length() return #self.items end +function HarpoonList:clear() + self.items = {} +end + ---@return HarpoonList function HarpoonList:append(item) item = item or self.config.add() diff --git a/lua/harpoon/utils.lua b/lua/harpoon/utils.lua index 8dfa86a8..61dcb445 100644 --- a/lua/harpoon/utils.lua +++ b/lua/harpoon/utils.lua @@ -1,9 +1,5 @@ -local Path = require("plenary.path") local M = {} -function M.normalize_path(item) - return Path:new(item):make_relative(M.project_key()) -end function M.is_white_space(str) return str:gsub("%s", "") == "" From 574a6422044629510e3492296728a9b5932d1c52 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 20:14:10 -0700 Subject: [PATCH 38/80] chore: style --- lua/harpoon/autocmd.lua | 2 -- lua/harpoon/buffer.lua | 23 +++++++++++------------ lua/harpoon/config.lua | 7 +++++-- lua/harpoon/data.lua | 1 - lua/harpoon/init.lua | 3 --- lua/harpoon/test/ui_spec.lua | 4 +--- lua/harpoon/ui.lua | 5 ++++- lua/harpoon/utils.lua | 1 - 8 files changed, 21 insertions(+), 25 deletions(-) diff --git a/lua/harpoon/autocmd.lua b/lua/harpoon/autocmd.lua index de76e217..5aa24ceb 100644 --- a/lua/harpoon/autocmd.lua +++ b/lua/harpoon/autocmd.lua @@ -1,4 +1,2 @@ local augroup = vim.api.nvim_create_augroup return augroup("Harpoon", {}) - - diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index 1c6f6232..c2245940 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -21,16 +21,15 @@ end ---@param bufnr number function M.setup_autocmds_and_keymaps(bufnr) local curr_file = vim.api.nvim_buf_get_name(0) - local cmd = - string.format( - "autocmd Filetype harpoon " - .. "let path = '%s' | call clearmatches() | " - -- move the cursor to the line containing the current filename - .. "call search('\\V'.path.'\\$') | " - -- add a hl group to that line - .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", - curr_file:gsub("\\", "\\\\") - ) + local cmd = string.format( + "autocmd Filetype harpoon " + .. "let path = '%s' | call clearmatches() | " + -- move the cursor to the line containing the current filename + .. "call search('\\V'.path.'\\$') | " + -- add a hl group to that line + .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", + curr_file:gsub("\\", "\\\\") + ) vim.cmd(cmd) if vim.api.nvim_buf_get_name(bufnr) == "" then @@ -90,7 +89,7 @@ function M.setup_autocmds_and_keymaps(bufnr) vim.schedule(function() require("harpoon").ui:toggle_quick_menu() end) - end + end, }) vim.api.nvim_create_autocmd({ "BufLeave" }, { @@ -98,7 +97,7 @@ function M.setup_autocmds_and_keymaps(bufnr) pattern = "__harpoon*", callback = function() require("harpoon").ui:toggle_quick_menu() - end + end, }) end diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 09f744bf..bfb1d481 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -3,7 +3,6 @@ local function normalize_path(buf_name) return Path:new(buf_name):make_relative(vim.loop.cwd()) end - local M = {} ---@alias HarpoonListItem {value: any, context: any} @@ -133,7 +132,11 @@ function M.get_default_config() -- path, if that is the case we can use the context to -- store the bufname and then have value be the normalized -- value - or normalize_path(vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf())) + or normalize_path( + vim.api.nvim_buf_get_name( + vim.api.nvim_get_current_buf() + ) + ) local bufnr = vim.fn.bufnr(name, false) diff --git a/lua/harpoon/data.lua b/lua/harpoon/data.lua index a031b7e6..96535640 100644 --- a/lua/harpoon/data.lua +++ b/lua/harpoon/data.lua @@ -26,7 +26,6 @@ function M.set_data_path(path) end local function has_keys(t) - -- luacheck: ignore 512 for _ in pairs(t) do return true diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 12ed8104..7a9db745 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -50,7 +50,6 @@ function Harpoon:setup(partial_config) ---TODO: should we go through every seen list and update its config? if self.hooks_setup == false then - vim.api.nvim_create_autocmd({ "BufLeave", "VimLeavePre" }, { group = HarpoonGroup, pattern = "*", @@ -145,5 +144,3 @@ function Harpoon:__debug_reset() end return Harpoon:new() - - diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index ed9ba761..db84591f 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -72,7 +72,6 @@ describe("harpoon", function() eq(list:length(), 3) eq(created_files, list:display()) - end) it("using :q to leave harpoon should quit everything", function() @@ -85,12 +84,11 @@ describe("harpoon", function() eq(vim.api.nvim_win_is_valid(win_id), true) eq(vim.api.nvim_get_current_buf(), bufnr) - vim.cmd [[ q! ]] -- TODO: I shouldn't need q! here + vim.cmd([[ q! ]]) -- TODO: I shouldn't need q! here eq(vim.api.nvim_buf_is_valid(bufnr), false) eq(vim.api.nvim_win_is_valid(win_id), false) eq(harpoon.ui.bufnr, nil) eq(harpoon.ui.win_id, nil) end) - end) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 6e427096..76fcaa69 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -39,7 +39,10 @@ function HarpoonUI:close_menu() vim.api.nvim_win_close(self.win_id, true) end - if self.border_win_id ~= nil and vim.api.nvim_win_is_valid(self.border_win_id) then + if + self.border_win_id ~= nil + and vim.api.nvim_win_is_valid(self.border_win_id) + then vim.api.nvim_win_close(self.border_win_id, true) end diff --git a/lua/harpoon/utils.lua b/lua/harpoon/utils.lua index 61dcb445..5660f635 100644 --- a/lua/harpoon/utils.lua +++ b/lua/harpoon/utils.lua @@ -1,4 +1,3 @@ - local M = {} function M.is_white_space(str) From 588ede12531af3e4095792e6ab092f2a19b6628f Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 20:14:45 -0700 Subject: [PATCH 39/80] chore: lint --- lua/harpoon/init.lua | 1 - lua/harpoon/list.lua | 1 - 2 files changed, 2 deletions(-) diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 7a9db745..8811d51c 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -1,4 +1,3 @@ -local Path = require("plenary.path") local Ui = require("harpoon.ui") local Data = require("harpoon.data") local Config = require("harpoon.config") diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index c760a10f..6303025b 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -1,5 +1,4 @@ local Listeners = require("harpoon.listeners") -local Buffer = require("harpoon.buffer") local function index_of(items, element, config) local equals = config and config.equals From 7a356bfb1291e2b2872ff7ea676bc94aa5d660db Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 20:42:31 -0700 Subject: [PATCH 40/80] feat: more extensive read me and `add` now comes with `config` ref --- README.md | 36 +++++++++++++++++++++++++++++++++++- lua/harpoon/config.lua | 17 +++++++++++------ lua/harpoon/init.lua | 14 +++++--------- lua/harpoon/list.lua | 6 +++--- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7111271b..719dcd40 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ You will want to add your style of remaps and such to your neovim dotfiles with the shortcuts you like. My shortcuts are for me. Me alone. Which also means they are designed with dvorak in mind (My layout btw, I use dvorak btw). -### harpoon:setup +### harpoon:setup() IS REQUIRED it is a requirement to call `harpoon:setup()`. This is required due to autocmds setup. @@ -59,7 +59,9 @@ Here is my basic setup ```lua local harpoon = require("harpoon") +-- REQUIRED harpoon:setup() +-- REQUIRED vim.keymap.set("n", "a", function() harpoon:list():append() end) vim.keymap.set("n", "", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end) @@ -126,6 +128,38 @@ harpoon:setup({ ``` +### Config +There is quite a bit of behavior you can configure via `harpoon:setup()` + +* `settings`: is the global settings. as of now there isn't a global setting in use, but once we have some custom behavior i'll put them here +* `default`: the default configuration for any list. it is simply a file harpoon +* `[name] = HarpoonPartialConfigItem`: any named lists config. it will be merged with `default` and override any behavior + +**HarpoonPartialConfigItem Definition** +``` +---@class HarpoonPartialConfigItem +---@field encode? (fun(list_item: HarpoonListItem): string) +---@field decode? (fun(obj: string): any) +---@field display? (fun(list_item: HarpoonListItem): string) +---@field select? (fun(list_item: HarpoonListItem, options: any?): nil) +---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) +---@field add? fun(item: any?): HarpoonListItem +---@field BufLeave? fun(evt: any, list: HarpoonList): nil +---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil +---@field get_root_dir? fun(): string +``` + +**Detailed Definitions** +* `encode`: how to encode the list item to the harpoon file. if encode is `false`, then the list will not be saved to disk (think terminals) +* `decode`: how to decode the list +* `display`: how to display the list item in the ui menu +* `select`: the action taken when selecting a list item. called from `list:select(idx, options)` +* `equals`: how to compare two list items for equality +* `add`: called when `list:append()` or `list:prepend()` is called. called with an item, which will be a string, when adding through the ui menu +* `BufLeave`: this function is called for every list on BufLeave. if you need custom behavior, this is the place +* `VimLeavePre`: this function is called for every list on VimLeavePre. +* `get_root_dir`: used for creating relative paths. defaults to `vim.loop.cwd()` + ## ⇁ Social For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. * [Discord](https://discord.gg/theprimeagen) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index bfb1d481..429443b7 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -1,9 +1,12 @@ local Path = require("plenary.path") -local function normalize_path(buf_name) - return Path:new(buf_name):make_relative(vim.loop.cwd()) +local function normalize_path(buf_name, root) + return Path:new(buf_name):make_relative(root) end + local M = {} +local DEFAULT_LIST = "__harpoon_files" +M.DEFAULT_LIST = DEFAULT_LIST ---@alias HarpoonListItem {value: any, context: any} ---@alias HarpoonListFileItem {value: string, context: {row: number, col: number}} @@ -15,7 +18,7 @@ local M = {} ---@field display? (fun(list_item: HarpoonListItem): string) ---@field select? (fun(list_item: HarpoonListItem, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ----@field add? fun(item: any?): HarpoonListItem +---@field add? fun(config: HarpoonPartialConfigItem, item: any?): HarpoonListItem ---@field BufLeave? fun(evt: any, list: HarpoonList): nil ---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil ---@field get_root_dir? fun(): string @@ -123,9 +126,10 @@ function M.get_default_config() return vim.loop.cwd() end, - ---@param name any + ---@param config HarpoonPartialConfigItem + ---@param name? any ---@return HarpoonListItem - add = function(name) + add = function(config, name) name = name -- TODO: should we do path normalization??? -- i know i have seen sometimes it becoming an absolute @@ -135,7 +139,8 @@ function M.get_default_config() or normalize_path( vim.api.nvim_buf_get_name( vim.api.nvim_get_current_buf() - ) + ), + config.get_root_dir() ) local bufnr = vim.fn.bufnr(name, false) diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 8811d51c..a2e0718f 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -5,14 +5,6 @@ local List = require("harpoon.list") local Listeners = require("harpoon.listeners") local HarpoonGroup = require("harpoon.autocmd") --- setup --- read from a config file --- - --- TODO: rename lists into something better... - -local DEFAULT_LIST = "__harpoon_files" - ---@class Harpoon ---@field config HarpoonConfig ---@field ui HarpoonUI @@ -75,7 +67,7 @@ end ---@param name string? ---@return HarpoonList function Harpoon:list(name) - name = name or DEFAULT_LIST + name = name or Config.DEFAULT_LIST local key = self.config.settings.key() local lists = self.lists[key] @@ -119,6 +111,10 @@ end function Harpoon:sync() local key = self.config.settings.key() self:_for_each_list(function(list, _, list_name) + if list.encode == false then + return + end + local encoded = list:encode() self.data:update(key, list_name, encoded) end) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 6303025b..77f92a36 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -48,7 +48,7 @@ end ---@return HarpoonList function HarpoonList:append(item) - item = item or self.config.add() + item = item or self.config.add(self.config) local index = index_of(self.items, item, self.config) if index == -1 then @@ -64,7 +64,7 @@ end ---@return HarpoonList function HarpoonList:prepend(item) - item = item or self.config.add() + item = item or self.config.add(self.config) local index = index_of(self.items, item, self.config) if index == -1 then Listeners.listeners:emit( @@ -139,7 +139,7 @@ function HarpoonList:resolve_displayed(displayed) Listeners.event_names.ADD, { list = self, item = v, idx = i } ) - new_list[i] = self.config.add(v) + new_list[i] = self.config.add(self.config, v) else local index_in_new_list = index_of(new_list, self.items[index], self.config) From b8d690f098605aa0e2642adea91cd0889c47b4ca Mon Sep 17 00:00:00 2001 From: mpaulson Date: Thu, 30 Nov 2023 20:43:29 -0700 Subject: [PATCH 41/80] chore: formatting and lint --- lua/harpoon/config.lua | 1 - lua/harpoon/init.lua | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 429443b7..d09b8d16 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -3,7 +3,6 @@ local function normalize_path(buf_name, root) return Path:new(buf_name):make_relative(root) end - local M = {} local DEFAULT_LIST = "__harpoon_files" M.DEFAULT_LIST = DEFAULT_LIST diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index a2e0718f..11a5b905 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -125,7 +125,7 @@ end function Harpoon:info() return { paths = Data.info(), - default_list_name = DEFAULT_LIST, + default_list_name = Config.DEFAULT_LIST, } end From 1092e6fe96b0faae1fb59958e9b0b2a4b3a4f19d Mon Sep 17 00:00:00 2001 From: Ryan Winchester Date: Thu, 30 Nov 2023 23:35:00 -0400 Subject: [PATCH 42/80] Change logo --- README.md | 5 ++--- assets/harpoon-icon.png | Bin 0 -> 213252 bytes harpoon.png => assets/harpoon-man.png | Bin 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 assets/harpoon-icon.png rename harpoon.png => assets/harpoon-man.png (100%) diff --git a/README.md b/README.md index 47ffeb6f..66cff829 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,9 @@ [![Lua](https://img.shields.io/badge/Lua-blue.svg?style=for-the-badge&logo=lua)](http://www.lua.org) [![Neovim](https://img.shields.io/badge/Neovim%200.5+-green.svg?style=for-the-badge&logo=neovim)](https://neovim.io) - -![Harpoon](harpoon.png) --- image provided by **Bob Rust** +Harpoon Man + ## ⇁ TOC * [The Problems](#-The-Problems) diff --git a/assets/harpoon-icon.png b/assets/harpoon-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f4071c4a993d93e27942e952496aa8061c22ec65 GIT binary patch literal 213252 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

F>b=J1r_u** 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%0AYAc8;waSx)-jMbET zo_+Szn}2=V!=6AI*s2CGb1@{+HdLBnLuv>E;*mA6G9NO29%*Q7k&LWd`N!jr%UIZ= z{?vyqmjIAhjq`0EkTiR!2|V%2`Ua`ocR=G1 zA=6QDj4ERZiOCUtd&FwpvPhc>JuH`j)T|#0J?QwZ!cNd#-puoCvR|N6%)xpm>fKX%i;}p>%|5 zOsTqbTycryVw2#3B(WD($&x@(>3KAagLv>uf*RTY

~BtjVbX@!*@>6i*;kc)c$P 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 Date: Fri, 1 Dec 2023 01:31:23 -0500 Subject: [PATCH 43/80] add self.config default item in remove function --- lua/harpoon/list.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 77f92a36..884a1e83 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -79,6 +79,7 @@ end ---@return HarpoonList function HarpoonList:remove(item) + item = item or self.config.add(self.config) for i, v in ipairs(self.items) do if self.config.equals(v, item) then Listeners.listeners:emit( From a539f664fdd0160ef2b7c45dc18a5bcc725cb2b5 Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 13:18:28 -0700 Subject: [PATCH 44/80] fix: save_on_toggle --- lua/harpoon/ui.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 76fcaa69..2ecab2bc 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -97,6 +97,9 @@ function HarpoonUI:toggle_quick_menu(list) count = count + 1 if list == nil or self.win_id ~= nil then + if self.settings.save_on_toggle then + self:save() + end self:close_menu() return end From 23fb0002cf466c8a4af96ffb7d8aed584114e97e Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 13:44:59 -0700 Subject: [PATCH 45/80] fix: broken unit tests --- lua/harpoon/buffer.lua | 2 +- lua/harpoon/config.lua | 2 +- lua/harpoon/test/ui_spec.lua | 16 ++++++++++++++++ lua/harpoon/test/utils.lua | 21 +++++++++++++++++++-- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index c2245940..1a59bcdb 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -8,7 +8,7 @@ local HARPOON_MENU = "__harpoon-menu__" -- simple reason here is that if we are deving harpoon, we will create several -- ui objects, each with their own buffer, which will cause the name to be -- duplicated and then we will get a vim error on nvim_buf_set_name -local harpoon_menu_id = 0 +local harpoon_menu_id = math.random(1000000) local function get_harpoon_menu_name() harpoon_menu_id = harpoon_menu_id + 1 diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index d09b8d16..a7617427 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -57,7 +57,7 @@ function M.get_default_config() return { settings = { - save_on_toggle = true, + save_on_toggle = false, jump_to_file_location = true, key = function() return vim.loop.cwd() diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index db84591f..39212a14 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -40,6 +40,7 @@ describe("harpoon", function() table.remove(created_files, 2) Buffer.set_contents(harpoon.ui.bufnr, created_files) harpoon.ui:save() + harpoon.ui:toggle_quick_menu() eq(harpoon:list():length(), 2) eq(harpoon:list():display(), created_files) @@ -55,6 +56,7 @@ describe("harpoon", function() harpoon.ui:toggle_quick_menu(list) Buffer.set_contents(harpoon.ui.bufnr, created_files) harpoon.ui:save() + harpoon.ui:toggle_quick_menu() eq(list:length(), 4) eq(list:display(), created_files) @@ -91,4 +93,18 @@ describe("harpoon", function() eq(harpoon.ui.bufnr, nil) eq(harpoon.ui.win_id, nil) end) + + it("closing toggle_quick_menu with save_on_toggle should save contents", function() + harpoon:setup({ settings = { save_on_toggle = true }}) + local list = harpoon:list() + local created_files = utils.fill_list_with_files(3, list) + + harpoon.ui:toggle_quick_menu(list) + table.remove(created_files, 2) + Buffer.set_contents(harpoon.ui.bufnr, created_files) + harpoon.ui:toggle_quick_menu() + + eq(list:length(), 2) + eq(list:display(), created_files) + end) end) diff --git a/lua/harpoon/test/utils.lua b/lua/harpoon/test/utils.lua index 0cf284ad..25bbe0dc 100644 --- a/lua/harpoon/test/utils.lua +++ b/lua/harpoon/test/utils.lua @@ -4,6 +4,22 @@ local M = {} M.created_files = {} +local checkpoint_file = nil +local checkpoint_file_bufnr = nil +function M.create_checkpoint_file() + checkpoint_file = os.tmpname() + checkpoint_file_bufnr = M.create_file(checkpoint_file, { "test" }) +end + +function M.return_to_checkpoint() + if checkpoint_file_bufnr == nil then + return + end + + vim.api.nvim_set_current_buf(checkpoint_file_bufnr) + M.clean_files() +end + ---@param name string function M.before_each(name) return function() @@ -15,7 +31,7 @@ function M.before_each(name) Data.set_data_path(name) local harpoon = require("harpoon") - M.clean_files() + M.return_to_checkpoint() harpoon:setup({ settings = { @@ -39,6 +55,7 @@ end ---@param contents string[] function M.create_file(name, contents, row, col) local bufnr = vim.fn.bufnr(name, true) + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "hide") vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_buf_set_text(0, 0, 0, 0, 0, contents) if row then @@ -54,7 +71,7 @@ end function M.fill_list_with_files(count, list) local files = {} - for _ = 1, count do + for i = 1, count do local name = os.tmpname() table.insert(files, name) M.create_file(name, { "test" }) From 689778b3aa7dc78117c20b7835e7f081728b461f Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 13:45:22 -0700 Subject: [PATCH 46/80] chore: style and lint --- lua/harpoon/test/ui_spec.lua | 29 ++++++++++++++++------------- lua/harpoon/test/utils.lua | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index 39212a14..ff656176 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -94,17 +94,20 @@ describe("harpoon", function() eq(harpoon.ui.win_id, nil) end) - it("closing toggle_quick_menu with save_on_toggle should save contents", function() - harpoon:setup({ settings = { save_on_toggle = true }}) - local list = harpoon:list() - local created_files = utils.fill_list_with_files(3, list) - - harpoon.ui:toggle_quick_menu(list) - table.remove(created_files, 2) - Buffer.set_contents(harpoon.ui.bufnr, created_files) - harpoon.ui:toggle_quick_menu() - - eq(list:length(), 2) - eq(list:display(), created_files) - end) + it( + "closing toggle_quick_menu with save_on_toggle should save contents", + function() + harpoon:setup({ settings = { save_on_toggle = true } }) + local list = harpoon:list() + local created_files = utils.fill_list_with_files(3, list) + + harpoon.ui:toggle_quick_menu(list) + table.remove(created_files, 2) + Buffer.set_contents(harpoon.ui.bufnr, created_files) + harpoon.ui:toggle_quick_menu() + + eq(list:length(), 2) + eq(list:display(), created_files) + end + ) end) diff --git a/lua/harpoon/test/utils.lua b/lua/harpoon/test/utils.lua index 25bbe0dc..294d5117 100644 --- a/lua/harpoon/test/utils.lua +++ b/lua/harpoon/test/utils.lua @@ -71,7 +71,7 @@ end function M.fill_list_with_files(count, list) local files = {} - for i = 1, count do + for _ = 1, count do local name = os.tmpname() table.insert(files, name) M.create_file(name, { "test" }) From 1e041f13b10f99c240381172e4097b8a08785c9e Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 13:56:46 -0700 Subject: [PATCH 47/80] feat: removed some duplicate tests that are present in the ui. --- lua/harpoon/test/config_spec.lua | 2 +- lua/harpoon/test/harpoon_spec.lua | 105 ------------------------------ 2 files changed, 1 insertion(+), 106 deletions(-) diff --git a/lua/harpoon/test/config_spec.lua b/lua/harpoon/test/config_spec.lua index b0b66d31..d22555ad 100644 --- a/lua/harpoon/test/config_spec.lua +++ b/lua/harpoon/test/config_spec.lua @@ -17,7 +17,7 @@ describe("config", function() }) vim.api.nvim_win_set_cursor(0, { 3, 1 }) - local item = config_item.add() + local item = config_item.add(config_item) eq(item, { value = "/tmp/harpoon-test", context = { diff --git a/lua/harpoon/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua index af71a035..ffdda18a 100644 --- a/lua/harpoon/test/harpoon_spec.lua +++ b/lua/harpoon/test/harpoon_spec.lua @@ -105,109 +105,4 @@ describe("harpoon", function() }) end) - it("ui - display resolve", function() - harpoon:setup({ - default = { - display = function(item) - -- split string on / - local parts = vim.split(item.value, "/") - return parts[#parts] - end, - }, - }) - - local file_names = { - "/tmp/harpoon-test-1", - "/tmp/harpoon-test-2", - "/tmp/harpoon-test-3", - "/tmp/harpoon-test-4", - } - - local contents = { "foo", "bar", "baz", "qux" } - - local bufnrs = {} - local list = harpoon:list() - for _, v in ipairs(file_names) do - table.insert(bufnrs, utils.create_file(v, contents)) - harpoon:list():append() - end - - local displayed = list:display() - eq(displayed, { - "harpoon-test-1", - "harpoon-test-2", - "harpoon-test-3", - "harpoon-test-4", - }) - - table.remove(displayed, 3) - table.remove(displayed, 2) - - list:resolve_displayed(displayed) - - eq(list.items, { - { value = file_names[1], context = { row = 4, col = 2 } }, - { value = file_names[4], context = { row = 4, col = 2 } }, - }) - end) - - it("ui - display resolve", function() - local file_names = { - "/tmp/harpoon-test-1", - "/tmp/harpoon-test-2", - "/tmp/harpoon-test-3", - "/tmp/harpoon-test-4", - } - - local contents = { "foo", "bar", "baz", "qux" } - - local bufnrs = {} - local list = harpoon:list() - for _, v in ipairs(file_names) do - table.insert(bufnrs, utils.create_file(v, contents)) - harpoon:list():append() - end - - local displayed = list:display() - eq(displayed, { - "/tmp/harpoon-test-1", - "/tmp/harpoon-test-2", - "/tmp/harpoon-test-3", - "/tmp/harpoon-test-4", - }) - - table.remove(displayed, 3) - table.remove(displayed, 2) - - table.insert(displayed, "/tmp/harpoon-test-other-file-1") - table.insert(displayed, "/tmp/harpoon-test-other-file-2") - - list:resolve_displayed(displayed) - - eq({ - { value = file_names[1], context = { row = 4, col = 2 } }, - { value = file_names[4], context = { row = 4, col = 2 } }, - { - value = "/tmp/harpoon-test-other-file-1", - context = { row = 1, col = 0 }, - }, - { - value = "/tmp/harpoon-test-other-file-2", - context = { row = 1, col = 0 }, - }, - }, list.items) - - table.remove(displayed, 3) - table.insert(displayed, "/tmp/harpoon-test-4") - list:resolve_displayed(displayed) - - eq({ - { value = file_names[1], context = { row = 4, col = 2 } }, - { value = file_names[4], context = { row = 4, col = 2 } }, - { - value = "/tmp/harpoon-test-other-file-2", - context = { row = 1, col = 0 }, - }, - }, list.items) - end) end) From 01bce04637cd6943d62dcefbf081100c6b3b0c1b Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:07:55 -0700 Subject: [PATCH 48/80] chore: style --- lua/harpoon/test/harpoon_spec.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lua/harpoon/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua index ffdda18a..d7784e68 100644 --- a/lua/harpoon/test/harpoon_spec.lua +++ b/lua/harpoon/test/harpoon_spec.lua @@ -104,5 +104,4 @@ describe("harpoon", function() { value = file_name_1, context = { row = row_1, col = col_1 } }, }) end) - end) From 0db652a6124e57e8e767f3eddf00465fba8d0646 Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:13:22 -0700 Subject: [PATCH 49/80] fix: removed jump_to settings def --- README.md | 4 ++++ lua/harpoon/config.lua | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e687e955..e3ba8090 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,10 @@ There is quite a bit of behavior you can configure via `harpoon:setup()` * `VimLeavePre`: this function is called for every list on VimLeavePre. * `get_root_dir`: used for creating relative paths. defaults to `vim.loop.cwd()` +### Settings +Settings can alter the experience of harpoon + + ## ⇁ Social For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. * [Discord](https://discord.gg/theprimeagen) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index a7617427..d556ab31 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -29,12 +29,10 @@ M.DEFAULT_LIST = DEFAULT_LIST ---notehunthoeunthoeunthoeunthoeunthoeunth ---@class HarpoonSettings ---@field save_on_toggle boolean defaults to true ----@field jump_to_file_location boolean defaults to true ---@field key (fun(): string) ---@class HarpoonPartialSettings ---@field save_on_toggle? boolean ----@field jump_to_file_location? boolean ---@field key? (fun(): string) ---@class HarpoonConfig @@ -58,7 +56,6 @@ function M.get_default_config() settings = { save_on_toggle = false, - jump_to_file_location = true, key = function() return vim.loop.cwd() end, From e8cd1ee3163e5f16835382cd11fd01f349b01d01 Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:15:38 -0700 Subject: [PATCH 50/80] fix: settings section --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index e3ba8090..964ea7bf 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,27 @@ There is quite a bit of behavior you can configure via `harpoon:setup()` ### Settings Settings can alter the experience of harpoon +**Definition** +```lua +---@class HarpoonSettings +---@field save_on_toggle boolean defaults to true +---@field key (fun(): string) + +``` + +**Descriptions** +* `save_on_toggle`: any time the ui menu is closed then we will sync the state back to the backing list +* `key` how the out list key is looked up. This can be useful when using worktrees and using git remote instead of file path + +**Defaults** +```lua +settings = { + save_on_toggle = false, + key = function() + return vim.loop.cwd() + end, +}, +``` ## ⇁ Social For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. From 5bb4d59d4d18537f95fb94bd00f853951869f4de Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:17:12 -0700 Subject: [PATCH 51/80] fix: license and deleting of non useful file --- HARPOON2.md | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 HARPOON2.md diff --git a/HARPOON2.md b/HARPOON2.md deleted file mode 100644 index ed90f431..00000000 --- a/HARPOON2.md +++ /dev/null @@ -1,18 +0,0 @@ -### TODO -* encode being false means no writing to disk -* bring over the ui from harpoon 1.0 -* autocmds for leaving buffer and quitteriousing vim -* write some tests around file moving within the display - -### LATER FEATUERS -frecency = later feature likely, but great idea -- https://github.com/agkozak/zsh-z -- https://en.wikipedia.org/wiki/Frecency - -// i don't understand this one -harpoon -> qfix : qfix -> harpoon - - -harpoon.list("notehu") -> HarpoonList -harpoon.list().add() - From 7e37a12b06b0d94274bc409824e8e6f4354b690e Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:25:03 -0700 Subject: [PATCH 52/80] feat: readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 964ea7bf..a7e6f892 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ * [The Solutions](#-The-Solutions) * [Installation](#-Installation) * [Getting Started](#-Getting-Started) +* [API](#-API) + * [Config](#config) + * [Settings](#settings) * [Social](#-Social) * [Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) @@ -71,7 +74,7 @@ vim.keymap.set("n", "", function() harpoon:list():select(3) end) vim.keymap.set("n", "", function() harpoon:list():select(4) end) ``` -### Custom Lists +## ⇁ API You can define custom behavior of a harpoon list by providing your own calls. Here is a simple example where i create a list named `cmd` that takes the From 33b03bc06b3d40af9e958b8d972d3a5bb45cc030 Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:26:11 -0700 Subject: [PATCH 53/80] feat: expanding the problems --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a7e6f892..9b58021e 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@ 1. You're working on a codebase. medium, large, tiny, whatever. You find yourself frequenting a small set of files and you are tired of using a fuzzy finder, `:bnext` & `:bprev` are getting too repetitive, alternate file doesn't quite cut it, etc etc. -1. You want to execute some project specific commands or have any number of -persistent terminals that can be easily navigated to. +1. You want to execute some project specific commands, have any number of +persistent terminals that can be easily navigated to, send commands to other +tmux windows, or dream up your own custom action and execute with a single key ## ⇁ The Solutions 1. The ability to specify, or on the fly, mark and create persisting key strokes From ced172d035fcc8082d3e47077d63ed95da6c4d3e Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:27:11 -0700 Subject: [PATCH 54/80] fix: more moar maoare --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9b58021e..8d173b14 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,8 @@ persistent terminals that can be easily navigated to, send commands to other tmux windows, or dream up your own custom action and execute with a single key ## ⇁ The Solutions -1. The ability to specify, or on the fly, mark and create persisting key strokes -to go to the files you want. -1. Unlimited terminals and navigation. +1. Specify either by altering a ui or by adding via hot key files +1. Unlimited lists and items within the lists ## ⇁ Installation * neovim 0.8.0+ required From 245875a29f1f50a4d4e21b76db92635e1b15c651 Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:28:56 -0700 Subject: [PATCH 55/80] readme: contributions --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 8d173b14..9e8dd5d9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ * [API](#-API) * [Config](#config) * [Settings](#settings) +* [Contribution](#-Contribution) * [Social](#-Social) * [Note to legacy Harpoon 1 users](#-Note-to-legacy-Harpoon-1-users) @@ -187,6 +188,12 @@ settings = { }, ``` +## ⇁ Contribution +This project is officially open source, not just public source. If you wish to +contribute start with an issue and I am totally willing for PRs, but I will be +very conservative on what I take. I don't want Harpoon _solving_ specific +issues, I want it to create the proper hooks to solve any problem + ## ⇁ Social For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. * [Discord](https://discord.gg/theprimeagen) From c131b4b61be089237fbd65ed93192a4c0208e661 Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 14:47:58 -0700 Subject: [PATCH 56/80] feat: select with nil --- lua/harpoon/config.lua | 14 +++++++------- lua/harpoon/list.lua | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index d556ab31..8e3710b8 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -12,21 +12,17 @@ M.DEFAULT_LIST = DEFAULT_LIST ---@alias HarpoonListFileOptions {split: boolean, vsplit: boolean} ---@class HarpoonPartialConfigItem +---@field select_with_nil? boolean defaults to false ---@field encode? (fun(list_item: HarpoonListItem): string) ---@field decode? (fun(obj: string): any) ---@field display? (fun(list_item: HarpoonListItem): string) ----@field select? (fun(list_item: HarpoonListItem, options: any?): nil) +---@field select? (fun(list_item?: HarpoonListItem, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ---@field add? fun(config: HarpoonPartialConfigItem, item: any?): HarpoonListItem ---@field BufLeave? fun(evt: any, list: HarpoonList): nil ---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil ---@field get_root_dir? fun(): string ----@class HarpoonWindowSettings ----@field width number ----@field height number - ----notehunthoeunthoeunthoeunthoeunthoeunth ---@class HarpoonSettings ---@field save_on_toggle boolean defaults to true ---@field key (fun(): string) @@ -62,6 +58,9 @@ function M.get_default_config() }, default = { + --- select_with_nill allows for a list to call select even if the provided item is nil + select_with_nil = false, + ---@param obj HarpoonListItem ---@return string encode = function(obj) @@ -79,7 +78,8 @@ function M.get_default_config() return list_item.value end, - ---@param list_item HarpoonListFileItem + --- the select function is called when a user selects an item from the corresponding list and can be nil if select_with_nil is true + ---@param list_item? HarpoonListFileItem ---@param options HarpoonListFileOptions select = function(list_item, options) options = options or {} diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 884a1e83..d88ff0f7 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -155,7 +155,7 @@ end function HarpoonList:select(index, options) local item = self.items[index] - if item then + if item or self.config.select_with_nil then Listeners.listeners:emit( Listeners.event_names.SELECT, { list = self, item = item, idx = index } From 8dd3d909fd46cef5abb49ef52558420e691524a2 Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 15:51:50 -0700 Subject: [PATCH 57/80] test: select with nil --- lua/harpoon/test/list_spec.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua index 476ad316..f5904597 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon/test/list_spec.lua @@ -30,4 +30,34 @@ describe("list", function() "baz---qux", }) end) + + it("select_with_nil", function() + local foo_selected = nil + local bar_selected = nil + + local config = Config.merge_config({ + foo = { + select_with_nil = true, + select = function (list_item, options) + foo_selected = {list_item, options} + end + }, + bar = { + select = function (list_item, options) + bar_selected = {list_item, options} + end + } + }) + local fooc = Config.get_config(config, "foo") + local barc = Config.get_config(config, "bar") + + local foo = List.decode(fooc, "foo", {}) + local bar = List.decode(fooc, "bar", {}) + + foo:select(4, {}) + bar:select(4, {}) + + eq({nil, {}}, foo_selected) + eq(nil, bar_selected) + end) end) From 61406ca0b4878f99db2104a2eda11fb2313900fc Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Fri, 1 Dec 2023 15:54:05 -0700 Subject: [PATCH 58/80] feat: select gets `list` added to its call. for #350 --- README.md | 5 +++-- lua/harpoon/config.lua | 5 +++-- lua/harpoon/list.lua | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e8dd5d9..f77af561 100644 --- a/README.md +++ b/README.md @@ -120,8 +120,9 @@ harpoon:setup({ --- This function gets invoked with the options being passed in from --- list:select(index, <...options...>) --- @param list_item {value: any, context: any} + --- @param list { ... } --- @param option any - select = function(list_item, option) + select = function(list_item, list, option) -- WOAH, IS THIS HTMX LEVEL XSS ATTACK?? vim.cmd(list_item.value) end @@ -144,7 +145,7 @@ There is quite a bit of behavior you can configure via `harpoon:setup()` ---@field encode? (fun(list_item: HarpoonListItem): string) ---@field decode? (fun(obj: string): any) ---@field display? (fun(list_item: HarpoonListItem): string) ----@field select? (fun(list_item: HarpoonListItem, options: any?): nil) +---@field select? (fun(list_item?: HarpoonListItem, list: HarpoonList, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ---@field add? fun(item: any?): HarpoonListItem ---@field BufLeave? fun(evt: any, list: HarpoonList): nil diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 8e3710b8..966f19eb 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -16,7 +16,7 @@ M.DEFAULT_LIST = DEFAULT_LIST ---@field encode? (fun(list_item: HarpoonListItem): string) ---@field decode? (fun(obj: string): any) ---@field display? (fun(list_item: HarpoonListItem): string) ----@field select? (fun(list_item?: HarpoonListItem, options: any?): nil) +---@field select? (fun(list_item?: HarpoonListItem, list: HarpoonList, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ---@field add? fun(config: HarpoonPartialConfigItem, item: any?): HarpoonListItem ---@field BufLeave? fun(evt: any, list: HarpoonList): nil @@ -80,8 +80,9 @@ function M.get_default_config() --- the select function is called when a user selects an item from the corresponding list and can be nil if select_with_nil is true ---@param list_item? HarpoonListFileItem + ---@param list HarpoonList ---@param options HarpoonListFileOptions - select = function(list_item, options) + select = function(list_item, list, options) options = options or {} if list_item == nil then return diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index d88ff0f7..52588d93 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -160,7 +160,7 @@ function HarpoonList:select(index, options) Listeners.event_names.SELECT, { list = self, item = item, idx = index } ) - self.config.select(item, options) + self.config.select(item, self, options) end end From 2d5e3443b7cc3ed8d289910b7985dd6dac5b4cb2 Mon Sep 17 00:00:00 2001 From: Arthur McLain Date: Sat, 2 Dec 2023 03:48:40 +0100 Subject: [PATCH 59/80] fix: update highlight groups --- lua/harpoon/ui.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 2ecab2bc..9d809c28 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -72,6 +72,8 @@ function HarpoonUI:_create_window() local _, popup_info = popup.create(bufnr, { title = "Harpoon", highlight = "HarpoonWindow", + borderhighlight = "HarpoonBorder", + titlehighlight = "HarpoonTitle", line = math.floor(((vim.o.lines - height) / 2) - 1), col = math.floor((vim.o.columns - width) / 2), minwidth = width, @@ -85,7 +87,6 @@ function HarpoonUI:_create_window() self.win_id = win_id self.border_win_id = popup_info.border.win_id vim.api.nvim_win_set_option(win_id, "number", true) - vim.api.nvim_win_set_option(win_id, "winhl", "Normal:HarpoonBorder") return win_id, bufnr end From 97ddd6fda568ced617f0e8c6d683fdfc8aff7c40 Mon Sep 17 00:00:00 2001 From: Arthur McLain Date: Sat, 2 Dec 2023 04:48:24 +0100 Subject: [PATCH 60/80] docs: add information about highlight groups --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index f77af561..1b6fd0ac 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,10 @@ settings = { }, ``` +### Highlight Groups +Currently available highlight groups are +`HarpoonWindow`, `HarpoonBorder`, and `HarpoonTitle`. + ## ⇁ Contribution This project is officially open source, not just public source. If you wish to contribute start with an issue and I am totally willing for PRs, but I will be From b2a2405a4c1662e4375eaa899479a86bba0a0626 Mon Sep 17 00:00:00 2001 From: Arthur McLain Date: Sat, 2 Dec 2023 22:23:21 +0100 Subject: [PATCH 61/80] docs: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b6fd0ac..60b2cac9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ##### Getting you where you want with the fewest keystrokes. [![Lua](https://img.shields.io/badge/Lua-blue.svg?style=for-the-badge&logo=lua)](http://www.lua.org) -[![Neovim](https://img.shields.io/badge/Neovim%200.5+-green.svg?style=for-the-badge&logo=neovim)](https://neovim.io) +[![Neovim](https://img.shields.io/badge/Neovim%200.8+-green.svg?style=for-the-badge&logo=neovim)](https://neovim.io) Harpoon Man From d9dc3bf8754ace98542e58a9df367c8597f3e1a6 Mon Sep 17 00:00:00 2001 From: Rodrigo Medina Date: Sat, 2 Dec 2023 16:19:14 -0600 Subject: [PATCH 62/80] fix: telescope integration --- lua/telescope/_extensions/marks.lua | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lua/telescope/_extensions/marks.lua b/lua/telescope/_extensions/marks.lua index 1d7cd59b..5101ff3e 100644 --- a/lua/telescope/_extensions/marks.lua +++ b/lua/telescope/_extensions/marks.lua @@ -5,12 +5,11 @@ local finders = require("telescope.finders") local pickers = require("telescope.pickers") local conf = require("telescope.config").values local harpoon = require("harpoon") -local harpoon_mark = require("harpoon.mark") local function filter_empty_string(list) local next = {} for idx = 1, #list do - if list[idx].filename ~= "" then + if list[idx].value ~= "" then table.insert(next, list[idx]) end end @@ -20,9 +19,13 @@ end local generate_new_finder = function() return finders.new_table({ - results = filter_empty_string(harpoon.get_mark_config().marks), + results = filter_empty_string(harpoon:list().items), entry_maker = function(entry) - local line = entry.filename .. ":" .. entry.row .. ":" .. entry.col + local line = entry.value + .. ":" + .. entry.context.row + .. ":" + .. entry.context.col local displayer = entry_display.create({ separator = " - ", items = { @@ -43,7 +46,7 @@ local generate_new_finder = function() display = make_display, lnum = entry.row, col = entry.col, - filename = entry.filename, + filename = entry.value, } end, }) @@ -61,7 +64,7 @@ local delete_harpoon_mark = function(prompt_bufnr) end local selection = action_state.get_selected_entry() - harpoon_mark.rm_file(selection.filename) + harpoon:list():remove(selection.value) local function get_selections() local results = {} @@ -73,7 +76,7 @@ local delete_harpoon_mark = function(prompt_bufnr) local selections = get_selections() for _, current_selection in ipairs(selections) do - harpoon_mark.rm_file(current_selection.filename) + harpoon:list():remove(current_selection.value) end local current_picker = action_state.get_current_picker(prompt_bufnr) @@ -82,13 +85,13 @@ end local move_mark_up = function(prompt_bufnr) local selection = action_state.get_selected_entry() - local length = harpoon_mark.get_length() + local length = harpoon:list():length() if selection.index == length then return end - local mark_list = harpoon.get_mark_config().marks + local mark_list = harpoon:list().items table.remove(mark_list, selection.index) table.insert(mark_list, selection.index + 1, selection.value) @@ -102,7 +105,7 @@ local move_mark_down = function(prompt_bufnr) if selection.index == 1 then return end - local mark_list = harpoon.get_mark_config().marks + local mark_list = harpoon:list().items table.remove(mark_list, selection.index) table.insert(mark_list, selection.index - 1, selection.value) local current_picker = action_state.get_current_picker(prompt_bufnr) From 581da797f9d66485f841525af596255270c2bcf5 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Sat, 2 Dec 2023 16:01:22 -0700 Subject: [PATCH 63/80] chore: style --- lua/harpoon/test/list_spec.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua index f5904597..d7696671 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon/test/list_spec.lua @@ -38,15 +38,15 @@ describe("list", function() local config = Config.merge_config({ foo = { select_with_nil = true, - select = function (list_item, options) - foo_selected = {list_item, options} - end + select = function(list_item, options) + foo_selected = { list_item, options } + end, }, bar = { - select = function (list_item, options) - bar_selected = {list_item, options} - end - } + select = function(list_item, options) + bar_selected = { list_item, options } + end, + }, }) local fooc = Config.get_config(config, "foo") local barc = Config.get_config(config, "bar") @@ -57,7 +57,7 @@ describe("list", function() foo:select(4, {}) bar:select(4, {}) - eq({nil, {}}, foo_selected) + eq({ nil, {} }, foo_selected) eq(nil, bar_selected) end) end) From 84e2eb79ede49ed1d8f996bf72c3362ca0c2930f Mon Sep 17 00:00:00 2001 From: Arthur McLain Date: Mon, 4 Dec 2023 05:26:50 +0100 Subject: [PATCH 64/80] feat: link harpoon highlights to default groups --- lua/harpoon/init.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 11a5b905..58b2e683 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -38,6 +38,15 @@ function Harpoon:setup(partial_config) self.config = Config.merge_config(partial_config, self.config) self.ui:configure(self.config.settings) + local highlights = { + HarpoonWindow = { default = true, link = "NormalFloat" }, + HarpoonBorder = { default = true, link = "FloatBorder" }, + HarpoonTitle = { default = true, link = "FloatTitle" }, + } + for k, v in pairs(highlights) do + vim.api.nvim_set_hl(0, k, v) + end + ---TODO: should we go through every seen list and update its config? if self.hooks_setup == false then From b546ffebf07134db0ab451c8272edd149afdf7f1 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 4 Dec 2023 10:48:07 -0700 Subject: [PATCH 65/80] feat: borders are now customizable --- lua/harpoon/config.lua | 2 ++ lua/harpoon/ui.lua | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 966f19eb..abd79989 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -24,6 +24,7 @@ M.DEFAULT_LIST = DEFAULT_LIST ---@field get_root_dir? fun(): string ---@class HarpoonSettings +---@field border_chars string[] defaults to { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } ---@field save_on_toggle boolean defaults to true ---@field key (fun(): string) @@ -52,6 +53,7 @@ function M.get_default_config() settings = { save_on_toggle = false, + border_chars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, key = function() return vim.loop.cwd() end, diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 9d809c28..1e9e9e79 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -66,8 +66,7 @@ function HarpoonUI:_create_window() end local height = 8 - local borderchars = - { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } + local borderchars = self.settings.border_chars local bufnr = vim.api.nvim_create_buf(false, false) local _, popup_info = popup.create(bufnr, { title = "Harpoon", From 80a428855f0852661c55cef7ad53f369d38721f9 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 4 Dec 2023 10:49:46 -0700 Subject: [PATCH 66/80] fix: #354 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 60b2cac9..ea85d563 100644 --- a/README.md +++ b/README.md @@ -177,12 +177,14 @@ Settings can alter the experience of harpoon **Descriptions** * `save_on_toggle`: any time the ui menu is closed then we will sync the state back to the backing list +* `border_chars`: the ui's border characters to be displayed * `key` how the out list key is looked up. This can be useful when using worktrees and using git remote instead of file path **Defaults** ```lua settings = { save_on_toggle = false, + border_chars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, key = function() return vim.loop.cwd() end, From 68b322326837f371ace9f6f2c57a0abda23696e4 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 4 Dec 2023 11:55:53 -0700 Subject: [PATCH 67/80] feat: logger --- lua/harpoon/config.lua | 8 ++++++++ lua/harpoon/init.lua | 12 +++--------- lua/harpoon/list.lua | 1 + lua/harpoon/logger.lua | 39 +++++++++++++++++++++++++++++++++++++++ lua/harpoon/ui.lua | 11 ++++++++--- 5 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 lua/harpoon/logger.lua diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index abd79989..d38cf89a 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -1,3 +1,4 @@ +local Logger = require("harpoon.logger") local Path = require("plenary.path") local function normalize_path(buf_name, root) return Path:new(buf_name):make_relative(root) @@ -85,6 +86,7 @@ function M.get_default_config() ---@param list HarpoonList ---@param options HarpoonListFileOptions select = function(list_item, list, options) + Logger:log("config_default#select", list_item, list.name, options) options = options or {} if list_item == nil then return @@ -142,6 +144,8 @@ function M.get_default_config() config.get_root_dir() ) + Logger:log("config_default#add", name) + local bufnr = vim.fn.bufnr(name, false) local pos = { 1, 0 } @@ -165,8 +169,12 @@ function M.get_default_config() if item then local pos = vim.api.nvim_win_get_cursor(0) + + Logger:log("config_default#BufLeave updating position", bufnr, bufname, item, "to position", pos) + item.context.row = pos[1] item.context.col = pos[2] + end end, diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 58b2e683..fd5677f5 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -1,3 +1,4 @@ +local Log = require("harpoon.logger") local Ui = require("harpoon.ui") local Data = require("harpoon.data") local Config = require("harpoon.config") @@ -10,6 +11,7 @@ local HarpoonGroup = require("harpoon.autocmd") ---@field ui HarpoonUI ---@field listeners HarpoonListeners ---@field data HarpoonData +---@field logger HarpoonLog ---@field lists {[string]: {[string]: HarpoonList}} ---@field hooks_setup boolean local Harpoon = {} @@ -23,6 +25,7 @@ function Harpoon:new() local harpoon = setmetatable({ config = config, data = Data.Data:new(), + logger = Log, ui = Ui:new(config.settings), listeners = Listeners.listeners, lists = {}, @@ -38,15 +41,6 @@ function Harpoon:setup(partial_config) self.config = Config.merge_config(partial_config, self.config) self.ui:configure(self.config.settings) - local highlights = { - HarpoonWindow = { default = true, link = "NormalFloat" }, - HarpoonBorder = { default = true, link = "FloatBorder" }, - HarpoonTitle = { default = true, link = "FloatTitle" }, - } - for k, v in pairs(highlights) do - vim.api.nvim_set_hl(0, k, v) - end - ---TODO: should we go through every seen list and update its config? if self.hooks_setup == false then diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 52588d93..7f889491 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -1,3 +1,4 @@ +local Log = require("harpoon.logger") local Listeners = require("harpoon.listeners") local function index_of(items, element, config) diff --git a/lua/harpoon/logger.lua b/lua/harpoon/logger.lua new file mode 100644 index 00000000..27ed51e4 --- /dev/null +++ b/lua/harpoon/logger.lua @@ -0,0 +1,39 @@ + +---@class HarpoonLog +---@field lines string[] +local HarpoonLog = {} + +HarpoonLog.__index = HarpoonLog + +---@return HarpoonLog +function HarpoonLog:new() + local logger = setmetatable({ + lines = {}, + }, self) + + return logger +end + +---@vararg any +function HarpoonLog:log(...) + + local msg = {} + for i = 1, select("#", ...) do + table.insert(msg, vim.inspect(select(i, ...))) + end + + table.insert(self.lines, table.concat(msg, " ")) +end + +function HarpoonLog:clear() + self.lines = {} +end + +function HarpoonLog:show() + local bufnr = vim.api.nvim_create_buf(false, true) + print(vim.inspect(self.lines)) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, self.lines) + vim.api.nvim_win_set_buf(0, bufnr) +end + +return HarpoonLog:new() diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 1e9e9e79..7c4548f7 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -1,5 +1,6 @@ local popup = require("plenary").popup local Buffer = require("harpoon.buffer") +local Logger = require("harpoon.logger") local DEFAULT_WINDOW_WIDTH = 69 -- nice ---@class HarpoonUI @@ -30,6 +31,10 @@ function HarpoonUI:close_menu() end self.closing = true + Logger:log("ui#close_menu name: ", self.active_list.name, "win and bufnr", { + win = self.win_id, + bufnr = self.bufnr + }) if self.bufnr ~= nil and vim.api.nvim_buf_is_valid(self.bufnr) then vim.api.nvim_buf_delete(self.bufnr, { force = true }) @@ -90,11 +95,10 @@ function HarpoonUI:_create_window() return win_id, bufnr end -local count = 0 - ---@param list? HarpoonList function HarpoonUI:toggle_quick_menu(list) - count = count + 1 + + Logger:log("ui#toggle_quick_menu", list and list.name) if list == nil or self.win_id ~= nil then if self.settings.save_on_toggle then @@ -129,6 +133,7 @@ end function HarpoonUI:save() local list = Buffer.get_contents(self.bufnr) + Logger:log("ui#save", list) self.active_list:resolve_displayed(list) end From 6041c605b09dcc31af92524a8f2d4178ec0b8464 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 4 Dec 2023 11:58:15 -0700 Subject: [PATCH 68/80] readme: update for logger --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index ea85d563..d48ceb88 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,14 @@ settings = { Currently available highlight groups are `HarpoonWindow`, `HarpoonBorder`, and `HarpoonTitle`. +### Logger +This can help debug issues on other's computer. To get your debug log please do the following. + +1. open up a new instance of vim +1. perform exact operation to cause bug +1. execute vim command `:lua require("harpoon").logger:show()` and copy the buffer +1. paste the buffer as part of the bug creation + ## ⇁ Contribution This project is officially open source, not just public source. If you wish to contribute start with an issue and I am totally willing for PRs, but I will be From c53305c2d00d0c1dfb5227e8a571cb95b0132fbc Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 4 Dec 2023 12:08:28 -0700 Subject: [PATCH 69/80] feat: logging more details about how you moved through harpoon --- lua/harpoon/buffer.lua | 9 ++++++--- lua/harpoon/ui.lua | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index 1a59bcdb..7c8151d6 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -1,3 +1,4 @@ +local Logger = require("harpoon.logger") local utils = require("harpoon.utils") local HarpoonGroup = require("harpoon.autocmd") @@ -44,21 +45,21 @@ function M.setup_autocmds_and_keymaps(bufnr) bufnr, "n", "q", - "lua require('harpoon').ui:toggle_quick_menu()", + "lua require('harpoon').logger:log('toggle by keymap \'q\''); require('harpoon').ui:toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( bufnr, "n", "", - "lua require('harpoon').ui:toggle_quick_menu()", + "lua require('harpoon').logger:log('toggle by keymap \'\''); require('harpoon').ui:toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( bufnr, "n", "", - "lua require('harpoon').ui:select_menu_item()", + "lua require('harpoon').logger:log('select by keymap \'\''); require('harpoon').ui:select_menu_item()", {} ) @@ -87,6 +88,7 @@ function M.setup_autocmds_and_keymaps(bufnr) callback = function() require("harpoon").ui:save() vim.schedule(function() + require("harpoon").logger:log("toggle by BufWriteCmd") require("harpoon").ui:toggle_quick_menu() end) end, @@ -96,6 +98,7 @@ function M.setup_autocmds_and_keymaps(bufnr) group = HarpoonGroup, pattern = "__harpoon*", callback = function() + require("harpoon").logger:log("toggle by BufLeave") require("harpoon").ui:toggle_quick_menu() end, }) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 7c4548f7..375d5e61 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -127,6 +127,8 @@ function HarpoonUI:select_menu_item(options) local list = Buffer.get_contents(self.bufnr) self.active_list:resolve_displayed(list) + Logger:log("ui#select_menu_item selecting item", idx, "from", list, "options", options) + self.active_list:select(idx, options) self:close_menu() end From dfcb8488df7bf89859b063a188d2be5c17ea75a8 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Mon, 4 Dec 2023 12:15:00 -0700 Subject: [PATCH 70/80] fix: tests were breaking due to logging --- lua/harpoon/config.lua | 9 ++++++++- lua/harpoon/logger.lua | 3 ++- lua/harpoon/test/list_spec.lua | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index d38cf89a..93011124 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -170,7 +170,14 @@ function M.get_default_config() if item then local pos = vim.api.nvim_win_get_cursor(0) - Logger:log("config_default#BufLeave updating position", bufnr, bufname, item, "to position", pos) + Logger:log( + "config_default#BufLeave updating position", + bufnr, + bufname, + item, + "to position", + pos + ) item.context.row = pos[1] item.context.col = pos[2] diff --git a/lua/harpoon/logger.lua b/lua/harpoon/logger.lua index 27ed51e4..b8d368a0 100644 --- a/lua/harpoon/logger.lua +++ b/lua/harpoon/logger.lua @@ -19,7 +19,8 @@ function HarpoonLog:log(...) local msg = {} for i = 1, select("#", ...) do - table.insert(msg, vim.inspect(select(i, ...))) + local item = select(i, ...) + table.insert(msg, vim.inspect(item)) end table.insert(self.lines, table.concat(msg, " ")) diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua index d7696671..6817cda9 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon/test/list_spec.lua @@ -38,7 +38,7 @@ describe("list", function() local config = Config.merge_config({ foo = { select_with_nil = true, - select = function(list_item, options) + select = function(list_item, _, options) foo_selected = { list_item, options } end, }, From ec03b3cc4ba16e6411f35f15230e1e4e776ebe3d Mon Sep 17 00:00:00 2001 From: Michael Paulson Date: Mon, 4 Dec 2023 17:41:27 -0700 Subject: [PATCH 71/80] fix: active list caused nil access error --- lua/harpoon/ui.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 375d5e61..8da4067d 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -25,13 +25,21 @@ function HarpoonUI:new(settings) }, self) end +local function get_name(list) + if list ~= nil then + return list.name + end + + return "(list nil)" +end + function HarpoonUI:close_menu() if self.closing then return end self.closing = true - Logger:log("ui#close_menu name: ", self.active_list.name, "win and bufnr", { + Logger:log("ui#close_menu name: ", get_name(self.active_list), "win and bufnr", { win = self.win_id, bufnr = self.bufnr }) From e2e582e776349a1f40880399728b29bde315583c Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 5 Dec 2023 07:27:16 -0700 Subject: [PATCH 72/80] fix: navigation --- lua/harpoon/buffer.lua | 19 +++++++++-- lua/harpoon/ui.lua | 71 +++++------------------------------------- 2 files changed, 23 insertions(+), 67 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index 7c8151d6..54723b6e 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -16,6 +16,19 @@ local function get_harpoon_menu_name() return HARPOON_MENU .. harpoon_menu_id end +function M.run_select_command() + local harpoon = require("harpoon") + harpoon.logger:log('select by keymap \'\'') + harpoon.ui:select_menu_item() +end + +function M.run_toggle_command(key) + local harpoon = require("harpoon") + harpoon.logger:log('toggle by keymap \'' .. key .. '\'') + harpoon.ui:select_menu_item() +end + + ---TODO: I don't know how to do what i want to do, but i want to be able to ---make this so we use callbacks for these buffer actions instead of using ---strings back into the ui. it feels gross and it puts odd coupling @@ -45,21 +58,21 @@ function M.setup_autocmds_and_keymaps(bufnr) bufnr, "n", "q", - "lua require('harpoon').logger:log('toggle by keymap \'q\''); require('harpoon').ui:toggle_quick_menu()", + "lua require('harpoon.buffer').run_toggle_command('q')", { silent = true } ) vim.api.nvim_buf_set_keymap( bufnr, "n", "", - "lua require('harpoon').logger:log('toggle by keymap \'\''); require('harpoon').ui:toggle_quick_menu()", + "lua require('harpoon.buffer').run_toggle_command('')", { silent = true } ) vim.api.nvim_buf_set_keymap( bufnr, "n", "", - "lua require('harpoon').logger:log('select by keymap \'\''); require('harpoon').ui:select_menu_item()", + "lua require('harpoon.buffer').run_select_command()", {} ) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 8da4067d..931e3cdb 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -11,6 +11,12 @@ local DEFAULT_WINDOW_WIDTH = 69 -- nice ---@field active_list HarpoonList local HarpoonUI = {} +---@param list HarpoonList +---@return string +local function list_name(list) + return list and list.name or "nil" +end + HarpoonUI.__index = HarpoonUI ---@param settings HarpoonSettings @@ -25,21 +31,13 @@ function HarpoonUI:new(settings) }, self) end -local function get_name(list) - if list ~= nil then - return list.name - end - - return "(list nil)" -end - function HarpoonUI:close_menu() if self.closing then return end self.closing = true - Logger:log("ui#close_menu name: ", get_name(self.active_list), "win and bufnr", { + Logger:log("ui#close_menu name: ", list_name(self.active_list), "win and bufnr", { win = self.win_id, bufnr = self.bufnr }) @@ -152,59 +150,4 @@ function HarpoonUI:configure(settings) self.settings = settings end ---[[ -function M.location_window(options) - local default_options = { - relative = "editor", - style = "minimal", - width = 30, - height = 15, - row = 2, - col = 2, - } - options = vim.tbl_extend("keep", options, default_options) - - local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true) - local win_id = vim.api.nvim_open_win(bufnr, true, options) - - return { - bufnr = bufnr, - win_id = win_id, - } -end - --- TODO: What is this used for? -function M.notification(text) - local win_stats = vim.api.nvim_list_uis()[1] - local win_width = win_stats.width - - local prev_win = vim.api.nvim_get_current_win() - - local info = M.location_window({ - width = 20, - height = 2, - row = 1, - col = win_width - 21, - }) - - vim.api.nvim_buf_set_lines( - info.bufnr, - 0, - 5, - false, - { "!!! Notification", text } - ) - vim.api.nvim_set_current_win(prev_win) - - return { - bufnr = info.bufnr, - win_id = info.win_id, - } -end - -function M.close_notification(bufnr) - vim.api.nvim_buf_delete(bufnr) -end ---]] - return HarpoonUI From 23fe6410bc8eae5cd0d06164ca1cd531774bfc3e Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 5 Dec 2023 07:35:01 -0700 Subject: [PATCH 73/80] feat: #372 -- configurable fallback width and width ratio --- lua/harpoon/config.lua | 4 ++++ lua/harpoon/ui.lua | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 93011124..501ffb4b 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -27,6 +27,8 @@ M.DEFAULT_LIST = DEFAULT_LIST ---@class HarpoonSettings ---@field border_chars string[] defaults to { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } ---@field save_on_toggle boolean defaults to true +---@field ui_fallback_width number defaults 69, nice +---@field ui_width_ratio number defaults to 0.62569 ---@field key (fun(): string) ---@class HarpoonPartialSettings @@ -55,6 +57,8 @@ function M.get_default_config() settings = { save_on_toggle = false, border_chars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + ui_fallback_width = 69, + ui_width_ratio = 0.62569, key = function() return vim.loop.cwd() end, diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 931e3cdb..3e77e5c6 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -1,7 +1,6 @@ local popup = require("plenary").popup local Buffer = require("harpoon.buffer") local Logger = require("harpoon.logger") -local DEFAULT_WINDOW_WIDTH = 69 -- nice ---@class HarpoonUI ---@field win_id number @@ -69,11 +68,11 @@ end function HarpoonUI:_create_window() local win = vim.api.nvim_list_uis() - local width = DEFAULT_WINDOW_WIDTH + local width = self.settings.ui_fallback_width if #win > 0 then -- no ackshual reason for 0.62569, just looks complicated, and i want -- to make my boss think i am smart - width = math.floor(win[1].width * 0.62569) + width = math.floor(win[1].width * self.settings.ui_width_ratio) end local height = 8 From c24c7119a26cfe9da187549d51d3b470add33e18 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 5 Dec 2023 07:37:22 -0700 Subject: [PATCH 74/80] chore: lint and fmt --- lua/harpoon/buffer.lua | 6 ++---- lua/harpoon/config.lua | 22 ++++++++++++++++++---- lua/harpoon/list.lua | 1 - lua/harpoon/logger.lua | 2 -- lua/harpoon/test/list_spec.lua | 2 +- lua/harpoon/ui.lua | 23 +++++++++++++++++------ 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index 54723b6e..fd0e05c9 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -1,4 +1,3 @@ -local Logger = require("harpoon.logger") local utils = require("harpoon.utils") local HarpoonGroup = require("harpoon.autocmd") @@ -18,17 +17,16 @@ end function M.run_select_command() local harpoon = require("harpoon") - harpoon.logger:log('select by keymap \'\'') + harpoon.logger:log("select by keymap ''") harpoon.ui:select_menu_item() end function M.run_toggle_command(key) local harpoon = require("harpoon") - harpoon.logger:log('toggle by keymap \'' .. key .. '\'') + harpoon.logger:log("toggle by keymap '" .. key .. "'") harpoon.ui:select_menu_item() end - ---TODO: I don't know how to do what i want to do, but i want to be able to ---make this so we use callbacks for these buffer actions instead of using ---strings back into the ui. it feels gross and it puts odd coupling diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 501ffb4b..3feac153 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -56,7 +56,16 @@ function M.get_default_config() settings = { save_on_toggle = false, - border_chars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + border_chars = { + "─", + "│", + "─", + "│", + "╭", + "╮", + "╯", + "╰", + }, ui_fallback_width = 69, ui_width_ratio = 0.62569, key = function() @@ -85,12 +94,18 @@ function M.get_default_config() return list_item.value end, - --- the select function is called when a user selects an item from the corresponding list and can be nil if select_with_nil is true + --- the select function is called when a user selects an item from + --- the corresponding list and can be nil if select_with_nil is true ---@param list_item? HarpoonListFileItem ---@param list HarpoonList ---@param options HarpoonListFileOptions select = function(list_item, list, options) - Logger:log("config_default#select", list_item, list.name, options) + Logger:log( + "config_default#select", + list_item, + list.name, + options + ) options = options or {} if list_item == nil then return @@ -185,7 +200,6 @@ function M.get_default_config() item.context.row = pos[1] item.context.col = pos[2] - end end, diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 7f889491..52588d93 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -1,4 +1,3 @@ -local Log = require("harpoon.logger") local Listeners = require("harpoon.listeners") local function index_of(items, element, config) diff --git a/lua/harpoon/logger.lua b/lua/harpoon/logger.lua index b8d368a0..6fd6f89b 100644 --- a/lua/harpoon/logger.lua +++ b/lua/harpoon/logger.lua @@ -1,4 +1,3 @@ - ---@class HarpoonLog ---@field lines string[] local HarpoonLog = {} @@ -16,7 +15,6 @@ end ---@vararg any function HarpoonLog:log(...) - local msg = {} for i = 1, select("#", ...) do local item = select(i, ...) diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua index 6817cda9..eaaed225 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon/test/list_spec.lua @@ -52,7 +52,7 @@ describe("list", function() local barc = Config.get_config(config, "bar") local foo = List.decode(fooc, "foo", {}) - local bar = List.decode(fooc, "bar", {}) + local bar = List.decode(barc, "bar", {}) foo:select(4, {}) bar:select(4, {}) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 3e77e5c6..66e682cd 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -36,10 +36,15 @@ function HarpoonUI:close_menu() end self.closing = true - Logger:log("ui#close_menu name: ", list_name(self.active_list), "win and bufnr", { - win = self.win_id, - bufnr = self.bufnr - }) + Logger:log( + "ui#close_menu name: ", + list_name(self.active_list), + "win and bufnr", + { + win = self.win_id, + bufnr = self.bufnr, + } + ) if self.bufnr ~= nil and vim.api.nvim_buf_is_valid(self.bufnr) then vim.api.nvim_buf_delete(self.bufnr, { force = true }) @@ -102,7 +107,6 @@ end ---@param list? HarpoonList function HarpoonUI:toggle_quick_menu(list) - Logger:log("ui#toggle_quick_menu", list and list.name) if list == nil or self.win_id ~= nil then @@ -132,7 +136,14 @@ function HarpoonUI:select_menu_item(options) local list = Buffer.get_contents(self.bufnr) self.active_list:resolve_displayed(list) - Logger:log("ui#select_menu_item selecting item", idx, "from", list, "options", options) + Logger:log( + "ui#select_menu_item selecting item", + idx, + "from", + list, + "options", + options + ) self.active_list:select(idx, options) self:close_menu() From e9d18fca95f324baba2ec6a486393369b1c46b57 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 5 Dec 2023 19:02:43 -0700 Subject: [PATCH 75/80] feat: logging and rename. --- lua/harpoon/config.lua | 6 +++--- lua/harpoon/list.lua | 13 +++++++++---- lua/harpoon/test/config_spec.lua | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 3feac153..260f0040 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -19,7 +19,7 @@ M.DEFAULT_LIST = DEFAULT_LIST ---@field display? (fun(list_item: HarpoonListItem): string) ---@field select? (fun(list_item?: HarpoonListItem, list: HarpoonList, options: any?): nil) ---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean) ----@field add? fun(config: HarpoonPartialConfigItem, item: any?): HarpoonListItem +---@field create_list_item? fun(config: HarpoonPartialConfigItem, item: any?): HarpoonListItem ---@field BufLeave? fun(evt: any, list: HarpoonList): nil ---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil ---@field get_root_dir? fun(): string @@ -149,7 +149,7 @@ function M.get_default_config() ---@param config HarpoonPartialConfigItem ---@param name? any ---@return HarpoonListItem - add = function(config, name) + create_list_item = function(config, name) name = name -- TODO: should we do path normalization??? -- i know i have seen sometimes it becoming an absolute @@ -163,7 +163,7 @@ function M.get_default_config() config.get_root_dir() ) - Logger:log("config_default#add", name) + Logger:log("config_default#create_list_item", name) local bufnr = vim.fn.bufnr(name, false) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 52588d93..20a44e36 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -1,3 +1,4 @@ +local Logger = require("harpoon.logger") local Listeners = require("harpoon.listeners") local function index_of(items, element, config) @@ -48,9 +49,10 @@ end ---@return HarpoonList function HarpoonList:append(item) - item = item or self.config.add(self.config) + item = item or self.config.create_list_item(self.config) local index = index_of(self.items, item, self.config) + Logger:log("HarpoonList:append", { item = item, index = index }) if index == -1 then Listeners.listeners:emit( Listeners.event_names.ADD, @@ -64,8 +66,9 @@ end ---@return HarpoonList function HarpoonList:prepend(item) - item = item or self.config.add(self.config) + item = item or self.config.create_list_item(self.config) local index = index_of(self.items, item, self.config) + Logger:log("HarpoonList:prepend", { item = item, index = index }) if index == -1 then Listeners.listeners:emit( Listeners.event_names.ADD, @@ -79,13 +82,14 @@ end ---@return HarpoonList function HarpoonList:remove(item) - item = item or self.config.add(self.config) + item = item or self.config.create_list_item(self.config) for i, v in ipairs(self.items) do if self.config.equals(v, item) then Listeners.listeners:emit( Listeners.event_names.REMOVE, { list = self, item = item, idx = i } ) + Logger:log("HarpoonList:remove", { item = item, index = i }) table.remove(self.items, i) break end @@ -99,6 +103,7 @@ function HarpoonList:removeAt(index) Listeners.event_names.REMOVE, { list = self, item = self.items[index], idx = index } ) + Logger:log("HarpoonList:removeAt", { item = self.items[index], index = index }) table.remove(self.items, index) return self end @@ -140,7 +145,7 @@ function HarpoonList:resolve_displayed(displayed) Listeners.event_names.ADD, { list = self, item = v, idx = i } ) - new_list[i] = self.config.add(self.config, v) + new_list[i] = self.config.create_list_item(self.config, v) else local index_in_new_list = index_of(new_list, self.items[index], self.config) diff --git a/lua/harpoon/test/config_spec.lua b/lua/harpoon/test/config_spec.lua index d22555ad..227b1eb2 100644 --- a/lua/harpoon/test/config_spec.lua +++ b/lua/harpoon/test/config_spec.lua @@ -2,7 +2,7 @@ local Config = require("harpoon.config") local eq = assert.are.same describe("config", function() - it("default.add", function() + it("default.create_list_item", function() local config = Config.get_default_config() local config_item = Config.get_config(config, "foo") @@ -17,7 +17,7 @@ describe("config", function() }) vim.api.nvim_win_set_cursor(0, { 3, 1 }) - local item = config_item.add(config_item) + local item = config_item.create_list_item(config_item) eq(item, { value = "/tmp/harpoon-test", context = { From f4265232bbfeef0095d765a1a85ed997d39b85bb Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 5 Dec 2023 19:18:06 -0700 Subject: [PATCH 76/80] small logger changes + spec --- lua/harpoon/logger.lua | 31 ++++++++++++++++++++++++++++--- lua/harpoon/test/logger_spec.lua | 22 ++++++++++++++++++++++ lua/harpoon/utils.lua | 18 ++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 lua/harpoon/test/logger_spec.lua diff --git a/lua/harpoon/logger.lua b/lua/harpoon/logger.lua index 6fd6f89b..d2576bfd 100644 --- a/lua/harpoon/logger.lua +++ b/lua/harpoon/logger.lua @@ -1,5 +1,8 @@ +local utils = require("harpoon.utils") + ---@class HarpoonLog ---@field lines string[] +---@field enabled boolean not used yet, but if we get reports of slow, we will use this local HarpoonLog = {} HarpoonLog.__index = HarpoonLog @@ -8,20 +11,42 @@ HarpoonLog.__index = HarpoonLog function HarpoonLog:new() local logger = setmetatable({ lines = {}, + enabled = true, }, self) return logger end +function HarpoonLog:disable() + self.enabled = false +end + +function HarpoonLog:enable() + self.enabled = true +end + ---@vararg any function HarpoonLog:log(...) - local msg = {} + local processed = {} for i = 1, select("#", ...) do local item = select(i, ...) - table.insert(msg, vim.inspect(item)) + if type(item) == "table" then + item = vim.inspect(item) + end + table.insert(processed, item) + end + + local lines = {} + for _, line in ipairs(processed) do + local split = utils.split(line, "\n") + for _, l in ipairs(split) do + if not utils.is_white_space(l) then + table.insert(lines, utils.trim(utils.remove_duplicate_whitespace(l))) + end + end end - table.insert(self.lines, table.concat(msg, " ")) + table.insert(self.lines, table.concat(lines, " ")) end function HarpoonLog:clear() diff --git a/lua/harpoon/test/logger_spec.lua b/lua/harpoon/test/logger_spec.lua new file mode 100644 index 00000000..19189ff0 --- /dev/null +++ b/lua/harpoon/test/logger_spec.lua @@ -0,0 +1,22 @@ +local utils = require("harpoon.test.utils") +local Logger = require("harpoon.logger") + +local eq = assert.are.same + +describe("harpoon", function() + before_each(function() + Logger:clear() + end) + + it("new lines are removed. every log call is one line", function() + Logger:log("hello\nworld") + eq(Logger.lines, { "hello world" }) + end) + + it("new lines with vim.inspect get removed too", function() + Logger:log({hello = "world", world = "hello"}) + eq({ "{ hello = \"world\", world = \"hello\" }" }, Logger.lines) + end) + +end) + diff --git a/lua/harpoon/utils.lua b/lua/harpoon/utils.lua index 5660f635..f99df987 100644 --- a/lua/harpoon/utils.lua +++ b/lua/harpoon/utils.lua @@ -1,5 +1,23 @@ local M = {} +function M.trim(str) + return str:gsub("^%s+", ""):gsub("%s+$", "") +end +function M.remove_duplicate_whitespace(str) + return str:gsub("%s+", " ") +end + +function M.split(str, sep) + if sep == nil then + sep = "%s" + end + local t={} + for s in string.gmatch(str, "([^"..sep.."]+)") do + table.insert(t, s) + end + return t +end + function M.is_white_space(str) return str:gsub("%s", "") == "" end From d8fa264598b73e913b7ec13a3c596b2c7ed7a1f9 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Tue, 5 Dec 2023 19:20:38 -0700 Subject: [PATCH 77/80] logger: max lines to prevent things taking too much memory --- lua/harpoon/logger.lua | 10 ++++++++-- lua/harpoon/test/logger_spec.lua | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lua/harpoon/logger.lua b/lua/harpoon/logger.lua index d2576bfd..9fd5fcc8 100644 --- a/lua/harpoon/logger.lua +++ b/lua/harpoon/logger.lua @@ -2,6 +2,7 @@ local utils = require("harpoon.utils") ---@class HarpoonLog ---@field lines string[] +---@field max_lines number ---@field enabled boolean not used yet, but if we get reports of slow, we will use this local HarpoonLog = {} @@ -12,6 +13,7 @@ function HarpoonLog:new() local logger = setmetatable({ lines = {}, enabled = true, + max_lines = 50, }, self) return logger @@ -41,12 +43,17 @@ function HarpoonLog:log(...) local split = utils.split(line, "\n") for _, l in ipairs(split) do if not utils.is_white_space(l) then - table.insert(lines, utils.trim(utils.remove_duplicate_whitespace(l))) + local ll = utils.trim(utils.remove_duplicate_whitespace(l)) + table.insert(lines, ll) end end end table.insert(self.lines, table.concat(lines, " ")) + + while #self.lines > self.max_lines do + table.remove(self.lines, 1) + end end function HarpoonLog:clear() @@ -55,7 +62,6 @@ end function HarpoonLog:show() local bufnr = vim.api.nvim_create_buf(false, true) - print(vim.inspect(self.lines)) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, self.lines) vim.api.nvim_win_set_buf(0, bufnr) end diff --git a/lua/harpoon/test/logger_spec.lua b/lua/harpoon/test/logger_spec.lua index 19189ff0..4d95a49c 100644 --- a/lua/harpoon/test/logger_spec.lua +++ b/lua/harpoon/test/logger_spec.lua @@ -18,5 +18,13 @@ describe("harpoon", function() eq({ "{ hello = \"world\", world = \"hello\" }" }, Logger.lines) end) + it("max lines", function() + Logger.max_lines = 1 + Logger:log("one") + eq({ "one" }, Logger.lines) + Logger:log("two") + eq({ "two" }, Logger.lines) + end) + end) From 05edc3ee827c09dc0f172b538555c0205a60fa1a Mon Sep 17 00:00:00 2001 From: mwishoff Date: Tue, 5 Dec 2023 18:48:18 -0800 Subject: [PATCH 78/80] If out_data is empty then write empty json to harpoon.json. Then read it back into out_data. --- lua/harpoon/data.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/harpoon/data.lua b/lua/harpoon/data.lua index 96535640..c23f40cf 100644 --- a/lua/harpoon/data.lua +++ b/lua/harpoon/data.lua @@ -58,6 +58,12 @@ local function read_data() end local out_data = path:read() + + if not out_data or out_data == '' then + write_data({}) + out_data = path:read() + end + local data = vim.json.decode(out_data) return data end From 07cca27cf14a458c469a759c897124f78d953db0 Mon Sep 17 00:00:00 2001 From: mpaulson Date: Wed, 6 Dec 2023 07:00:46 -0700 Subject: [PATCH 79/80] feat: better logging for toggle --- lua/harpoon/ui.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 66e682cd..4cd598ec 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -107,9 +107,9 @@ end ---@param list? HarpoonList function HarpoonUI:toggle_quick_menu(list) - Logger:log("ui#toggle_quick_menu", list and list.name) if list == nil or self.win_id ~= nil then + Logger:log("ui#toggle_quick_menu#closing", list and list.name) if self.settings.save_on_toggle then self:save() end @@ -117,6 +117,7 @@ function HarpoonUI:toggle_quick_menu(list) return end + Logger:log("ui#toggle_quick_menu#opening", list and list.name) local win_id, bufnr = self:_create_window() self.win_id = win_id From 90a17f14e2e2ca6cfdc64761684abb9d7374dae2 Mon Sep 17 00:00:00 2001 From: tris203 Date: Sat, 9 Dec 2023 17:55:53 +0000 Subject: [PATCH 80/80] feat: select_current option --- README.md | 3 +++ lua/harpoon/buffer.lua | 12 ------------ lua/harpoon/config.lua | 11 +++++------ lua/harpoon/ui.lua | 16 ++++++++++++++++ lua/harpoon/utils.lua | 12 +++++++++--- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d48ceb88..c916f727 100644 --- a/README.md +++ b/README.md @@ -171,12 +171,14 @@ Settings can alter the experience of harpoon ```lua ---@class HarpoonSettings ---@field save_on_toggle boolean defaults to true +---@field select_current boolean defalts to false ---@field key (fun(): string) ``` **Descriptions** * `save_on_toggle`: any time the ui menu is closed then we will sync the state back to the backing list +* `select_current`: when the ui menu is opened, if the current file is present on the list, select it * `border_chars`: the ui's border characters to be displayed * `key` how the out list key is looked up. This can be useful when using worktrees and using git remote instead of file path @@ -184,6 +186,7 @@ Settings can alter the experience of harpoon ```lua settings = { save_on_toggle = false, + select_current = false, border_chars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, key = function() return vim.loop.cwd() diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index fd0e05c9..eab91c51 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -32,18 +32,6 @@ end ---strings back into the ui. it feels gross and it puts odd coupling ---@param bufnr number function M.setup_autocmds_and_keymaps(bufnr) - local curr_file = vim.api.nvim_buf_get_name(0) - local cmd = string.format( - "autocmd Filetype harpoon " - .. "let path = '%s' | call clearmatches() | " - -- move the cursor to the line containing the current filename - .. "call search('\\V'.path.'\\$') | " - -- add a hl group to that line - .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", - curr_file:gsub("\\", "\\\\") - ) - vim.cmd(cmd) - if vim.api.nvim_buf_get_name(bufnr) == "" then vim.api.nvim_buf_set_name(bufnr, get_harpoon_menu_name()) end diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 260f0040..30db8730 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -1,9 +1,5 @@ local Logger = require("harpoon.logger") -local Path = require("plenary.path") -local function normalize_path(buf_name, root) - return Path:new(buf_name):make_relative(root) -end - +local utils = require("harpoon.utils") local M = {} local DEFAULT_LIST = "__harpoon_files" M.DEFAULT_LIST = DEFAULT_LIST @@ -28,11 +24,13 @@ M.DEFAULT_LIST = DEFAULT_LIST ---@field border_chars string[] defaults to { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } ---@field save_on_toggle boolean defaults to true ---@field ui_fallback_width number defaults 69, nice +---@field select_current boolean default to false ---@field ui_width_ratio number defaults to 0.62569 ---@field key (fun(): string) ---@class HarpoonPartialSettings ---@field save_on_toggle? boolean +---@field select_current? boolean ---@field key? (fun(): string) ---@class HarpoonConfig @@ -56,6 +54,7 @@ function M.get_default_config() settings = { save_on_toggle = false, + select_current = false, border_chars = { "─", "│", @@ -156,7 +155,7 @@ function M.get_default_config() -- path, if that is the case we can use the context to -- store the bufname and then have value be the normalized -- value - or normalize_path( + or utils.normalize_path( vim.api.nvim_buf_get_name( vim.api.nvim_get_current_buf() ), diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 4cd598ec..3f5381d5 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -1,6 +1,8 @@ local popup = require("plenary").popup local Buffer = require("harpoon.buffer") local Logger = require("harpoon.logger") +local utils = require("harpoon.utils") + ---@class HarpoonUI ---@field win_id number @@ -107,6 +109,13 @@ end ---@param list? HarpoonList function HarpoonUI:toggle_quick_menu(list) + local currentFileName = nil + if self.settings.select_current then + currentFileName = utils.normalize_path( + vim.api.nvim_buf_get_name( + vim.api.nvim_get_current_buf() + )) + end if list == nil or self.win_id ~= nil then Logger:log("ui#toggle_quick_menu#closing", list and list.name) @@ -126,6 +135,13 @@ function HarpoonUI:toggle_quick_menu(list) local contents = self.active_list:display() vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, contents) + if self.settings.select_current then + for idx, line in ipairs(contents) do + if line == currentFileName then + vim.api.nvim_win_set_cursor(self.win_id, { idx, 0 }) + end + end + end end ---@param options? any diff --git a/lua/harpoon/utils.lua b/lua/harpoon/utils.lua index f99df987..fe6abcd6 100644 --- a/lua/harpoon/utils.lua +++ b/lua/harpoon/utils.lua @@ -1,8 +1,14 @@ +local Path = require("plenary.path") local M = {} +function M.normalize_path(buf_name, root) + return Path:new(buf_name):make_relative(root) +end + function M.trim(str) - return str:gsub("^%s+", ""):gsub("%s+$", "") + return str:gsub("^%s+", ""):gsub("%s+$", "") end + function M.remove_duplicate_whitespace(str) return str:gsub("%s+", " ") end @@ -11,8 +17,8 @@ function M.split(str, sep) if sep == nil then sep = "%s" end - local t={} - for s in string.gmatch(str, "([^"..sep.."]+)") do + local t = {} + for s in string.gmatch(str, "([^" .. sep .. "]+)") do table.insert(t, s) end return t