Skip to content

Commit f440b0a

Browse files
authored
feat: prune scope save files based on last modified time (#143)
* 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
1 parent df9dc16 commit f440b0a

File tree

5 files changed

+138
-9
lines changed

5 files changed

+138
-9
lines changed

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,20 @@ require("grapple").setup({
244244

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

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

254+
---Time limit used for pruning unused scope (IDs). If a scope's save file
255+
---modified time exceeds this limit, then it will be deleted when a prune
256+
---requested. Can be an integer (in milliseconds) or a string time limit
257+
---(e.g. "30d" or "2h" or "15m")
258+
---@type integer | string
259+
prune = "30d",
260+
254261
---User-defined tags title function for Grapple windows
255262
---By default, uses the resolved scope's ID
256263
---@type fun(scope: grapple.resolved_scope): string?

lua/grapple.lua

+40-6
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ function Grapple.statusline(opts)
378378
return statusline
379379
end
380380

381+
---Unload tags for a give (scope) name or loaded scope (id)
381382
---@param opts? { scope?: string, id?: string, notify?: boolean }
382383
---@return string? error
383384
function Grapple.unload(opts)
@@ -389,10 +390,9 @@ function Grapple.unload(opts)
389390
local err = app:unload(opts)
390391
if err then
391392
if opts.notify then
392-
return vim.notify(err, vim.log.levels.ERROR)
393-
else
394-
return err
393+
vim.notify(err, vim.log.levels.ERROR)
395394
end
395+
return err
396396
end
397397

398398
if opts.notify then
@@ -413,17 +413,50 @@ function Grapple.reset(opts)
413413
local err = app:reset(opts)
414414
if err then
415415
if opts.notify then
416-
return vim.notify(err, vim.log.levels.ERROR)
417-
else
418-
return err
416+
vim.notify(err, vim.log.levels.ERROR)
419417
end
418+
return err
420419
end
421420

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

426+
---Prune save files based on their last modified time
427+
---@param opts? { limit?: integer | string, notify?: boolean }
428+
---@return string[] | nil, string? error
429+
function Grapple.prune(opts)
430+
local Util = require("grapple.util")
431+
local App = require("grapple.app")
432+
local app = App.get()
433+
434+
opts = opts or {}
435+
436+
local pruned_ids, err = app.tag_manager:prune(opts.limit or app.settings.prune)
437+
if not pruned_ids then
438+
if opts.notify then
439+
vim.notify(err, vim.log.levels.ERROR)
440+
end
441+
return nil, err
442+
end
443+
444+
if opts.notify then
445+
if #pruned_ids == 0 then
446+
vim.notify("Pruned 0 save files", vim.log.levels.INFO)
447+
elseif #pruned_ids == 1 then
448+
vim.notify(string.format("Pruned %d save file: %s", #pruned_ids, pruned_ids[1]), vim.log.levels.INFO)
449+
else
450+
vim.print(pruned_ids)
451+
local output_tbl = vim.tbl_map(Util.with_prefix(" "), pruned_ids)
452+
local output = table.concat(output_tbl, "\n")
453+
vim.notify(string.format("Pruned %d save files\n%s", #pruned_ids, output), vim.log.levels.INFO)
454+
end
455+
end
456+
457+
return pruned_ids, nil
458+
end
459+
427460
---Create a user-defined scope
428461
---@param definition grapple.scope_definition
429462
---@return string? error
@@ -665,6 +698,7 @@ function Grapple.initialize()
665698
open_loaded = { args = {}, kwargs = { "all" } },
666699
open_scopes = { args = {}, kwargs = {} },
667700
open_tags = { args = {}, kwargs = window_kwargs },
701+
prune = { args = {}, kwargs = { "limit" } },
668702
quickfix = { args = {}, kwargs = scope_kwargs },
669703
reset = { args = {}, kwargs = scope_kwargs },
670704
select = { args = {}, kwargs = use_kwargs },

lua/grapple/settings.lua

+8-1
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,20 @@ local DEFAULT_SETTINGS = {
4141

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

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

51+
---Time limit used for pruning unused scope (IDs). If a scope's save file
52+
---modified time exceeds this limit, then it will be deleted when a prune
53+
---requested. Can be an integer (in milliseconds) or a string time limit
54+
---(e.g. "30d" or "2h" or "15m")
55+
---@type integer | string
56+
prune = "30d",
57+
5158
---@class grapple.scope_definition
5259
---@field name string
5360
---@field force? boolean

lua/grapple/state.lua

+42-1
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ end
5959
---@return string[]
6060
function State:list()
6161
local files = {}
62-
for name, type in vim.fs.dir(self.save_dir) do
62+
for file_name, type in vim.fs.dir(self.save_dir) do
6363
if type ~= "file" then
6464
goto continue
6565
end
6666

67+
local name = file_name
6768
name = path_decode(name)
6869
name = string.gsub(name, "%.json", "")
70+
6971
table.insert(files, name)
7072

7173
::continue::
@@ -82,6 +84,45 @@ function State:remove(name)
8284
end
8385
end
8486

87+
---@return string[] | nil pruned, string? error, string? error_kind
88+
function State:prune(limit_sec)
89+
local now = os.time(os.date("*t"))
90+
local pruned = {}
91+
92+
for file_name, type in vim.fs.dir(self.save_dir) do
93+
if type ~= "file" then
94+
goto continue
95+
end
96+
97+
local path = Path.join(self.save_dir, file_name)
98+
99+
local name = file_name
100+
name = path_decode(name)
101+
name = string.gsub(name, "%.json", "")
102+
103+
---@diagnostic disable-next-line: redefined-local
104+
local stat, err, err_kind = vim.loop.fs_stat(path)
105+
if not stat then
106+
return nil, err, err_kind
107+
end
108+
109+
local elapsed_sec = now - stat.mtime.sec
110+
if elapsed_sec > limit_sec then
111+
table.insert(pruned, path_decode(name))
112+
113+
---@diagnostic disable-next-line: redefined-local
114+
local err, err_kind = self:remove(name)
115+
if err then
116+
return nil, err, err_kind
117+
end
118+
end
119+
120+
::continue::
121+
end
122+
123+
return pruned, nil, nil
124+
end
125+
85126
---@param name string
86127
---@return any decoded, string? error, string? error_kind
87128
function State:read(name)

lua/grapple/tag_manager.lua

+40
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,46 @@ function TagManager:reset(id)
152152
end
153153
end
154154

155+
---@param time_limit integer | string
156+
---@return string[] | nil pruned, string? error
157+
function TagManager:prune(time_limit)
158+
vim.validate({
159+
time_limit = { time_limit, { "number", "string" } },
160+
})
161+
162+
local limit_sec
163+
if type(time_limit) == "number" then
164+
limit_sec = time_limit
165+
elseif type(time_limit) == "string" then
166+
local n, kind = string.match(time_limit, "^(%d+)(%S)$")
167+
if not n or not kind then
168+
return nil, string.format("Could not parse time limit: %s", time_limit)
169+
end
170+
171+
n = assert(tonumber(n))
172+
if kind == "d" then
173+
limit_sec = n * 24 * 60 * 60
174+
elseif kind == "h" then
175+
limit_sec = n * 60 * 60
176+
elseif kind == "m" then
177+
limit_sec = n * 60
178+
elseif kind == "s" then
179+
limit_sec = n
180+
else
181+
return nil, string.format("Invalid time limit kind: %s", time_limit)
182+
end
183+
else
184+
return nil, string.format("Invalid time limit: %s", vim.inspect(time_limit))
185+
end
186+
187+
local pruned_ids, err = self.state:prune(limit_sec)
188+
if not pruned_ids then
189+
return nil, err
190+
end
191+
192+
return pruned_ids, nil
193+
end
194+
155195
---@param id string
156196
---@return string? error
157197
function TagManager:sync(id)

0 commit comments

Comments
 (0)