diff --git a/doc/pending.txt b/doc/pending.txt index 486ea32..fc04dc4 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -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. diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index 09412f3..e9d7318 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -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) diff --git a/lua/pending/complete.lua b/lua/pending/complete.lua index 6c2b964..ceeecc9 100644 --- a/lua/pending/complete.lua +++ b/lua/pending/complete.lua @@ -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 diff --git a/lua/pending/diff.lua b/lua/pending/diff.lua index c731d95..b507179 100644 --- a/lua/pending/diff.lua +++ b/lua/pending/diff.lua @@ -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 ---@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 diff --git a/lua/pending/health.lua b/lua/pending/health.lua index 93f7c72..ca28298 100644 --- a/lua/pending/health.lua +++ b/lua/pending/health.lua @@ -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)') diff --git a/lua/pending/init.lua b/lua/pending/init.lua index 73b3051..5205182 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -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 ', 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 diff --git a/lua/pending/store.lua b/lua/pending/store.lua index b9a4e38..5a5b370 100644 --- a/lua/pending/store.lua +++ b/lua/pending/store.lua @@ -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 ---@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 diff --git a/lua/pending/sync/gcal.lua b/lua/pending/sync/gcal.lua index 843f310..a2d9992 100644 --- a/lua/pending/sync/gcal.lua +++ b/lua/pending/sync/gcal.lua @@ -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( diff --git a/plugin/pending.lua b/plugin/pending.lua index ce62d1b..4814f50 100644 --- a/plugin/pending.lua +++ b/plugin/pending.lua @@ -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) diff --git a/spec/archive_spec.lua b/spec/archive_spec.lua index df1a912..e7046fa 100644 --- a/spec/archive_spec.lua +++ b/spec/archive_spec.lua @@ -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) diff --git a/spec/complete_spec.lua b/spec/complete_spec.lua index 7b45e5b..98547e8 100644 --- a/spec/complete_spec.lua +++ b/spec/complete_spec.lua @@ -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) diff --git a/spec/diff_spec.lua b/spec/diff_spec.lua index d8e25c2..2322ded 100644 --- a/spec/diff_spec.lua +++ b/spec/diff_spec.lua @@ -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) diff --git a/spec/edit_spec.lua b/spec/edit_spec.lua index ba9f98e..08ef9e0 100644 --- a/spec/edit_spec.lua +++ b/spec/edit_spec.lua @@ -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 })) diff --git a/spec/file_spec.lua b/spec/file_spec.lua index 9835387..c7e3151 100644 --- a/spec/file_spec.lua +++ b/spec/file_spec.lua @@ -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) diff --git a/spec/filter_spec.lua b/spec/filter_spec.lua index 8756c5f..5e00b60 100644 --- a/spec/filter_spec.lua +++ b/spec/filter_spec.lua @@ -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) diff --git a/spec/status_spec.lua b/spec/status_spec.lua index ecbe127..e2d4223 100644 --- a/spec/status_spec.lua +++ b/spec/status_spec.lua @@ -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) diff --git a/spec/store_spec.lua b/spec/store_spec.lua index ebe4da1..0bed750 100644 --- a/spec/store_spec.lua +++ b/spec/store_spec.lua @@ -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) diff --git a/spec/sync_spec.lua b/spec/sync_spec.lua index 4d8a3dc..28bd0e3 100644 --- a/spec/sync_spec.lua +++ b/spec/sync_spec.lua @@ -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) diff --git a/spec/views_spec.lua b/spec/views_spec.lua index e8d5c2d..c9785f9 100644 --- a/spec/views_spec.lua +++ b/spec/views_spec.lua @@ -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