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
64b19360b1
commit
0e0568769d
19 changed files with 819 additions and 703 deletions
|
|
@ -356,6 +356,13 @@ COMMANDS *pending-commands*
|
|||
Equivalent to the `U` buffer-local key (see |pending-mappings|). Up to 20
|
||||
levels of undo are persisted across sessions.
|
||||
|
||||
*:Pending-init*
|
||||
:Pending init
|
||||
Create a project-local `.pending.json` file in the current working
|
||||
directory. After creation, `:Pending` will use this file instead of the
|
||||
global store (see |pending-store-resolution|). Errors if `.pending.json`
|
||||
already exists in the current directory.
|
||||
|
||||
*:PendingTab*
|
||||
:PendingTab
|
||||
Open the task buffer in a new tab.
|
||||
|
|
@ -614,9 +621,11 @@ All fields are optional. Unset fields use the defaults shown above.
|
|||
*pending.Config*
|
||||
Fields: ~
|
||||
{data_path} (string)
|
||||
Path to the JSON file where tasks are stored.
|
||||
Path to the global JSON file where tasks are stored.
|
||||
Default: `stdpath('data') .. '/pending/tasks.json'`.
|
||||
The directory is created automatically on first save.
|
||||
See |pending-store-resolution| for how the active
|
||||
store is chosen at runtime.
|
||||
|
||||
{default_view} ('category'|'priority', default: 'category')
|
||||
The view to use when the buffer is opened for the
|
||||
|
|
@ -1060,10 +1069,31 @@ Checks performed: ~
|
|||
- Discovers sync backends under `lua/pending/sync/` and runs each backend's
|
||||
`health()` function if it exists (e.g. gcal checks for `curl` and `openssl`)
|
||||
|
||||
==============================================================================
|
||||
STORE RESOLUTION *pending-store-resolution*
|
||||
|
||||
When pending.nvim opens the task buffer it resolves which store file to use:
|
||||
|
||||
1. Search upward from `vim.fn.getcwd()` for a file named `.pending.json`.
|
||||
2. If found, use that file as the active store (project-local store).
|
||||
3. If not found, fall back to `data_path` from |pending-config| (global
|
||||
store).
|
||||
|
||||
This means placing a `.pending.json` file in a project root makes that
|
||||
project use an isolated task list. Tasks in the project store are completely
|
||||
separate from tasks in the global store; there is no aggregation.
|
||||
|
||||
To create a project-local store in the current directory: >vim
|
||||
:Pending init
|
||||
<
|
||||
|
||||
The `:checkhealth pending` report shows which store file is currently active.
|
||||
|
||||
==============================================================================
|
||||
DATA FORMAT *pending-data*
|
||||
|
||||
Tasks are stored as JSON at `data_path`. The file is safe to edit by hand and
|
||||
Tasks are stored as JSON at the active store path (see
|
||||
|pending-store-resolution|). The file is safe to edit by hand and
|
||||
is forward-compatible — unknown fields are preserved on every read/write cycle
|
||||
via the `_extra` table.
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -100,9 +100,10 @@ local function complete_edit(arg_lead, cmd_line)
|
|||
local trailing_space = after_edit:match('%s$')
|
||||
if #parts == 0 or (#parts == 1 and not trailing_space) then
|
||||
local store = require('pending.store')
|
||||
store.load()
|
||||
local s = store.new(store.resolve_path())
|
||||
s:load()
|
||||
local ids = {}
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
for _, task in ipairs(s:active_tasks()) do
|
||||
table.insert(ids, tostring(task.id))
|
||||
end
|
||||
return filter_candidates(arg_lead, ids)
|
||||
|
|
@ -138,10 +139,11 @@ local function complete_edit(arg_lead, cmd_line)
|
|||
if cat_prefix then
|
||||
local after_colon = arg_lead:sub(#cat_prefix + 1)
|
||||
local store = require('pending.store')
|
||||
store.load()
|
||||
local s = store.new(store.resolve_path())
|
||||
s:load()
|
||||
local seen = {}
|
||||
local cats = {}
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
for _, task in ipairs(s:active_tasks()) do
|
||||
if task.category and not seen[task.category] then
|
||||
seen[task.category] = true
|
||||
table.insert(cats, task.category)
|
||||
|
|
@ -166,7 +168,7 @@ end, {
|
|||
bar = true,
|
||||
nargs = '*',
|
||||
complete = function(arg_lead, cmd_line)
|
||||
local subcmds = { 'add', 'archive', 'due', 'edit', 'filter', 'sync', 'undo' }
|
||||
local subcmds = { 'add', 'archive', 'due', 'edit', 'filter', 'init', 'sync', 'undo' }
|
||||
if not cmd_line:match('^Pending%s+%S') then
|
||||
return filter_candidates(arg_lead, subcmds)
|
||||
end
|
||||
|
|
@ -178,9 +180,10 @@ end, {
|
|||
end
|
||||
local candidates = { 'clear', 'overdue', 'today', 'priority' }
|
||||
local store = require('pending.store')
|
||||
store.load()
|
||||
local s = store.new(store.resolve_path())
|
||||
s:load()
|
||||
local seen = {}
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
for _, task in ipairs(s:active_tasks()) do
|
||||
if task.category and not seen[task.category] then
|
||||
seen[task.category] = true
|
||||
table.insert(candidates, 'cat:' .. task.category)
|
||||
|
|
|
|||
|
|
@ -1,87 +1,96 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('archive', function()
|
||||
local tmpdir
|
||||
local pending = require('pending')
|
||||
local pending
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
it('removes done tasks completed more than 30 days ago', function()
|
||||
local t = store.add({ description = 'Old done task' })
|
||||
store.update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Old done task' })
|
||||
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
pending.archive()
|
||||
assert.are.equal(0, #store.active_tasks())
|
||||
assert.are.equal(0, #s:active_tasks())
|
||||
end)
|
||||
|
||||
it('keeps done tasks completed fewer than 30 days ago', function()
|
||||
local s = pending.store()
|
||||
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t = store.add({ description = 'Recent done task' })
|
||||
store.update(t.id, { status = 'done', ['end'] = recent_end })
|
||||
local t = s:add({ description = 'Recent done task' })
|
||||
s:update(t.id, { status = 'done', ['end'] = recent_end })
|
||||
pending.archive()
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('Recent done task', active[1].description)
|
||||
end)
|
||||
|
||||
it('respects a custom day count', function()
|
||||
local s = pending.store()
|
||||
local eight_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (8 * 86400))
|
||||
local t = store.add({ description = 'Old for 7 days' })
|
||||
store.update(t.id, { status = 'done', ['end'] = eight_days_ago })
|
||||
local t = s:add({ description = 'Old for 7 days' })
|
||||
s:update(t.id, { status = 'done', ['end'] = eight_days_ago })
|
||||
pending.archive(7)
|
||||
assert.are.equal(0, #store.active_tasks())
|
||||
assert.are.equal(0, #s:active_tasks())
|
||||
end)
|
||||
|
||||
it('keeps tasks within the custom day cutoff', function()
|
||||
local s = pending.store()
|
||||
local five_days_ago = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t = store.add({ description = 'Recent for 7 days' })
|
||||
store.update(t.id, { status = 'done', ['end'] = five_days_ago })
|
||||
local t = s:add({ description = 'Recent for 7 days' })
|
||||
s:update(t.id, { status = 'done', ['end'] = five_days_ago })
|
||||
pending.archive(7)
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
end)
|
||||
|
||||
it('never archives pending tasks regardless of age', function()
|
||||
store.add({ description = 'Still pending' })
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Still pending' })
|
||||
pending.archive()
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('pending', active[1].status)
|
||||
end)
|
||||
|
||||
it('removes deleted tasks past the cutoff', function()
|
||||
local t = store.add({ description = 'Old deleted task' })
|
||||
store.update(t.id, { status = 'deleted', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Old deleted task' })
|
||||
s:update(t.id, { status = 'deleted', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
pending.archive()
|
||||
local all = store.tasks()
|
||||
local all = s:tasks()
|
||||
assert.are.equal(0, #all)
|
||||
end)
|
||||
|
||||
it('keeps deleted tasks within the cutoff', function()
|
||||
local s = pending.store()
|
||||
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t = store.add({ description = 'Recent deleted' })
|
||||
store.update(t.id, { status = 'deleted', ['end'] = recent_end })
|
||||
local t = s:add({ description = 'Recent deleted' })
|
||||
s:update(t.id, { status = 'deleted', ['end'] = recent_end })
|
||||
pending.archive()
|
||||
local all = store.tasks()
|
||||
local all = s:tasks()
|
||||
assert.are.equal(1, #all)
|
||||
end)
|
||||
|
||||
it('reports the correct count in vim.notify', function()
|
||||
local s = pending.store()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, ...)
|
||||
|
|
@ -89,11 +98,11 @@ describe('archive', function()
|
|||
return orig_notify(msg, ...)
|
||||
end
|
||||
|
||||
local t1 = store.add({ description = 'Old 1' })
|
||||
local t2 = store.add({ description = 'Old 2' })
|
||||
store.add({ description = 'Keep' })
|
||||
store.update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
store.update(t2.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local t1 = s:add({ description = 'Old 1' })
|
||||
local t2 = s:add({ description = 'Old 2' })
|
||||
s:add({ description = 'Keep' })
|
||||
s:update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
s:update(t2.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
|
||||
pending.archive()
|
||||
|
||||
|
|
@ -110,16 +119,17 @@ describe('archive', function()
|
|||
end)
|
||||
|
||||
it('leaves only kept tasks in store.active_tasks after archive', function()
|
||||
local t1 = store.add({ description = 'Old done' })
|
||||
store.add({ description = 'Keep pending' })
|
||||
local s = pending.store()
|
||||
local t1 = s:add({ description = 'Old done' })
|
||||
s:add({ description = 'Keep pending' })
|
||||
local recent_end = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (5 * 86400))
|
||||
local t3 = store.add({ description = 'Keep recent done' })
|
||||
store.update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
store.update(t3.id, { status = 'done', ['end'] = recent_end })
|
||||
local t3 = s:add({ description = 'Keep recent done' })
|
||||
s:update(t1.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
s:update(t3.id, { status = 'done', ['end'] = recent_end })
|
||||
|
||||
pending.archive()
|
||||
|
||||
local active = store.active_tasks()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(2, #active)
|
||||
local descs = {}
|
||||
for _, task in ipairs(active) do
|
||||
|
|
@ -130,11 +140,11 @@ describe('archive', function()
|
|||
end)
|
||||
|
||||
it('persists archived tasks to disk after unload/reload', function()
|
||||
local t = store.add({ description = 'Archived task' })
|
||||
store.update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Archived task' })
|
||||
s:update(t.id, { status = 'done', ['end'] = '2020-01-01T00:00:00Z' })
|
||||
pending.archive()
|
||||
store.unload()
|
||||
store.load()
|
||||
assert.are.equal(0, #store.active_tasks())
|
||||
s:load()
|
||||
assert.are.equal(0, #s:active_tasks())
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local buffer = require('pending.buffer')
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('complete', function()
|
||||
local tmpdir
|
||||
local s
|
||||
local complete = require('pending.complete')
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
buffer.set_store(s)
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
buffer.set_store(nil)
|
||||
end)
|
||||
|
||||
describe('findstart', function()
|
||||
|
|
@ -66,9 +68,9 @@ describe('complete', function()
|
|||
|
||||
describe('completions', function()
|
||||
it('returns existing categories for cat:', function()
|
||||
store.add({ description = 'A', category = 'Work' })
|
||||
store.add({ description = 'B', category = 'Home' })
|
||||
store.add({ description = 'C', category = 'Work' })
|
||||
s:add({ description = 'A', category = 'Work' })
|
||||
s:add({ description = 'B', category = 'Home' })
|
||||
s:add({ description = 'C', category = 'Work' })
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat: x' })
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
@ -85,8 +87,8 @@ describe('complete', function()
|
|||
end)
|
||||
|
||||
it('filters categories by base', function()
|
||||
store.add({ description = 'A', category = 'Work' })
|
||||
store.add({ description = 'B', category = 'Home' })
|
||||
s:add({ description = 'A', category = 'Work' })
|
||||
s:add({ description = 'B', category = 'Home' })
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat:W' })
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('diff', function()
|
||||
local tmpdir
|
||||
local s
|
||||
local diff = require('pending.diff')
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('parse_buffer', function()
|
||||
|
|
@ -107,121 +103,112 @@ describe('diff', function()
|
|||
'- [ ] First task',
|
||||
'- [ ] Second task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(2, #tasks)
|
||||
assert.are.equal('First task', tasks[1].description)
|
||||
assert.are.equal('Second task', tasks[2].description)
|
||||
end)
|
||||
|
||||
it('deletes tasks removed from buffer', function()
|
||||
store.add({ description = 'Keep me' })
|
||||
store.add({ description = 'Delete me' })
|
||||
store.save()
|
||||
s:add({ description = 'Keep me' })
|
||||
s:add({ description = 'Delete me' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Keep me',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local active = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('Keep me', active[1].description)
|
||||
local deleted = store.get(2)
|
||||
local deleted = s:get(2)
|
||||
assert.are.equal('deleted', deleted.status)
|
||||
end)
|
||||
|
||||
it('updates modified tasks', function()
|
||||
store.add({ description = 'Original' })
|
||||
store.save()
|
||||
s:add({ description = 'Original' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Renamed',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('Renamed', task.description)
|
||||
end)
|
||||
|
||||
it('updates modified when description is renamed', function()
|
||||
local t = store.add({ description = 'Original', category = 'Inbox' })
|
||||
local t = s:add({ description = 'Original', category = 'Inbox' })
|
||||
t.modified = '2020-01-01T00:00:00Z'
|
||||
store.save()
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Renamed',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('Renamed', task.description)
|
||||
assert.is_not.equal('2020-01-01T00:00:00Z', task.modified)
|
||||
end)
|
||||
|
||||
it('handles duplicate ids as copies', function()
|
||||
store.add({ description = 'Original' })
|
||||
store.save()
|
||||
s:add({ description = 'Original' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Original',
|
||||
'/1/- [ ] Copy of original',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(2, #tasks)
|
||||
end)
|
||||
|
||||
it('moves tasks between categories', function()
|
||||
store.add({ description = 'Moving task', category = 'Inbox' })
|
||||
store.save()
|
||||
s:add({ description = 'Moving task', category = 'Inbox' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Work',
|
||||
'/1/- [ ] Moving task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('Work', task.category)
|
||||
end)
|
||||
|
||||
it('does not update modified when task is unchanged', function()
|
||||
store.add({ description = 'Stable task', category = 'Inbox' })
|
||||
store.save()
|
||||
s:add({ description = 'Stable task', category = 'Inbox' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Stable task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local modified_after_first = store.get(1).modified
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local modified_after_first = s:get(1).modified
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal(modified_after_first, task.modified)
|
||||
end)
|
||||
|
||||
it('clears due when removed from buffer line', function()
|
||||
store.add({ description = 'Pay bill', due = '2026-03-15' })
|
||||
store.save()
|
||||
s:add({ description = 'Pay bill', due = '2026-03-15' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Pay bill',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_nil(task.due)
|
||||
end)
|
||||
|
||||
|
|
@ -230,39 +217,36 @@ describe('diff', function()
|
|||
'## Inbox',
|
||||
'- [ ] Take out trash rec:weekly',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(1, #tasks)
|
||||
assert.are.equal('weekly', tasks[1].recur)
|
||||
end)
|
||||
|
||||
it('updates recur field when changed inline', function()
|
||||
store.add({ description = 'Task', recur = 'daily' })
|
||||
store.save()
|
||||
s:add({ description = 'Task', recur = 'daily' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Todo',
|
||||
'/1/- [ ] Task rec:weekly',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('weekly', task.recur)
|
||||
end)
|
||||
|
||||
it('clears recur when token removed from line', function()
|
||||
store.add({ description = 'Task', recur = 'daily' })
|
||||
store.save()
|
||||
s:add({ description = 'Task', recur = 'daily' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Todo',
|
||||
'/1/- [ ] Task',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_nil(task.recur)
|
||||
end)
|
||||
|
||||
|
|
@ -271,25 +255,23 @@ describe('diff', function()
|
|||
'## Inbox',
|
||||
'- [ ] Water plants rec:!weekly',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal('weekly', tasks[1].recur)
|
||||
assert.are.equal('completion', tasks[1].recur_mode)
|
||||
end)
|
||||
|
||||
it('clears priority when [N] is removed from buffer line', function()
|
||||
store.add({ description = 'Task name', priority = 1 })
|
||||
store.save()
|
||||
s:add({ description = 'Task name', priority = 1 })
|
||||
s:save()
|
||||
local lines = {
|
||||
'## Inbox',
|
||||
'/1/- [ ] Task name',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal(0, task.priority)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,34 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('edit', function()
|
||||
local tmpdir
|
||||
local pending = require('pending')
|
||||
local pending
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
it('sets due date with resolve_date vocabulary', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:tomorrow')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
local expected =
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 }))
|
||||
|
|
@ -34,111 +36,123 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('sets due date with literal YYYY-MM-DD', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:2026-06-15')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('2026-06-15', updated.due)
|
||||
end)
|
||||
|
||||
it('sets category', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Work', updated.category)
|
||||
end)
|
||||
|
||||
it('adds priority', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '+!')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal(1, updated.priority)
|
||||
end)
|
||||
|
||||
it('removes priority', function()
|
||||
local t = store.add({ description = 'Task one', priority = 1 })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', priority = 1 })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-!')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal(0, updated.priority)
|
||||
end)
|
||||
|
||||
it('removes due date', function()
|
||||
local t = store.add({ description = 'Task one', due = '2026-06-15' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', due = '2026-06-15' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-due')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated.due)
|
||||
end)
|
||||
|
||||
it('removes category', function()
|
||||
local t = store.add({ description = 'Task one', category = 'Work' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', category = 'Work' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-cat')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated.category)
|
||||
end)
|
||||
|
||||
it('sets recurrence', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'rec:weekly')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('weekly', updated.recur)
|
||||
assert.is_nil(updated.recur_mode)
|
||||
end)
|
||||
|
||||
it('sets completion-based recurrence', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'rec:!daily')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('daily', updated.recur)
|
||||
assert.are.equal('completion', updated.recur_mode)
|
||||
end)
|
||||
|
||||
it('removes recurrence', function()
|
||||
local t = store.add({ description = 'Task one', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-rec')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated.recur)
|
||||
assert.is_nil(updated.recur_mode)
|
||||
end)
|
||||
|
||||
it('applies multiple operations at once', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:today cat:Errands +!')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal(os.date('%Y-%m-%d'), updated.due)
|
||||
assert.are.equal('Errands', updated.category)
|
||||
assert.are.equal(1, updated.priority)
|
||||
end)
|
||||
|
||||
it('pushes to undo stack', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local stack_before = #store.undo_stack()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local stack_before = #s:undo_stack()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
assert.are.equal(stack_before + 1, #store.undo_stack())
|
||||
assert.are.equal(stack_before + 1, #s:undo_stack())
|
||||
end)
|
||||
|
||||
it('persists changes to disk', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
store.unload()
|
||||
store.load()
|
||||
local updated = store.get(t.id)
|
||||
s:load()
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Work', updated.category)
|
||||
end)
|
||||
|
||||
it('errors on unknown task ID', function()
|
||||
store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -152,8 +166,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors on invalid date', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -167,8 +182,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors on unknown operation token', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -182,8 +198,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors on invalid recurrence pattern', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -197,8 +214,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('errors when no operations given', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -238,8 +256,9 @@ describe('edit', function()
|
|||
end)
|
||||
|
||||
it('shows feedback message on success', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -255,12 +274,14 @@ describe('edit', function()
|
|||
it('respects custom date_syntax', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', date_syntax = 'by' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
local s = pending.store()
|
||||
s:load()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'by:tomorrow')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
local expected =
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 }))
|
||||
|
|
@ -270,32 +291,36 @@ describe('edit', function()
|
|||
it('respects custom recur_syntax', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', recur_syntax = 'repeat' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
local s = pending.store()
|
||||
s:load()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'repeat:weekly')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('weekly', updated.recur)
|
||||
end)
|
||||
|
||||
it('does not modify store on error', function()
|
||||
local t = store.add({ description = 'Task one', category = 'Original' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one', category = 'Original' })
|
||||
s:save()
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function() end
|
||||
pending.edit(tostring(t.id), 'due:notadate')
|
||||
vim.notify = orig_notify
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Original', updated.category)
|
||||
assert.is_nil(updated.due)
|
||||
end)
|
||||
|
||||
it('sets due date with datetime format', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), 'due:tomorrow@14:00')
|
||||
local updated = store.get(t.id)
|
||||
local updated = s:get(t.id)
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
local expected =
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 }))
|
||||
|
|
|
|||
|
|
@ -8,21 +8,25 @@ local views = require('pending.views')
|
|||
|
||||
describe('file token', function()
|
||||
local tmpdir
|
||||
local s
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
end)
|
||||
|
||||
describe('parse.body', function()
|
||||
|
|
@ -78,89 +82,88 @@ describe('file token', function()
|
|||
|
||||
describe('diff reconciliation', function()
|
||||
it('stores file field in _extra on write', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one file:src/auth.lua:42',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated._extra)
|
||||
assert.are.equal('src/auth.lua:42', updated._extra.file)
|
||||
end)
|
||||
|
||||
it('updates file field when token changes', function()
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'old.lua:1' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'old.lua:1' } })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one file:new.lua:99',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('new.lua:99', updated._extra.file)
|
||||
end)
|
||||
|
||||
it('clears file field when token is removed from line', function()
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated._extra)
|
||||
end)
|
||||
|
||||
it('preserves other _extra fields when file is cleared', function()
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42', _gcal_event_id = 'abc123' },
|
||||
})
|
||||
store.save()
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one',
|
||||
}
|
||||
diff.apply(lines)
|
||||
local updated = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated._extra)
|
||||
assert.is_nil(updated._extra.file)
|
||||
assert.are.equal('abc123', updated._extra._gcal_event_id)
|
||||
end)
|
||||
|
||||
it('round-trips file field through JSON', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one file:src/auth.lua:42',
|
||||
}
|
||||
diff.apply(lines)
|
||||
store.unload()
|
||||
store.load()
|
||||
local loaded = store.get(t.id)
|
||||
diff.apply(lines, s)
|
||||
s:load()
|
||||
local loaded = s:get(t.id)
|
||||
assert.is_not_nil(loaded._extra)
|
||||
assert.are.equal('src/auth.lua:42', loaded._extra.file)
|
||||
end)
|
||||
|
||||
it('accepts optional hidden_ids parameter without error', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local lines = {
|
||||
'/' .. t.id .. '/- [ ] Task one',
|
||||
}
|
||||
assert.has_no_error(function()
|
||||
diff.apply(lines, {})
|
||||
diff.apply(lines, s, {})
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('LineMeta', function()
|
||||
it('category_view populates file field in LineMeta', function()
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42' },
|
||||
})
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local _, meta = views.category_view(tasks)
|
||||
local task_meta = nil
|
||||
for _, m in ipairs(meta) do
|
||||
|
|
@ -174,12 +177,12 @@ describe('file token', function()
|
|||
end)
|
||||
|
||||
it('priority_view populates file field in LineMeta', function()
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42' },
|
||||
})
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local _, meta = views.priority_view(tasks)
|
||||
local task_meta = nil
|
||||
for _, m in ipairs(meta) do
|
||||
|
|
@ -193,9 +196,9 @@ describe('file token', function()
|
|||
end)
|
||||
|
||||
it('file field is nil in LineMeta when task has no file', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local _, meta = views.category_view(tasks)
|
||||
local task_meta = nil
|
||||
for _, m in ipairs(meta) do
|
||||
|
|
@ -212,17 +215,18 @@ describe('file token', function()
|
|||
describe(':Pending edit -file', function()
|
||||
it('clears file reference from task', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-file')
|
||||
local updated = store.get(t.id)
|
||||
s:load()
|
||||
local updated = s:get(t.id)
|
||||
assert.is_nil(updated._extra)
|
||||
end)
|
||||
|
||||
it('shows feedback when file reference is removed', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||
s:save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
|
|
@ -236,8 +240,8 @@ describe('file token', function()
|
|||
|
||||
it('does not error when task has no file', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
assert.has_no_error(function()
|
||||
pending.edit(tostring(t.id), '-file')
|
||||
end)
|
||||
|
|
@ -245,13 +249,14 @@ describe('file token', function()
|
|||
|
||||
it('preserves other _extra fields when -file is used', function()
|
||||
local pending = require('pending')
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'src/auth.lua:42', _gcal_event_id = 'abc' },
|
||||
})
|
||||
store.save()
|
||||
s:save()
|
||||
pending.edit(tostring(t.id), '-file')
|
||||
local updated = store.get(t.id)
|
||||
s:load()
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated._extra)
|
||||
assert.is_nil(updated._extra.file)
|
||||
assert.are.equal('abc', updated._extra._gcal_event_id)
|
||||
|
|
@ -263,9 +268,10 @@ describe('file token', function()
|
|||
local pending = require('pending')
|
||||
local buffer = require('pending.buffer')
|
||||
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local t = s:add({ description = 'Task one' })
|
||||
s:save()
|
||||
|
||||
buffer.set_store(s)
|
||||
local bufnr = buffer.open()
|
||||
vim.bo[bufnr].filetype = 'pending'
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
@ -306,12 +312,13 @@ describe('file token', function()
|
|||
local pending = require('pending')
|
||||
local buffer = require('pending.buffer')
|
||||
|
||||
local t = store.add({
|
||||
local t = s:add({
|
||||
description = 'Task one',
|
||||
_extra = { file = 'nonexistent/path.lua:1' },
|
||||
})
|
||||
store.save()
|
||||
s:save()
|
||||
|
||||
buffer.set_store(s)
|
||||
local bufnr = buffer.open()
|
||||
vim.bo[bufnr].filetype = 'pending'
|
||||
vim.api.nvim_set_current_buf(bufnr)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ require('spec.helpers')
|
|||
|
||||
local config = require('pending.config')
|
||||
local diff = require('pending.diff')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('filter', function()
|
||||
local tmpdir
|
||||
|
|
@ -14,32 +13,31 @@ describe('filter', function()
|
|||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
pending = require('pending')
|
||||
buffer = require('pending.buffer')
|
||||
buffer.set_filter({}, {})
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
package.loaded['pending.buffer'] = nil
|
||||
end)
|
||||
|
||||
describe('filter predicates', function()
|
||||
it('cat: hides tasks with non-matching category', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local work_task = nil
|
||||
local home_task = nil
|
||||
for _, t in ipairs(tasks) do
|
||||
|
|
@ -57,13 +55,13 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('cat: hides tasks with no category (default category)', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Inbox task' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Inbox task' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local inbox_task = nil
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.category ~= 'Work' then
|
||||
|
|
@ -75,14 +73,14 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('overdue hides non-overdue tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.add({ description = 'Future task', due = '2099-01-01' })
|
||||
store.add({ description = 'No due task' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:add({ description = 'Future task', due = '2099-01-01' })
|
||||
s:add({ description = 'No due task' })
|
||||
s:save()
|
||||
pending.filter('overdue')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local overdue_task, future_task, nodue_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.due == '2020-01-01' then
|
||||
|
|
@ -101,15 +99,15 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('today hides non-today tasks', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today task', due = today })
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.add({ description = 'Future task', due = '2099-01-01' })
|
||||
store.save()
|
||||
s:add({ description = 'Today task', due = today })
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:add({ description = 'Future task', due = '2099-01-01' })
|
||||
s:save()
|
||||
pending.filter('today')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local today_task, old_task, future_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.due == today then
|
||||
|
|
@ -128,13 +126,13 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('priority hides non-priority tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Important', priority = 1 })
|
||||
store.add({ description = 'Normal' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Important', priority = 1 })
|
||||
s:add({ description = 'Normal' })
|
||||
s:save()
|
||||
pending.filter('priority')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local important_task, normal_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.priority and t.priority > 0 then
|
||||
|
|
@ -149,14 +147,14 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('multi-predicate AND: cat:Work + overdue', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work overdue', category = 'Work', due = '2020-01-01' })
|
||||
store.add({ description = 'Work future', category = 'Work', due = '2099-01-01' })
|
||||
store.add({ description = 'Home overdue', category = 'Home', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work overdue', category = 'Work', due = '2020-01-01' })
|
||||
s:add({ description = 'Work future', category = 'Work', due = '2099-01-01' })
|
||||
s:add({ description = 'Home overdue', category = 'Home', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending.filter('cat:Work overdue')
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local work_overdue, work_future, home_overdue
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Work overdue' then
|
||||
|
|
@ -175,10 +173,10 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter clear removes all predicates and hidden ids', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
assert.are.equal(1, #buffer.filter_predicates())
|
||||
pending.filter('clear')
|
||||
|
|
@ -187,9 +185,9 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter empty string clears filter', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
assert.are.equal(1, #buffer.filter_predicates())
|
||||
pending.filter('')
|
||||
|
|
@ -197,16 +195,16 @@ describe('filter', function()
|
|||
end)
|
||||
|
||||
it('filter predicates persist across set_filter calls', function()
|
||||
store.load()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
s:save()
|
||||
pending.filter('cat:Work')
|
||||
local preds = buffer.filter_predicates()
|
||||
assert.are.equal(1, #preds)
|
||||
assert.are.equal('cat:Work', preds[1])
|
||||
local hidden = buffer.hidden_ids()
|
||||
local tasks = store.active_tasks()
|
||||
local tasks = s:active_tasks()
|
||||
local home_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.category == 'Home' then
|
||||
|
|
@ -219,11 +217,11 @@ describe('filter', function()
|
|||
|
||||
describe('diff.apply with hidden_ids', function()
|
||||
it('does not mark hidden tasks as deleted', function()
|
||||
store.load()
|
||||
store.add({ description = 'Visible task' })
|
||||
store.add({ description = 'Hidden task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Visible task' })
|
||||
s:add({ description = 'Hidden task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local hidden_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Hidden task' then
|
||||
|
|
@ -234,19 +232,18 @@ describe('filter', function()
|
|||
local lines = {
|
||||
'/1/- [ ] Visible task',
|
||||
}
|
||||
diff.apply(lines, hidden_ids)
|
||||
store.unload()
|
||||
store.load()
|
||||
local hidden = store.get(hidden_task.id)
|
||||
diff.apply(lines, s, hidden_ids)
|
||||
s:load()
|
||||
local hidden = s:get(hidden_task.id)
|
||||
assert.are.equal('pending', hidden.status)
|
||||
end)
|
||||
|
||||
it('marks tasks deleted when not hidden and not in buffer', function()
|
||||
store.load()
|
||||
store.add({ description = 'Keep task' })
|
||||
store.add({ description = 'Delete task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Keep task' })
|
||||
s:add({ description = 'Delete task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local keep_task, delete_task
|
||||
for _, t in ipairs(tasks) do
|
||||
if t.description == 'Keep task' then
|
||||
|
|
@ -259,27 +256,25 @@ describe('filter', function()
|
|||
local lines = {
|
||||
'/' .. keep_task.id .. '/- [ ] Keep task',
|
||||
}
|
||||
diff.apply(lines, {})
|
||||
store.unload()
|
||||
store.load()
|
||||
local deleted = store.get(delete_task.id)
|
||||
diff.apply(lines, s, {})
|
||||
s:load()
|
||||
local deleted = s:get(delete_task.id)
|
||||
assert.are.equal('deleted', deleted.status)
|
||||
end)
|
||||
|
||||
it('strips FILTER: line before parsing', function()
|
||||
store.load()
|
||||
store.add({ description = 'My task' })
|
||||
store.save()
|
||||
local tasks = store.active_tasks()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'My task' })
|
||||
s:save()
|
||||
local tasks = s:active_tasks()
|
||||
local task = tasks[1]
|
||||
local lines = {
|
||||
'FILTER: cat:Work',
|
||||
'/' .. task.id .. '/- [ ] My task',
|
||||
}
|
||||
diff.apply(lines, {})
|
||||
store.unload()
|
||||
store.load()
|
||||
local t = store.get(task.id)
|
||||
diff.apply(lines, s, {})
|
||||
s:load()
|
||||
local t = s:get(task.id)
|
||||
assert.are.equal('pending', t.status)
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ require('spec.helpers')
|
|||
|
||||
local config = require('pending.config')
|
||||
local parse = require('pending.parse')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('status', function()
|
||||
local tmpdir
|
||||
|
|
@ -13,22 +12,20 @@ describe('status', function()
|
|||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
pending.store():load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
describe('counts', function()
|
||||
it('returns zeroes for empty store', function()
|
||||
store.load()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
assert.are.equal(0, c.today)
|
||||
|
|
@ -38,48 +35,48 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('counts pending tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'One' })
|
||||
store.add({ description = 'Two' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'One' })
|
||||
s:add({ description = 'Two' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(2, c.pending)
|
||||
end)
|
||||
|
||||
it('counts priority tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Urgent', priority = 1 })
|
||||
store.add({ description = 'Normal' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Urgent', priority = 1 })
|
||||
s:add({ description = 'Normal' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.priority)
|
||||
end)
|
||||
|
||||
it('counts overdue tasks with date-only', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old task', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
end)
|
||||
|
||||
it('counts overdue tasks with datetime', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01T08:00' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old task', due = '2020-01-01T08:00' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
end)
|
||||
|
||||
it('counts today tasks', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today task', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Today task', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.today)
|
||||
|
|
@ -87,11 +84,11 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('counts mixed overdue and today', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Overdue', due = '2020-01-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Overdue', due = '2020-01-01' })
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
|
|
@ -99,10 +96,10 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('excludes done tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Done', due = '2020-01-01' })
|
||||
store.update(t.id, { status = 'done' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Done', due = '2020-01-01' })
|
||||
s:update(t.id, { status = 'done' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
|
|
@ -110,10 +107,10 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('excludes deleted tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Deleted', due = '2020-01-01' })
|
||||
store.delete(t.id)
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
local t = s:add({ description = 'Deleted', due = '2020-01-01' })
|
||||
s:delete(t.id)
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
|
|
@ -121,9 +118,9 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('excludes someday sentinel', function()
|
||||
store.load()
|
||||
store.add({ description = 'Someday', due = '9999-12-30' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Someday', due = '9999-12-30' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
|
|
@ -132,12 +129,12 @@ describe('status', function()
|
|||
end)
|
||||
|
||||
it('picks earliest future date as next_due', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Soon', due = '2099-06-01' })
|
||||
store.add({ description = 'Sooner', due = '2099-03-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Soon', due = '2099-06-01' })
|
||||
s:add({ description = 'Sooner', due = '2099-03-01' })
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(today, c.next_due)
|
||||
|
|
@ -161,7 +158,6 @@ describe('status', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
local c = pending.counts()
|
||||
|
|
@ -171,35 +167,35 @@ describe('status', function()
|
|||
|
||||
describe('statusline', function()
|
||||
it('returns empty string when nothing actionable', function()
|
||||
store.load()
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats overdue only', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 overdue', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats today only', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 today', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats overdue and today', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Old', due = '2020-01-01' })
|
||||
s:add({ description = 'Today', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 overdue, 1 today', pending.statusline())
|
||||
end)
|
||||
|
|
@ -207,26 +203,26 @@ describe('status', function()
|
|||
|
||||
describe('has_due', function()
|
||||
it('returns false when nothing due', function()
|
||||
store.load()
|
||||
store.add({ description = 'Future', due = '2099-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Future', due = '2099-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.is_false(pending.has_due())
|
||||
end)
|
||||
|
||||
it('returns true when overdue', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.save()
|
||||
local s = pending.store()
|
||||
s:add({ description = 'Old', due = '2020-01-01' })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.is_true(pending.has_due())
|
||||
end)
|
||||
|
||||
it('returns true when today', function()
|
||||
store.load()
|
||||
local s = pending.store()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Now', due = today })
|
||||
store.save()
|
||||
s:add({ description = 'Now', due = today })
|
||||
s:save()
|
||||
pending._recompute_counts()
|
||||
assert.is_true(pending.has_due())
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -5,31 +5,30 @@ local store = require('pending.store')
|
|||
|
||||
describe('store', function()
|
||||
local tmpdir
|
||||
local s
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('load', function()
|
||||
it('returns empty data when no file exists', function()
|
||||
local data = store.load()
|
||||
local data = s:load()
|
||||
assert.are.equal(1, data.version)
|
||||
assert.are.equal(1, data.next_id)
|
||||
assert.are.same({}, data.tasks)
|
||||
end)
|
||||
|
||||
it('loads existing data', function()
|
||||
local path = config.get().data_path
|
||||
local path = tmpdir .. '/tasks.json'
|
||||
local f = io.open(path, 'w')
|
||||
f:write(vim.json.encode({
|
||||
version = 1,
|
||||
|
|
@ -52,7 +51,7 @@ describe('store', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
local data = store.load()
|
||||
local data = s:load()
|
||||
assert.are.equal(3, data.next_id)
|
||||
assert.are.equal(2, #data.tasks)
|
||||
assert.are.equal('Pending one', data.tasks[1].description)
|
||||
|
|
@ -60,7 +59,7 @@ describe('store', function()
|
|||
end)
|
||||
|
||||
it('preserves unknown fields', function()
|
||||
local path = config.get().data_path
|
||||
local path = tmpdir .. '/tasks.json'
|
||||
local f = io.open(path, 'w')
|
||||
f:write(vim.json.encode({
|
||||
version = 1,
|
||||
|
|
@ -77,8 +76,8 @@ describe('store', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_not_nil(task._extra)
|
||||
assert.are.equal('hello', task._extra.custom_field)
|
||||
end)
|
||||
|
|
@ -86,9 +85,8 @@ describe('store', function()
|
|||
|
||||
describe('add', function()
|
||||
it('creates a task with incremented id', function()
|
||||
store.load()
|
||||
local t1 = store.add({ description = 'First' })
|
||||
local t2 = store.add({ description = 'Second' })
|
||||
local t1 = s:add({ description = 'First' })
|
||||
local t2 = s:add({ description = 'Second' })
|
||||
assert.are.equal(1, t1.id)
|
||||
assert.are.equal(2, t2.id)
|
||||
assert.are.equal('pending', t1.status)
|
||||
|
|
@ -96,60 +94,54 @@ describe('store', function()
|
|||
end)
|
||||
|
||||
it('uses provided category', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Test', category = 'Work' })
|
||||
local t = s:add({ description = 'Test', category = 'Work' })
|
||||
assert.are.equal('Work', t.category)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('update', function()
|
||||
it('updates fields and sets modified', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Original' })
|
||||
local t = s:add({ description = 'Original' })
|
||||
t.modified = '2025-01-01T00:00:00Z'
|
||||
store.update(t.id, { description = 'Updated' })
|
||||
local updated = store.get(t.id)
|
||||
s:update(t.id, { description = 'Updated' })
|
||||
local updated = s:get(t.id)
|
||||
assert.are.equal('Updated', updated.description)
|
||||
assert.is_not.equal('2025-01-01T00:00:00Z', updated.modified)
|
||||
end)
|
||||
|
||||
it('sets end timestamp on completion', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Test' })
|
||||
local t = s:add({ description = 'Test' })
|
||||
assert.is_nil(t['end'])
|
||||
store.update(t.id, { status = 'done' })
|
||||
local updated = store.get(t.id)
|
||||
s:update(t.id, { status = 'done' })
|
||||
local updated = s:get(t.id)
|
||||
assert.is_not_nil(updated['end'])
|
||||
end)
|
||||
|
||||
it('does not overwrite id or entry', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Immutable fields' })
|
||||
local t = s:add({ description = 'Immutable fields' })
|
||||
local original_id = t.id
|
||||
local original_entry = t.entry
|
||||
store.update(t.id, { id = 999, entry = 'x' })
|
||||
local updated = store.get(original_id)
|
||||
s:update(t.id, { id = 999, entry = 'x' })
|
||||
local updated = s:get(original_id)
|
||||
assert.are.equal(original_id, updated.id)
|
||||
assert.are.equal(original_entry, updated.entry)
|
||||
end)
|
||||
|
||||
it('does not overwrite end on second completion', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Complete twice' })
|
||||
store.update(t.id, { status = 'done', ['end'] = '2026-01-15T10:00:00Z' })
|
||||
local first_end = store.get(t.id)['end']
|
||||
store.update(t.id, { status = 'done' })
|
||||
local task = store.get(t.id)
|
||||
local t = s:add({ description = 'Complete twice' })
|
||||
s:update(t.id, { status = 'done', ['end'] = '2026-01-15T10:00:00Z' })
|
||||
local first_end = s:get(t.id)['end']
|
||||
s:update(t.id, { status = 'done' })
|
||||
local task = s:get(t.id)
|
||||
assert.are.equal(first_end, task['end'])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('delete', function()
|
||||
it('marks task as deleted', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'To delete' })
|
||||
store.delete(t.id)
|
||||
local deleted = store.get(t.id)
|
||||
local t = s:add({ description = 'To delete' })
|
||||
s:delete(t.id)
|
||||
local deleted = s:get(t.id)
|
||||
assert.are.equal('deleted', deleted.status)
|
||||
assert.is_not_nil(deleted['end'])
|
||||
end)
|
||||
|
|
@ -157,12 +149,10 @@ describe('store', function()
|
|||
|
||||
describe('save and round-trip', function()
|
||||
it('persists and reloads correctly', function()
|
||||
store.load()
|
||||
store.add({ description = 'Persisted', category = 'Work', priority = 1 })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local tasks = store.active_tasks()
|
||||
s:add({ description = 'Persisted', category = 'Work', priority = 1 })
|
||||
s:save()
|
||||
s:load()
|
||||
local tasks = s:active_tasks()
|
||||
assert.are.equal(1, #tasks)
|
||||
assert.are.equal('Persisted', tasks[1].description)
|
||||
assert.are.equal('Work', tasks[1].category)
|
||||
|
|
@ -170,7 +160,7 @@ describe('store', function()
|
|||
end)
|
||||
|
||||
it('round-trips unknown fields', function()
|
||||
local path = config.get().data_path
|
||||
local path = tmpdir .. '/tasks.json'
|
||||
local f = io.open(path, 'w')
|
||||
f:write(vim.json.encode({
|
||||
version = 1,
|
||||
|
|
@ -187,45 +177,38 @@ describe('store', function()
|
|||
},
|
||||
}))
|
||||
f:close()
|
||||
store.load()
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:load()
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('abc123', task._extra._gcal_event_id)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('recurrence fields', function()
|
||||
it('persists recur and recur_mode through round-trip', function()
|
||||
store.load()
|
||||
store.add({ description = 'Recurring', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:add({ description = 'Recurring', recur = 'weekly', recur_mode = 'scheduled' })
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('weekly', task.recur)
|
||||
assert.are.equal('scheduled', task.recur_mode)
|
||||
end)
|
||||
|
||||
it('persists recur without recur_mode', function()
|
||||
store.load()
|
||||
store.add({ description = 'Simple recur', recur = 'daily' })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:add({ description = 'Simple recur', recur = 'daily' })
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.are.equal('daily', task.recur)
|
||||
assert.is_nil(task.recur_mode)
|
||||
end)
|
||||
|
||||
it('omits recur fields when not set', function()
|
||||
store.load()
|
||||
store.add({ description = 'No recur' })
|
||||
store.save()
|
||||
store.unload()
|
||||
store.load()
|
||||
local task = store.get(1)
|
||||
s:add({ description = 'No recur' })
|
||||
s:save()
|
||||
s:load()
|
||||
local task = s:get(1)
|
||||
assert.is_nil(task.recur)
|
||||
assert.is_nil(task.recur_mode)
|
||||
end)
|
||||
|
|
@ -233,11 +216,10 @@ describe('store', function()
|
|||
|
||||
describe('active_tasks', function()
|
||||
it('excludes deleted tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Active' })
|
||||
local t2 = store.add({ description = 'To delete' })
|
||||
store.delete(t2.id)
|
||||
local active = store.active_tasks()
|
||||
s:add({ description = 'Active' })
|
||||
local t2 = s:add({ description = 'To delete' })
|
||||
s:delete(t2.id)
|
||||
local active = s:active_tasks()
|
||||
assert.are.equal(1, #active)
|
||||
assert.are.equal('Active', active[1].description)
|
||||
end)
|
||||
|
|
@ -245,27 +227,24 @@ describe('store', function()
|
|||
|
||||
describe('snapshot', function()
|
||||
it('returns a table of tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Snap one' })
|
||||
store.add({ description = 'Snap two' })
|
||||
local snap = store.snapshot()
|
||||
s:add({ description = 'Snap one' })
|
||||
s:add({ description = 'Snap two' })
|
||||
local snap = s:snapshot()
|
||||
assert.are.equal(2, #snap)
|
||||
end)
|
||||
|
||||
it('returns a copy that does not affect the store', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Original' })
|
||||
local snap = store.snapshot()
|
||||
local t = s:add({ description = 'Original' })
|
||||
local snap = s:snapshot()
|
||||
snap[1].description = 'Mutated'
|
||||
local live = store.get(t.id)
|
||||
local live = s:get(t.id)
|
||||
assert.are.equal('Original', live.description)
|
||||
end)
|
||||
|
||||
it('excludes deleted tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Will be deleted' })
|
||||
store.delete(t.id)
|
||||
local snap = store.snapshot()
|
||||
local t = s:add({ description = 'Will be deleted' })
|
||||
s:delete(t.id)
|
||||
local snap = s:snapshot()
|
||||
assert.are.equal(0, #snap)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('sync', function()
|
||||
local tmpdir
|
||||
|
|
@ -12,7 +11,6 @@ describe('sync', function()
|
|||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
end)
|
||||
|
|
@ -21,7 +19,6 @@ describe('sync', function()
|
|||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,28 +5,27 @@ local store = require('pending.store')
|
|||
|
||||
describe('views', function()
|
||||
local tmpdir
|
||||
local s
|
||||
local views = require('pending.views')
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
store.load()
|
||||
s = store.new(tmpdir .. '/tasks.json')
|
||||
s:load()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('category_view', function()
|
||||
it('groups tasks under their category header', function()
|
||||
store.add({ description = 'Task A', category = 'Work' })
|
||||
store.add({ description = 'Task B', category = 'Work' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Task A', category = 'Work' })
|
||||
s:add({ description = 'Task B', category = 'Work' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
assert.are.equal('## Work', lines[1])
|
||||
assert.are.equal('header', meta[1].type)
|
||||
assert.is_true(lines[2]:find('Task A') ~= nil)
|
||||
|
|
@ -34,10 +33,10 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('places pending tasks before done tasks within a category', function()
|
||||
local t1 = store.add({ description = 'Done task', category = 'Work' })
|
||||
store.add({ description = 'Pending task', category = 'Work' })
|
||||
store.update(t1.id, { status = 'done' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t1 = s:add({ description = 'Done task', category = 'Work' })
|
||||
s:add({ description = 'Pending task', category = 'Work' })
|
||||
s:update(t1.id, { status = 'done' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local pending_row, done_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.status == 'pending' then
|
||||
|
|
@ -50,9 +49,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts high-priority tasks before normal tasks within pending group', function()
|
||||
store.add({ description = 'Normal', category = 'Work', priority = 0 })
|
||||
store.add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Normal', category = 'Work', priority = 0 })
|
||||
s:add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local high_row, normal_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -68,11 +67,11 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts high-priority tasks before normal tasks within done group', function()
|
||||
local t1 = store.add({ description = 'Done Normal', category = 'Work', priority = 0 })
|
||||
local t2 = store.add({ description = 'Done High', category = 'Work', priority = 1 })
|
||||
store.update(t1.id, { status = 'done' })
|
||||
store.update(t2.id, { status = 'done' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
local t1 = s:add({ description = 'Done Normal', category = 'Work', priority = 0 })
|
||||
local t2 = s:add({ description = 'Done High', category = 'Work', priority = 1 })
|
||||
s:update(t1.id, { status = 'done' })
|
||||
s:update(t2.id, { status = 'done' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local high_row, normal_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -88,9 +87,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('gives each category its own header with blank lines between them', function()
|
||||
store.add({ description = 'Task A', category = 'Work' })
|
||||
store.add({ description = 'Task B', category = 'Personal' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Task A', category = 'Work' })
|
||||
s:add({ description = 'Task B', category = 'Personal' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local headers = {}
|
||||
local blank_found = false
|
||||
for i, m in ipairs(meta) do
|
||||
|
|
@ -105,8 +104,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('formats task lines as /ID/ description', function()
|
||||
store.add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local task_line
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -117,8 +116,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('formats priority task lines as /ID/- [!] description', function()
|
||||
store.add({ description = 'Important', category = 'Inbox', priority = 1 })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Important', category = 'Inbox', priority = 1 })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local task_line
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -129,15 +128,15 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sets LineMeta type=header for header lines with correct category', function()
|
||||
store.add({ description = 'T', category = 'School' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'T', category = 'School' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
assert.are.equal('header', meta[1].type)
|
||||
assert.are.equal('School', meta[1].category)
|
||||
end)
|
||||
|
||||
it('sets LineMeta type=task with correct id and status', function()
|
||||
local t = store.add({ description = 'Do something', category = 'Inbox' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Do something', category = 'Inbox' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -150,9 +149,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sets LineMeta type=blank for blank separator lines', function()
|
||||
store.add({ description = 'A', category = 'Work' })
|
||||
store.add({ description = 'B', category = 'Home' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'A', category = 'Work' })
|
||||
s:add({ description = 'B', category = 'Home' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local blank_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'blank' then
|
||||
|
|
@ -166,8 +165,8 @@ describe('views', function()
|
|||
|
||||
it('marks overdue pending tasks with meta.overdue=true', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Overdue task', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Overdue task', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -179,8 +178,8 @@ describe('views', function()
|
|||
|
||||
it('does not mark future pending tasks as overdue', function()
|
||||
local tomorrow = os.date('%Y-%m-%d', os.time() + 86400)
|
||||
local t = store.add({ description = 'Future task', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Future task', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -192,9 +191,9 @@ describe('views', function()
|
|||
|
||||
it('does not mark done tasks with overdue due dates as overdue', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
store.update(t.id, { status = 'done' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
s:update(t.id, { status = 'done' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -205,8 +204,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('includes recur in LineMeta for recurring tasks', function()
|
||||
store.add({ description = 'Recurring', category = 'Inbox', recur = 'weekly' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Recurring', category = 'Inbox', recur = 'weekly' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -217,8 +216,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('has nil recur in LineMeta for non-recurring tasks', function()
|
||||
store.add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.category_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -231,9 +230,9 @@ describe('views', function()
|
|||
it('respects category_order when set', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', category_order = { 'Work', 'Inbox' } }
|
||||
config.reset()
|
||||
store.add({ description = 'Inbox task', category = 'Inbox' })
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Inbox task', category = 'Inbox' })
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local first_header, second_header
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'header' then
|
||||
|
|
@ -251,9 +250,9 @@ describe('views', function()
|
|||
it('appends categories not in category_order after ordered ones', function()
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json', category_order = { 'Work' } }
|
||||
config.reset()
|
||||
store.add({ description = 'Errand', category = 'Errands' })
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Errand', category = 'Errands' })
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local headers = {}
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'header' then
|
||||
|
|
@ -265,9 +264,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('preserves insertion order when category_order is empty', function()
|
||||
store.add({ description = 'Alpha task', category = 'Alpha' })
|
||||
store.add({ description = 'Beta task', category = 'Beta' })
|
||||
local lines, meta = views.category_view(store.active_tasks())
|
||||
s:add({ description = 'Alpha task', category = 'Alpha' })
|
||||
s:add({ description = 'Beta task', category = 'Beta' })
|
||||
local lines, meta = views.category_view(s:active_tasks())
|
||||
local headers = {}
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'header' then
|
||||
|
|
@ -281,10 +280,10 @@ describe('views', function()
|
|||
|
||||
describe('priority_view', function()
|
||||
it('places all pending tasks before done tasks', function()
|
||||
local t1 = store.add({ description = 'Done A', category = 'Work' })
|
||||
store.add({ description = 'Pending B', category = 'Work' })
|
||||
store.update(t1.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t1 = s:add({ description = 'Done A', category = 'Work' })
|
||||
s:add({ description = 'Pending B', category = 'Work' })
|
||||
s:update(t1.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local last_pending_row, first_done_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -299,9 +298,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts pending tasks by priority desc within pending group', function()
|
||||
store.add({ description = 'Low', category = 'Work', priority = 0 })
|
||||
store.add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Low', category = 'Work', priority = 0 })
|
||||
s:add({ description = 'High', category = 'Work', priority = 1 })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local high_row, low_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -316,9 +315,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts pending tasks with due dates before those without', function()
|
||||
store.add({ description = 'No due', category = 'Work' })
|
||||
store.add({ description = 'Has due', category = 'Work', due = '2099-12-31' })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'No due', category = 'Work' })
|
||||
s:add({ description = 'Has due', category = 'Work', due = '2099-12-31' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local due_row, nodue_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -333,9 +332,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sorts pending tasks with earlier due dates before later due dates', function()
|
||||
store.add({ description = 'Later', category = 'Work', due = '2099-12-31' })
|
||||
store.add({ description = 'Earlier', category = 'Work', due = '2050-01-01' })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Later', category = 'Work', due = '2099-12-31' })
|
||||
s:add({ description = 'Earlier', category = 'Work', due = '2050-01-01' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local earlier_row, later_row
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -350,15 +349,15 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('formats task lines as /ID/- [ ] description', function()
|
||||
store.add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, _ = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'My task', category = 'Inbox' })
|
||||
local lines, _ = views.priority_view(s:active_tasks())
|
||||
assert.are.equal('/1/- [ ] My task', lines[1])
|
||||
end)
|
||||
|
||||
it('sets show_category=true for all task meta entries', function()
|
||||
store.add({ description = 'T1', category = 'Work' })
|
||||
store.add({ description = 'T2', category = 'Personal' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'T1', category = 'Work' })
|
||||
s:add({ description = 'T2', category = 'Personal' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
assert.is_true(m.show_category == true)
|
||||
|
|
@ -367,9 +366,9 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('sets meta.category correctly for each task', function()
|
||||
store.add({ description = 'Work task', category = 'Work' })
|
||||
store.add({ description = 'Home task', category = 'Home' })
|
||||
local lines, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Work task', category = 'Work' })
|
||||
s:add({ description = 'Home task', category = 'Home' })
|
||||
local lines, meta = views.priority_view(s:active_tasks())
|
||||
local categories = {}
|
||||
for i, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -386,8 +385,8 @@ describe('views', function()
|
|||
|
||||
it('marks overdue pending tasks with meta.overdue=true', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Overdue', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Overdue', category = 'Inbox', due = yesterday })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -399,8 +398,8 @@ describe('views', function()
|
|||
|
||||
it('does not mark future pending tasks as overdue', function()
|
||||
local tomorrow = os.date('%Y-%m-%d', os.time() + 86400)
|
||||
local t = store.add({ description = 'Future', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Future', category = 'Inbox', due = tomorrow })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -412,9 +411,9 @@ describe('views', function()
|
|||
|
||||
it('does not mark done tasks with overdue due dates as overdue', function()
|
||||
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||
local t = store.add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
store.update(t.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
local t = s:add({ description = 'Done late', category = 'Inbox', due = yesterday })
|
||||
s:update(t.id, { status = 'done' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.id == t.id then
|
||||
|
|
@ -425,8 +424,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('includes recur in LineMeta for recurring tasks', function()
|
||||
store.add({ description = 'Recurring', category = 'Inbox', recur = 'daily' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Recurring', category = 'Inbox', recur = 'daily' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
@ -437,8 +436,8 @@ describe('views', function()
|
|||
end)
|
||||
|
||||
it('has nil recur in LineMeta for non-recurring tasks', function()
|
||||
store.add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.priority_view(store.active_tasks())
|
||||
s:add({ description = 'Normal', category = 'Inbox' })
|
||||
local _, meta = views.priority_view(s:active_tasks())
|
||||
local task_meta
|
||||
for _, m in ipairs(meta) do
|
||||
if m.type == 'task' then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue