feat: complete task editing coverage (#109)

Problem: the task editing surface had gaps — category and recurrence had
no keymaps, `:Pending edit` required knowing the task ID, tasks couldn't
be reordered with a keymap, priority was binary (0/1), and `wip`/`blocked`
states were documented but unimplemented.

Solution: fill every cell so every property is editable in every way.

- `gc`/`gr` keymaps for category select and recurrence prompt
- cursor-aware `:Pending edit` (omit ID to use task under cursor)
- `J`/`K` keymaps to reorder tasks within a category
- multi-level priorities (`max_priority` config, `g!` cycles 0→1→2→3→0)
- `+!!`/`+!!!` tokens in `:Pending edit`, `:Pending add`, `parse.body()`
- `PendingPriority2`/`PendingPriority3` highlight groups
- `gw`/`gb` keymaps toggle `wip`/`blocked` status
- `>`/`=` state chars in buffer rendering and diff parsing
- `PendingWip`/`PendingBlocked` highlight groups
- sort order: wip → pending → blocked → done
- `wip`/`blocked` filter predicates and icons
This commit is contained in:
Barrett Ruth 2026-03-08 19:44:03 -04:00 committed by GitHub
parent 073541424e
commit b06249f101
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 498 additions and 101 deletions

View file

@ -41,9 +41,32 @@ local function format_due(due)
return formatted
end
---@type table<string, integer>
local status_rank = { wip = 0, pending = 1, blocked = 2, done = 3 }
---@param task pending.Task
---@return string
local function state_char(task)
if task.status == 'done' then
return 'x'
elseif task.status == 'wip' then
return '>'
elseif task.status == 'blocked' then
return '='
elseif task.priority > 0 then
return '!'
end
return ' '
end
---@param tasks pending.Task[]
local function sort_tasks(tasks)
table.sort(tasks, function(a, b)
local ra = status_rank[a.status] or 1
local rb = status_rank[b.status] or 1
if ra ~= rb then
return ra < rb
end
if a.priority ~= b.priority then
return a.priority > b.priority
end
@ -57,6 +80,11 @@ end
---@param tasks pending.Task[]
local function sort_tasks_priority(tasks)
table.sort(tasks, function(a, b)
local ra = status_rank[a.status] or 1
local rb = status_rank[b.status] or 1
if ra ~= rb then
return ra < rb
end
if a.priority ~= b.priority then
return a.priority > b.priority
end
@ -95,7 +123,7 @@ function M.category_view(tasks)
by_cat[cat] = {}
done_by_cat[cat] = {}
end
if task.status == 'done' then
if task.status == 'done' or task.status == 'deleted' then
table.insert(done_by_cat[cat], task)
else
table.insert(by_cat[cat], task)
@ -146,7 +174,7 @@ function M.category_view(tasks)
for _, task in ipairs(all) do
local prefix = '/' .. task.id .. '/'
local state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ')
local state = state_char(task)
local line = prefix .. '- [' .. state .. '] ' .. task.description
table.insert(lines, line)
table.insert(meta, {
@ -157,7 +185,7 @@ function M.category_view(tasks)
status = task.status,
category = cat,
priority = task.priority,
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due)
overdue = task.status ~= 'done' and task.due ~= nil and parse.is_overdue(task.due)
or nil,
recur = task.recur,
})
@ -209,7 +237,7 @@ function M.priority_view(tasks)
status = task.status,
category = task.category,
priority = task.priority,
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due) or nil,
overdue = task.status ~= 'done' and task.due ~= nil and parse.is_overdue(task.due) or nil,
show_category = true,
recur = task.recur,
})