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.
This commit is contained in:
parent
c69afacc87
commit
ee2d125846
11 changed files with 369 additions and 118 deletions
|
|
@ -1,4 +1,5 @@
|
|||
local buffer = require('pending.buffer')
|
||||
local config = require('pending.config')
|
||||
local diff = require('pending.diff')
|
||||
local parse = require('pending.parse')
|
||||
local store = require('pending.store')
|
||||
|
|
@ -6,8 +7,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 +18,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 +49,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 +92,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 +143,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 +165,7 @@ function M.toggle_complete()
|
|||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.toggle_priority()
|
||||
local bufnr = buffer.bufnr()
|
||||
if not bufnr then
|
||||
|
|
@ -191,6 +196,7 @@ function M.toggle_priority()
|
|||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.prompt_date()
|
||||
local bufnr = buffer.bufnr()
|
||||
if not bufnr then
|
||||
|
|
@ -205,7 +211,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 (YYYY-MM-DD[Thh:mm]): ' }, function(input)
|
||||
if not input then
|
||||
return
|
||||
end
|
||||
|
|
@ -214,8 +220,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 +235,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 +262,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 +273,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 +310,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 +357,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 +371,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 +392,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