feat: :Pending edit command for CLI metadata editing (#41)
* feat: :Pending edit command for CLI metadata editing
Problem: editing task metadata (due date, category, priority,
recurrence) requires opening the buffer and editing inline. No way
to make quick metadata changes from the command line.
Solution: add :Pending edit {id} [operations...] command that applies
metadata changes by numeric task ID. Supports due:<date>, cat:<name>,
rec:<pattern>, +!, -!, -due, -cat, -rec operations with full date
vocabulary and recurrence validation. Pushes to undo stack, re-renders
the buffer if open, and provides feedback messages. Tab completion for
IDs, field names, date vocabulary, categories, and recurrence patterns.
Also fixes store.update() to properly clear fields set to vim.NIL.
* ci: formt
This commit is contained in:
parent
e62e09f609
commit
8d3d21b330
4 changed files with 644 additions and 5 deletions
|
|
@ -514,6 +514,178 @@ function M.due()
|
|||
vim.cmd('copen')
|
||||
end
|
||||
|
||||
---@param token string
|
||||
---@return string|nil field
|
||||
---@return any value
|
||||
---@return string|nil err
|
||||
local function parse_edit_token(token)
|
||||
local recur = require('pending.recur')
|
||||
local cfg = require('pending.config').get()
|
||||
local dk = cfg.date_syntax or 'due'
|
||||
local rk = cfg.recur_syntax or 'rec'
|
||||
|
||||
if token == '+!' then
|
||||
return 'priority', 1, nil
|
||||
end
|
||||
if token == '-!' then
|
||||
return 'priority', 0, nil
|
||||
end
|
||||
if token == '-due' or token == '-' .. dk then
|
||||
return 'due', vim.NIL, nil
|
||||
end
|
||||
if token == '-cat' then
|
||||
return 'category', vim.NIL, nil
|
||||
end
|
||||
if token == '-rec' or token == '-' .. rk then
|
||||
return 'recur', vim.NIL, nil
|
||||
end
|
||||
|
||||
local due_val = token:match('^' .. vim.pesc(dk) .. ':(.+)$')
|
||||
if due_val then
|
||||
local resolved = parse.resolve_date(due_val)
|
||||
if resolved then
|
||||
return 'due', resolved, nil
|
||||
end
|
||||
if
|
||||
due_val:match('^%d%d%d%d%-%d%d%-%d%d$') or due_val:match('^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d$')
|
||||
then
|
||||
return 'due', due_val, nil
|
||||
end
|
||||
return nil,
|
||||
nil,
|
||||
'Invalid date: ' .. due_val .. '. Use YYYY-MM-DD, today, tomorrow, +Nd, weekday names, etc.'
|
||||
end
|
||||
|
||||
local cat_val = token:match('^cat:(.+)$')
|
||||
if cat_val then
|
||||
return 'category', cat_val, nil
|
||||
end
|
||||
|
||||
local rec_val = token:match('^' .. vim.pesc(rk) .. ':(.+)$')
|
||||
if rec_val then
|
||||
local raw_spec = rec_val
|
||||
local rec_mode = nil
|
||||
if raw_spec:sub(1, 1) == '!' then
|
||||
rec_mode = 'completion'
|
||||
raw_spec = raw_spec:sub(2)
|
||||
end
|
||||
if not recur.validate(raw_spec) then
|
||||
return nil, nil, 'Invalid recurrence pattern: ' .. rec_val
|
||||
end
|
||||
return 'recur', { spec = raw_spec, mode = rec_mode }, nil
|
||||
end
|
||||
|
||||
return nil,
|
||||
nil,
|
||||
'Unknown operation: '
|
||||
.. token
|
||||
.. '. Valid: '
|
||||
.. dk
|
||||
.. ':<date>, cat:<name>, '
|
||||
.. rk
|
||||
.. ':<pattern>, +!, -!, -'
|
||||
.. dk
|
||||
.. ', -cat, -'
|
||||
.. rk
|
||||
end
|
||||
|
||||
---@param id_str string
|
||||
---@param rest string
|
||||
---@return nil
|
||||
function M.edit(id_str, rest)
|
||||
if not id_str or id_str == '' then
|
||||
vim.notify(
|
||||
'Usage: :Pending edit <id> [due:<date>] [cat:<name>] [rec:<pattern>] [+!] [-!] [-due] [-cat] [-rec]',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local id = tonumber(id_str)
|
||||
if not id then
|
||||
vim.notify('Invalid task ID: ' .. id_str, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
store.load()
|
||||
local task = store.get(id)
|
||||
if not task then
|
||||
vim.notify('No task with ID ' .. id .. '.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if not rest or rest == '' then
|
||||
vim.notify(
|
||||
'Usage: :Pending edit <id> [due:<date>] [cat:<name>] [rec:<pattern>] [+!] [-!] [-due] [-cat] [-rec]',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local tokens = {}
|
||||
for tok in rest:gmatch('%S+') do
|
||||
table.insert(tokens, tok)
|
||||
end
|
||||
|
||||
local updates = {}
|
||||
local feedback = {}
|
||||
|
||||
for _, tok in ipairs(tokens) do
|
||||
local field, value, err = parse_edit_token(tok)
|
||||
if err then
|
||||
vim.notify(err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if field == 'recur' then
|
||||
if value == vim.NIL then
|
||||
updates.recur = vim.NIL
|
||||
updates.recur_mode = vim.NIL
|
||||
table.insert(feedback, 'recurrence removed')
|
||||
else
|
||||
updates.recur = value.spec
|
||||
updates.recur_mode = value.mode
|
||||
table.insert(feedback, 'recurrence set to ' .. value.spec)
|
||||
end
|
||||
elseif field == 'due' then
|
||||
if value == vim.NIL then
|
||||
updates.due = vim.NIL
|
||||
table.insert(feedback, 'due date removed')
|
||||
else
|
||||
updates.due = value
|
||||
table.insert(feedback, 'due date set to ' .. tostring(value))
|
||||
end
|
||||
elseif field == 'category' then
|
||||
if value == vim.NIL then
|
||||
updates.category = vim.NIL
|
||||
table.insert(feedback, 'category removed')
|
||||
else
|
||||
updates.category = value
|
||||
table.insert(feedback, 'category set to ' .. tostring(value))
|
||||
end
|
||||
elseif field == 'priority' then
|
||||
updates.priority = value
|
||||
table.insert(feedback, value == 1 and 'priority added' or 'priority removed')
|
||||
end
|
||||
end
|
||||
|
||||
local snapshot = store.snapshot()
|
||||
local stack = store.undo_stack()
|
||||
table.insert(stack, snapshot)
|
||||
if #stack > UNDO_MAX then
|
||||
table.remove(stack, 1)
|
||||
end
|
||||
|
||||
store.update(id, updates)
|
||||
store.save()
|
||||
|
||||
local bufnr = buffer.bufnr()
|
||||
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
||||
buffer.render(bufnr)
|
||||
end
|
||||
|
||||
vim.notify('Task #' .. id .. ' updated: ' .. table.concat(feedback, ', '))
|
||||
end
|
||||
|
||||
---@param args string
|
||||
---@return nil
|
||||
function M.command(args)
|
||||
|
|
@ -524,6 +696,9 @@ function M.command(args)
|
|||
local cmd, rest = args:match('^(%S+)%s*(.*)')
|
||||
if cmd == 'add' then
|
||||
M.add(rest)
|
||||
elseif cmd == 'edit' then
|
||||
local id_str, edit_rest = rest:match('^(%S+)%s*(.*)')
|
||||
M.edit(id_str, edit_rest)
|
||||
elseif cmd == 'sync' then
|
||||
M.sync()
|
||||
elseif cmd == 'archive' then
|
||||
|
|
|
|||
|
|
@ -293,7 +293,11 @@ function M.update(id, fields)
|
|||
local now = timestamp()
|
||||
for k, v in pairs(fields) do
|
||||
if k ~= 'id' and k ~= 'entry' then
|
||||
task[k] = v
|
||||
if v == vim.NIL then
|
||||
task[k] = nil
|
||||
else
|
||||
task[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
task.modified = now
|
||||
|
|
|
|||
|
|
@ -3,17 +3,173 @@ if vim.g.loaded_pending then
|
|||
end
|
||||
vim.g.loaded_pending = true
|
||||
|
||||
---@return string[]
|
||||
local function edit_field_candidates()
|
||||
local cfg = require('pending.config').get()
|
||||
local dk = cfg.date_syntax or 'due'
|
||||
local rk = cfg.recur_syntax or 'rec'
|
||||
return {
|
||||
dk .. ':',
|
||||
'cat:',
|
||||
rk .. ':',
|
||||
'+!',
|
||||
'-!',
|
||||
'-' .. dk,
|
||||
'-cat',
|
||||
'-' .. rk,
|
||||
}
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
local function edit_date_values()
|
||||
return {
|
||||
'today',
|
||||
'tomorrow',
|
||||
'yesterday',
|
||||
'+1d',
|
||||
'+2d',
|
||||
'+3d',
|
||||
'+1w',
|
||||
'+2w',
|
||||
'+1m',
|
||||
'mon',
|
||||
'tue',
|
||||
'wed',
|
||||
'thu',
|
||||
'fri',
|
||||
'sat',
|
||||
'sun',
|
||||
'eod',
|
||||
'eow',
|
||||
'eom',
|
||||
'eoq',
|
||||
'eoy',
|
||||
'sow',
|
||||
'som',
|
||||
'soq',
|
||||
'soy',
|
||||
'later',
|
||||
}
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
local function edit_recur_values()
|
||||
local ok, recur = pcall(require, 'pending.recur')
|
||||
if not ok then
|
||||
return {}
|
||||
end
|
||||
local result = {}
|
||||
for _, s in ipairs(recur.shorthand_list()) do
|
||||
table.insert(result, s)
|
||||
end
|
||||
for _, s in ipairs(recur.shorthand_list()) do
|
||||
table.insert(result, '!' .. s)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---@param lead string
|
||||
---@param candidates string[]
|
||||
---@return string[]
|
||||
local function filter_candidates(lead, candidates)
|
||||
return vim.tbl_filter(function(s)
|
||||
return s:find(lead, 1, true) == 1
|
||||
end, candidates)
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@param cmd_line string
|
||||
---@return string[]
|
||||
local function complete_edit(arg_lead, cmd_line)
|
||||
local cfg = require('pending.config').get()
|
||||
local dk = cfg.date_syntax or 'due'
|
||||
local rk = cfg.recur_syntax or 'rec'
|
||||
|
||||
local after_edit = cmd_line:match('^Pending%s+edit%s+(.*)')
|
||||
if not after_edit then
|
||||
return {}
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
for part in after_edit:gmatch('%S+') do
|
||||
table.insert(parts, part)
|
||||
end
|
||||
|
||||
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 ids = {}
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
table.insert(ids, tostring(task.id))
|
||||
end
|
||||
return filter_candidates(arg_lead, ids)
|
||||
end
|
||||
|
||||
local prefix = arg_lead:match('^(' .. vim.pesc(dk) .. ':)(.*)$')
|
||||
if prefix then
|
||||
local after_colon = arg_lead:sub(#prefix + 1)
|
||||
local dates = edit_date_values()
|
||||
local result = {}
|
||||
for _, d in ipairs(dates) do
|
||||
if d:find(after_colon, 1, true) == 1 then
|
||||
table.insert(result, prefix .. d)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local rec_prefix = arg_lead:match('^(' .. vim.pesc(rk) .. ':)(.*)$')
|
||||
if rec_prefix then
|
||||
local after_colon = arg_lead:sub(#rec_prefix + 1)
|
||||
local pats = edit_recur_values()
|
||||
local result = {}
|
||||
for _, p in ipairs(pats) do
|
||||
if p:find(after_colon, 1, true) == 1 then
|
||||
table.insert(result, rec_prefix .. p)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local cat_prefix = arg_lead:match('^(cat:)(.*)$')
|
||||
if cat_prefix then
|
||||
local after_colon = arg_lead:sub(#cat_prefix + 1)
|
||||
local store = require('pending.store')
|
||||
store.load()
|
||||
local seen = {}
|
||||
local cats = {}
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
if task.category and not seen[task.category] then
|
||||
seen[task.category] = true
|
||||
table.insert(cats, task.category)
|
||||
end
|
||||
end
|
||||
table.sort(cats)
|
||||
local result = {}
|
||||
for _, c in ipairs(cats) do
|
||||
if c:find(after_colon, 1, true) == 1 then
|
||||
table.insert(result, cat_prefix .. c)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return filter_candidates(arg_lead, edit_field_candidates())
|
||||
end
|
||||
|
||||
vim.api.nvim_create_user_command('Pending', function(opts)
|
||||
require('pending').command(opts.args)
|
||||
end, {
|
||||
bar = true,
|
||||
nargs = '*',
|
||||
complete = function(arg_lead, cmd_line)
|
||||
local subcmds = { 'add', 'sync', 'archive', 'due', 'undo' }
|
||||
local subcmds = { 'add', 'archive', 'due', 'edit', 'sync', 'undo' }
|
||||
if not cmd_line:match('^Pending%s+%S') then
|
||||
return vim.tbl_filter(function(s)
|
||||
return s:find(arg_lead, 1, true) == 1
|
||||
end, subcmds)
|
||||
return filter_candidates(arg_lead, subcmds)
|
||||
end
|
||||
if cmd_line:match('^Pending%s+edit') then
|
||||
return complete_edit(arg_lead, cmd_line)
|
||||
end
|
||||
return {}
|
||||
end,
|
||||
|
|
|
|||
304
spec/edit_spec.lua
Normal file
304
spec/edit_spec.lua
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('edit', function()
|
||||
local tmpdir
|
||||
local pending = require('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()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
it('sets due date with resolve_date vocabulary', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
pending.edit(tostring(t.id), 'due:tomorrow')
|
||||
local updated = store.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 }))
|
||||
assert.are.equal(expected, updated.due)
|
||||
end)
|
||||
|
||||
it('sets due date with literal YYYY-MM-DD', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
pending.edit(tostring(t.id), 'due:2026-06-15')
|
||||
local updated = store.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()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
local updated = store.get(t.id)
|
||||
assert.are.equal('Work', updated.category)
|
||||
end)
|
||||
|
||||
it('adds priority', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
pending.edit(tostring(t.id), '+!')
|
||||
local updated = store.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()
|
||||
pending.edit(tostring(t.id), '-!')
|
||||
local updated = store.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()
|
||||
pending.edit(tostring(t.id), '-due')
|
||||
local updated = store.get(t.id)
|
||||
assert.is_nil(updated.due)
|
||||
end)
|
||||
|
||||
it('removes category', function()
|
||||
local t = store.add({ description = 'Task one', category = 'Work' })
|
||||
store.save()
|
||||
pending.edit(tostring(t.id), '-cat')
|
||||
local updated = store.get(t.id)
|
||||
assert.is_nil(updated.category)
|
||||
end)
|
||||
|
||||
it('sets recurrence', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
pending.edit(tostring(t.id), 'rec:weekly')
|
||||
local updated = store.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()
|
||||
pending.edit(tostring(t.id), 'rec:!daily')
|
||||
local updated = store.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()
|
||||
pending.edit(tostring(t.id), '-rec')
|
||||
local updated = store.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()
|
||||
pending.edit(tostring(t.id), 'due:today cat:Errands +!')
|
||||
local updated = store.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()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
assert.are.equal(stack_before + 1, #store.undo_stack())
|
||||
end)
|
||||
|
||||
it('persists changes to disk', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
store.unload()
|
||||
store.load()
|
||||
local updated = store.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 messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit('999', 'cat:Work')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('No task with ID 999'))
|
||||
assert.are.equal(vim.log.levels.ERROR, messages[1].level)
|
||||
end)
|
||||
|
||||
it('errors on invalid date', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit(tostring(t.id), 'due:notadate')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('Invalid date'))
|
||||
assert.are.equal(vim.log.levels.ERROR, messages[1].level)
|
||||
end)
|
||||
|
||||
it('errors on unknown operation token', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit(tostring(t.id), 'bogus')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('Unknown operation'))
|
||||
assert.are.equal(vim.log.levels.ERROR, messages[1].level)
|
||||
end)
|
||||
|
||||
it('errors on invalid recurrence pattern', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit(tostring(t.id), 'rec:nope')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('Invalid recurrence'))
|
||||
assert.are.equal(vim.log.levels.ERROR, messages[1].level)
|
||||
end)
|
||||
|
||||
it('errors when no operations given', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit(tostring(t.id), '')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('Usage'))
|
||||
assert.are.equal(vim.log.levels.ERROR, messages[1].level)
|
||||
end)
|
||||
|
||||
it('errors when no id given', function()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit('', '')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('Usage'))
|
||||
assert.are.equal(vim.log.levels.ERROR, messages[1].level)
|
||||
end)
|
||||
|
||||
it('errors on non-numeric id', function()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit('abc', 'cat:Work')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('Invalid task ID'))
|
||||
assert.are.equal(vim.log.levels.ERROR, messages[1].level)
|
||||
end)
|
||||
|
||||
it('shows feedback message on success', function()
|
||||
local t = store.add({ description = 'Task one' })
|
||||
store.save()
|
||||
local messages = {}
|
||||
local orig_notify = vim.notify
|
||||
vim.notify = function(msg, level)
|
||||
table.insert(messages, { msg = msg, level = level })
|
||||
end
|
||||
pending.edit(tostring(t.id), 'cat:Work')
|
||||
vim.notify = orig_notify
|
||||
assert.are.equal(1, #messages)
|
||||
assert.truthy(messages[1].msg:find('Task #' .. t.id .. ' updated'))
|
||||
assert.truthy(messages[1].msg:find('category set to Work'))
|
||||
end)
|
||||
|
||||
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()
|
||||
pending.edit(tostring(t.id), 'by:tomorrow')
|
||||
local updated = store.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 }))
|
||||
assert.are.equal(expected, updated.due)
|
||||
end)
|
||||
|
||||
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()
|
||||
pending.edit(tostring(t.id), 'repeat:weekly')
|
||||
local updated = store.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 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)
|
||||
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()
|
||||
pending.edit(tostring(t.id), 'due:tomorrow@14:00')
|
||||
local updated = store.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 }))
|
||||
assert.are.equal(expected .. 'T14:00', updated.due)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue