refactor: organize tests and dry (#49)
* refactor(store): convert singleton to Store.new() factory
Problem: store.lua used module-level _data singleton, making
project-local stores impossible and creating hidden global state.
Solution: introduce Store metatable with all operations as instance
methods. M.new(path) constructs an instance; M.resolve_path()
searches upward for .pending.json and falls back to
config.get().data_path. Singleton module API is removed.
* refactor(diff): accept store instance as parameter
Problem: diff.apply called store singleton methods directly, coupling
it to global state and preventing use with project-local stores.
Solution: change signature to apply(lines, s, hidden_ids?) where s is
a pending.Store instance. All store operations now go through s.
* refactor(buffer): add set_store/store accessors, drop singleton dep
Problem: buffer.lua imported store directly and called singleton
methods, preventing it from working with per-project store instances.
Solution: add module-level _store, M.set_store(s), and M.store()
accessors. open() and render() use _store instead of the singleton.
init.lua will call buffer.set_store(s) before buffer.open().
* refactor(complete,health,sync,plugin): update callers to store instance API
Problem: complete.lua, health.lua, sync/gcal.lua, and plugin/pending.lua
all called singleton store methods directly.
Solution: complete.lua uses buffer.store() for category lookups;
health.lua uses store.new(store.resolve_path()) and reports the
resolved path; gcal.lua calls require('pending').store() for task
access; plugin tab-completion creates ephemeral store instances via
store.new(store.resolve_path()). Add 'init' to the subcommands list.
* feat(init): thread Store instance through init, add :Pending init
Problem: init.lua called singleton store methods throughout, and there
was no way to create a project-local .pending.json file.
Solution: add module-level _store and private get_store() that
lazy-constructs via store.new(store.resolve_path()). Add public
M.store() accessor used by specs and sync backends. M.open() calls
buffer.set_store(get_store()) before buffer.open(). All store
callsites converted to get_store():method(). goto_file() and
add_here() derive the data directory from get_store().path.
Add M.init() which creates .pending.json in cwd and dispatches from
M.command() as ':Pending init'.
* test: update all specs for Store instance API
Problem: every spec used the old singleton API (store.unload(),
store.load(), store.add(), etc.) and diff.apply(lines, hidden).
Solution: lower-level specs (store, diff, views, complete, file) use
s = store.new(path); s:load() directly. Higher-level specs (archive,
edit, filter, status, sync) reset package.loaded['pending'] in
before_each and use pending.store() to access the live instance.
diff.apply calls updated to diff.apply(lines, s, hidden_ids).
* docs(pending): document :Pending init and store resolution
Add *pending-store-resolution* section explaining upward .pending.json
discovery and fallback to the global data_path. Document :Pending init
under COMMANDS. Add a cross-reference from the data_path config field.
* ci: format
* ci: remove unused variable
This commit is contained in:
parent
dbd76d6759
commit
41bda24570
19 changed files with 819 additions and 703 deletions
|
|
@ -1,10 +1,12 @@
|
|||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
local views = require('pending.views')
|
||||
|
||||
---@class pending.buffer
|
||||
local M = {}
|
||||
|
||||
---@type pending.Store?
|
||||
local _store = nil
|
||||
|
||||
---@type integer?
|
||||
local task_bufnr = nil
|
||||
---@type integer?
|
||||
|
|
@ -41,6 +43,17 @@ function M.current_view_name()
|
|||
return current_view
|
||||
end
|
||||
|
||||
---@param s pending.Store?
|
||||
---@return nil
|
||||
function M.set_store(s)
|
||||
_store = s
|
||||
end
|
||||
|
||||
---@return pending.Store?
|
||||
function M.store()
|
||||
return _store
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function M.filter_predicates()
|
||||
return _filter_predicates
|
||||
|
|
@ -281,7 +294,7 @@ function M.render(bufnr)
|
|||
current_view = current_view or config.get().default_view
|
||||
local view_label = current_view == 'priority' and 'queue' or current_view
|
||||
vim.api.nvim_buf_set_name(bufnr, 'pending://' .. view_label)
|
||||
local all_tasks = store.active_tasks()
|
||||
local all_tasks = _store and _store:active_tasks() or {}
|
||||
local tasks = {}
|
||||
for _, task in ipairs(all_tasks) do
|
||||
if not _hidden_ids[task.id] then
|
||||
|
|
@ -341,7 +354,9 @@ end
|
|||
---@return integer bufnr
|
||||
function M.open()
|
||||
setup_highlights()
|
||||
store.load()
|
||||
if _store then
|
||||
_store:load()
|
||||
end
|
||||
|
||||
if task_winid and vim.api.nvim_win_is_valid(task_winid) then
|
||||
vim.api.nvim_set_current_win(task_winid)
|
||||
|
|
|
|||
|
|
@ -15,10 +15,13 @@ end
|
|||
|
||||
---@return string[]
|
||||
local function get_categories()
|
||||
local store = require('pending.store')
|
||||
local s = require('pending.buffer').store()
|
||||
if not s then
|
||||
return {}
|
||||
end
|
||||
local seen = {}
|
||||
local result = {}
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
for _, task in ipairs(s:active_tasks()) do
|
||||
local cat = task.category
|
||||
if cat and not seen[cat] then
|
||||
seen[cat] = true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
local config = require('pending.config')
|
||||
local parse = require('pending.parse')
|
||||
local store = require('pending.store')
|
||||
|
||||
---@class pending.ParsedEntry
|
||||
---@field type 'task'|'header'|'blank'
|
||||
|
|
@ -72,12 +71,13 @@ function M.parse_buffer(lines)
|
|||
end
|
||||
|
||||
---@param lines string[]
|
||||
---@param s pending.Store
|
||||
---@param hidden_ids? table<integer, true>
|
||||
---@return nil
|
||||
function M.apply(lines, hidden_ids)
|
||||
function M.apply(lines, s, hidden_ids)
|
||||
local parsed = M.parse_buffer(lines)
|
||||
local now = timestamp()
|
||||
local data = store.data()
|
||||
local data = s:data()
|
||||
|
||||
local old_by_id = {}
|
||||
for _, task in ipairs(data.tasks) do
|
||||
|
|
@ -98,7 +98,7 @@ function M.apply(lines, hidden_ids)
|
|||
|
||||
if entry.id and old_by_id[entry.id] then
|
||||
if seen_ids[entry.id] then
|
||||
store.add({
|
||||
s:add({
|
||||
description = entry.description,
|
||||
category = entry.category,
|
||||
priority = entry.priority,
|
||||
|
|
@ -166,7 +166,7 @@ function M.apply(lines, hidden_ids)
|
|||
end
|
||||
end
|
||||
else
|
||||
store.add({
|
||||
s:add({
|
||||
description = entry.description,
|
||||
category = entry.category,
|
||||
priority = entry.priority,
|
||||
|
|
@ -188,7 +188,7 @@ function M.apply(lines, hidden_ids)
|
|||
end
|
||||
end
|
||||
|
||||
store.save()
|
||||
s:save()
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -12,36 +12,47 @@ function M.check()
|
|||
|
||||
local cfg = config.get()
|
||||
vim.health.ok('Config loaded')
|
||||
vim.health.info('Data path: ' .. cfg.data_path)
|
||||
|
||||
local data_dir = vim.fn.fnamemodify(cfg.data_path, ':h')
|
||||
local store_ok, store = pcall(require, 'pending.store')
|
||||
if not store_ok then
|
||||
vim.health.error('Failed to load pending.store')
|
||||
return
|
||||
end
|
||||
|
||||
local resolved_path = store.resolve_path()
|
||||
vim.health.info('Store path: ' .. resolved_path)
|
||||
if resolved_path ~= cfg.data_path then
|
||||
vim.health.info('(project-local store; global path: ' .. cfg.data_path .. ')')
|
||||
end
|
||||
|
||||
local data_dir = vim.fn.fnamemodify(resolved_path, ':h')
|
||||
if vim.fn.isdirectory(data_dir) == 1 then
|
||||
vim.health.ok('Data directory exists: ' .. data_dir)
|
||||
else
|
||||
vim.health.warn('Data directory does not exist yet: ' .. data_dir)
|
||||
end
|
||||
|
||||
if vim.fn.filereadable(cfg.data_path) == 1 then
|
||||
local store_ok, store = pcall(require, 'pending.store')
|
||||
if store_ok then
|
||||
local load_ok, err = pcall(store.load)
|
||||
if load_ok then
|
||||
local tasks = store.tasks()
|
||||
vim.health.ok('Data file loaded: ' .. #tasks .. ' tasks')
|
||||
local recur = require('pending.recur')
|
||||
local invalid_count = 0
|
||||
for _, task in ipairs(tasks) do
|
||||
if task.recur and not recur.validate(task.recur) then
|
||||
invalid_count = invalid_count + 1
|
||||
vim.health.warn('Task ' .. task.id .. ' has invalid recurrence spec: ' .. task.recur)
|
||||
end
|
||||
if vim.fn.filereadable(resolved_path) == 1 then
|
||||
local s = store.new(resolved_path)
|
||||
local load_ok, err = pcall(function()
|
||||
s:load()
|
||||
end)
|
||||
if load_ok then
|
||||
local tasks = s:tasks()
|
||||
vim.health.ok('Data file loaded: ' .. #tasks .. ' tasks')
|
||||
local recur = require('pending.recur')
|
||||
local invalid_count = 0
|
||||
for _, task in ipairs(tasks) do
|
||||
if task.recur and not recur.validate(task.recur) then
|
||||
invalid_count = invalid_count + 1
|
||||
vim.health.warn('Task ' .. task.id .. ' has invalid recurrence spec: ' .. task.recur)
|
||||
end
|
||||
if invalid_count == 0 then
|
||||
vim.health.ok('All recurrence specs are valid')
|
||||
end
|
||||
else
|
||||
vim.health.error('Failed to load data file: ' .. tostring(err))
|
||||
end
|
||||
if invalid_count == 0 then
|
||||
vim.health.ok('All recurrence specs are valid')
|
||||
end
|
||||
else
|
||||
vim.health.error('Failed to load data file: ' .. tostring(err))
|
||||
end
|
||||
else
|
||||
vim.health.info('No data file yet (will be created on first save)')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
local buffer = require('pending.buffer')
|
||||
local config = require('pending.config')
|
||||
local diff = require('pending.diff')
|
||||
local parse = require('pending.parse')
|
||||
local store = require('pending.store')
|
||||
|
|
@ -19,6 +18,22 @@ local UNDO_MAX = 20
|
|||
---@type pending.Counts?
|
||||
local _counts = nil
|
||||
|
||||
---@type pending.Store?
|
||||
local _store = nil
|
||||
|
||||
---@return pending.Store
|
||||
local function get_store()
|
||||
if not _store then
|
||||
_store = store.new(store.resolve_path())
|
||||
end
|
||||
return _store
|
||||
end
|
||||
|
||||
---@return pending.Store
|
||||
function M.store()
|
||||
return get_store()
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M._recompute_counts()
|
||||
local cfg = require('pending.config').get()
|
||||
|
|
@ -30,7 +45,7 @@ function M._recompute_counts()
|
|||
local next_due = nil ---@type string?
|
||||
local today_str = os.date('%Y-%m-%d') --[[@as string]]
|
||||
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
for _, task in ipairs(get_store():active_tasks()) do
|
||||
if task.status == 'pending' then
|
||||
pending = pending + 1
|
||||
if task.priority > 0 then
|
||||
|
|
@ -63,14 +78,14 @@ end
|
|||
|
||||
---@return nil
|
||||
local function _save_and_notify()
|
||||
store.save()
|
||||
get_store():save()
|
||||
M._recompute_counts()
|
||||
end
|
||||
|
||||
---@return pending.Counts
|
||||
function M.counts()
|
||||
if not _counts then
|
||||
store.load()
|
||||
get_store():load()
|
||||
M._recompute_counts()
|
||||
end
|
||||
return _counts --[[@as pending.Counts]]
|
||||
|
|
@ -138,6 +153,8 @@ end
|
|||
|
||||
---@return integer bufnr
|
||||
function M.open()
|
||||
local s = get_store()
|
||||
buffer.set_store(s)
|
||||
local bufnr = buffer.open()
|
||||
M._setup_autocmds(bufnr)
|
||||
M._setup_buf_mappings(bufnr)
|
||||
|
|
@ -159,7 +176,7 @@ function M.filter(pred_str)
|
|||
for word in pred_str:gmatch('%S+') do
|
||||
table.insert(predicates, word)
|
||||
end
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = get_store():active_tasks()
|
||||
local hidden = compute_hidden_ids(tasks, predicates)
|
||||
buffer.set_filter(predicates, hidden)
|
||||
local bufnr = buffer.bufnr()
|
||||
|
|
@ -184,7 +201,7 @@ function M._setup_autocmds(bufnr)
|
|||
buffer = bufnr,
|
||||
callback = function()
|
||||
if not vim.bo[bufnr].modified then
|
||||
store.load()
|
||||
get_store():load()
|
||||
buffer.render(bufnr)
|
||||
end
|
||||
end,
|
||||
|
|
@ -333,29 +350,31 @@ function M._on_write(bufnr)
|
|||
elseif #buffer.filter_predicates() > 0 then
|
||||
predicates = {}
|
||||
end
|
||||
local tasks = store.active_tasks()
|
||||
local s = get_store()
|
||||
local tasks = s:active_tasks()
|
||||
local hidden = compute_hidden_ids(tasks, predicates)
|
||||
buffer.set_filter(predicates, hidden)
|
||||
local snapshot = store.snapshot()
|
||||
local stack = store.undo_stack()
|
||||
local snapshot = s:snapshot()
|
||||
local stack = s:undo_stack()
|
||||
table.insert(stack, snapshot)
|
||||
if #stack > UNDO_MAX then
|
||||
table.remove(stack, 1)
|
||||
end
|
||||
diff.apply(lines, hidden)
|
||||
diff.apply(lines, s, hidden)
|
||||
M._recompute_counts()
|
||||
buffer.render(bufnr)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.undo_write()
|
||||
local stack = store.undo_stack()
|
||||
local s = get_store()
|
||||
local stack = s:undo_stack()
|
||||
if #stack == 0 then
|
||||
vim.notify('Nothing to undo.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local state = table.remove(stack)
|
||||
store.replace_tasks(state)
|
||||
s:replace_tasks(state)
|
||||
_save_and_notify()
|
||||
buffer.render(buffer.bufnr())
|
||||
end
|
||||
|
|
@ -375,18 +394,19 @@ function M.toggle_complete()
|
|||
if not id then
|
||||
return
|
||||
end
|
||||
local task = store.get(id)
|
||||
local s = get_store()
|
||||
local task = s:get(id)
|
||||
if not task then
|
||||
return
|
||||
end
|
||||
if task.status == 'done' then
|
||||
store.update(id, { status = 'pending', ['end'] = vim.NIL })
|
||||
s:update(id, { status = 'pending', ['end'] = vim.NIL })
|
||||
else
|
||||
if task.recur and task.due then
|
||||
local recur = require('pending.recur')
|
||||
local mode = task.recur_mode or 'scheduled'
|
||||
local next_date = recur.next_due(task.due, task.recur, mode)
|
||||
store.add({
|
||||
s:add({
|
||||
description = task.description,
|
||||
category = task.category,
|
||||
priority = task.priority,
|
||||
|
|
@ -395,7 +415,7 @@ function M.toggle_complete()
|
|||
recur_mode = task.recur_mode,
|
||||
})
|
||||
end
|
||||
store.update(id, { status = 'done' })
|
||||
s:update(id, { status = 'done' })
|
||||
end
|
||||
_save_and_notify()
|
||||
buffer.render(bufnr)
|
||||
|
|
@ -422,12 +442,13 @@ function M.toggle_priority()
|
|||
if not id then
|
||||
return
|
||||
end
|
||||
local task = store.get(id)
|
||||
local s = get_store()
|
||||
local task = s:get(id)
|
||||
if not task then
|
||||
return
|
||||
end
|
||||
local new_priority = task.priority > 0 and 0 or 1
|
||||
store.update(id, { priority = new_priority })
|
||||
s:update(id, { priority = new_priority })
|
||||
_save_and_notify()
|
||||
buffer.render(bufnr)
|
||||
for lnum, m in ipairs(buffer.meta()) do
|
||||
|
|
@ -470,7 +491,7 @@ function M.prompt_date()
|
|||
return
|
||||
end
|
||||
end
|
||||
store.update(id, { due = due })
|
||||
get_store():update(id, { due = due })
|
||||
_save_and_notify()
|
||||
buffer.render(bufnr)
|
||||
end)
|
||||
|
|
@ -483,13 +504,14 @@ function M.add(text)
|
|||
vim.notify('Usage: :Pending add <description>', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
store.load()
|
||||
local s = get_store()
|
||||
s:load()
|
||||
local description, metadata = parse.command_add(text)
|
||||
if not description or description == '' then
|
||||
vim.notify('Pending must have a description.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
store.add({
|
||||
s:add({
|
||||
description = description,
|
||||
category = metadata.cat,
|
||||
due = metadata.due,
|
||||
|
|
@ -530,12 +552,13 @@ end
|
|||
function M.archive(days)
|
||||
days = days or 30
|
||||
local cutoff = os.time() - (days * 86400)
|
||||
local tasks = store.tasks()
|
||||
local s = get_store()
|
||||
local tasks = s:tasks()
|
||||
local archived = 0
|
||||
local kept = {}
|
||||
for _, task in ipairs(tasks) do
|
||||
if (task.status == 'done' or task.status == 'deleted') and task['end'] then
|
||||
local y, mo, d, h, mi, s = task['end']:match('^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z$')
|
||||
local y, mo, d, h, mi, sec = task['end']:match('^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z$')
|
||||
if y then
|
||||
local t = os.time({
|
||||
year = tonumber(y) --[[@as integer]],
|
||||
|
|
@ -543,7 +566,7 @@ function M.archive(days)
|
|||
day = tonumber(d) --[[@as integer]],
|
||||
hour = tonumber(h) --[[@as integer]],
|
||||
min = tonumber(mi) --[[@as integer]],
|
||||
sec = tonumber(s) --[[@as integer]],
|
||||
sec = tonumber(sec) --[[@as integer]],
|
||||
})
|
||||
if t < cutoff then
|
||||
archived = archived + 1
|
||||
|
|
@ -554,7 +577,7 @@ function M.archive(days)
|
|||
table.insert(kept, task)
|
||||
::skip::
|
||||
end
|
||||
store.replace_tasks(kept)
|
||||
s:replace_tasks(kept)
|
||||
_save_and_notify()
|
||||
vim.notify('Archived ' .. archived .. ' tasks.')
|
||||
local bufnr = buffer.bufnr()
|
||||
|
|
@ -578,7 +601,7 @@ function M.due()
|
|||
and m.status ~= 'done'
|
||||
and (parse.is_overdue(m.raw_due) or parse.is_today(m.raw_due))
|
||||
then
|
||||
local task = store.get(m.id or 0)
|
||||
local task = get_store():get(m.id or 0)
|
||||
local label = parse.is_overdue(m.raw_due) and '[OVERDUE] ' or '[DUE] '
|
||||
table.insert(qf_items, {
|
||||
bufnr = bufnr,
|
||||
|
|
@ -589,8 +612,9 @@ function M.due()
|
|||
end
|
||||
end
|
||||
else
|
||||
store.load()
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
local s = get_store()
|
||||
s:load()
|
||||
for _, task in ipairs(s:active_tasks()) do
|
||||
if
|
||||
task.status == 'pending'
|
||||
and task.due
|
||||
|
|
@ -712,8 +736,9 @@ function M.edit(id_str, rest)
|
|||
return
|
||||
end
|
||||
|
||||
store.load()
|
||||
local task = store.get(id)
|
||||
local s = get_store()
|
||||
s:load()
|
||||
local task = s:get(id)
|
||||
if not task then
|
||||
vim.notify('No task with ID ' .. id .. '.', vim.log.levels.ERROR)
|
||||
return
|
||||
|
|
@ -776,17 +801,17 @@ function M.edit(id_str, rest)
|
|||
end
|
||||
end
|
||||
|
||||
local snapshot = store.snapshot()
|
||||
local stack = store.undo_stack()
|
||||
local snapshot = s:snapshot()
|
||||
local stack = s:undo_stack()
|
||||
table.insert(stack, snapshot)
|
||||
if #stack > UNDO_MAX then
|
||||
table.remove(stack, 1)
|
||||
end
|
||||
|
||||
store.update(id, updates)
|
||||
s:update(id, updates)
|
||||
|
||||
if updates.file_clear then
|
||||
local t = store.get(id)
|
||||
local t = s:get(id)
|
||||
if t and t._extra then
|
||||
t._extra.file = nil
|
||||
if next(t._extra) == nil then
|
||||
|
|
@ -796,7 +821,7 @@ function M.edit(id_str, rest)
|
|||
end
|
||||
end
|
||||
|
||||
store.save()
|
||||
s:save()
|
||||
|
||||
local bufnr = buffer.bufnr()
|
||||
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
||||
|
|
@ -819,7 +844,7 @@ function M.goto_file()
|
|||
vim.notify('No task on this line', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local task = store.get(m.id)
|
||||
local task = get_store():get(m.id)
|
||||
if not task or not task._extra or not task._extra.file then
|
||||
vim.notify('No file attached to this task', vim.log.levels.WARN)
|
||||
return
|
||||
|
|
@ -830,7 +855,7 @@ function M.goto_file()
|
|||
vim.notify('Invalid file spec: ' .. file_spec, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local data_dir = vim.fn.fnamemodify(config.get().data_path, ':h')
|
||||
local data_dir = vim.fn.fnamemodify(get_store().path, ':h')
|
||||
local abs_path = data_dir .. '/' .. rel_path
|
||||
if vim.fn.filereadable(abs_path) == 0 then
|
||||
vim.notify('File not found: ' .. abs_path, vim.log.levels.ERROR)
|
||||
|
|
@ -854,7 +879,8 @@ function M.add_here()
|
|||
return
|
||||
end
|
||||
local cur_lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local data_dir = vim.fn.fnamemodify(config.get().data_path, ':h')
|
||||
local s = get_store()
|
||||
local data_dir = vim.fn.fnamemodify(s.path, ':h')
|
||||
local abs_file = vim.fn.fnamemodify(cur_file, ':p')
|
||||
local rel_file
|
||||
if abs_file:sub(1, #data_dir + 1) == data_dir .. '/' then
|
||||
|
|
@ -863,8 +889,8 @@ function M.add_here()
|
|||
rel_file = abs_file
|
||||
end
|
||||
local file_spec = rel_file .. ':' .. cur_lnum
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
if #tasks == 0 then
|
||||
vim.notify('No active tasks', vim.log.levels.INFO)
|
||||
return
|
||||
|
|
@ -885,11 +911,24 @@ function M.add_here()
|
|||
task._extra = task._extra or {}
|
||||
task._extra.file = file_spec
|
||||
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
||||
store.save()
|
||||
s:save()
|
||||
vim.notify('Attached ' .. file_spec .. ' to task ' .. task.id)
|
||||
end)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.init()
|
||||
local path = vim.fn.getcwd() .. '/.pending.json'
|
||||
if vim.fn.filereadable(path) == 1 then
|
||||
vim.notify('pending.nvim: .pending.json already exists', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local s = store.new(path)
|
||||
s:load()
|
||||
s:save()
|
||||
vim.notify('pending.nvim: created ' .. path)
|
||||
end
|
||||
|
||||
---@param args string
|
||||
---@return nil
|
||||
function M.command(args)
|
||||
|
|
@ -915,6 +954,8 @@ function M.command(args)
|
|||
M.filter(rest)
|
||||
elseif cmd == 'undo' then
|
||||
M.undo_write()
|
||||
elseif cmd == 'init' then
|
||||
M.init()
|
||||
else
|
||||
vim.notify('Unknown Pending subcommand: ' .. cmd, vim.log.levels.ERROR)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,14 +21,17 @@ local config = require('pending.config')
|
|||
---@field tasks pending.Task[]
|
||||
---@field undo pending.Task[][]
|
||||
|
||||
---@class pending.Store
|
||||
---@field path string
|
||||
---@field _data pending.Data?
|
||||
local Store = {}
|
||||
Store.__index = Store
|
||||
|
||||
---@class pending.store
|
||||
local M = {}
|
||||
|
||||
local SUPPORTED_VERSION = 1
|
||||
|
||||
---@type pending.Data?
|
||||
local _data = nil
|
||||
|
||||
---@return pending.Data
|
||||
local function empty_data()
|
||||
return {
|
||||
|
|
@ -137,18 +140,18 @@ local function table_to_task(t)
|
|||
end
|
||||
|
||||
---@return pending.Data
|
||||
function M.load()
|
||||
local path = config.get().data_path
|
||||
function Store:load()
|
||||
local path = self.path
|
||||
local f = io.open(path, 'r')
|
||||
if not f then
|
||||
_data = empty_data()
|
||||
return _data
|
||||
self._data = empty_data()
|
||||
return self._data
|
||||
end
|
||||
local content = f:read('*a')
|
||||
f:close()
|
||||
if content == '' then
|
||||
_data = empty_data()
|
||||
return _data
|
||||
self._data = empty_data()
|
||||
return self._data
|
||||
end
|
||||
local ok, decoded = pcall(vim.json.decode, content)
|
||||
if not ok then
|
||||
|
|
@ -163,14 +166,14 @@ function M.load()
|
|||
.. '. Please update the plugin.'
|
||||
)
|
||||
end
|
||||
_data = {
|
||||
self._data = {
|
||||
version = decoded.version or SUPPORTED_VERSION,
|
||||
next_id = decoded.next_id or 1,
|
||||
tasks = {},
|
||||
undo = {},
|
||||
}
|
||||
for _, t in ipairs(decoded.tasks or {}) do
|
||||
table.insert(_data.tasks, table_to_task(t))
|
||||
table.insert(self._data.tasks, table_to_task(t))
|
||||
end
|
||||
for _, snapshot in ipairs(decoded.undo or {}) do
|
||||
if type(snapshot) == 'table' then
|
||||
|
|
@ -178,29 +181,29 @@ function M.load()
|
|||
for _, raw in ipairs(snapshot) do
|
||||
table.insert(tasks, table_to_task(raw))
|
||||
end
|
||||
table.insert(_data.undo, tasks)
|
||||
table.insert(self._data.undo, tasks)
|
||||
end
|
||||
end
|
||||
return _data
|
||||
return self._data
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.save()
|
||||
if not _data then
|
||||
function Store:save()
|
||||
if not self._data then
|
||||
return
|
||||
end
|
||||
local path = config.get().data_path
|
||||
local path = self.path
|
||||
ensure_dir(path)
|
||||
local out = {
|
||||
version = _data.version,
|
||||
next_id = _data.next_id,
|
||||
version = self._data.version,
|
||||
next_id = self._data.next_id,
|
||||
tasks = {},
|
||||
undo = {},
|
||||
}
|
||||
for _, task in ipairs(_data.tasks) do
|
||||
for _, task in ipairs(self._data.tasks) do
|
||||
table.insert(out.tasks, task_to_table(task))
|
||||
end
|
||||
for _, snapshot in ipairs(_data.undo) do
|
||||
for _, snapshot in ipairs(self._data.undo) do
|
||||
local serialized = {}
|
||||
for _, task in ipairs(snapshot) do
|
||||
table.insert(serialized, task_to_table(task))
|
||||
|
|
@ -223,22 +226,22 @@ function M.save()
|
|||
end
|
||||
|
||||
---@return pending.Data
|
||||
function M.data()
|
||||
if not _data then
|
||||
M.load()
|
||||
function Store:data()
|
||||
if not self._data then
|
||||
self:load()
|
||||
end
|
||||
return _data --[[@as pending.Data]]
|
||||
return self._data --[[@as pending.Data]]
|
||||
end
|
||||
|
||||
---@return pending.Task[]
|
||||
function M.tasks()
|
||||
return M.data().tasks
|
||||
function Store:tasks()
|
||||
return self:data().tasks
|
||||
end
|
||||
|
||||
---@return pending.Task[]
|
||||
function M.active_tasks()
|
||||
function Store:active_tasks()
|
||||
local result = {}
|
||||
for _, task in ipairs(M.tasks()) do
|
||||
for _, task in ipairs(self:tasks()) do
|
||||
if task.status ~= 'deleted' then
|
||||
table.insert(result, task)
|
||||
end
|
||||
|
|
@ -248,8 +251,8 @@ end
|
|||
|
||||
---@param id integer
|
||||
---@return pending.Task?
|
||||
function M.get(id)
|
||||
for _, task in ipairs(M.tasks()) do
|
||||
function Store:get(id)
|
||||
for _, task in ipairs(self:tasks()) do
|
||||
if task.id == id then
|
||||
return task
|
||||
end
|
||||
|
|
@ -259,8 +262,8 @@ end
|
|||
|
||||
---@param fields { description: string, status?: string, category?: string, priority?: integer, due?: string, recur?: string, recur_mode?: string, order?: integer, _extra?: table }
|
||||
---@return pending.Task
|
||||
function M.add(fields)
|
||||
local data = M.data()
|
||||
function Store:add(fields)
|
||||
local data = self:data()
|
||||
local now = timestamp()
|
||||
local task = {
|
||||
id = data.next_id,
|
||||
|
|
@ -285,8 +288,8 @@ end
|
|||
---@param id integer
|
||||
---@param fields table<string, any>
|
||||
---@return pending.Task?
|
||||
function M.update(id, fields)
|
||||
local task = M.get(id)
|
||||
function Store:update(id, fields)
|
||||
local task = self:get(id)
|
||||
if not task then
|
||||
return nil
|
||||
end
|
||||
|
|
@ -309,14 +312,14 @@ end
|
|||
|
||||
---@param id integer
|
||||
---@return pending.Task?
|
||||
function M.delete(id)
|
||||
return M.update(id, { status = 'deleted', ['end'] = timestamp() })
|
||||
function Store:delete(id)
|
||||
return self:update(id, { status = 'deleted', ['end'] = timestamp() })
|
||||
end
|
||||
|
||||
---@param id integer
|
||||
---@return integer?
|
||||
function M.find_index(id)
|
||||
for i, task in ipairs(M.tasks()) do
|
||||
function Store:find_index(id)
|
||||
for i, task in ipairs(self:tasks()) do
|
||||
if task.id == id then
|
||||
return i
|
||||
end
|
||||
|
|
@ -326,14 +329,14 @@ end
|
|||
|
||||
---@param tasks pending.Task[]
|
||||
---@return nil
|
||||
function M.replace_tasks(tasks)
|
||||
M.data().tasks = tasks
|
||||
function Store:replace_tasks(tasks)
|
||||
self:data().tasks = tasks
|
||||
end
|
||||
|
||||
---@return pending.Task[]
|
||||
function M.snapshot()
|
||||
function Store:snapshot()
|
||||
local result = {}
|
||||
for _, task in ipairs(M.active_tasks()) do
|
||||
for _, task in ipairs(self:active_tasks()) do
|
||||
local copy = {}
|
||||
for k, v in pairs(task) do
|
||||
if k ~= '_extra' then
|
||||
|
|
@ -352,25 +355,44 @@ function M.snapshot()
|
|||
end
|
||||
|
||||
---@return pending.Task[][]
|
||||
function M.undo_stack()
|
||||
return M.data().undo
|
||||
function Store:undo_stack()
|
||||
return self:data().undo
|
||||
end
|
||||
|
||||
---@param stack pending.Task[][]
|
||||
---@return nil
|
||||
function M.set_undo_stack(stack)
|
||||
M.data().undo = stack
|
||||
function Store:set_undo_stack(stack)
|
||||
self:data().undo = stack
|
||||
end
|
||||
|
||||
---@param id integer
|
||||
---@return nil
|
||||
function M.set_next_id(id)
|
||||
M.data().next_id = id
|
||||
function Store:set_next_id(id)
|
||||
self:data().next_id = id
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.unload()
|
||||
_data = nil
|
||||
function Store:unload()
|
||||
self._data = nil
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return pending.Store
|
||||
function M.new(path)
|
||||
return setmetatable({ path = path, _data = nil }, Store)
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.resolve_path()
|
||||
local results = vim.fs.find('.pending.json', {
|
||||
upward = true,
|
||||
path = vim.fn.getcwd(),
|
||||
type = 'file',
|
||||
})
|
||||
if results and #results > 0 then
|
||||
return results[1]
|
||||
end
|
||||
return config.get().data_path
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
local M = {}
|
||||
|
||||
|
|
@ -458,7 +457,7 @@ function M.sync()
|
|||
return
|
||||
end
|
||||
|
||||
local tasks = store.tasks()
|
||||
local tasks = require('pending').store():tasks()
|
||||
local created, updated, deleted = 0, 0, 0
|
||||
|
||||
for _, task in ipairs(tasks) do
|
||||
|
|
@ -504,7 +503,7 @@ function M.sync()
|
|||
end
|
||||
end
|
||||
|
||||
store.save()
|
||||
require('pending').store():save()
|
||||
require('pending')._recompute_counts()
|
||||
vim.notify(
|
||||
string.format(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue