Skip to content

Commit

Permalink
feat: prune scope save files based on last modified time (#143)
Browse files Browse the repository at this point in the history
* feat: prune tag containers based on ttl

* feat: add "prune" setting with a default of "30d"

* docs: update docs with new "prune" setting

* fix: still return error when notify = true

* refactor: rename "ttl" parameter to "mtime"

* fix: add success notification for Grapple.prune

* refactor: update Grapple.prune

rename "mtime" to "limit" in Grapple.prune
update prune notification based on number of ids pruned
use os.time to get current unix timestamp instead of jank way
delete files when Grapple.prune is called

* docs: reword "time delta" as "time limit"

* fix: pass name to path_decode
  • Loading branch information
cbochs authored Mar 21, 2024
1 parent df9dc16 commit f440b0a
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 9 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,20 @@ require("grapple").setup({

---A string of characters used for quick selecting in Grapple windows
---An empty string or false will disable quick select
---@type string | nil
---@type string | boolean
quick_select = "123456789",

---Default command to use when selecting a tag
---@type fun(path: string)
command = vim.cmd.edit,

---Time limit used for pruning unused scope (IDs). If a scope's save file
---modified time exceeds this limit, then it will be deleted when a prune
---requested. Can be an integer (in milliseconds) or a string time limit
---(e.g. "30d" or "2h" or "15m")
---@type integer | string
prune = "30d",

---User-defined tags title function for Grapple windows
---By default, uses the resolved scope's ID
---@type fun(scope: grapple.resolved_scope): string?
Expand Down
46 changes: 40 additions & 6 deletions lua/grapple.lua
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ function Grapple.statusline(opts)
return statusline
end

---Unload tags for a give (scope) name or loaded scope (id)
---@param opts? { scope?: string, id?: string, notify?: boolean }
---@return string? error
function Grapple.unload(opts)
Expand All @@ -389,10 +390,9 @@ function Grapple.unload(opts)
local err = app:unload(opts)
if err then
if opts.notify then
return vim.notify(err, vim.log.levels.ERROR)
else
return err
vim.notify(err, vim.log.levels.ERROR)
end
return err
end

if opts.notify then
Expand All @@ -413,17 +413,50 @@ function Grapple.reset(opts)
local err = app:reset(opts)
if err then
if opts.notify then
return vim.notify(err, vim.log.levels.ERROR)
else
return err
vim.notify(err, vim.log.levels.ERROR)
end
return err
end

if opts.notify then
vim.notify(string.format("Scope reset: %s", opts.scope or opts.id), vim.log.levels.INFO)
end
end

---Prune save files based on their last modified time
---@param opts? { limit?: integer | string, notify?: boolean }
---@return string[] | nil, string? error
function Grapple.prune(opts)
local Util = require("grapple.util")
local App = require("grapple.app")
local app = App.get()

opts = opts or {}

local pruned_ids, err = app.tag_manager:prune(opts.limit or app.settings.prune)
if not pruned_ids then
if opts.notify then
vim.notify(err, vim.log.levels.ERROR)
end
return nil, err
end

if opts.notify then
if #pruned_ids == 0 then
vim.notify("Pruned 0 save files", vim.log.levels.INFO)
elseif #pruned_ids == 1 then
vim.notify(string.format("Pruned %d save file: %s", #pruned_ids, pruned_ids[1]), vim.log.levels.INFO)
else
vim.print(pruned_ids)
local output_tbl = vim.tbl_map(Util.with_prefix(" "), pruned_ids)
local output = table.concat(output_tbl, "\n")
vim.notify(string.format("Pruned %d save files\n%s", #pruned_ids, output), vim.log.levels.INFO)
end
end

return pruned_ids, nil
end

---Create a user-defined scope
---@param definition grapple.scope_definition
---@return string? error
Expand Down Expand Up @@ -665,6 +698,7 @@ function Grapple.initialize()
open_loaded = { args = {}, kwargs = { "all" } },
open_scopes = { args = {}, kwargs = {} },
open_tags = { args = {}, kwargs = window_kwargs },
prune = { args = {}, kwargs = { "limit" } },
quickfix = { args = {}, kwargs = scope_kwargs },
reset = { args = {}, kwargs = scope_kwargs },
select = { args = {}, kwargs = use_kwargs },
Expand Down
9 changes: 8 additions & 1 deletion lua/grapple/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,20 @@ local DEFAULT_SETTINGS = {

---A string of characters used for quick selecting in Grapple windows
---An empty string or false will disable quick select
---@type string
---@type string | boolean
quick_select = "123456789",

---Default command to use when selecting a tag
---@type fun(path: string)
command = vim.cmd.edit,

---Time limit used for pruning unused scope (IDs). If a scope's save file
---modified time exceeds this limit, then it will be deleted when a prune
---requested. Can be an integer (in milliseconds) or a string time limit
---(e.g. "30d" or "2h" or "15m")
---@type integer | string
prune = "30d",

---@class grapple.scope_definition
---@field name string
---@field force? boolean
Expand Down
43 changes: 42 additions & 1 deletion lua/grapple/state.lua
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ end
---@return string[]
function State:list()
local files = {}
for name, type in vim.fs.dir(self.save_dir) do
for file_name, type in vim.fs.dir(self.save_dir) do
if type ~= "file" then
goto continue
end

local name = file_name
name = path_decode(name)
name = string.gsub(name, "%.json", "")

table.insert(files, name)

::continue::
Expand All @@ -82,6 +84,45 @@ function State:remove(name)
end
end

---@return string[] | nil pruned, string? error, string? error_kind
function State:prune(limit_sec)
local now = os.time(os.date("*t"))
local pruned = {}

for file_name, type in vim.fs.dir(self.save_dir) do
if type ~= "file" then
goto continue
end

local path = Path.join(self.save_dir, file_name)

local name = file_name
name = path_decode(name)
name = string.gsub(name, "%.json", "")

---@diagnostic disable-next-line: redefined-local
local stat, err, err_kind = vim.loop.fs_stat(path)
if not stat then
return nil, err, err_kind
end

local elapsed_sec = now - stat.mtime.sec
if elapsed_sec > limit_sec then
table.insert(pruned, path_decode(name))

---@diagnostic disable-next-line: redefined-local
local err, err_kind = self:remove(name)
if err then
return nil, err, err_kind
end
end

::continue::
end

return pruned, nil, nil
end

---@param name string
---@return any decoded, string? error, string? error_kind
function State:read(name)
Expand Down
40 changes: 40 additions & 0 deletions lua/grapple/tag_manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,46 @@ function TagManager:reset(id)
end
end

---@param time_limit integer | string
---@return string[] | nil pruned, string? error
function TagManager:prune(time_limit)
vim.validate({
time_limit = { time_limit, { "number", "string" } },
})

local limit_sec
if type(time_limit) == "number" then
limit_sec = time_limit
elseif type(time_limit) == "string" then
local n, kind = string.match(time_limit, "^(%d+)(%S)$")
if not n or not kind then
return nil, string.format("Could not parse time limit: %s", time_limit)
end

n = assert(tonumber(n))
if kind == "d" then
limit_sec = n * 24 * 60 * 60
elseif kind == "h" then
limit_sec = n * 60 * 60
elseif kind == "m" then
limit_sec = n * 60
elseif kind == "s" then
limit_sec = n
else
return nil, string.format("Invalid time limit kind: %s", time_limit)
end
else
return nil, string.format("Invalid time limit: %s", vim.inspect(time_limit))
end

local pruned_ids, err = self.state:prune(limit_sec)
if not pruned_ids then
return nil, err
end

return pruned_ids, nil
end

---@param id string
---@return string? error
function TagManager:sync(id)
Expand Down

0 comments on commit f440b0a

Please sign in to comment.