* 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
244 lines
5.9 KiB
Lua
244 lines
5.9 KiB
Lua
if vim.g.loaded_pending then
|
|
return
|
|
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', 'archive', 'due', 'edit', 'sync', 'undo' }
|
|
if not cmd_line:match('^Pending%s+%S') then
|
|
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,
|
|
})
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-open)', function()
|
|
require('pending').open()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-close)', function()
|
|
require('pending.buffer').close()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-toggle)', function()
|
|
require('pending').toggle_complete()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-view)', function()
|
|
require('pending.buffer').toggle_view()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-priority)', function()
|
|
require('pending').toggle_priority()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-date)', function()
|
|
require('pending').prompt_date()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-undo)', function()
|
|
require('pending').undo_write()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-open-line)', function()
|
|
require('pending.buffer').open_line(false)
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-open-line-above)', function()
|
|
require('pending.buffer').open_line(true)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-a-task)', function()
|
|
require('pending.textobj').a_task(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-i-task)', function()
|
|
require('pending.textobj').i_task(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-a-category)', function()
|
|
require('pending.textobj').a_category(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-i-category)', function()
|
|
require('pending.textobj').i_category(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-next-header)', function()
|
|
require('pending.textobj').next_header(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-prev-header)', function()
|
|
require('pending.textobj').prev_header(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-next-task)', function()
|
|
require('pending.textobj').next_task(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-prev-task)', function()
|
|
require('pending.textobj').prev_task(vim.v.count1)
|
|
end)
|