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
cd1cd1afd4
commit
85cf0d42ed
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue