feat: hashing on a per config.settings.key() basis

This commit is contained in:
theprimeagen 2024-04-02 15:32:03 -06:00
parent 4ad05be8fe
commit 3e32576076
8 changed files with 179 additions and 116 deletions

View File

@ -57,6 +57,7 @@ function M.get_default_config()
settings = { settings = {
save_on_toggle = false, save_on_toggle = false,
sync_on_ui_close = false, sync_on_ui_close = false,
key = function() key = function()
return vim.loop.cwd() return vim.loop.cwd()
end, end,
@ -205,6 +206,11 @@ function M.get_default_config()
item.context.row = pos[1] item.context.row = pos[1]
item.context.col = pos[2] item.context.col = pos[2]
Extensions.extensions:emit(
Extensions.event_names.POSITION_UPDATED,
item
)
end end
end, end,
@ -231,4 +237,13 @@ function M.merge_config(partial_config, latest_config)
return config return config
end 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 return M

View File

@ -1,43 +1,44 @@
local Path = require("plenary.path") local Path = require("plenary.path")
local data_path = vim.fn.stdpath("data") 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 ---@param data any
local function write_data(data) ---@param config HarpoonConfig
Path:new(full_data_path):write(vim.json.encode(data), "w") local function write_data(data, config)
Path:new(fullpath(config)):write(vim.json.encode(data), "w")
end end
local M = {} local M = {}
function M.__dangerously_clear_data() ---@param config HarpoonConfig
write_data({}) function M.__dangerously_clear_data(config)
write_data({}, config)
end end
function M.info() function M.info()
return { return {
data_path = data_path, data_path = data_path,
full_data_path = full_data_path,
} }
end 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[]}} --- @alias HarpoonRawData {[string]: {[string]: string[]}}
--- @class HarpoonData --- @class HarpoonData
--- @field seen {[string]: {[string]: boolean}}
--- @field _data HarpoonRawData --- @field _data HarpoonRawData
--- @field has_error boolean --- @field has_error boolean
local Data = {} local Data = {}
@ -48,34 +49,37 @@ local Data = {}
Data.__index = Data Data.__index = Data
---@param config HarpoonConfig
---@param provided_path string?
---@return HarpoonRawData ---@return HarpoonRawData
local function read_data() local function read_data(config, provided_path)
local path = Path:new(full_data_path) provided_path = provided_path or fullpath(config)
local path = Path:new(provided_path)
local exists = path:exists() local exists = path:exists()
if not exists then if not exists then
write_data({}) write_data({}, config)
end end
local out_data = path:read() local out_data = path:read()
if not out_data or out_data == "" then if not out_data or out_data == "" then
write_data({}) write_data({}, config)
out_data = path:read() out_data = "{}"
end end
local data = vim.json.decode(out_data) local data = vim.json.decode(out_data)
return data return data
end end
---@param config HarpoonConfig
---@return HarpoonData ---@return HarpoonData
function Data:new() function Data:new(config)
local ok, data = pcall(read_data) local ok, data = pcall(read_data, config)
return setmetatable({ return setmetatable({
_data = data, _data = data,
has_error = not ok, has_error = not ok,
seen = {},
}, self) }, self)
end end
@ -100,12 +104,6 @@ function Data:data(key, name)
) )
end end
if not self.seen[key] then
self.seen[key] = {}
end
self.seen[key][name] = true
return self:_get_data(key, name) return self:_get_data(key, name)
end end
@ -126,10 +124,6 @@ function Data:sync()
return return
end end
if not has_keys(self.seen) then
return
end
local ok, data = pcall(read_data) local ok, data = pcall(read_data)
if not ok then if not ok then
error("Harpoon: unable to sync data, error reading data file") error("Harpoon: unable to sync data, error reading data file")
@ -139,13 +133,16 @@ function Data:sync()
data[k] = v data[k] = v
end end
ok = pcall(write_data, data) pcall(write_data, data)
if ok then
self.seen = {}
end
end end
M.Data = Data M.Data = Data
M.test = {
set_fullpath = function(fp)
fullpath = fp
end,
read_data = read_data,
}
return M return M

View File

@ -12,6 +12,7 @@ local HarpoonExtensions = {}
---@field LIST_CREATED? fun(...): nil ---@field LIST_CREATED? fun(...): nil
---@field LIST_READ? fun(...): nil ---@field LIST_READ? fun(...): nil
---@field NAVIGATE? fun(...): nil ---@field NAVIGATE? fun(...): nil
---@field POSITION_UPDATED? fun(...): nil
HarpoonExtensions.__index = HarpoonExtensions HarpoonExtensions.__index = HarpoonExtensions
@ -71,6 +72,13 @@ return {
ADD = "ADD", ADD = "ADD",
SELECT = "SELECT", SELECT = "SELECT",
REMOVE = "REMOVE", 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", REORDER = "REORDER",
UI_CREATE = "UI_CREATE", UI_CREATE = "UI_CREATE",
SETUP_CALLED = "SETUP_CALLED", SETUP_CALLED = "SETUP_CALLED",

View File

@ -18,19 +18,37 @@ local Harpoon = {}
Harpoon.__index = 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 ---@return Harpoon
function Harpoon:new() function Harpoon:new()
local config = Config.get_default_config() local config = Config.get_default_config()
local harpoon = setmetatable({ local harpoon = setmetatable({
config = config, config = config,
data = Data.Data:new(), data = Data.Data:new(config),
logger = Log, logger = Log,
ui = Ui:new(config.settings), ui = Ui:new(config.settings),
_extensions = Extensions.extensions, _extensions = Extensions.extensions,
lists = {}, lists = {},
hooks_setup = false, hooks_setup = false,
}, self) }, self)
sync_on_change(harpoon)
return harpoon return harpoon
end end
@ -51,10 +69,6 @@ function Harpoon:list(name)
local existing_list = lists[name] local existing_list = lists[name]
if existing_list then 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) self._extensions:emit(Extensions.event_names.LIST_READ, existing_list)
return existing_list return existing_list
end end
@ -72,16 +86,14 @@ end
---@param cb fun(list: HarpoonList, config: HarpoonPartialConfigItem, name: string) ---@param cb fun(list: HarpoonList, config: HarpoonPartialConfigItem, name: string)
function Harpoon:_for_each_list(cb) function Harpoon:_for_each_list(cb)
local key = self.config.settings.key() local key = self.config.settings.key()
local seen = self.data.seen[key]
local lists = self.lists[key] local lists = self.lists[key]
if not lists then
if not seen then
return return
end end
for list_name, _ in pairs(seen) do for name, list in pairs(lists) do
local list_config = Config.get_config(self.config, list_name) local list_config = Config.get_config(self.config, name)
cb(lists[list_name], list_config, list_name) cb(list, list_config, name)
end end
end end

View File

