feat(picker): persistent history. Closes #528

This commit is contained in:
Folke Lemaitre 2025-01-20 18:30:05 +01:00
parent 8d9677fc47
commit ea665ebad1
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
3 changed files with 103 additions and 15 deletions

View file

@ -23,8 +23,7 @@ Async.BUDGET = 10
---@field start_time number
---@field title string
---@field closed? boolean
---@field hist_idx number
---@field hist_cursor number
---@field history snacks.picker.History
---@field visual? snacks.picker.Visual
local M = {}
M.__index = M
@ -46,8 +45,7 @@ M._active = {}
---@type snacks.picker.Last?
M.last = nil
---@type {pattern: string, search: string, live?: boolean}[]
M.history = {}
---@alias snacks.picker.history.Record {pattern: string, search: string, live?: boolean}
---@hide
---@param opts? snacks.picker.Config
@ -58,6 +56,16 @@ function M.new(opts)
return M.resume()
end
self.history = require("snacks.picker.util.history").new("picker", {
---@param hist snacks.picker.history.Record
filter = function(hist)
if hist.pattern == "" and hist.search == "" then
return false
end
return true
end,
})
local picker_count = vim.tbl_count(M._pickers)
if picker_count > 0 then
-- clear items from previous pickers for garbage collection
@ -96,8 +104,6 @@ function M.new(opts)
self.opts.win.input.actions = actions
self.opts.win.list.actions = actions
self.opts.win.preview.actions = actions
self.hist_idx = #M.history + 1
self.hist_cursor = self.hist_idx
local sort = self.opts.sort or require("snacks.picker.sort").default()
sort = type(sort) == "table" and require("snacks.picker.sort").default(sort) or sort
@ -451,6 +457,7 @@ function M:close()
if self.closed then
return
end
self:hist_record(true)
self.closed = true
M.last.selected = self:selected({ fallback = false })
M.last.cursor = self.list.cursor
@ -572,23 +579,31 @@ function M:find(opts)
end
--- Add current filter to history
---@param force? boolean
---@private
function M:hist_record()
M.history[self.hist_idx] = {
function M:hist_record(force)
if not force and not self.history:is_current() then
return
end
self.history:record({
pattern = self.input.filter.pattern,
search = self.input.filter.search,
live = self.opts.live,
}
})
end
--- Move the history cursor
---@param forward? boolean
function M:hist(forward)
self:hist_record()
self.hist_cursor = self.hist_cursor + (forward and 1 or -1)
self.hist_cursor = math.min(math.max(self.hist_cursor, 1), #M.history)
self.opts.live = M.history[self.hist_cursor].live
self.input:set(M.history[self.hist_cursor].pattern, M.history[self.hist_cursor].search)
if forward then
self.history:next()
else
self.history:prev()
end
local hist = self.history:get() --[[@as snacks.picker.history.Record]]
self.opts.live = hist.live
self.input:set(hist.pattern, hist.search)
end
--- Run the matcher with the current pattern.

View file

@ -0,0 +1,73 @@
---@class snacks.picker.History
---@field path string
---@field kv snacks.picker.KeyValue
---@field idx number
---@field cursor number
local M = {}
M.__index = M
---@type table<string, snacks.picker.KeyValue>
M.stores = {}
-- Save the history on exit
vim.api.nvim_create_autocmd("ExitPre", {
group = vim.api.nvim_create_augroup("snacks_history", { clear = true }),
callback = function()
for n, kv in pairs(M.stores) do
kv:close()
M.stores[n] = nil
end
end,
})
---@param name string
---@param opts? {filter?: fun(value: string): boolean}
function M.new(name, opts)
opts = opts or {}
local self = setmetatable({}, M)
self.path = vim.fn.stdpath("data") .. "/snacks/" .. name .. ".history"
if not M.stores[name] then
M.stores[name] = require("snacks.picker.util.kv").new(self.path, {
max_size = 1000,
---@param a snacks.picker.KeyValue.entry
---@param b snacks.picker.KeyValue.entry
cmp = function(a, b)
return a.key > b.key
end,
})
end
self.kv = M.stores[name]
-- re-index the data
self.kv.data = vim.tbl_values(self.kv.data)
if opts.filter then
self.kv.data = vim.tbl_filter(opts.filter, self.kv.data)
end
self.idx = #self.kv.data + 1
self.cursor = self.idx
return self
end
function M:is_current()
return self.cursor == self.idx
end
function M:record(value)
self.kv:set(self.idx, value)
end
function M:next()
self.cursor = math.min(self.cursor + 1, self.idx)
return self:get()
end
function M:prev()
self.cursor = math.max(self.cursor - 1, 1)
return self:get()
end
function M:get()
return self.kv:get(self.cursor)
end
return M

View file

@ -11,7 +11,7 @@ local uv = vim.uv or vim.loop
---@alias snacks.picker.KeyValue.entry {key:string, value:number}
---@param path string
---@param opts? {max_size?: number, cmp?: fun(a,b)}
---@param opts? {max_size?: number, cmp?: fun(a:snacks.picker.KeyValue.entry, b:snacks.picker.KeyValue.entry): boolean}
function M.new(path, opts)
local self = setmetatable({}, M)
self.data = {}
@ -39,7 +39,7 @@ function M:set(key, value)
end
function M:get(key)
return self.data[key] or 0
return self.data[key]
end
function M:close()