* fix(plugin): allow command chaining with bar separator
Problem: :Pending|only failed because the command definition lacked the
bar attribute, causing | to be consumed as an argument.
Solution: Add bar = true to nvim_create_user_command so | is treated as
a command separator, matching fugitive's :Git behavior.
* refactor(buffer): remove opinionated window options
Problem: The plugin hardcoded number, relativenumber, wrap, spell,
signcolumn, foldcolumn, and cursorline in set_win_options, overriding
user preferences with no way to opt out.
Solution: Remove all cosmetic window options. Users who want them can
set them in after/ftplugin/pending.lua. Only conceallevel,
concealcursor, and winfixheight remain as functionally required.
* feat: time-aware due dates, persistent undo, @return audit
Problem: Due dates had no time component, the undo stack was lost on
restart and stored in a separate file, and many public functions lacked
required @return annotations.
Solution: Add YYYY-MM-DDThh:mm support across parse, views, recur,
complete, and init with time-aware overdue checks. Merge the undo stack
into the task store JSON so a single file holds all state. Add @return
nil annotations to all 27 void public functions across every module.
* feat(parse): flexible time parsing for @ suffix
Problem: the @HH:MM time suffix required zero-padded 24-hour format,
forcing users to write due:tomorrow@14:00 instead of due:tomorrow@2pm.
Solution: add normalize_time() that accepts bare hours (9, 14),
H:MM (9:30), am/pm (2pm, 9:30am, 12am), and existing HH:MM format,
normalizing all to canonical HH:MM on save.
* feat(complete): add info descriptions to omnifunc items
Problem: completion menu items had no description, making it hard to
distinguish between similar entries like date shorthands and recurrence
patterns.
Solution: return { word, info } tables from date_completions() and
recur_completions(), surfacing human-readable descriptions in the
completion popup.
* ci: format
236 lines
5.6 KiB
Lua
236 lines
5.6 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
|
|
---@field priority? integer
|
|
---@field recur? string
|
|
|
|
---@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, hh, mm = due:match('^(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d)$')
|
|
if not y then
|
|
y, m, d = due:match('^(%d%d%d%d)-(%d%d)-(%d%d)$')
|
|
end
|
|
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]],
|
|
})
|
|
local formatted = os.date(config.get().date_format, t) --[[@as string]]
|
|
if hh then
|
|
formatted = formatted .. ' ' .. hh .. ':' .. mm
|
|
end
|
|
return formatted
|
|
end
|
|
|
|
---@param due string
|
|
---@return boolean
|
|
local function is_overdue(due)
|
|
local now = os.date('*t') --[[@as osdate]]
|
|
local today = os.date('%Y-%m-%d') --[[@as string]]
|
|
local date_part, time_part = due:match('^(.+)T(.+)$')
|
|
if not date_part then
|
|
return due < today
|
|
end
|
|
if date_part < today then
|
|
return true
|
|
end
|
|
if date_part > today then
|
|
return false
|
|
end
|
|
local current_time = string.format('%02d:%02d', now.hour, now.min)
|
|
return time_part < current_time
|
|
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 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 state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ')
|
|
local line = prefix .. '- [' .. state .. '] ' .. 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 is_overdue(task.due) or nil,
|
|
recur = task.recur,
|
|
})
|
|
end
|
|
end
|
|
|
|
return lines, meta
|
|
end
|
|
|
|
---@param tasks pending.Task[]
|
|
---@return string[] lines
|
|
---@return pending.LineMeta[] meta
|
|
function M.priority_view(tasks)
|
|
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 state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ')
|
|
local line = prefix .. '- [' .. state .. '] ' .. 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 is_overdue(task.due) or nil,
|
|
show_category = true,
|
|
recur = task.recur,
|
|
})
|
|
end
|
|
|
|
return lines, meta
|
|
end
|
|
|
|
return M
|