@ -134,15 +134,15 @@ function HarpoonList:add(item)
end end
end end
Extensions.extensions:emit(
Extensions.event_names.ADD,
{ list = self, item = item, idx = idx }
)
self.items[idx] = item self.items[idx] = item
if idx > self._length then if idx > self._length then
self._length = idx self._length = idx
end end
Extensions.extensions:emit(
Extensions.event_names.ADD,
{ list = self, item = item, idx = idx }
)
end end
return self return self
@ -154,14 +154,15 @@ function HarpoonList:prepend(item)
local index = index_of(self.items, item, self.config) local index = index_of(self.items, item, self.config)
Logger:log("HarpoonList:prepend", { item = item, index = index }) Logger:log("HarpoonList:prepend", { item = item, index = index })
if index == -1 then 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) local stop_idx = prepend_to_array(self.items, item)
if stop_idx > self._length then if stop_idx > self._length then
self._length = stop_idx self._length = stop_idx
end end
Extensions.extensions:emit(
Extensions.event_names.ADD,
{ list = self, item = item, idx = 1 }
)
end end
return self return self
@ -173,15 +174,15 @@ function HarpoonList:remove(item)
for i = 1, self._length do for i = 1, self._length do
local v = self.items[i] local v = self.items[i]
if self.config.equals(v, item) then 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 }) Logger:log("HarpoonList:remove", { item = item, index = i })
self.items[i] = nil self.items[i] = nil
if i == self._length then if i == self._length then
self._length = determine_length(self.items, self._length) self._length = determine_length(self.items, self._length)
end end
Extensions.extensions:emit(
Extensions.event_names.REMOVE,
{ list = self, item = item, idx = i }
)
break break
end end
end end
@ -195,14 +196,14 @@ function HarpoonList:remove_at(index)
"HarpoonList:removeAt", "HarpoonList:removeAt",
{ item = self.items[index], index = index } { 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 self.items[index] = nil
if index == self._length then if index == self._length then
self._length = determine_length(self.items, self._length) self._length = determine_length(self.items, self._length)
end end
Extensions.extensions:emit(
Extensions.event_names.REMOVE,
{ list = self, item = self.items[index], idx = index }
)
end end
return self return self
end end
@ -228,14 +229,12 @@ function HarpoonList:resolve_displayed(displayed, length)
local list_displayed = self:display() local list_displayed = self:display()
local change = 0
for i = 1, self._length do for i = 1, self._length do
local v = self.items[i] 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( change = change + 1
Extensions.event_names.REMOVE,
{ list = self, item = self.items[i], idx = i }
)
end end
end end
@ -246,28 +245,26 @@ function HarpoonList:resolve_displayed(displayed, length)
new_list[i] = nil new_list[i] = nil
elseif index == -1 then 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( change = change + 1
Extensions.event_names.ADD,
{ list = self, item = new_list[i], idx = i }
)
else 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 = 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
if index ~= i then
change = change + 1
end
end end
end end
self.items = new_list self.items = new_list
self._length = length self._length = length
if change > 0 then
Extensions.extensions:emit(Extensions.event_names.LIST_CHANGE)
end
end end
function HarpoonList:select(index, options) function HarpoonList:select(index, options)

View File

@ -2,11 +2,23 @@ local utils = require("harpoon.test.utils")
local harpoon = require("harpoon") local harpoon = require("harpoon")
local Extensions = require("harpoon.extensions") local Extensions = require("harpoon.extensions")
local Config = require("harpoon.config") local Config = require("harpoon.config")
local Data = require("harpoon.data")
local List = require("harpoon.list")
local eq = assert.are.same local eq = assert.are.same
local config = Config.get_default_config()
local be = utils.before_each(os.tmpname()) 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() describe("harpoon", function()
before_each(function() before_each(function()
be() be()
@ -24,7 +36,8 @@ describe("harpoon", function()
"qux", "qux",
}, row, col) }, row, col)
local list = harpoon:list():add() harpoon:setup()
harpoon:list():add()
local other_buf = utils.create_file("other-file", { local other_buf = utils.create_file("other-file", {
"foo", "foo",
"bar", "bar",
@ -36,11 +49,17 @@ describe("harpoon", function()
vim.api.nvim_win_set_cursor(0, { row + 1, col }) vim.api.nvim_win_set_cursor(0, { row + 1, col })
vim.api.nvim_set_current_buf(other_buf) vim.api.nvim_set_current_buf(other_buf)
local expected = { expect_data({
{ value = file_name, context = { row = row + 1, col = col } }, [Config.DEFAULT_LIST] = {
} {
context = {
eq(expected, list.items) col = 0,
row = 2,
},
value = "/tmp/harpoon-test",
},
},
})
end) end)
it("full harpoon add sync cycle", function() it("full harpoon add sync cycle", function()
@ -67,7 +86,6 @@ describe("harpoon", function()
end) end)
it("prepend/add 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 file_name_1 = "/tmp/harpoon-test"
local row_1 = 3 local row_1 = 3
local col_1 = 1 local col_1 = 1
@ -79,31 +97,38 @@ 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():add() harpoon:list():add()
utils.create_file(file_name_2, contents, row_2, col_2) expect_data({
harpoon:list():prepend() [Config.DEFAULT_LIST] = {
{ value = file_name_1, context = { row = row_1, col = col_1 } },
harpoon:sync()
eq(harpoon:dump(), {
testies = {
[default_list_name] = list:encode(),
}, },
}) })
eq(list.items, { utils.create_file(file_name_2, contents, row_2, col_2)
{ value = file_name_2, context = { row = row_2, col = col_2 } }, harpoon:list():prepend()
{ 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 } },
},
}) })
harpoon:list():add() 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 } },
},
})
vim.api.nvim_set_current_buf(bufnr_1) vim.api.nvim_set_current_buf(bufnr_1)
harpoon:list():prepend() harpoon:list():prepend()
expect_data({
eq(list.items, { [Config.DEFAULT_LIST] = {
{ value = file_name_2, context = { row = row_2, col = col_2 } }, { value = file_name_2, context = { row = row_2, col = col_2 } },
{ value = file_name_1, context = { row = row_1, col = col_1 } }, { value = file_name_1, context = { row = row_1, col = col_1 } },
},
}) })
end) end)
@ -111,7 +136,7 @@ describe("harpoon", function()
local list_created = false local list_created = false
local list_name = "" local list_name = ""
local setup = false local setup = false
local config = {} local ext_config = {}
harpoon:extend({ harpoon:extend({
[Extensions.event_names.LIST_CREATED] = function(list) [Extensions.event_names.LIST_CREATED] = function(list)
@ -120,7 +145,7 @@ describe("harpoon", function()
end, end,
[Extensions.event_names.SETUP_CALLED] = function(c) [Extensions.event_names.SETUP_CALLED] = function(c)
setup = true setup = true
config = c ext_config = c
end, end,
}) })
@ -130,7 +155,7 @@ describe("harpoon", function()
harpoon:list() harpoon:list()
eq(true, setup) eq(true, setup)
eq({}, config.foo) eq({}, ext_config.foo)
eq(true, list_created) eq(true, list_created)
eq(Config.DEFAULT_LIST, list_name) eq(Config.DEFAULT_LIST, list_name)

View File

@ -1,4 +1,5 @@
local Data = require("harpoon.data") local Data = require("harpoon.data")
local Config = require("harpoon.config")
local M = {} local M = {}
@ -20,15 +21,24 @@ function M.return_to_checkpoint()
M.clean_files() M.clean_files()
end end
local function fullpath(name)
return function()
return name
end
end
---@param name string ---@param name string
function M.before_each(name) function M.before_each(name)
local set_fullpath = fullpath(name)
local config = Config.get_default_config()
return function() return function()
Data.set_data_path(name) Data.test.set_fullpath(set_fullpath)
Data.__dangerously_clear_data() --- we don't use the config
Data.__dangerously_clear_data(config)
require("plenary.reload").reload_module("harpoon") require("plenary.reload").reload_module("harpoon")
Data = require("harpoon.data") Data = require("harpoon.data")
Data.set_data_path(name) Data.test.set_fullpath(set_fullpath)
local harpoon = require("harpoon") local harpoon = require("harpoon")
M.return_to_checkpoint() M.return_to_checkpoint()

View File

@ -193,7 +193,6 @@ function HarpoonUI:save()
end end
Logger:log("ui#save", list) Logger:log("ui#save", list)
print("saving", vim.inspect(list))
self.active_list:resolve_displayed(list, length) 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()