feat: replace_at with harpoon lists

This commit is contained in:
theprimeagen 2024-04-02 13:52:28 -06:00
parent a38be6e0dd
commit 4ad05be8fe
9 changed files with 381 additions and 31 deletions

View File

@ -74,7 +74,7 @@ local harpoon = require("harpoon")
harpoon:setup() harpoon:setup()
-- REQUIRED -- REQUIRED
vim.keymap.set("n", "<leader>a", function() harpoon:list():append() end) vim.keymap.set("n", "<leader>a", function() harpoon:list():add() end)
vim.keymap.set("n", "<C-e>", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end) vim.keymap.set("n", "<C-e>", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end)
vim.keymap.set("n", "<C-h>", function() harpoon:list():select(1) end) vim.keymap.set("n", "<C-h>", function() harpoon:list():select(1) end)
@ -89,7 +89,7 @@ vim.keymap.set("n", "<C-S-N>", function() harpoon:list():next() end)
### Telescope ### 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. make sure to add `telescope` to your dependencies and paste this following snippet into your configuration.
```lua ```lua
@ -137,7 +137,7 @@ harpoon:setup({
-- Setting up custom behavior for a list named "cmd" -- Setting up custom behavior for a list named "cmd"
"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. -- value will be put in the list at the end.
-- --
-- which means same behavior for prepend except where in the list the -- 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 * `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)` * `select`: the action taken when selecting a list item. called from `list:select(idx, options)`
* `equals`: how to compare two list items for equality * `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 * `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. * `VimLeavePre`: this function is called for every list on VimLeavePre.
* `get_root_dir`: used for creating relative paths. defaults to `vim.loop.cwd()` * `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 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 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`. 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 ## ⇁ Social

View File

@ -139,6 +139,11 @@ function M.get_default_config()
---@param list_item_a HarpoonListItem ---@param list_item_a HarpoonListItem
---@param list_item_b HarpoonListItem ---@param list_item_b HarpoonListItem
equals = function(list_item_a, list_item_b) 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 return list_item_a.value == list_item_b.value
end, end,
@ -208,7 +213,7 @@ function M.get_default_config()
} }
end end
---@param partial_config HarpoonPartialConfig ---@param partial_config HarpoonPartialConfig?
---@param latest_config HarpoonConfig? ---@param latest_config HarpoonConfig?
---@return HarpoonConfig ---@return HarpoonConfig
function M.merge_config(partial_config, latest_config) function M.merge_config(partial_config, latest_config)

View File

@ -67,6 +67,7 @@ return {
builtins = Builtins, builtins = Builtins,
extensions = extensions, extensions = extensions,
event_names = { event_names = {
REPLACE = "REPLACE",
ADD = "ADD", ADD = "ADD",
SELECT = "SELECT", SELECT = "SELECT",
REMOVE = "REMOVE", REMOVE = "REMOVE",

View File

@ -1,9 +1,34 @@
local Logger = require("harpoon.logger") local Logger = require("harpoon.logger")
local Extensions = require("harpoon.extensions") 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 --- @class HarpoonNavOptions
--- @field ui_nav_wrap? boolean --- @field ui_nav_wrap? boolean
---@param items any[]
---@param element any
---@param config HarpoonPartialConfigItem?
local function index_of(items, element, config) local function index_of(items, element, config)
local equals = config and config.equals local equals = config and config.equals
or function(a, b) or function(a, b)
@ -20,6 +45,24 @@ local function index_of(items, element, config)
return index return index
end 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 --- @class HarpoonItem
--- @field value string --- @field value string
--- @field context any --- @field context any
@ -27,15 +70,18 @@ end
--- @class HarpoonList --- @class HarpoonList
--- @field config HarpoonPartialConfigItem --- @field config HarpoonPartialConfigItem
--- @field name string --- @field name string
--- @field _length number
--- @field _index number --- @field _index number
--- @field items HarpoonItem[] --- @field items HarpoonItem[]
local HarpoonList = {} local HarpoonList = {}
HarpoonList.__index = HarpoonList HarpoonList.__index = HarpoonList
function HarpoonList:new(config, name, items) function HarpoonList:new(config, name, items)
items = items or {}
return setmetatable({ return setmetatable({
items = items, items = items,
config = config, config = config,
_length = guess_length(items),
name = name, name = name,
_index = 1, _index = 1,
}, self) }, self)
@ -43,25 +89,60 @@ end
---@return number ---@return number
function HarpoonList:length() function HarpoonList:length()
return #self.items return self._length
end end
function HarpoonList:clear() function HarpoonList:clear()
self.items = {} self.items = {}
self._length = 0
end end
---@param item? HarpoonListItem
---@return HarpoonList ---@return HarpoonList
function HarpoonList:append(item) 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) 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, item, self.config)
Logger:log("HarpoonList:append", { item = item, index = index }) Logger:log("HarpoonList:add", { item = item, index = index })
if index == -1 then 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.extensions:emit(
Extensions.event_names.ADD, 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 end
return self return self
@ -77,7 +158,10 @@ function HarpoonList:prepend(item)
Extensions.event_names.ADD, Extensions.event_names.ADD,
{ list = self, item = item, idx = 1 } { 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 end
return self return self
@ -86,14 +170,18 @@ end
---@return HarpoonList ---@return HarpoonList
function HarpoonList:remove(item) function HarpoonList:remove(item)
item = item or self.config.create_list_item(self.config) 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 if self.config.equals(v, item) then
Extensions.extensions:emit( Extensions.extensions:emit(
Extensions.event_names.REMOVE, Extensions.event_names.REMOVE,
{ list = self, item = item, idx = i } { list = self, item = item, idx = i }
) )
Logger:log("HarpoonList:remove", { item = item, index = 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 break
end end
end end
@ -101,7 +189,7 @@ function HarpoonList:remove(item)
end end
---@return HarpoonList ---@return HarpoonList
function HarpoonList:removeAt(index) function HarpoonList:remove_at(index)
if self.items[index] then if self.items[index] then
Logger:log( Logger:log(
"HarpoonList:removeAt", "HarpoonList:removeAt",
@ -111,7 +199,10 @@ function HarpoonList:removeAt(index)
Extensions.event_names.REMOVE, Extensions.event_names.REMOVE,
{ list = self, item = self.items[index], idx = index } { 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 end
return self return self
end end
@ -122,7 +213,7 @@ end
function HarpoonList:get_by_display(name) function HarpoonList:get_by_display(name)
local displayed = self:display() local displayed = self:display()
local index = index_of(displayed, name) local index = index_of(displayed, name, self.config)
if index == -1 then if index == -1 then
return nil return nil
end end
@ -131,12 +222,14 @@ end
--- much inefficiencies. dun care --- much inefficiencies. dun care
---@param displayed string[] ---@param displayed string[]
function HarpoonList:resolve_displayed(displayed) ---@param length number
function HarpoonList:resolve_displayed(displayed, length)
local new_list = {} local new_list = {}
local list_displayed = self:display() 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) local index = index_of(displayed, v)
if index == -1 then if index == -1 then
Extensions.extensions:emit( Extensions.extensions:emit(
@ -146,9 +239,12 @@ function HarpoonList:resolve_displayed(displayed)
end end
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) 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) new_list[i] = self.config.create_list_item(self.config, v)
Extensions.extensions:emit( Extensions.extensions:emit(
Extensions.event_names.ADD, Extensions.event_names.ADD,
@ -163,6 +259,7 @@ function HarpoonList:resolve_displayed(displayed)
end end
local index_in_new_list = local index_in_new_list =
index_of(new_list, self.items[index], self.config) index_of(new_list, self.items[index], self.config)
if index_in_new_list == -1 then if index_in_new_list == -1 then
new_list[i] = self.items[index] new_list[i] = self.items[index]
end end
@ -170,6 +267,7 @@ function HarpoonList:resolve_displayed(displayed)
end end
self.items = new_list self.items = new_list
self._length = length
end end
function HarpoonList:select(index, options) function HarpoonList:select(index, options)
@ -189,11 +287,11 @@ function HarpoonList:next(opts)
opts = opts or {} opts = opts or {}
self._index = self._index + 1 self._index = self._index + 1
if self._index > #self.items then if self._index > self._length then
if opts.ui_nav_wrap then if opts.ui_nav_wrap then
self._index = 1 self._index = 1
else else
self._index = #self.items self._index = self._length
end end
end end
@ -220,8 +318,9 @@ end
--- @return string[] --- @return string[]
function HarpoonList:display() function HarpoonList:display()
local out = {} local out = {}
for _, v in ipairs(self.items) do for i = 1, self._length do
table.insert(out, self.config.display(v)) local v = self.items[i]
out[i] = v == nil and "" or self.config.display(v)
end end
return out return out

View File

@ -24,7 +24,7 @@ describe("harpoon", function()
"qux", "qux",
}, row, col) }, row, col)
local list = harpoon:list():append() local list = harpoon:list():add()
local other_buf = utils.create_file("other-file", { local other_buf = utils.create_file("other-file", {
"foo", "foo",
"bar", "bar",
@ -56,7 +56,7 @@ describe("harpoon", function()
}, row, col) }, row, col)
local list = harpoon:list() local list = harpoon:list()
list:append() list:add()
harpoon:sync() harpoon:sync()
eq(harpoon:dump(), { eq(harpoon:dump(), {
@ -66,7 +66,7 @@ describe("harpoon", function()
}) })
end) end)
it("prepend/append double add", function() it("prepend/add double add", function()
local default_list_name = harpoon:info().default_list_name local default_list_name = harpoon:info().default_list_name
local file_name_1 = "/tmp/harpoon-test" local file_name_1 = "/tmp/harpoon-test"
local row_1 = 3 local row_1 = 3
@ -79,7 +79,7 @@ describe("harpoon", function()
local contents = { "foo", "bar", "baz", "qux" } local contents = { "foo", "bar", "baz", "qux" }
local bufnr_1 = utils.create_file(file_name_1, contents, row_1, col_1) 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) utils.create_file(file_name_2, contents, row_2, col_2)
harpoon:list():prepend() harpoon:list():prepend()
@ -97,7 +97,7 @@ describe("harpoon", function()
{ value = file_name_1, context = { row = row_1, col = col_1 } }, { 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) vim.api.nvim_set_current_buf(bufnr_1)
harpoon:list():prepend() harpoon:list():prepend()

View File

@ -60,4 +60,238 @@ describe("list", function()
eq({ nil, {} }, foo_selected) eq({ nil, {} }, foo_selected)
eq(nil, bar_selected) eq(nil, bar_selected)
end) 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) end)

View File

@ -77,7 +77,7 @@ function M.fill_list_with_files(count, list)
local name = os.tmpname() local name = os.tmpname()
table.insert(files, name) table.insert(files, name)
M.create_file(name, { "test" }) M.create_file(name, { "test" })
list:append() list:add()
end end
return files return files

View File

@ -185,8 +185,16 @@ end
function HarpoonUI:save() function HarpoonUI:save()
local list = Buffer.get_contents(self.bufnr) 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) 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 if self.settings.sync_on_ui_close then
require("harpoon"):sync() require("harpoon"):sync()
end end

3
scripts/test.lua Normal file
View File

@ -0,0 +1,3 @@
local a = {}
a[3] = "foo"
print(#a)