From a38be6e0dd4c6db66997deab71fc4453ace97f9c Mon Sep 17 00:00:00 2001 From: mpaulson Date: Fri, 26 Jan 2024 14:53:58 -0700 Subject: [PATCH 01/15] feat: small fixes to minimal and Makefile --- Makefile | 2 +- scripts/tests/minimal.vim | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d6b2e5c0..1cb386a8 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ test: nvim --headless --noplugin -u scripts/tests/minimal.vim \ -c "PlenaryBustedDirectory lua/harpoon/test/ {minimal_init = 'scripts/tests/minimal.vim'}" -clean: +clean: echo "===> Cleaning" rm /tmp/lua_* diff --git a/scripts/tests/minimal.vim b/scripts/tests/minimal.vim index 7547e39f..23618bfb 100644 --- a/scripts/tests/minimal.vim +++ b/scripts/tests/minimal.vim @@ -1,3 +1,4 @@ +set noswapfile set rtp+=. set rtp+=../plenary.nvim runtime! plugin/plenary.vim From c6446e971f1a34c46deee1a22d06049ea2de0603 Mon Sep 17 00:00:00 2001 From: abeldekat Date: Sun, 24 Mar 2024 11:26:29 +0100 Subject: [PATCH 02/15] harpoon select: ask for the bufnr using the exact name --- lua/harpoon/config.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index a759297b..19aae3b2 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -4,6 +4,9 @@ local Path = require("plenary.path") local function normalize_path(buf_name, root) return Path:new(buf_name):make_relative(root) end +local function to_exact_name(value) + return "^" .. value .. "$" +end local M = {} local DEFAULT_LIST = "__harpoon_files" @@ -101,11 +104,12 @@ function M.get_default_config() return end - local bufnr = vim.fn.bufnr(list_item.value) + local bufnr = vim.fn.bufnr(to_exact_name(list_item.value)) local set_position = false - if bufnr == -1 then + if bufnr == -1 then -- must create a buffer! set_position = true - bufnr = vim.fn.bufnr(list_item.value, true) + -- bufnr = vim.fn.bufnr(list_item.value, true) + bufnr = vim.fn.bufadd(list_item.value) end if not vim.api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) From 3292a609c6ec8a5133ca43ad17826e5206062110 Mon Sep 17 00:00:00 2001 From: Radvil Date: Tue, 2 Apr 2024 00:18:39 +0800 Subject: [PATCH 03/15] feat(list-api): expose index for later usage - I have a custom append/prepend list where I need to notify the user if current file is already in the list, if it is, I want to hint the use for the exact index, so we can go faster --- lua/harpoon/list.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 07c19f55..2d5afa14 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -126,7 +126,7 @@ function HarpoonList:get_by_display(name) if index == -1 then return nil end - return self.items[index] + return self.items[index], index end --- much inefficiencies. dun care From 4ad05be8fe98092f0dec3bc3b47abebb59c3814a Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Tue, 2 Apr 2024 13:52:28 -0600 Subject: [PATCH 04/15] feat: replace_at with harpoon lists --- README.md | 10 +- lua/harpoon/config.lua | 7 +- lua/harpoon/extensions/init.lua | 1 + lua/harpoon/list.lua | 135 ++++++++++++++--- lua/harpoon/test/harpoon_spec.lua | 10 +- lua/harpoon/test/list_spec.lua | 234 ++++++++++++++++++++++++++++++ lua/harpoon/test/utils.lua | 2 +- lua/harpoon/ui.lua | 10 +- scripts/test.lua | 3 + 9 files changed, 381 insertions(+), 31 deletions(-) create mode 100644 scripts/test.lua diff --git a/README.md b/README.md index f2cd862a..44e8ebd6 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ local harpoon = require("harpoon") harpoon:setup() -- REQUIRED -vim.keymap.set("n", "a", function() harpoon:list():append() end) +vim.keymap.set("n", "a", function() harpoon:list():add() end) vim.keymap.set("n", "", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end) vim.keymap.set("n", "", function() harpoon:list():select(1) end) @@ -89,7 +89,7 @@ vim.keymap.set("n", "", function() harpoon:list():next() end) ### Telescope -In order to use [Telescope](https://github.com/nvim-telescope/telescope.nvim) as a UI, +In order to use [Telescope](https://github.com/nvim-telescope/telescope.nvim) as a UI, make sure to add `telescope` to your dependencies and paste this following snippet into your configuration. ```lua @@ -137,7 +137,7 @@ harpoon:setup({ -- Setting up custom behavior for a list named "cmd" "cmd" = { - -- When you call list:append() this function is called and the return + -- When you call list:add() 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 @@ -204,7 +204,7 @@ There is quite a bit of behavior you can configure via `harpoon:setup()` * `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 -* `create_list_item`: called when `list:append()` or `list:prepend()` is called. called with an item, which will be a string, when adding through the ui menu +* `create_list_item`: called when `list:add()` 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()` @@ -287,7 +287,7 @@ 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 -**Running Tests** +**Running Tests** To run the tests make sure [plenary](https://github.com/nvim-lua/plenary.nvim) is checked out in the parent directory of *this* repository, then run `make test`. ## ⇁ Social diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index a759297b..fa146102 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -139,6 +139,11 @@ function M.get_default_config() ---@param list_item_a HarpoonListItem ---@param list_item_b HarpoonListItem equals = function(list_item_a, list_item_b) + if list_item_a == nil and list_item_b == nil then + return true + elseif list_item_a == nil or list_item_b == nil then + return false + end return list_item_a.value == list_item_b.value end, @@ -208,7 +213,7 @@ function M.get_default_config() } end ----@param partial_config HarpoonPartialConfig +---@param partial_config HarpoonPartialConfig? ---@param latest_config HarpoonConfig? ---@return HarpoonConfig function M.merge_config(partial_config, latest_config) diff --git a/lua/harpoon/extensions/init.lua b/lua/harpoon/extensions/init.lua index 2cbbae4e..d673f0d1 100644 --- a/lua/harpoon/extensions/init.lua +++ b/lua/harpoon/extensions/init.lua @@ -67,6 +67,7 @@ return { builtins = Builtins, extensions = extensions, event_names = { + REPLACE = "REPLACE", ADD = "ADD", SELECT = "SELECT", REMOVE = "REMOVE", diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 07c19f55..bf8008ac 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -1,9 +1,34 @@ local Logger = require("harpoon.logger") local Extensions = require("harpoon.extensions") +local function guess_length(arr) + local last_known = #arr + for i = 1, 20 do + if arr[i] ~= nil and last_known < i then + last_known = i + end + end + + return last_known +end + +local function determine_length(arr, previous_length) + local idx = previous_length + for i = previous_length, 1, -1 do + if arr[i] ~= nil then + idx = i + break + end + end + return idx +end + --- @class HarpoonNavOptions --- @field ui_nav_wrap? boolean +---@param items any[] +---@param element any +---@param config HarpoonPartialConfigItem? local function index_of(items, element, config) local equals = config and config.equals or function(a, b) @@ -20,6 +45,24 @@ local function index_of(items, element, config) return index end +---@param arr any[] +---@param value any +---@return number +local function prepend_to_array(arr, value) + local idx = 1 + local prev = value + while true do + local curr = arr[idx] + arr[idx] = prev + if curr == nil then + break + end + prev = curr + idx = idx + 1 + end + return idx +end + --- @class HarpoonItem --- @field value string --- @field context any @@ -27,15 +70,18 @@ end --- @class HarpoonList --- @field config HarpoonPartialConfigItem --- @field name string +--- @field _length number --- @field _index number --- @field items HarpoonItem[] local HarpoonList = {} HarpoonList.__index = HarpoonList function HarpoonList:new(config, name, items) + items = items or {} return setmetatable({ items = items, config = config, + _length = guess_length(items), name = name, _index = 1, }, self) @@ -43,25 +89,60 @@ end ---@return number function HarpoonList:length() - return #self.items + return self._length end function HarpoonList:clear() self.items = {} + self._length = 0 end +---@param item? HarpoonListItem ---@return HarpoonList function HarpoonList:append(item) + print("APPEND IS DEPRICATED -- PLEASE USE `add`") + return self:add(item) +end + +---@param idx number +---@param item? HarpoonListItem +function HarpoonList:replace_at(idx, item) + item = item or self.config.create_list_item(self.config) + Extensions.extensions:emit( + Extensions.event_names.REPLACE, + { list = self, item = item, idx = idx } + ) + self.items[idx] = item + if idx > self._length then + self._length = idx + end +end + +---@param item? HarpoonListItem +function HarpoonList:add(item) 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 }) + Logger:log("HarpoonList:add", { item = item, index = index }) + if index == -1 then + local idx = self._length + 1 + for i = 1, self._length + 1 do + if self.items[i] == nil then + idx = i + break + end + end + Extensions.extensions:emit( Extensions.event_names.ADD, - { list = self, item = item, idx = #self.items + 1 } + { list = self, item = item, idx = idx } ) - table.insert(self.items, item) + + self.items[idx] = item + if idx > self._length then + self._length = idx + end end return self @@ -77,7 +158,10 @@ function HarpoonList:prepend(item) Extensions.event_names.ADD, { list = self, item = item, idx = 1 } ) - table.insert(self.items, 1, item) + local stop_idx = prepend_to_array(self.items, item) + if stop_idx > self._length then + self._length = stop_idx + end end return self @@ -86,14 +170,18 @@ end ---@return HarpoonList function HarpoonList:remove(item) item = item or self.config.create_list_item(self.config) - for i, v in ipairs(self.items) do + for i = 1, self._length do + local v = self.items[i] if self.config.equals(v, item) then Extensions.extensions:emit( Extensions.event_names.REMOVE, { list = self, item = item, idx = i } ) Logger:log("HarpoonList:remove", { item = item, index = i }) - table.remove(self.items, i) + self.items[i] = nil + if i == self._length then + self._length = determine_length(self.items, self._length) + end break end end @@ -101,7 +189,7 @@ function HarpoonList:remove(item) end ---@return HarpoonList -function HarpoonList:removeAt(index) +function HarpoonList:remove_at(index) if self.items[index] then Logger:log( "HarpoonList:removeAt", @@ -111,7 +199,10 @@ function HarpoonList:removeAt(index) Extensions.event_names.REMOVE, { list = self, item = self.items[index], idx = index } ) - table.remove(self.items, index) + self.items[index] = nil + if index == self._length then + self._length = determine_length(self.items, self._length) + end end return self end @@ -122,7 +213,7 @@ end function HarpoonList:get_by_display(name) local displayed = self:display() - local index = index_of(displayed, name) + local index = index_of(displayed, name, self.config) if index == -1 then return nil end @@ -131,12 +222,14 @@ end --- much inefficiencies. dun care ---@param displayed string[] -function HarpoonList:resolve_displayed(displayed) +---@param length number +function HarpoonList:resolve_displayed(displayed, length) local new_list = {} local list_displayed = self:display() - for i, v in ipairs(list_displayed) do + for i = 1, self._length do + local v = self.items[i] local index = index_of(displayed, v) if index == -1 then Extensions.extensions:emit( @@ -146,9 +239,12 @@ function HarpoonList:resolve_displayed(displayed) end end - for i, v in ipairs(displayed) do + for i = 1, length do + local v = displayed[i] local index = index_of(list_displayed, v) - if index == -1 then + if v == "" then + new_list[i] = nil + elseif index == -1 then new_list[i] = self.config.create_list_item(self.config, v) Extensions.extensions:emit( Extensions.event_names.ADD, @@ -163,6 +259,7 @@ function HarpoonList:resolve_displayed(displayed) end 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 @@ -170,6 +267,7 @@ function HarpoonList:resolve_displayed(displayed) end self.items = new_list + self._length = length end function HarpoonList:select(index, options) @@ -189,11 +287,11 @@ function HarpoonList:next(opts) opts = opts or {} self._index = self._index + 1 - if self._index > #self.items then + if self._index > self._length then if opts.ui_nav_wrap then self._index = 1 else - self._index = #self.items + self._index = self._length end end @@ -220,8 +318,9 @@ end --- @return string[] function HarpoonList:display() local out = {} - for _, v in ipairs(self.items) do - table.insert(out, self.config.display(v)) + for i = 1, self._length do + local v = self.items[i] + out[i] = v == nil and "" or self.config.display(v) end return out diff --git a/lua/harpoon/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua index d1202047..f172e052 100644 --- a/lua/harpoon/test/harpoon_spec.lua +++ b/lua/harpoon/test/harpoon_spec.lua @@ -24,7 +24,7 @@ describe("harpoon", function() "qux", }, row, col) - local list = harpoon:list():append() + local list = harpoon:list():add() local other_buf = utils.create_file("other-file", { "foo", "bar", @@ -56,7 +56,7 @@ describe("harpoon", function() }, row, col) local list = harpoon:list() - list:append() + list:add() harpoon:sync() eq(harpoon:dump(), { @@ -66,7 +66,7 @@ describe("harpoon", function() }) end) - it("prepend/append double add", function() + it("prepend/add double add", function() local default_list_name = harpoon:info().default_list_name local file_name_1 = "/tmp/harpoon-test" local row_1 = 3 @@ -79,7 +79,7 @@ describe("harpoon", function() 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() + local list = harpoon:list():add() utils.create_file(file_name_2, contents, row_2, col_2) harpoon:list():prepend() @@ -97,7 +97,7 @@ describe("harpoon", function() { value = file_name_1, context = { row = row_1, col = col_1 } }, }) - harpoon:list():append() + harpoon:list():add() vim.api.nvim_set_current_buf(bufnr_1) harpoon:list():prepend() diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua index eaaed225..9bd8c9ca 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon/test/list_spec.lua @@ -60,4 +60,238 @@ describe("list", function() eq({ nil, {} }, foo_selected) eq(nil, bar_selected) end) + + it("add", function() + local config = Config.merge_config({ + foo = { + equals = function(a, b) + return a == b + end, + }, + }) + + local c = Config.get_config(config, "foo") + local list = List:new(c, "foo", { + nil, + nil, + { three = true }, + { four = true }, + }) + + eq(list.items, { + nil, + nil, + { three = true }, + { four = true }, + }) + eq(list:length(), 4) + + local one = { one = true } + list:add(one) + eq(list.items, { + { one = true }, + nil, + { three = true }, + { four = true }, + }) + eq(list:length(), 4) + + local two = { two = true } + list:add(two) + eq(list.items, { + { one = true }, + { two = true }, + { three = true }, + { four = true }, + }) + eq(list:length(), 4) + + list:add({ five = true }) + eq(list.items, { + { one = true }, + { two = true }, + { three = true }, + { four = true }, + { five = true }, + }) + eq(list:length(), 5) + end) + + it("prepend", function() + local config = Config.merge_config({ + foo = { + equals = function(a, b) + return a == b + end, + }, + }) + + local c = Config.get_config(config, "foo") + local list = List:new(c, "foo", { + { three = true }, + nil, + nil, + { four = true }, + }) + + eq(list.items, { + { three = true }, + nil, + nil, + { four = true }, + }) + eq(list:length(), 4) + + local one = { one = true } + list:prepend(one) + eq(list.items, { + { one = true }, + { three = true }, + nil, + { four = true }, + }) + eq(list:length(), 4) + + local two = { two = true } + list:prepend(two) + eq(list.items, { + { two = true }, + { one = true }, + { three = true }, + { four = true }, + }) + eq(list:length(), 4) + + list:prepend({ five = true }) + eq(list.items, { + { five = true }, + { two = true }, + { one = true }, + { three = true }, + { four = true }, + }) + eq(list:length(), 5) + end) + + it("remove", function() + local config = Config.merge_config() + local c = Config.get_config(config, "foo") + local list = List:new(c, "foo", { + { value = "one" }, + nil, + { value = "three" }, + { value = "four" }, + }) + + eq(4, list:length()) + list:remove({ value = "three" }) + eq(4, list:length()) + list:remove({ value = "four" }) + eq(1, list:length()) + eq({ + { value = "one" }, + }, list.items) + end) + + it("remove_at", function() + local config = Config.merge_config() + local c = Config.get_config(config, "foo") + local list = List:new(c, "foo", { + { value = "one" }, + nil, + { value = "three" }, + { value = "four" }, + }) + + eq(4, list:length()) + list:remove_at(3) + + eq(4, list:length()) + eq({ + { value = "one" }, + nil, + nil, + { value = "four" }, + }, list.items) + + list:remove_at(4) + eq(1, list:length()) + eq({ + { value = "one" }, + }, list.items) + end) + + it("replace_at", function() + local config = Config.merge_config() + local c = Config.get_config(config, "foo") + local list = List:new(c, "foo") + + list:replace_at(3, { value = "threethree" }) + eq(3, list:length()) + eq({ + nil, + nil, + { value = "threethree" }, + }, list.items) + + list:replace_at(4, { value = "four" }) + eq(4, list:length()) + eq({ + nil, + nil, + { value = "threethree" }, + { value = "four" }, + }, list.items) + + list:replace_at(1, { value = "one" }) + eq(4, list:length()) + eq({ + { value = "one" }, + nil, + { value = "threethree" }, + { value = "four" }, + }, list.items) + end) + + it("resolve_displayed", function() + local config = Config.merge_config() + local c = Config.get_config(config, "foo") + local list = List:new(c, "foo", { + nil, + nil, + { value = "threethree" }, + }) + + eq(3, list:length()) + + list:resolve_displayed({ + "", + "", + "", + "threethree", + }, 4) + + eq(4, list:length()) + eq({ + nil, + nil, + nil, + { value = "threethree" }, + }, list.items) + + list:resolve_displayed({ + "oneone", + "", + "", + "threethree", + }, 4) + + eq(4, list:length()) + eq({ + { value = "oneone", context = { row = 1, col = 0 } }, + nil, + nil, + { value = "threethree" }, + }, list.items) + end) end) diff --git a/lua/harpoon/test/utils.lua b/lua/harpoon/test/utils.lua index b83b8240..97192072 100644 --- a/lua/harpoon/test/utils.lua +++ b/lua/harpoon/test/utils.lua @@ -77,7 +77,7 @@ function M.fill_list_with_files(count, list) local name = os.tmpname() table.insert(files, name) M.create_file(name, { "test" }) - list:append() + list:add() end return files diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index d04441f8..30ddfe65 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -185,8 +185,16 @@ end function HarpoonUI:save() local list = Buffer.get_contents(self.bufnr) + local length = #list + for i, v in ipairs(list) do + if v == "" then + list[i] = nil + end + end + Logger:log("ui#save", list) - self.active_list:resolve_displayed(list) + print("saving", vim.inspect(list)) + self.active_list:resolve_displayed(list, length) if self.settings.sync_on_ui_close then require("harpoon"):sync() end diff --git a/scripts/test.lua b/scripts/test.lua new file mode 100644 index 00000000..d935addb --- /dev/null +++ b/scripts/test.lua @@ -0,0 +1,3 @@ +local a = {} +a[3] = "foo" +print(#a) From e76cb03c420bb74a5900a5b3e1dde776156af45f Mon Sep 17 00:00:00 2001 From: Christopher Lane Date: Tue, 2 Apr 2024 21:03:14 -0400 Subject: [PATCH 05/15] Fix error on item select: Missing length argument Addresses the following error resulting from not passing `length` param on line 170: ``` E5108: Error executing lua: ...kaze/.local/share/nvim/lazy/harpoon/lua/harpoon/list.lua:242: 'for' limit must be a number stack traceback: ...kaze/.local/share/nvim/lazy/harpoon/lua/harpoon/list.lua:242: in function 'resolve_displayed' ...makaze/.local/share/nvim/lazy/harpoon/lua/harpoon/ui.lua:170: in function 'select_menu_item' ...ze/.local/share/nvim/lazy/harpoon/lua/harpoon/buffer.lua:21: in function 'run_select_command' ...ze/.local/share/nvim/lazy/harpoon/lua/harpoon/buffer.lua:61: in function <...ze/.local/share/nvim/lazy/harpoon/lua/harpoon/buffer.lua:60> ``` --- 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 30ddfe65..900b3149 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -167,7 +167,8 @@ function HarpoonUI:select_menu_item(options) -- 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) + local length = #list + self.active_list:resolve_displayed(list, length) Logger:log( "ui#select_menu_item selecting item", From 3e32576076a7897eb806f1437cac928ac60d47c3 Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Tue, 2 Apr 2024 15:32:03 -0600 Subject: [PATCH 06/15] feat: hashing on a per config.settings.key() basis --- lua/harpoon/config.lua | 15 ++++++ lua/harpoon/data.lua | 83 +++++++++++++++---------------- lua/harpoon/extensions/init.lua | 8 +++ lua/harpoon/init.lua | 34 +++++++++---- lua/harpoon/list.lua | 59 +++++++++++----------- lua/harpoon/test/harpoon_spec.lua | 77 ++++++++++++++++++---------- lua/harpoon/test/utils.lua | 16 ++++-- lua/harpoon/ui.lua | 1 - 8 files changed, 178 insertions(+), 115 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index fa146102..596d2983 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -57,6 +57,7 @@ function M.get_default_config() settings = { save_on_toggle = false, sync_on_ui_close = false, + key = function() return vim.loop.cwd() end, @@ -205,6 +206,11 @@ function M.get_default_config() item.context.row = pos[1] item.context.col = pos[2] + + Extensions.extensions:emit( + Extensions.event_names.POSITION_UPDATED, + item + ) end end, @@ -231,4 +237,13 @@ function M.merge_config(partial_config, latest_config) return config end +---@param settings HarpoonPartialSettings +function M.create_config(settings) + local config = M.get_default_config() + for k, v in ipairs(settings) do + config.settings[k] = v + end + return config +end + return M diff --git a/lua/harpoon/data.lua b/lua/harpoon/data.lua index 2efd1931..1177fae2 100644 --- a/lua/harpoon/data.lua +++ b/lua/harpoon/data.lua @@ -1,43 +1,44 @@ local Path = require("plenary.path") local data_path = vim.fn.stdpath("data") -local full_data_path = string.format("%s/harpoon.json", data_path) + +---@param config HarpoonConfig +local filename = function(config) + return config.settings.key() +end + +local function hash(path) + return vim.fn.sha256(path) +end + +---@param config HarpoonConfig +local function fullpath(config) + local h = hash(filename(config)) + return string.format("%s/%s.json", data_path, h) +end ---@param data any -local function write_data(data) - Path:new(full_data_path):write(vim.json.encode(data), "w") +---@param config HarpoonConfig +local function write_data(data, config) + Path:new(fullpath(config)):write(vim.json.encode(data), "w") end local M = {} -function M.__dangerously_clear_data() - write_data({}) +---@param config HarpoonConfig +function M.__dangerously_clear_data(config) + write_data({}, config) 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 - -local function has_keys(t) - -- luacheck: ignore 512 - for _ in pairs(t) do - return true - end - - return false -end - --- @alias HarpoonRawData {[string]: {[string]: string[]}} --- @class HarpoonData ---- @field seen {[string]: {[string]: boolean}} --- @field _data HarpoonRawData --- @field has_error boolean local Data = {} @@ -48,34 +49,37 @@ local Data = {} Data.__index = Data +---@param config HarpoonConfig +---@param provided_path string? ---@return HarpoonRawData -local function read_data() - local path = Path:new(full_data_path) +local function read_data(config, provided_path) + provided_path = provided_path or fullpath(config) + local path = Path:new(provided_path) local exists = path:exists() if not exists then - write_data({}) + write_data({}, config) end local out_data = path:read() if not out_data or out_data == "" then - write_data({}) - out_data = path:read() + write_data({}, config) + out_data = "{}" end local data = vim.json.decode(out_data) return data end +---@param config HarpoonConfig ---@return HarpoonData -function Data:new() - local ok, data = pcall(read_data) +function Data:new(config) + local ok, data = pcall(read_data, config) return setmetatable({ _data = data, has_error = not ok, - seen = {}, }, self) end @@ -100,12 +104,6 @@ function Data:data(key, name) ) end - if not self.seen[key] then - self.seen[key] = {} - end - - self.seen[key][name] = true - return self:_get_data(key, name) end @@ -126,10 +124,6 @@ function Data:sync() 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") @@ -139,13 +133,16 @@ function Data:sync() data[k] = v end - ok = pcall(write_data, data) - - if ok then - self.seen = {} - end + pcall(write_data, data) end M.Data = Data +M.test = { + set_fullpath = function(fp) + fullpath = fp + end, + + read_data = read_data, +} return M diff --git a/lua/harpoon/extensions/init.lua b/lua/harpoon/extensions/init.lua index d673f0d1..a6638a4a 100644 --- a/lua/harpoon/extensions/init.lua +++ b/lua/harpoon/extensions/init.lua @@ -12,6 +12,7 @@ local HarpoonExtensions = {} ---@field LIST_CREATED? fun(...): nil ---@field LIST_READ? fun(...): nil ---@field NAVIGATE? fun(...): nil +---@field POSITION_UPDATED? fun(...): nil HarpoonExtensions.__index = HarpoonExtensions @@ -71,6 +72,13 @@ return { ADD = "ADD", SELECT = "SELECT", REMOVE = "REMOVE", + POSITION_UPDATED = "POSITION_UPDATED", + + --- This exists because the ui can change the list in dramatic ways + --- so instead of emitting a REMOVE, then an ADD, then a REORDER, we + --- instead just emit LIST_CHANGE + LIST_CHANGE = "LIST_CHANGE", + REORDER = "REORDER", UI_CREATE = "UI_CREATE", SETUP_CALLED = "SETUP_CALLED", diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index a323f626..7482d11f 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -18,19 +18,37 @@ local Harpoon = {} Harpoon.__index = Harpoon +---@param harpoon Harpoon +local function sync_on_change(harpoon) + local function sync(_) + return function() + harpoon:sync() + end + end + + Extensions.extensions:add_listener({ + ADD = sync("ADD"), + REMOVE = sync("REMOVE"), + REORDER = sync("REORDER"), + LIST_CHANGE = sync("LIST_CHANGE"), + POSITION_UPDATED = sync("POSITION_UPDATED"), + }) +end + ---@return Harpoon function Harpoon:new() local config = Config.get_default_config() local harpoon = setmetatable({ config = config, - data = Data.Data:new(), + data = Data.Data:new(config), logger = Log, ui = Ui:new(config.settings), _extensions = Extensions.extensions, lists = {}, hooks_setup = false, }, self) + sync_on_change(harpoon) return harpoon end @@ -51,10 +69,6 @@ function Harpoon:list(name) local existing_list = lists[name] if existing_list then - if not self.data.seen[key] then - self.data.seen[key] = {} - end - self.data.seen[key][name] = true self._extensions:emit(Extensions.event_names.LIST_READ, existing_list) return existing_list end @@ -72,16 +86,14 @@ end ---@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 + if not lists then return end - for list_name, _ in pairs(seen) do - local list_config = Config.get_config(self.config, list_name) - cb(lists[list_name], list_config, list_name) + for name, list in pairs(lists) do + local list_config = Config.get_config(self.config, name) + cb(list, list_config, name) end end diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index bf8008ac..df594662 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -134,15 +134,15 @@ function HarpoonList:add(item) end end - Extensions.extensions:emit( - Extensions.event_names.ADD, - { list = self, item = item, idx = idx } - ) - self.items[idx] = item if idx > self._length then self._length = idx end + + Extensions.extensions:emit( + Extensions.event_names.ADD, + { list = self, item = item, idx = idx } + ) end return self @@ -154,14 +154,15 @@ function HarpoonList:prepend(item) local index = index_of(self.items, item, self.config) Logger:log("HarpoonList:prepend", { item = item, index = index }) if index == -1 then - Extensions.extensions:emit( - Extensions.event_names.ADD, - { list = self, item = item, idx = 1 } - ) local stop_idx = prepend_to_array(self.items, item) if stop_idx > self._length then self._length = stop_idx end + + Extensions.extensions:emit( + Extensions.event_names.ADD, + { list = self, item = item, idx = 1 } + ) end return self @@ -173,15 +174,15 @@ function HarpoonList:remove(item) for i = 1, self._length do local v = self.items[i] if self.config.equals(v, item) then - Extensions.extensions:emit( - Extensions.event_names.REMOVE, - { list = self, item = item, idx = i } - ) Logger:log("HarpoonList:remove", { item = item, index = i }) self.items[i] = nil if i == self._length then self._length = determine_length(self.items, self._length) end + Extensions.extensions:emit( + Extensions.event_names.REMOVE, + { list = self, item = item, idx = i } + ) break end end @@ -195,14 +196,14 @@ function HarpoonList:remove_at(index) "HarpoonList:removeAt", { item = self.items[index], index = index } ) - Extensions.extensions:emit( - Extensions.event_names.REMOVE, - { list = self, item = self.items[index], idx = index } - ) self.items[index] = nil if index == self._length then self._length = determine_length(self.items, self._length) end + Extensions.extensions:emit( + Extensions.event_names.REMOVE, + { list = self, item = self.items[index], idx = index } + ) end return self end @@ -228,14 +229,12 @@ function HarpoonList:resolve_displayed(displayed, length) local list_displayed = self:display() + local change = 0 for i = 1, self._length do local v = self.items[i] local index = index_of(displayed, v) if index == -1 then - Extensions.extensions:emit( - Extensions.event_names.REMOVE, - { list = self, item = self.items[i], idx = i } - ) + change = change + 1 end end @@ -246,28 +245,26 @@ function HarpoonList:resolve_displayed(displayed, length) new_list[i] = nil elseif index == -1 then new_list[i] = self.config.create_list_item(self.config, v) - Extensions.extensions:emit( - Extensions.event_names.ADD, - { list = self, item = new_list[i], idx = i } - ) + change = change + 1 else - if index ~= i then - Extensions.extensions:emit( - Extensions.event_names.REORDER, - { list = self, item = self.items[index], idx = i } - ) - end 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 + + if index ~= i then + change = change + 1 + end end end self.items = new_list self._length = length + if change > 0 then + Extensions.extensions:emit(Extensions.event_names.LIST_CHANGE) + end end function HarpoonList:select(index, options) diff --git a/lua/harpoon/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua index f172e052..0b0e4640 100644 --- a/lua/harpoon/test/harpoon_spec.lua +++ b/lua/harpoon/test/harpoon_spec.lua @@ -2,11 +2,23 @@ local utils = require("harpoon.test.utils") local harpoon = require("harpoon") local Extensions = require("harpoon.extensions") local Config = require("harpoon.config") +local Data = require("harpoon.data") +local List = require("harpoon.list") local eq = assert.are.same - +local config = Config.get_default_config() local be = utils.before_each(os.tmpname()) +local function expect_data(data) + local read_data = Data.test.read_data(config) + local testies = read_data.testies + + for k, v in pairs(data) do + local list = List.decode(Config.get_config(config, k), k, testies[k]) + eq(v, list.items) + end +end + describe("harpoon", function() before_each(function() be() @@ -24,7 +36,8 @@ describe("harpoon", function() "qux", }, row, col) - local list = harpoon:list():add() + harpoon:setup() + harpoon:list():add() local other_buf = utils.create_file("other-file", { "foo", "bar", @@ -36,11 +49,17 @@ describe("harpoon", function() 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(expected, list.items) + expect_data({ + [Config.DEFAULT_LIST] = { + { + context = { + col = 0, + row = 2, + }, + value = "/tmp/harpoon-test", + }, + }, + }) end) it("full harpoon add sync cycle", function() @@ -67,7 +86,6 @@ describe("harpoon", function() end) it("prepend/add double add", function() - local default_list_name = harpoon:info().default_list_name local file_name_1 = "/tmp/harpoon-test" local row_1 = 3 local col_1 = 1 @@ -79,31 +97,38 @@ describe("harpoon", function() local contents = { "foo", "bar", "baz", "qux" } local bufnr_1 = utils.create_file(file_name_1, contents, row_1, col_1) - local list = harpoon:list():add() + harpoon:list():add() + + expect_data({ + [Config.DEFAULT_LIST] = { + { value = file_name_1, context = { row = row_1, col = col_1 } }, + }, + }) utils.create_file(file_name_2, contents, row_2, col_2) harpoon:list():prepend() - - harpoon:sync() - - eq(harpoon:dump(), { - testies = { - [default_list_name] = list:encode(), + expect_data({ + [Config.DEFAULT_LIST] = { + { value = file_name_2, context = { row = row_2, col = col_2 } }, + { value = file_name_1, context = { row = row_1, col = col_1 } }, }, }) - 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():add() + expect_data({ + [Config.DEFAULT_LIST] = { + { value = file_name_2, context = { row = row_2, col = col_2 } }, + { value = file_name_1, context = { row = row_1, col = col_1 } }, + }, }) - harpoon:list():add() 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 } }, + expect_data({ + [Config.DEFAULT_LIST] = { + { value = file_name_2, context = { row = row_2, col = col_2 } }, + { value = file_name_1, context = { row = row_1, col = col_1 } }, + }, }) end) @@ -111,7 +136,7 @@ describe("harpoon", function() local list_created = false local list_name = "" local setup = false - local config = {} + local ext_config = {} harpoon:extend({ [Extensions.event_names.LIST_CREATED] = function(list) @@ -120,7 +145,7 @@ describe("harpoon", function() end, [Extensions.event_names.SETUP_CALLED] = function(c) setup = true - config = c + ext_config = c end, }) @@ -130,7 +155,7 @@ describe("harpoon", function() harpoon:list() eq(true, setup) - eq({}, config.foo) + eq({}, ext_config.foo) eq(true, list_created) eq(Config.DEFAULT_LIST, list_name) diff --git a/lua/harpoon/test/utils.lua b/lua/harpoon/test/utils.lua index 97192072..75f0a5b0 100644 --- a/lua/harpoon/test/utils.lua +++ b/lua/harpoon/test/utils.lua @@ -1,4 +1,5 @@ local Data = require("harpoon.data") +local Config = require("harpoon.config") local M = {} @@ -20,15 +21,24 @@ function M.return_to_checkpoint() M.clean_files() end +local function fullpath(name) + return function() + return name + end +end + ---@param name string function M.before_each(name) + local set_fullpath = fullpath(name) + local config = Config.get_default_config() return function() - Data.set_data_path(name) - Data.__dangerously_clear_data() + Data.test.set_fullpath(set_fullpath) + --- we don't use the config + Data.__dangerously_clear_data(config) require("plenary.reload").reload_module("harpoon") Data = require("harpoon.data") - Data.set_data_path(name) + Data.test.set_fullpath(set_fullpath) local harpoon = require("harpoon") M.return_to_checkpoint() diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 30ddfe65..a6492919 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -193,7 +193,6 @@ function HarpoonUI:save() end Logger:log("ui#save", list) - print("saving", vim.inspect(list)) self.active_list:resolve_displayed(list, length) if self.settings.sync_on_ui_close then require("harpoon"):sync() From 527686e06de614364d4ceb675b9e882d5f7db379 Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Wed, 3 Apr 2024 15:39:53 -0600 Subject: [PATCH 07/15] fix: emergency fix --- lua/harpoon/data.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/harpoon/data.lua b/lua/harpoon/data.lua index 1177fae2..048313f9 100644 --- a/lua/harpoon/data.lua +++ b/lua/harpoon/data.lua @@ -41,6 +41,7 @@ end --- @class HarpoonData --- @field _data HarpoonRawData --- @field has_error boolean +--- @field config HarpoonConfig local Data = {} -- 1. load the data @@ -80,6 +81,7 @@ function Data:new(config) return setmetatable({ _data = data, has_error = not ok, + config = config, }, self) end @@ -124,7 +126,7 @@ function Data:sync() return end - local ok, data = pcall(read_data) + local ok, data = pcall(read_data, self.config) if not ok then error("Harpoon: unable to sync data, error reading data file") end @@ -133,7 +135,7 @@ function Data:sync() data[k] = v end - pcall(write_data, data) + pcall(write_data, data, self.config) end M.Data = Data From 886ebae02dc2b7219bef0824599f0ed4f9a3efca Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Wed, 3 Apr 2024 15:59:11 -0600 Subject: [PATCH 08/15] feat: move everything into the harpoon directory --- lua/harpoon/data.lua | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lua/harpoon/data.lua b/lua/harpoon/data.lua index 048313f9..fabcc123 100644 --- a/lua/harpoon/data.lua +++ b/lua/harpoon/data.lua @@ -1,6 +1,18 @@ local Path = require("plenary.path") -local data_path = vim.fn.stdpath("data") +local data_path = string.format("%s/harpoon", vim.fn.stdpath("data")) +local ensured_data_path = false +local function ensure_data_path() + if ensured_data_path then + return + end + + local path = Path:new(data_path) + if not path:exists() then + path:mkdir() + end + ensured_data_path = true +end ---@param config HarpoonConfig local filename = function(config) @@ -54,6 +66,8 @@ Data.__index = Data ---@param provided_path string? ---@return HarpoonRawData local function read_data(config, provided_path) + ensure_data_path() + provided_path = provided_path or fullpath(config) local path = Path:new(provided_path) local exists = path:exists() From 0d959f34c04fff2b16849e72a409df976dc823eb Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Thu, 4 Apr 2024 09:30:55 -0600 Subject: [PATCH 09/15] fix: replace_at causes duplicates --- lua/harpoon/config.lua | 1 + lua/harpoon/list.lua | 37 ++++++++++++++++++++++------------ lua/harpoon/test/list_spec.lua | 20 ++++++++++++++++++ 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index b1b9ca15..6f58bde3 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -149,6 +149,7 @@ function M.get_default_config() elseif list_item_a == nil or list_item_b == nil then return false end + return list_item_a.value == list_item_b.value end, diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index c44c1576..1b2554a2 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -29,13 +29,14 @@ end ---@param items any[] ---@param element any ---@param config HarpoonPartialConfigItem? -local function index_of(items, element, config) +local function index_of(items, length, 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 + for i = 1, length do + local item = items[i] if equals(element, item) then index = i break @@ -81,8 +82,8 @@ function HarpoonList:new(config, name, items) return setmetatable({ items = items, config = config, - _length = guess_length(items), name = name, + _length = guess_length(items), _index = 1, }, self) end @@ -108,21 +109,31 @@ end ---@param item? HarpoonListItem function HarpoonList:replace_at(idx, item) item = item or self.config.create_list_item(self.config) - Extensions.extensions:emit( - Extensions.event_names.REPLACE, - { list = self, item = item, idx = idx } - ) + local current_idx = index_of(self.items, self._length, item, self.config) + self.items[idx] = item + + if current_idx ~= idx then + self.items[current_idx] = nil + end + if idx > self._length then self._length = idx + else + self._length = determine_length(self.items, self._length) end + + Extensions.extensions:emit( + Extensions.event_names.REPLACE, + { list = self, item = item, idx = idx } + ) end ---@param item? HarpoonListItem function HarpoonList:add(item) item = item or self.config.create_list_item(self.config) - local index = index_of(self.items, item, self.config) + local index = index_of(self.items, self._length, item, self.config) Logger:log("HarpoonList:add", { item = item, index = index }) if index == -1 then @@ -151,7 +162,7 @@ end ---@return HarpoonList function HarpoonList:prepend(item) item = item or self.config.create_list_item(self.config) - local index = index_of(self.items, item, self.config) + local index = index_of(self.items, self._length, item, self.config) Logger:log("HarpoonList:prepend", { item = item, index = index }) if index == -1 then local stop_idx = prepend_to_array(self.items, item) @@ -214,7 +225,7 @@ end function HarpoonList:get_by_display(name) local displayed = self:display() - local index = index_of(displayed, name, self.config) + local index = index_of(displayed, #displayed, name, self.config) if index == -1 then return nil end @@ -232,7 +243,7 @@ function HarpoonList:resolve_displayed(displayed, length) local change = 0 for i = 1, self._length do local v = self.items[i] - local index = index_of(displayed, v) + local index = index_of(displayed, self._length, v) if index == -1 then change = change + 1 end @@ -240,7 +251,7 @@ function HarpoonList:resolve_displayed(displayed, length) for i = 1, length do local v = displayed[i] - local index = index_of(list_displayed, v) + local index = index_of(list_displayed, self._length, v) if v == "" then new_list[i] = nil elseif index == -1 then @@ -248,7 +259,7 @@ function HarpoonList:resolve_displayed(displayed, length) change = change + 1 else local index_in_new_list = - index_of(new_list, self.items[index], self.config) + index_of(new_list, length, self.items[index], self.config) if index_in_new_list == -1 then new_list[i] = self.items[index] diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua index 9bd8c9ca..505e254f 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon/test/list_spec.lua @@ -251,6 +251,26 @@ describe("list", function() { value = "threethree" }, { value = "four" }, }, list.items) + + list:replace_at(2, { value = "one" }) + eq(4, list:length()) + eq({ + nil, + { value = "one" }, + { value = "threethree" }, + { value = "four" }, + }, list.items) + + list:replace_at(5, { value = "one" }) + eq(5, list:length()) + eq({ + nil, + nil, + { value = "threethree" }, + { value = "four" }, + { value = "one" }, + }, list.items) + end) it("resolve_displayed", function() From 77d52b2a88d6fadac90673f3a024b35fc3b25bb4 Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Thu, 4 Apr 2024 09:58:14 -0600 Subject: [PATCH 10/15] feat: holed ui lists even with bonus whitespace --- lua/harpoon/buffer.lua | 4 +--- lua/harpoon/list.lua | 3 ++- lua/harpoon/test/ui_spec.lua | 29 +++++++++++++++++++++++++++++ lua/harpoon/ui.lua | 17 ++++++++--------- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index 0a5576ee..2afec323 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -89,9 +89,7 @@ function M.get_contents(bufnr) local indices = {} for _, line in pairs(lines) do - if not utils.is_white_space(line) then - table.insert(indices, line) - end + table.insert(indices, line) end return indices diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 1b2554a2..0c4b7914 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -1,4 +1,5 @@ local Logger = require("harpoon.logger") +local utils = require("harpoon.utils") local Extensions = require("harpoon.extensions") local function guess_length(arr) @@ -252,7 +253,7 @@ function HarpoonList:resolve_displayed(displayed, length) for i = 1, length do local v = displayed[i] local index = index_of(list_displayed, self._length, v) - if v == "" then + if utils.is_white_space(v) then new_list[i] = nil elseif index == -1 then new_list[i] = self.config.create_list_item(self.config, v) diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index c4e70388..02b318dd 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -81,6 +81,35 @@ describe("harpoon", function() eq(created_files, list:display()) end) + it("ui with replace_at", function() + local one_f = os.tmpname() + local one = utils.create_file(one_f, { "one", }) + local three_f = os.tmpname() + local three = utils.create_file(three_f, { "three", }) + local context = { row = 1, col = 0 } + + eq(0, harpoon:list():length()) + vim.api.nvim_set_current_buf(three) + + harpoon:list():replace_at(3) + eq(3, harpoon:list():length()) + + vim.api.nvim_set_current_buf(one) + harpoon:list():replace_at(1) + eq(3, harpoon:list():length()) + + harpoon.ui:toggle_quick_menu(harpoon:list()) + + key("") + + eq(3, harpoon:list():length()) + eq({ + { value = one_f, context = context }, + nil, + { value = three_f, context = context }, + }, harpoon:list().items) + end) + it("using :q to leave harpoon should quit everything", function() harpoon.ui:toggle_quick_menu(harpoon:list()) diff --git a/lua/harpoon/ui.lua b/lua/harpoon/ui.lua index 492e8ee6..e6c568c7 100644 --- a/lua/harpoon/ui.lua +++ b/lua/harpoon/ui.lua @@ -160,14 +160,19 @@ function HarpoonUI:toggle_quick_menu(list, opts) }) end +function HarpoonUI:_get_processed_ui_contents() + local list = Buffer.get_contents(self.bufnr) + local length = #list + return list, length +end + ---@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 -- navigating - local list = Buffer.get_contents(self.bufnr) - local length = #list + local list, length = self:_get_processed_ui_contents() self.active_list:resolve_displayed(list, length) Logger:log( @@ -185,13 +190,7 @@ function HarpoonUI:select_menu_item(options) end function HarpoonUI:save() - local list = Buffer.get_contents(self.bufnr) - local length = #list - for i, v in ipairs(list) do - if v == "" then - list[i] = nil - end - end + local list, length = self:_get_processed_ui_contents() Logger:log("ui#save", list) self.active_list:resolve_displayed(list, length) From 5b344710b7b3625435d639697375b2c3f715a94e Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Thu, 4 Apr 2024 10:58:56 -0600 Subject: [PATCH 11/15] feat: closes #542 : out of bounds navigation. this was a pain in the ass --- lua/harpoon/buffer.lua | 1 + lua/harpoon/config.lua | 28 ++++++++++++- lua/harpoon/init.lua | 2 +- lua/harpoon/list.lua | 2 +- lua/harpoon/test/harpoon_spec.lua | 68 +++++++++++++++++++++++++++++++ lua/harpoon/test/ui_spec.lua | 17 ++------ lua/harpoon/test/utils.lua | 12 +++++- 7 files changed, 113 insertions(+), 17 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index 2afec323..60a207d3 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -16,6 +16,7 @@ local function get_harpoon_menu_name() end function M.run_select_command() + ---@type Harpoon local harpoon = require("harpoon") harpoon.logger:log("select by keymap ''") harpoon.ui:select_menu_item() diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 6f58bde3..0edb78e9 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -100,11 +100,12 @@ function M.get_default_config() list.name, options ) - options = options or {} if list_item == nil then return end + options = options or {} + local bufnr = vim.fn.bufnr(to_exact_name(list_item.value)) local set_position = false if bufnr == -1 then -- must create a buffer! @@ -130,10 +131,33 @@ function M.get_default_config() vim.api.nvim_set_current_buf(bufnr) if set_position then + local lines = vim.api.nvim_buf_line_count(bufnr) + + local edited = false + if list_item.context.row > lines then + list_item.context.row = lines + edited = true + end + + local row = list_item.context.row + local row_text = vim.api.nvim_buf_get_lines(0, row - 1, row, false) + local col = #row_text[1] + + if list_item.context.col > col then + list_item.context.col = col + edited = true + end + vim.api.nvim_win_set_cursor(0, { list_item.context.row or 1, list_item.context.col or 0, }) + + if edited then + Extensions.extensions:emit(Extensions.event_names.POSITION_UPDATED, { + list_item = list_item + }) + end end Extensions.extensions:emit(Extensions.event_names.NAVIGATE, { @@ -192,6 +216,8 @@ function M.get_default_config() } end, + ---@param arg {buf: number} + ---@param list HarpoonList BufLeave = function(arg, list) local bufnr = arg.buf local bufname = vim.api.nvim_buf_get_name(bufnr) diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 7482d11f..e34a7971 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -135,7 +135,7 @@ end local the_harpoon = Harpoon:new() ---@param self Harpoon ----@param partial_config HarpoonPartialConfig +---@param partial_config HarpoonPartialConfig? ---@return Harpoon function Harpoon.setup(self, partial_config) if self ~= the_harpoon then diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 0c4b7914..2f9989ed 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -226,7 +226,7 @@ end function HarpoonList:get_by_display(name) local displayed = self:display() - local index = index_of(displayed, #displayed, name, self.config) + local index = index_of(displayed, #displayed, name) if index == -1 then return nil end diff --git a/lua/harpoon/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua index 0b0e4640..71bf2ab8 100644 --- a/lua/harpoon/test/harpoon_spec.lua +++ b/lua/harpoon/test/harpoon_spec.lua @@ -1,5 +1,9 @@ local utils = require("harpoon.test.utils") +local logger = require("harpoon.logger") + +---@type Harpoon local harpoon = require("harpoon") + local Extensions = require("harpoon.extensions") local Config = require("harpoon.config") local Data = require("harpoon.data") @@ -19,9 +23,43 @@ local function expect_data(data) end end +---@param out {row: number, col: number} +---@param expected {row: number, col: number} +local function out_of_bounds_test(out, expected) + local file_name = "/tmp/harpoon-test" + local list = harpoon:list() + local to_unload = utils.create_file(file_name, { + "foo", + "bar", + "baz", + "qux", + }) + list:add() + + utils.create_file(file_name .. "2", { + "foo", + "bar", + "baz", + "qux", + }) + + vim.api.nvim_buf_delete(to_unload, {force = true}) + + -- i have to force it to be out of bounds + list.items[1].context = out + + harpoon:list():select(1) + + eq({ + { value = file_name, context = expected} + }, harpoon:list().items) + +end + describe("harpoon", function() before_each(function() be() + logger:clear() harpoon = require("harpoon") end) @@ -85,6 +123,36 @@ describe("harpoon", function() }) end) + it("out of bounds test: row over", function() + out_of_bounds_test({ + row = 5, + col = 3 + }, { + row = 4, + col = 3 + }) + end) + + it("out of bounds test: col over", function() + out_of_bounds_test({ + row = 4, + col = 4 + }, { + row = 4, + col = 3 + }) + end) + + it("out of bounds test: both over", function() + out_of_bounds_test({ + row = 5, + col = 4 + }, { + row = 4, + col = 3 + }) + end) + it("prepend/add double add", function() local file_name_1 = "/tmp/harpoon-test" local row_1 = 3 diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index 02b318dd..26a06d7a 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -5,15 +5,6 @@ local harpoon = require("harpoon") local eq = assert.are.same local be = utils.before_each(os.tmpname()) ----@param k string -local function key(k) - vim.api.nvim_feedkeys( - vim.api.nvim_replace_termcodes(k, true, false, true), - "x", - true - ) -end - describe("harpoon", function() before_each(function() be() @@ -100,7 +91,7 @@ describe("harpoon", function() harpoon.ui:toggle_quick_menu(harpoon:list()) - key("") + utils.key("") eq(3, harpoon:list():length()) eq({ @@ -155,7 +146,7 @@ describe("harpoon", function() eq(vim.api.nvim_win_is_valid(win_id), true) eq(vim.api.nvim_get_current_buf(), bufnr) - key("") + utils.key("") eq(vim.api.nvim_buf_is_valid(bufnr), false) eq(vim.api.nvim_win_is_valid(win_id), false) @@ -173,7 +164,7 @@ describe("harpoon", function() eq(vim.api.nvim_win_is_valid(win_id), true) eq(vim.api.nvim_get_current_buf(), bufnr) - key("q") + utils.key("q") eq(vim.api.nvim_buf_is_valid(bufnr), false) eq(vim.api.nvim_win_is_valid(win_id), false) @@ -191,7 +182,7 @@ describe("harpoon", function() eq(vim.api.nvim_win_is_valid(win_id), true) eq(vim.api.nvim_get_current_buf(), bufnr) - key("") + utils.key("") eq(vim.api.nvim_buf_is_valid(bufnr), false) eq(vim.api.nvim_win_is_valid(win_id), false) diff --git a/lua/harpoon/test/utils.lua b/lua/harpoon/test/utils.lua index 75f0a5b0..82f9dd7e 100644 --- a/lua/harpoon/test/utils.lua +++ b/lua/harpoon/test/utils.lua @@ -1,4 +1,5 @@ local Data = require("harpoon.data") +local Path = require("plenary.path") local Config = require("harpoon.config") local M = {} @@ -21,6 +22,15 @@ function M.return_to_checkpoint() M.clean_files() end +---@param k string +function M.key(k) + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes(k, true, false, true), + "x", + true + ) +end + local function fullpath(name) return function() return name @@ -64,12 +74,12 @@ end ---@param name string ---@param contents string[] function M.create_file(name, contents, row, col) + Path:new(name):write(table.concat(contents, "\n"), "w") local bufnr = vim.fn.bufnr(name, true) vim.api.nvim_set_option_value("bufhidden", "hide", { buf = bufnr, }) 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 or 1, col or 0 }) end From 1efff797a73cdf184cf358dc50b57b3b874cb1d2 Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Thu, 4 Apr 2024 10:59:34 -0600 Subject: [PATCH 12/15] chore: formatting + lint --- lua/harpoon/buffer.lua | 1 - lua/harpoon/config.lua | 12 ++++++++---- lua/harpoon/test/harpoon_spec.lua | 17 ++++++++--------- lua/harpoon/test/list_spec.lua | 1 - lua/harpoon/test/ui_spec.lua | 4 ++-- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lua/harpoon/buffer.lua b/lua/harpoon/buffer.lua index 60a207d3..a6a9fd30 100644 --- a/lua/harpoon/buffer.lua +++ b/lua/harpoon/buffer.lua @@ -1,4 +1,3 @@ -local utils = require("harpoon.utils") local HarpoonGroup = require("harpoon.autocmd") local M = {} diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index 0edb78e9..a8ddcd97 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -140,7 +140,8 @@ function M.get_default_config() end local row = list_item.context.row - local row_text = vim.api.nvim_buf_get_lines(0, row - 1, row, false) + local row_text = + vim.api.nvim_buf_get_lines(0, row - 1, row, false) local col = #row_text[1] if list_item.context.col > col then @@ -154,9 +155,12 @@ function M.get_default_config() }) if edited then - Extensions.extensions:emit(Extensions.event_names.POSITION_UPDATED, { - list_item = list_item - }) + Extensions.extensions:emit( + Extensions.event_names.POSITION_UPDATED, + { + list_item = list_item, + } + ) end end diff --git a/lua/harpoon/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua index 71bf2ab8..1d9398c3 100644 --- a/lua/harpoon/test/harpoon_spec.lua +++ b/lua/harpoon/test/harpoon_spec.lua @@ -43,7 +43,7 @@ local function out_of_bounds_test(out, expected) "qux", }) - vim.api.nvim_buf_delete(to_unload, {force = true}) + vim.api.nvim_buf_delete(to_unload, { force = true }) -- i have to force it to be out of bounds list.items[1].context = out @@ -51,9 +51,8 @@ local function out_of_bounds_test(out, expected) harpoon:list():select(1) eq({ - { value = file_name, context = expected} + { value = file_name, context = expected }, }, harpoon:list().items) - end describe("harpoon", function() @@ -126,30 +125,30 @@ describe("harpoon", function() it("out of bounds test: row over", function() out_of_bounds_test({ row = 5, - col = 3 + col = 3, }, { row = 4, - col = 3 + col = 3, }) end) it("out of bounds test: col over", function() out_of_bounds_test({ row = 4, - col = 4 + col = 4, }, { row = 4, - col = 3 + col = 3, }) end) it("out of bounds test: both over", function() out_of_bounds_test({ row = 5, - col = 4 + col = 4, }, { row = 4, - col = 3 + col = 3, }) end) diff --git a/lua/harpoon/test/list_spec.lua b/lua/harpoon/test/list_spec.lua index 505e254f..7adbbc39 100644 --- a/lua/harpoon/test/list_spec.lua +++ b/lua/harpoon/test/list_spec.lua @@ -270,7 +270,6 @@ describe("list", function() { value = "four" }, { value = "one" }, }, list.items) - end) it("resolve_displayed", function() diff --git a/lua/harpoon/test/ui_spec.lua b/lua/harpoon/test/ui_spec.lua index 26a06d7a..58d5d984 100644 --- a/lua/harpoon/test/ui_spec.lua +++ b/lua/harpoon/test/ui_spec.lua @@ -74,9 +74,9 @@ describe("harpoon", function() it("ui with replace_at", function() local one_f = os.tmpname() - local one = utils.create_file(one_f, { "one", }) + local one = utils.create_file(one_f, { "one" }) local three_f = os.tmpname() - local three = utils.create_file(three_f, { "three", }) + local three = utils.create_file(three_f, { "three" }) local context = { row = 1, col = 0 } eq(0, harpoon:list():length()) From da326d0438ac68dee9b6b62a734be940a8bd8405 Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Thu, 4 Apr 2024 11:00:16 -0600 Subject: [PATCH 13/15] fix: other-file kept being created --- lua/harpoon/test/harpoon_spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/harpoon/test/harpoon_spec.lua b/lua/harpoon/test/harpoon_spec.lua index 1d9398c3..2dca4c09 100644 --- a/lua/harpoon/test/harpoon_spec.lua +++ b/lua/harpoon/test/harpoon_spec.lua @@ -75,7 +75,7 @@ describe("harpoon", function() harpoon:setup() harpoon:list():add() - local other_buf = utils.create_file("other-file", { + local other_buf = utils.create_file("/tmp/other-file", { "foo", "bar", "baz", From 7543bd308069921e91327d9d9585483b34e9f7da Mon Sep 17 00:00:00 2001 From: Dan Loman Date: Fri, 5 Apr 2024 20:44:56 -0700 Subject: [PATCH 14/15] Fix typo --- lua/harpoon/list.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 2f9989ed..d7865011 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -102,7 +102,7 @@ end ---@param item? HarpoonListItem ---@return HarpoonList function HarpoonList:append(item) - print("APPEND IS DEPRICATED -- PLEASE USE `add`") + print("APPEND IS DEPRECATED -- PLEASE USE `add`") return self:add(item) end From d345631162ffec744315c8b1e89a42edb1f1924b Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Tue, 9 Apr 2024 12:34:59 -0600 Subject: [PATCH 15/15] possible fix: hoping to address #566 #567 --- lua/harpoon/config.lua | 12 +++++------- lua/harpoon/list.lua | 12 +++++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lua/harpoon/config.lua b/lua/harpoon/config.lua index a8ddcd97..316ec43a 100644 --- a/lua/harpoon/config.lua +++ b/lua/harpoon/config.lua @@ -190,11 +190,6 @@ function M.get_default_config() ---@return HarpoonListItem create_list_item = function(config, name) 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 or normalize_path( vim.api.nvim_buf_get_name( vim.api.nvim_get_current_buf() @@ -224,8 +219,11 @@ function M.get_default_config() ---@param list HarpoonList BufLeave = function(arg, list) local bufnr = arg.buf - local bufname = vim.api.nvim_buf_get_name(bufnr) - local item = list:get_by_display(bufname) + local bufname = normalize_path( + vim.api.nvim_buf_get_name(bufnr), + list.config.get_root_dir() + ) + local item = list:get_by_value(bufname) if item then local pos = vim.api.nvim_win_get_cursor(0) diff --git a/lua/harpoon/list.lua b/lua/harpoon/list.lua index 2f9989ed..e860c4c4 100644 --- a/lua/harpoon/list.lua +++ b/lua/harpoon/list.lua @@ -224,9 +224,15 @@ function HarpoonList:get(index) return self.items[index] end -function HarpoonList:get_by_display(name) - local displayed = self:display() - local index = index_of(displayed, #displayed, name) +function HarpoonList:get_by_value(value) + local index = index_of(self.items, self._length, value, { + equals = function(element, item) + if item == nil then + return false + end + return element == item.value + end, + }) if index == -1 then return nil end