* feat(config): add category_order field Problem: category display order was always insertion order with no way to configure it. Solution: add category_order to config defaults so users can declare a preferred category ordering; unspecified categories append after. * feat(parse): add relative date resolution Problem: due dates required full YYYY-MM-DD input, adding friction for common cases like "today" or "next monday". Solution: add resolve_date() supporting today, tomorrow, +Nd, and weekday abbreviations; extend inline token parsing to resolve relative values before falling back to strict date validation. * feat(views): overdue flag, category in priority view, category ordering Problem: overdue tasks were visually indistinct from upcoming ones; priority view had no category context; category display order was not configurable. Solution: compute overdue meta flag for pending tasks past their due date; set show_category on priority view task meta; reorder categories according to config.category_order when present. * feat(buffer): overdue highlight, category virt text in priority view Problem: overdue tasks had no visual distinction; priority view showed no category context alongside due dates. Solution: add PendingOverdue highlight group; render category name as right-aligned virtual text in priority view, composited with the due date when both are present. * feat(init): undo write and buffer-local default mappings Problem: _undo_state was captured on every save but never consumed; toggle_priority and prompt_date had no buffer-local defaults, requiring manual <Plug> configuration. Solution: implement undo_write() to restore pre-save task state; add !, d, and U as buffer-local defaults following fugitive's philosophy of owning the buffer; expose :Pending undo as a command alias. * test(views): add views spec Problem: views.lua had no test coverage. Solution: add 26 tests covering category_view and priority_view including sort order, line format, overdue detection, show_category meta, and category_order config behavior. * test(archive): add archive spec Problem: archive had no test coverage. Solution: add 9 tests covering cutoff logic, custom day counts, pending task preservation, deleted task cleanup, and notify output. * docs: add vimdoc Problem: no :help documentation existed. Solution: add doc/pending.txt covering all features — commands, mappings, views, configuration, Google Calendar sync, highlight groups, data format, and health check — following standard vimdoc conventions. * ci: format * fix: resolve lint and type check errors Problem: selene flagged unused variables in new spec files; LuaLS flagged os.date/os.time return type mismatches, integer? assignments, and stale task.Task/task.GcalConfig type references. Solution: prefix unused spec variables with _ or drop unnecessary assignments; add --[[@as string/integer]] casts for os.date and os.time calls; add category_order field to pending.Config annotation; fix task.GcalConfig -> pending.GcalConfig and task.Task[] -> pending.Task[]; add nil guards on meta[row].id before store calls; cast store.data() return to non-optional. * ci: format * fix: sync * ci: format
210 lines
4.9 KiB
Lua
210 lines
4.9 KiB
Lua
local config = require('pending.config')
|
|
|
|
---@class pending.LineMeta
|
|
---@field type 'task'|'header'|'blank'
|
|
---@field id? integer
|
|
---@field due? string
|
|
---@field raw_due? string
|
|
---@field status? string
|
|
---@field category? string
|
|
---@field overdue? boolean
|
|
---@field show_category? boolean
|
|
|
|
---@class pending.views
|
|
local M = {}
|
|
|
|
---@param due? string
|
|
---@return string?
|
|
local function format_due(due)
|
|
if not due then
|
|
return nil
|
|
end
|
|
local y, m, d = due:match('^(%d%d%d%d)-(%d%d)-(%d%d)$')
|
|
if not y then
|
|
return due
|
|
end
|
|
local t = os.time({
|
|
year = tonumber(y) --[[@as integer]],
|
|
month = tonumber(m) --[[@as integer]],
|
|
day = tonumber(d) --[[@as integer]],
|
|
})
|
|
return os.date(config.get().date_format, t) --[[@as string]]
|
|
end
|
|
|
|
---@param tasks pending.Task[]
|
|
local function sort_tasks(tasks)
|
|
table.sort(tasks, function(a, b)
|
|
if a.priority ~= b.priority then
|
|
return a.priority > b.priority
|
|
end
|
|
if a.order ~= b.order then
|
|
return a.order < b.order
|
|
end
|
|
return a.id < b.id
|
|
end)
|
|
end
|
|
|
|
---@param tasks pending.Task[]
|
|
local function sort_tasks_priority(tasks)
|
|
table.sort(tasks, function(a, b)
|
|
if a.priority ~= b.priority then
|
|
return a.priority > b.priority
|
|
end
|
|
local a_due = a.due or ''
|
|
local b_due = b.due or ''
|
|
if a_due ~= b_due then
|
|
if a_due == '' then
|
|
return false
|
|
end
|
|
if b_due == '' then
|
|
return true
|
|
end
|
|
return a_due < b_due
|
|
end
|
|
if a.order ~= b.order then
|
|
return a.order < b.order
|
|
end
|
|
return a.id < b.id
|
|
end)
|
|
end
|
|
|
|
---@param tasks pending.Task[]
|
|
---@return string[] lines
|
|
---@return pending.LineMeta[] meta
|
|
function M.category_view(tasks)
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
local by_cat = {}
|
|
local cat_order = {}
|
|
local cat_seen = {}
|
|
local done_by_cat = {}
|
|
|
|
for _, task in ipairs(tasks) do
|
|
local cat = task.category or config.get().default_category
|
|
if not cat_seen[cat] then
|
|
cat_seen[cat] = true
|
|
table.insert(cat_order, cat)
|
|
by_cat[cat] = {}
|
|
done_by_cat[cat] = {}
|
|
end
|
|
if task.status == 'done' then
|
|
table.insert(done_by_cat[cat], task)
|
|
else
|
|
table.insert(by_cat[cat], task)
|
|
end
|
|
end
|
|
|
|
local cfg_order = config.get().category_order
|
|
if cfg_order and #cfg_order > 0 then
|
|
local ordered = {}
|
|
local seen = {}
|
|
for _, name in ipairs(cfg_order) do
|
|
if cat_seen[name] then
|
|
table.insert(ordered, name)
|
|
seen[name] = true
|
|
end
|
|
end
|
|
for _, name in ipairs(cat_order) do
|
|
if not seen[name] then
|
|
table.insert(ordered, name)
|
|
end
|
|
end
|
|
cat_order = ordered
|
|
end
|
|
|
|
for _, cat in ipairs(cat_order) do
|
|
sort_tasks(by_cat[cat])
|
|
sort_tasks(done_by_cat[cat])
|
|
end
|
|
|
|
local lines = {}
|
|
local meta = {}
|
|
|
|
for i, cat in ipairs(cat_order) do
|
|
if i > 1 then
|
|
table.insert(lines, '')
|
|
table.insert(meta, { type = 'blank' })
|
|
end
|
|
table.insert(lines, cat)
|
|
table.insert(meta, { type = 'header', category = cat })
|
|
|
|
local all = {}
|
|
for _, t in ipairs(by_cat[cat]) do
|
|
table.insert(all, t)
|
|
end
|
|
for _, t in ipairs(done_by_cat[cat]) do
|
|
table.insert(all, t)
|
|
end
|
|
|
|
for _, task in ipairs(all) do
|
|
local prefix = '/' .. task.id .. '/'
|
|
local indent = ' '
|
|
local prio = task.priority == 1 and '! ' or ''
|
|
local line = prefix .. indent .. prio .. task.description
|
|
table.insert(lines, line)
|
|
table.insert(meta, {
|
|
type = 'task',
|
|
id = task.id,
|
|
due = format_due(task.due),
|
|
raw_due = task.due,
|
|
status = task.status,
|
|
category = cat,
|
|
overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil,
|
|
})
|
|
end
|
|
end
|
|
|
|
return lines, meta
|
|
end
|
|
|
|
---@param tasks pending.Task[]
|
|
---@return string[] lines
|
|
---@return pending.LineMeta[] meta
|
|
function M.priority_view(tasks)
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
local pending = {}
|
|
local done = {}
|
|
|
|
for _, task in ipairs(tasks) do
|
|
if task.status == 'done' then
|
|
table.insert(done, task)
|
|
else
|
|
table.insert(pending, task)
|
|
end
|
|
end
|
|
|
|
sort_tasks_priority(pending)
|
|
sort_tasks_priority(done)
|
|
|
|
local lines = {}
|
|
local meta = {}
|
|
|
|
local all = {}
|
|
for _, t in ipairs(pending) do
|
|
table.insert(all, t)
|
|
end
|
|
for _, t in ipairs(done) do
|
|
table.insert(all, t)
|
|
end
|
|
|
|
for _, task in ipairs(all) do
|
|
local prefix = '/' .. task.id .. '/'
|
|
local indent = ' '
|
|
local prio = task.priority == 1 and '! ' or ''
|
|
local line = prefix .. indent .. prio .. task.description
|
|
table.insert(lines, line)
|
|
table.insert(meta, {
|
|
type = 'task',
|
|
id = task.id,
|
|
due = format_due(task.due),
|
|
raw_due = task.due,
|
|
status = task.status,
|
|
category = task.category,
|
|
overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil,
|
|
show_category = true,
|
|
})
|
|
end
|
|
|
|
return lines, meta
|
|
end
|
|
|
|
return M
|