From 4ad05be8fe98092f0dec3bc3b47abebb59c3814a Mon Sep 17 00:00:00 2001 From: theprimeagen Date: Tue, 2 Apr 2024 13:52:28 -0600 Subject: [PATCH] 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 f2cd862..44e8ebd 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 a759297..fa14610 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 2cbbae4..d673f0d 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 07c19f5..bf8008a 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 d120204..f172e05 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 eaaed22..9bd8c9c 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 b83b824..9719207 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 d04441f..30ddfe6 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 0000000..d935add --- /dev/null +++ b/scripts/test.lua @@ -0,0 +1,3 @@ +local a = {} +a[3] = "foo" +print(#a)