feat: time-aware due dates, persistent undo, @return audit (#33)
* 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
This commit is contained in:
parent
72dbf037c7
commit
c57cc0845b
12 changed files with 580 additions and 158 deletions
|
|
@ -6,8 +6,6 @@ local store = require('pending.store')
|
|||
---@class pending.init
|
||||
local M = {}
|
||||
|
||||
---@type pending.Task[][]
|
||||
local _undo_states = {}
|
||||
local UNDO_MAX = 20
|
||||
|
||||
---@return integer bufnr
|
||||
|
|
@ -19,6 +17,7 @@ function M.open()
|
|||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return nil
|
||||
function M._setup_autocmds(bufnr)
|
||||
local group = vim.api.nvim_create_augroup('PendingBuffer', { clear = true })
|
||||
vim.api.nvim_create_autocmd('BufWriteCmd', {
|
||||
|
|
@ -49,6 +48,7 @@ function M._setup_autocmds(bufnr)
|
|||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return nil
|
||||
function M._setup_buf_mappings(bufnr)
|
||||
local cfg = require('pending.config').get()
|
||||
local km = cfg.keymaps
|
||||
|
|
@ -91,28 +91,33 @@ function M._setup_buf_mappings(bufnr)
|
|||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return nil
|
||||
function M._on_write(bufnr)
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local snapshot = store.snapshot()
|
||||
table.insert(_undo_states, snapshot)
|
||||
if #_undo_states > UNDO_MAX then
|
||||
table.remove(_undo_states, 1)
|
||||
local stack = store.undo_stack()
|
||||
table.insert(stack, snapshot)
|
||||
if #stack > UNDO_MAX then
|
||||
table.remove(stack, 1)
|
||||
end
|
||||
diff.apply(lines)
|
||||
buffer.render(bufnr)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.undo_write()
|
||||
if #_undo_states == 0 then
|
||||
local stack = store.undo_stack()
|
||||
if #stack == 0 then
|
||||
vim.notify('Nothing to undo.', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local state = table.remove(_undo_states)
|
||||
local state = table.remove(stack)
|
||||
store.replace_tasks(state)
|
||||
store.save()
|
||||
buffer.render(buffer.bufnr())
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.toggle_complete()
|
||||
local bufnr = buffer.bufnr()
|
||||
if not bufnr then
|
||||
|
|
@ -137,9 +142,7 @@ function M.toggle_complete()
|
|||
if task.recur and task.due then
|
||||
local recur = require('pending.recur')
|
||||
local mode = task.recur_mode or 'scheduled'
|
||||
local base = mode == 'completion' and os.date('%Y-%m-%d') --[[@as string]]
|
||||
or task.due
|
||||
local next_date = recur.next_due(base, task.recur, mode)
|
||||
local next_date = recur.next_due(task.due, task.recur, mode)
|
||||
store.add({
|
||||
description = task.description,
|
||||
category = task.category,
|
||||
|
|
@ -161,6 +164,7 @@ function M.toggle_complete()
|
|||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.toggle_priority()
|
||||
local bufnr = buffer.bufnr()
|
||||
if not bufnr then
|
||||
|
|
@ -191,6 +195,7 @@ function M.toggle_priority()
|
|||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.prompt_date()
|
||||
local bufnr = buffer.bufnr()
|
||||
if not bufnr then
|
||||
|
|
@ -205,7 +210,7 @@ function M.prompt_date()
|
|||
if not id then
|
||||
return
|
||||
end
|
||||
vim.ui.input({ prompt = 'Due date (YYYY-MM-DD): ' }, function(input)
|
||||
vim.ui.input({ prompt = 'Due date (today, +3d, fri@2pm, etc.): ' }, function(input)
|
||||
if not input then
|
||||
return
|
||||
end
|
||||
|
|
@ -214,8 +219,11 @@ function M.prompt_date()
|
|||
local resolved = parse.resolve_date(due)
|
||||
if resolved then
|
||||
due = resolved
|
||||
elseif not due:match('^%d%d%d%d%-%d%d%-%d%d$') then
|
||||
vim.notify('Invalid date format. Use YYYY-MM-DD.', vim.log.levels.ERROR)
|
||||
elseif
|
||||
not due:match('^%d%d%d%d%-%d%d%-%d%d$')
|
||||
and not due:match('^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d$')
|
||||
then
|
||||
vim.notify('Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDThh:mm.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
|
@ -226,6 +234,7 @@ function M.prompt_date()
|
|||
end
|
||||
|
||||
---@param text string
|
||||
---@return nil
|
||||
function M.add(text)
|
||||
if not text or text == '' then
|
||||
vim.notify('Usage: :Pending add <description>', vim.log.levels.ERROR)
|
||||
|
|
@ -252,6 +261,7 @@ function M.add(text)
|
|||
vim.notify('Pending added: ' .. description)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.sync()
|
||||
local ok, gcal = pcall(require, 'pending.sync.gcal')
|
||||
if not ok then
|
||||
|
|
@ -262,6 +272,7 @@ function M.sync()
|
|||
end
|
||||
|
||||
---@param days? integer
|
||||
---@return nil
|
||||
function M.archive(days)
|
||||
days = days or 30
|
||||
local cutoff = os.time() - (days * 86400)
|
||||
|
|
@ -298,8 +309,46 @@ function M.archive(days)
|
|||
end
|
||||
end
|
||||
|
||||
function M.due()
|
||||
---@param due string
|
||||
---@return boolean
|
||||
local function is_due_or_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 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
|
||||
|
||||
---@return nil
|
||||
function M.due()
|
||||
local bufnr = buffer.bufnr()
|
||||
local is_valid = bufnr ~= nil and vim.api.nvim_buf_is_valid(bufnr)
|
||||
local meta = is_valid and buffer.meta() or nil
|
||||
|
|
@ -307,9 +356,9 @@ function M.due()
|
|||
|
||||
if meta and bufnr then
|
||||
for lnum, m in ipairs(meta) do
|
||||
if m.type == 'task' and m.raw_due and m.status ~= 'done' and m.raw_due <= today then
|
||||
if m.type == 'task' and m.raw_due and m.status ~= 'done' and is_due_or_overdue(m.raw_due) then
|
||||
local task = store.get(m.id or 0)
|
||||
local label = m.raw_due < today and '[OVERDUE] ' or '[DUE] '
|
||||
local label = is_overdue(m.raw_due) and '[OVERDUE] ' or '[DUE] '
|
||||
table.insert(qf_items, {
|
||||
bufnr = bufnr,
|
||||
lnum = lnum,
|
||||
|
|
@ -321,8 +370,8 @@ function M.due()
|
|||
else
|
||||
store.load()
|
||||
for _, task in ipairs(store.active_tasks()) do
|
||||
if task.status == 'pending' and task.due and task.due <= today then
|
||||
local label = task.due < today and '[OVERDUE] ' or '[DUE] '
|
||||
if task.status == 'pending' and task.due and is_due_or_overdue(task.due) then
|
||||
local label = is_overdue(task.due) and '[OVERDUE] ' or '[DUE] '
|
||||
local text = label .. task.description
|
||||
if task.category then
|
||||
text = text .. ' [' .. task.category .. ']'
|
||||
|
|
@ -342,6 +391,7 @@ function M.due()
|
|||
end
|
||||
|
||||
---@param args string
|
||||
---@return nil
|
||||
function M.command(args)
|
||||
if not args or args == '' then
|
||||
M.open()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue