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
|
|
@ -37,10 +37,12 @@ function M.current_view_name()
|
|||
return current_view
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.clear_winid()
|
||||
task_winid = nil
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.close()
|
||||
if task_winid and vim.api.nvim_win_is_valid(task_winid) then
|
||||
vim.api.nvim_win_close(task_winid, false)
|
||||
|
|
@ -79,6 +81,7 @@ local function setup_syntax(bufnr)
|
|||
end
|
||||
|
||||
---@param above boolean
|
||||
---@return nil
|
||||
function M.open_line(above)
|
||||
local bufnr = task_bufnr
|
||||
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
|
|
@ -205,6 +208,7 @@ local function restore_folds(bufnr)
|
|||
end
|
||||
|
||||
---@param bufnr? integer
|
||||
---@return nil
|
||||
function M.render(bufnr)
|
||||
bufnr = bufnr or task_bufnr
|
||||
if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
|
|
@ -249,6 +253,7 @@ function M.render(bufnr)
|
|||
restore_folds(bufnr)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.toggle_view()
|
||||
if current_view == 'category' then
|
||||
current_view = 'priority'
|
||||
|
|
|
|||
|
|
@ -58,6 +58,18 @@ local function date_completions()
|
|||
'soq',
|
||||
'soy',
|
||||
'later',
|
||||
'today@08:00',
|
||||
'today@09:00',
|
||||
'today@10:00',
|
||||
'today@12:00',
|
||||
'today@14:00',
|
||||
'today@17:00',
|
||||
'tomorrow@08:00',
|
||||
'tomorrow@09:00',
|
||||
'tomorrow@10:00',
|
||||
'tomorrow@12:00',
|
||||
'tomorrow@14:00',
|
||||
'tomorrow@17:00',
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function M.get()
|
|||
return _resolved
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.reset()
|
||||
_resolved = nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ function M.parse_buffer(lines)
|
|||
end
|
||||
|
||||
---@param lines string[]
|
||||
---@return nil
|
||||
function M.apply(lines)
|
||||
local parsed = M.parse_buffer(lines)
|
||||
local now = timestamp()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
local M = {}
|
||||
|
||||
---@return nil
|
||||
function M.check()
|
||||
vim.health.start('pending.nvim')
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -24,6 +24,28 @@ local function is_valid_date(s)
|
|||
return check.year == yn and check.month == mn and check.day == dn
|
||||
end
|
||||
|
||||
---@param s string
|
||||
---@return boolean
|
||||
local function is_valid_time(s)
|
||||
local h, m = s:match('^(%d%d):(%d%d)$')
|
||||
if not h then
|
||||
return false
|
||||
end
|
||||
local hn = tonumber(h) --[[@as integer]]
|
||||
local mn = tonumber(m) --[[@as integer]]
|
||||
return hn >= 0 and hn <= 23 and mn >= 0 and mn <= 59
|
||||
end
|
||||
|
||||
---@param s string
|
||||
---@return boolean
|
||||
local function is_valid_datetime(s)
|
||||
local date_part, time_part = s:match('^(.+)T(.+)$')
|
||||
if not date_part then
|
||||
return is_valid_date(s)
|
||||
end
|
||||
return is_valid_date(date_part) and is_valid_time(time_part)
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function date_key()
|
||||
return config.get().date_syntax or 'due'
|
||||
|
|
@ -65,146 +87,217 @@ local function today_str(today)
|
|||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day })) --[[@as string]]
|
||||
end
|
||||
|
||||
---@param date_part string
|
||||
---@param time_suffix? string
|
||||
---@return string
|
||||
local function append_time(date_part, time_suffix)
|
||||
if time_suffix then
|
||||
return date_part .. 'T' .. time_suffix
|
||||
end
|
||||
return date_part
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@return string|nil
|
||||
function M.resolve_date(text)
|
||||
local lower = text:lower()
|
||||
local date_input, time_suffix = text:match('^(.+)@(%d%d:%d%d)$')
|
||||
if time_suffix then
|
||||
if not is_valid_time(time_suffix) then
|
||||
return nil
|
||||
end
|
||||
else
|
||||
date_input = text
|
||||
end
|
||||
|
||||
local dt = date_input:match('^(%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d)$')
|
||||
if dt then
|
||||
local dp, tp = dt:match('^(.+)T(.+)$')
|
||||
if is_valid_date(dp) and is_valid_time(tp) then
|
||||
return dt
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if is_valid_date(date_input) then
|
||||
return append_time(date_input, time_suffix)
|
||||
end
|
||||
|
||||
local lower = date_input:lower()
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
|
||||
if lower == 'today' or lower == 'eod' then
|
||||
return today_str(today)
|
||||
return append_time(today_str(today), time_suffix)
|
||||
end
|
||||
|
||||
if lower == 'yesterday' then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day - 1 })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day - 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'tomorrow' then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + 1 })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'sow' then
|
||||
local delta = -((today.wday - 2) % 7)
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eow' then
|
||||
local delta = (1 - today.wday) % 7
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'som' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eom' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'soq' then
|
||||
local q = math.ceil(today.month / 3)
|
||||
local first_month = (q - 1) * 3 + 1
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = first_month, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = first_month, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eoq' then
|
||||
local q = math.ceil(today.month / 3)
|
||||
local last_month = q * 3
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'soy' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = 1, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = 1, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eoy' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = 12, day = 31 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = 12, day = 31 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'later' or lower == 'someday' then
|
||||
return config.get().someday_date
|
||||
return append_time(config.get().someday_date, time_suffix)
|
||||
end
|
||||
|
||||
local n = lower:match('^%+(%d+)d$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%+(%d+)w$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%+(%d+)m$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
day = today.day,
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
day = today.day,
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%-(%d+)d$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%-(%d+)w$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
local ord = lower:match('^(%d+)[snrt][tdh]$')
|
||||
|
|
@ -222,7 +315,7 @@ function M.resolve_date(text)
|
|||
local t = os.time({ year = y, month = m, day = day_num })
|
||||
local check = os.date('*t', t) --[[@as osdate]]
|
||||
if check.day == day_num then
|
||||
return os.date('%Y-%m-%d', t) --[[@as string]]
|
||||
return append_time(os.date('%Y-%m-%d', t) --[[@as string]], time_suffix)
|
||||
end
|
||||
m = m + 1
|
||||
if m > 12 then
|
||||
|
|
@ -232,7 +325,7 @@ function M.resolve_date(text)
|
|||
t = os.time({ year = y, month = m, day = day_num })
|
||||
check = os.date('*t', t) --[[@as osdate]]
|
||||
if check.day == day_num then
|
||||
return os.date('%Y-%m-%d', t) --[[@as string]]
|
||||
return append_time(os.date('%Y-%m-%d', t) --[[@as string]], time_suffix)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
|
@ -244,17 +337,23 @@ function M.resolve_date(text)
|
|||
if today.month >= target_month then
|
||||
y = y + 1
|
||||
end
|
||||
return os.date('%Y-%m-%d', os.time({ year = y, month = target_month, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = y, month = target_month, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
local target_wday = weekday_map[lower]
|
||||
if target_wday then
|
||||
local current_wday = today.wday
|
||||
local delta = (target_wday - current_wday) % 7
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
return nil
|
||||
|
|
@ -273,7 +372,7 @@ function M.body(text)
|
|||
local i = #tokens
|
||||
local dk = date_key()
|
||||
local rk = recur_key()
|
||||
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d)$'
|
||||
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d[T%d:]*)$'
|
||||
local date_pattern_any = '^' .. vim.pesc(dk) .. ':(.+)$'
|
||||
local rec_pattern = '^' .. vim.pesc(rk) .. ':(%S+)$'
|
||||
|
||||
|
|
@ -284,7 +383,7 @@ function M.body(text)
|
|||
if metadata.due then
|
||||
break
|
||||
end
|
||||
if not is_valid_date(due_val) then
|
||||
if not is_valid_datetime(due_val) then
|
||||
break
|
||||
end
|
||||
metadata.due = due_val
|
||||
|
|
|
|||
|
|
@ -80,20 +80,33 @@ function M.validate(spec)
|
|||
return M.parse(spec) ~= nil
|
||||
end
|
||||
|
||||
---@param due string
|
||||
---@return string date_part
|
||||
---@return string? time_part
|
||||
local function split_datetime(due)
|
||||
local dp, tp = due:match('^(.+)T(.+)$')
|
||||
if dp then
|
||||
return dp, tp
|
||||
end
|
||||
return due, nil
|
||||
end
|
||||
|
||||
---@param base_date string
|
||||
---@param freq string
|
||||
---@param interval integer
|
||||
---@return string
|
||||
local function advance_date(base_date, freq, interval)
|
||||
local y, m, d = base_date:match('^(%d+)-(%d+)-(%d+)$')
|
||||
local date_part, time_part = split_datetime(base_date)
|
||||
local y, m, d = date_part:match('^(%d+)-(%d+)-(%d+)$')
|
||||
local yn = tonumber(y) --[[@as integer]]
|
||||
local mn = tonumber(m) --[[@as integer]]
|
||||
local dn = tonumber(d) --[[@as integer]]
|
||||
|
||||
local result
|
||||
if freq == 'daily' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = yn, month = mn, day = dn + interval })) --[[@as string]]
|
||||
result = os.date('%Y-%m-%d', os.time({ year = yn, month = mn, day = dn + interval })) --[[@as string]]
|
||||
elseif freq == 'weekly' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = yn, month = mn, day = dn + interval * 7 })) --[[@as string]]
|
||||
result = os.date('%Y-%m-%d', os.time({ year = yn, month = mn, day = dn + interval * 7 })) --[[@as string]]
|
||||
elseif freq == 'monthly' then
|
||||
local new_m = mn + interval
|
||||
local new_y = yn
|
||||
|
|
@ -103,14 +116,20 @@ local function advance_date(base_date, freq, interval)
|
|||
end
|
||||
local last_day = os.date('*t', os.time({ year = new_y, month = new_m + 1, day = 0 })) --[[@as osdate]]
|
||||
local clamped_d = math.min(dn, last_day.day --[[@as integer]])
|
||||
return os.date('%Y-%m-%d', os.time({ year = new_y, month = new_m, day = clamped_d })) --[[@as string]]
|
||||
result = os.date('%Y-%m-%d', os.time({ year = new_y, month = new_m, day = clamped_d })) --[[@as string]]
|
||||
elseif freq == 'yearly' then
|
||||
local new_y = yn + interval
|
||||
local last_day = os.date('*t', os.time({ year = new_y, month = mn + 1, day = 0 })) --[[@as osdate]]
|
||||
local clamped_d = math.min(dn, last_day.day --[[@as integer]])
|
||||
return os.date('%Y-%m-%d', os.time({ year = new_y, month = mn, day = clamped_d })) --[[@as string]]
|
||||
result = os.date('%Y-%m-%d', os.time({ year = new_y, month = mn, day = clamped_d })) --[[@as string]]
|
||||
else
|
||||
return base_date
|
||||
end
|
||||
return base_date
|
||||
|
||||
if time_part then
|
||||
return result .. 'T' .. time_part
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
---@param base_date string
|
||||
|
|
@ -124,13 +143,16 @@ function M.next_due(base_date, spec, mode)
|
|||
end
|
||||
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
local _, time_part = split_datetime(base_date)
|
||||
|
||||
if mode == 'completion' then
|
||||
return advance_date(today, parsed.freq, parsed.interval)
|
||||
local base = time_part and (today .. 'T' .. time_part) or today
|
||||
return advance_date(base, parsed.freq, parsed.interval)
|
||||
end
|
||||
|
||||
local next_date = advance_date(base_date, parsed.freq, parsed.interval)
|
||||
while next_date <= today do
|
||||
local compare_today = time_part and (today .. 'T' .. time_part) or today
|
||||
while next_date <= compare_today do
|
||||
next_date = advance_date(next_date, parsed.freq, parsed.interval)
|
||||
end
|
||||
return next_date
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ local config = require('pending.config')
|
|||
---@field version integer
|
||||
---@field next_id integer
|
||||
---@field tasks pending.Task[]
|
||||
---@field undo pending.Task[][]
|
||||
|
||||
---@class pending.store
|
||||
local M = {}
|
||||
|
|
@ -34,6 +35,7 @@ local function empty_data()
|
|||
version = SUPPORTED_VERSION,
|
||||
next_id = 1,
|
||||
tasks = {},
|
||||
undo = {},
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -165,13 +167,24 @@ function M.load()
|
|||
version = decoded.version or SUPPORTED_VERSION,
|
||||
next_id = decoded.next_id or 1,
|
||||
tasks = {},
|
||||
undo = {},
|
||||
}
|
||||
for _, t in ipairs(decoded.tasks or {}) do
|
||||
table.insert(_data.tasks, table_to_task(t))
|
||||
end
|
||||
for _, snapshot in ipairs(decoded.undo or {}) do
|
||||
if type(snapshot) == 'table' then
|
||||
local tasks = {}
|
||||
for _, raw in ipairs(snapshot) do
|
||||
table.insert(tasks, table_to_task(raw))
|
||||
end
|
||||
table.insert(_data.undo, tasks)
|
||||
end
|
||||
end
|
||||
return _data
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.save()
|
||||
if not _data then
|
||||
return
|
||||
|
|
@ -182,10 +195,18 @@ function M.save()
|
|||
version = _data.version,
|
||||
next_id = _data.next_id,
|
||||
tasks = {},
|
||||
undo = {},
|
||||
}
|
||||
for _, task in ipairs(_data.tasks) do
|
||||
table.insert(out.tasks, task_to_table(task))
|
||||
end
|
||||
for _, snapshot in ipairs(_data.undo) do
|
||||
local serialized = {}
|
||||
for _, task in ipairs(snapshot) do
|
||||
table.insert(serialized, task_to_table(task))
|
||||
end
|
||||
table.insert(out.undo, serialized)
|
||||
end
|
||||
local encoded = vim.json.encode(out)
|
||||
local tmp = path .. '.tmp'
|
||||
local f = io.open(tmp, 'w')
|
||||
|
|
@ -300,6 +321,7 @@ function M.find_index(id)
|
|||
end
|
||||
|
||||
---@param tasks pending.Task[]
|
||||
---@return nil
|
||||
function M.replace_tasks(tasks)
|
||||
M.data().tasks = tasks
|
||||
end
|
||||
|
|
@ -325,11 +347,24 @@ function M.snapshot()
|
|||
return result
|
||||
end
|
||||
|
||||
---@return pending.Task[][]
|
||||
function M.undo_stack()
|
||||
return M.data().undo
|
||||
end
|
||||
|
||||
---@param stack pending.Task[][]
|
||||
---@return nil
|
||||
function M.set_undo_stack(stack)
|
||||
M.data().undo = stack
|
||||
end
|
||||
|
||||
---@param id integer
|
||||
---@return nil
|
||||
function M.set_next_id(id)
|
||||
M.data().next_id = id
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.unload()
|
||||
_data = nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ 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)$')
|
||||
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
|
||||
|
|
@ -30,7 +33,30 @@ local function format_due(due)
|
|||
month = tonumber(m) --[[@as integer]],
|
||||
day = tonumber(d) --[[@as integer]],
|
||||
})
|
||||
return os.date(config.get().date_format, t) --[[@as string]]
|
||||
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[]
|
||||
|
|
@ -74,7 +100,6 @@ end
|
|||
---@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 = {}
|
||||
|
|
@ -149,7 +174,7 @@ function M.category_view(tasks)
|
|||
raw_due = task.due,
|
||||
status = task.status,
|
||||
category = cat,
|
||||
overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil,
|
||||
overdue = task.status == 'pending' and task.due ~= nil and is_overdue(task.due) or nil,
|
||||
recur = task.recur,
|
||||
})
|
||||
end
|
||||
|
|
@ -162,7 +187,6 @@ end
|
|||
---@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 = {}
|
||||
|
||||
|
|
@ -200,7 +224,7 @@ function M.priority_view(tasks)
|
|||
raw_due = task.due,
|
||||
status = task.status,
|
||||
category = task.category,
|
||||
overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil,
|
||||
overdue = task.status == 'pending' and task.due ~= nil and is_overdue(task.due) or nil,
|
||||
show_category = true,
|
||||
recur = task.recur,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue