diff --git a/doc/pending.txt b/doc/pending.txt index aad924c..66882b9 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -35,7 +35,7 @@ Features: ~ names, month names, ordinals, and more - Recurring tasks with automatic next-date spawning on completion - Two views: category (default) and priority flat list -- Multi-level undo (up to 20 `:w` saves, persisted across sessions) +- Multi-level undo (up to 20 `:w` saves, session-only) - Quick-add from the command line with `:Pending add` - Quickfix list of overdue/due-today tasks via `:Pending due` - Foldable category sections (`zc`/`zo`) in category view @@ -149,23 +149,6 @@ token, the `D` prompt, and `:Pending add`. `soy` / `eoy` January 1 / December 31 of current year `later` / `someday` Sentinel date (default: `9999-12-30`) -Time suffix: ~ *pending-dates-time* -Any named date or absolute date accepts an `@` time suffix. Supported -formats: `HH:MM` (24h), `H:MM`, bare hour (`9`, `14`), and am/pm -(`2pm`, `9:30am`, `12am`). All forms are normalized to `HH:MM` on save. > - - due:tomorrow@2pm " tomorrow at 14:00 - due:fri@9 " next Friday at 09:00 - due:+1w@17:00 " one week from today at 17:00 - due:tomorrow@9:30am " tomorrow at 09:30 - due:2026-03-15@08:00 " absolute date with time - due:2026-03-15T14:30 " ISO 8601 datetime (also accepted) -< - -Tasks with a time component are not considered overdue until after the -specified time. The time is displayed alongside the date in virtual text -and preserved across recurrence advances. - ============================================================================== RECURRENCE *pending-recurrence* @@ -259,7 +242,7 @@ COMMANDS *pending-commands* :Pending undo Undo the last `:w` save, restoring the task store to its previous state. Equivalent to the `U` buffer-local key (see |pending-mappings|). Up to 20 - levels of undo are persisted across sessions. + levels of undo are retained per session. ============================================================================== MAPPINGS *pending-mappings* @@ -434,19 +417,6 @@ Fields: ~ |pending.GcalConfig|. Omit this field entirely to disable Google Calendar sync. -============================================================================== -RECIPES *pending-recipes* - -Configure blink.cmp to use pending.nvim's omnifunc as a completion source: >lua - require('blink.cmp').setup({ - sources = { - per_filetype = { - pending = { 'omni', 'buffer' }, - }, - }, - }) -< - ============================================================================== GOOGLE CALENDAR *pending-gcal* diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index 932c414..553be7d 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -37,12 +37,10 @@ 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) @@ -81,7 +79,6 @@ 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 @@ -208,7 +205,6 @@ 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 @@ -253,7 +249,6 @@ function M.render(bufnr) restore_folds(bufnr) end ----@return nil function M.toggle_view() if current_view == 'category' then current_view = 'priority' diff --git a/lua/pending/complete.lua b/lua/pending/complete.lua index 79f338b..f83b6a4 100644 --- a/lua/pending/complete.lua +++ b/lua/pending/complete.lua @@ -29,75 +29,48 @@ local function get_categories() return result end ----@return { word: string, info: string }[] +---@return string[] local function date_completions() return { - { word = 'today', info = "Today's date" }, - { word = 'tomorrow', info = "Tomorrow's date" }, - { word = 'yesterday', info = "Yesterday's date" }, - { word = '+1d', info = '1 day from today' }, - { word = '+2d', info = '2 days from today' }, - { word = '+3d', info = '3 days from today' }, - { word = '+1w', info = '1 week from today' }, - { word = '+2w', info = '2 weeks from today' }, - { word = '+1m', info = '1 month from today' }, - { word = 'mon', info = 'Next Monday' }, - { word = 'tue', info = 'Next Tuesday' }, - { word = 'wed', info = 'Next Wednesday' }, - { word = 'thu', info = 'Next Thursday' }, - { word = 'fri', info = 'Next Friday' }, - { word = 'sat', info = 'Next Saturday' }, - { word = 'sun', info = 'Next Sunday' }, - { word = 'eod', info = 'End of day (today)' }, - { word = 'eow', info = 'End of week (Sunday)' }, - { word = 'eom', info = 'End of month' }, - { word = 'eoq', info = 'End of quarter' }, - { word = 'eoy', info = 'End of year (Dec 31)' }, - { word = 'sow', info = 'Start of week (Monday)' }, - { word = 'som', info = 'Start of month' }, - { word = 'soq', info = 'Start of quarter' }, - { word = 'soy', info = 'Start of year (Jan 1)' }, - { word = 'later', info = 'Someday (sentinel date)' }, - { word = 'today@08:00', info = 'Today at 08:00' }, - { word = 'today@09:00', info = 'Today at 09:00' }, - { word = 'today@10:00', info = 'Today at 10:00' }, - { word = 'today@12:00', info = 'Today at 12:00' }, - { word = 'today@14:00', info = 'Today at 14:00' }, - { word = 'today@17:00', info = 'Today at 17:00' }, + 'today', + 'tomorrow', + 'yesterday', + '+1d', + '+2d', + '+3d', + '+1w', + '+2w', + '+1m', + 'mon', + 'tue', + 'wed', + 'thu', + 'fri', + 'sat', + 'sun', + 'eod', + 'eow', + 'eom', + 'eoq', + 'eoy', + 'sow', + 'som', + 'soq', + 'soy', + 'later', } end ----@type table -local recur_descriptions = { - daily = 'Every day', - weekdays = 'Monday through Friday', - weekly = 'Every week', - biweekly = 'Every 2 weeks', - monthly = 'Every month', - quarterly = 'Every 3 months', - yearly = 'Every year', - ['2d'] = 'Every 2 days', - ['3d'] = 'Every 3 days', - ['2w'] = 'Every 2 weeks', - ['3w'] = 'Every 3 weeks', - ['2m'] = 'Every 2 months', - ['3m'] = 'Every 3 months', - ['6m'] = 'Every 6 months', - ['2y'] = 'Every 2 years', -} - ----@return { word: string, info: string }[] +---@return string[] local function recur_completions() local recur = require('pending.recur') local list = recur.shorthand_list() local result = {} for _, s in ipairs(list) do - local desc = recur_descriptions[s] or s - table.insert(result, { word = s, info = desc }) + table.insert(result, s) end for _, s in ipairs(list) do - local desc = recur_descriptions[s] or s - table.insert(result, { word = '!' .. s, info = desc .. ' (from completion date)' }) + table.insert(result, '!' .. s) end return result end @@ -138,29 +111,24 @@ function M.omnifunc(findstart, base) return -1 end - local matches = {} + local candidates = {} local source = _complete_source or '' local dk = date_key() local rk = recur_key() if source == dk then - for _, c in ipairs(date_completions()) do - if base == '' or c.word:sub(1, #base) == base then - table.insert(matches, { word = c.word, menu = '[' .. source .. ']', info = c.info }) - end - end + candidates = date_completions() elseif source == 'cat' then - for _, c in ipairs(get_categories()) do - if base == '' or c:sub(1, #base) == base then - table.insert(matches, { word = c, menu = '[cat]' }) - end - end + candidates = get_categories() elseif source == rk then - for _, c in ipairs(recur_completions()) do - if base == '' or c.word:sub(1, #base) == base then - table.insert(matches, { word = c.word, menu = '[' .. source .. ']', info = c.info }) - end + candidates = recur_completions() + end + + local matches = {} + for _, c in ipairs(candidates) do + if base == '' or c:sub(1, #base) == base then + table.insert(matches, { word = c, menu = '[' .. source .. ']' }) end end diff --git a/lua/pending/config.lua b/lua/pending/config.lua index ec89cb2..3318b3d 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -63,7 +63,6 @@ function M.get() return _resolved end ----@return nil function M.reset() _resolved = nil end diff --git a/lua/pending/diff.lua b/lua/pending/diff.lua index daab788..bec3baa 100644 --- a/lua/pending/diff.lua +++ b/lua/pending/diff.lua @@ -65,7 +65,6 @@ function M.parse_buffer(lines) end ---@param lines string[] ----@return nil function M.apply(lines) local parsed = M.parse_buffer(lines) local now = timestamp() diff --git a/lua/pending/health.lua b/lua/pending/health.lua index cc285e0..78311d2 100644 --- a/lua/pending/health.lua +++ b/lua/pending/health.lua @@ -1,6 +1,5 @@ local M = {} ----@return nil function M.check() vim.health.start('pending.nvim') diff --git a/lua/pending/init.lua b/lua/pending/init.lua index 631c0e3..216b8b3 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -6,6 +6,8 @@ local store = require('pending.store') ---@class pending.init local M = {} +---@type pending.Task[][] +local _undo_states = {} local UNDO_MAX = 20 ---@return integer bufnr @@ -17,7 +19,6 @@ 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', { @@ -48,7 +49,6 @@ 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,33 +91,28 @@ 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() - local stack = store.undo_stack() - table.insert(stack, snapshot) - if #stack > UNDO_MAX then - table.remove(stack, 1) + table.insert(_undo_states, snapshot) + if #_undo_states > UNDO_MAX then + table.remove(_undo_states, 1) end diff.apply(lines) buffer.render(bufnr) end ----@return nil function M.undo_write() - local stack = store.undo_stack() - if #stack == 0 then + if #_undo_states == 0 then vim.notify('Nothing to undo.', vim.log.levels.WARN) return end - local state = table.remove(stack) + local state = table.remove(_undo_states) 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 @@ -142,7 +137,9 @@ function M.toggle_complete() if task.recur and task.due then local recur = require('pending.recur') local mode = task.recur_mode or 'scheduled' - local next_date = recur.next_due(task.due, task.recur, mode) + 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) store.add({ description = task.description, category = task.category, @@ -164,7 +161,6 @@ function M.toggle_complete() end end ----@return nil function M.toggle_priority() local bufnr = buffer.bufnr() if not bufnr then @@ -195,7 +191,6 @@ function M.toggle_priority() end end ----@return nil function M.prompt_date() local bufnr = buffer.bufnr() if not bufnr then @@ -210,7 +205,7 @@ function M.prompt_date() if not id then return end - vim.ui.input({ prompt = 'Due date (today, +3d, fri@2pm, etc.): ' }, function(input) + vim.ui.input({ prompt = 'Due date (YYYY-MM-DD): ' }, function(input) if not input then return end @@ -219,11 +214,8 @@ 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$') - 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) + 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) return end end @@ -234,7 +226,6 @@ 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 ', vim.log.levels.ERROR) @@ -261,7 +252,6 @@ 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 @@ -272,7 +262,6 @@ function M.sync() end ---@param days? integer ----@return nil function M.archive(days) days = days or 30 local cutoff = os.time() - (days * 86400) @@ -309,46 +298,8 @@ function M.archive(days) end end ----@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 today = os.date('%Y-%m-%d') --[[@as string]] 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 @@ -356,9 +307,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 is_due_or_overdue(m.raw_due) then + if m.type == 'task' and m.raw_due and m.status ~= 'done' and m.raw_due <= today then local task = store.get(m.id or 0) - local label = is_overdue(m.raw_due) and '[OVERDUE] ' or '[DUE] ' + local label = m.raw_due < today and '[OVERDUE] ' or '[DUE] ' table.insert(qf_items, { bufnr = bufnr, lnum = lnum, @@ -370,8 +321,8 @@ function M.due() else store.load() for _, task in ipairs(store.active_tasks()) do - 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] ' + if task.status == 'pending' and task.due and task.due <= today then + local label = task.due < today and '[OVERDUE] ' or '[DUE] ' local text = label .. task.description if task.category then text = text .. ' [' .. task.category .. ']' @@ -391,7 +342,6 @@ function M.due() end ---@param args string ----@return nil function M.command(args) if not args or args == '' then M.open() diff --git a/lua/pending/parse.lua b/lua/pending/parse.lua index e234269..853fa2c 100644 --- a/lua/pending/parse.lua +++ b/lua/pending/parse.lua @@ -24,82 +24,6 @@ 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 string|nil -local function normalize_time(s) - local h, m, period - - h, m, period = s:match('^(%d+):(%d%d)([ap]m)$') - if not h then - h, period = s:match('^(%d+)([ap]m)$') - if h then - m = '00' - end - end - if not h then - h, m = s:match('^(%d%d):(%d%d)$') - end - if not h then - h, m = s:match('^(%d):(%d%d)$') - end - if not h then - h = s:match('^(%d+)$') - if h then - m = '00' - end - end - - if not h then - return nil - end - - local hn = tonumber(h) --[[@as integer]] - local mn = tonumber(m) --[[@as integer]] - - if period then - if hn < 1 or hn > 12 then - return nil - end - if period == 'am' then - hn = hn == 12 and 0 or hn - else - hn = hn == 12 and 12 or hn + 12 - end - else - if hn < 0 or hn > 23 then - return nil - end - end - - if mn < 0 or mn > 59 then - return nil - end - - return string.format('%02d:%02d', hn, mn) -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' @@ -141,218 +65,146 @@ 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 date_input, time_suffix = text:match('^(.+)@(.+)$') - if time_suffix then - time_suffix = normalize_time(time_suffix) - if not 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 lower = text:lower() local today = os.date('*t') --[[@as osdate]] if lower == 'today' or lower == 'eod' then - return append_time(today_str(today), time_suffix) + return today_str(today) end if lower == 'yesterday' then - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day - 1 })) --[[@as string]], - time_suffix - ) + return os.date( + '%Y-%m-%d', + os.time({ year = today.year, month = today.month, day = today.day - 1 }) + ) --[[@as string]] end if lower == 'tomorrow' then - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 })) --[[@as string]], - time_suffix - ) + return os.date( + '%Y-%m-%d', + os.time({ year = today.year, month = today.month, day = today.day + 1 }) + ) --[[@as string]] end if lower == 'sow' then local delta = -((today.wday - 2) % 7) - return append_time( - os.date( - '%Y-%m-%d', - os.time({ year = today.year, month = today.month, day = today.day + delta }) - ) --[[@as string]], - time_suffix - ) + return os.date( + '%Y-%m-%d', + os.time({ year = today.year, month = today.month, day = today.day + delta }) + ) --[[@as string]] end if lower == 'eow' then local delta = (1 - today.wday) % 7 - return append_time( - os.date( - '%Y-%m-%d', - os.time({ year = today.year, month = today.month, day = today.day + delta }) - ) --[[@as string]], - time_suffix - ) + return os.date( + '%Y-%m-%d', + os.time({ year = today.year, month = today.month, day = today.day + delta }) + ) --[[@as string]] end if lower == 'som' then - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = 1 })) --[[@as string]], - time_suffix - ) + return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = 1 })) --[[@as string]] end if lower == 'eom' then - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 })) --[[@as string]], - time_suffix - ) + return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 })) --[[@as string]] end if lower == 'soq' then local q = math.ceil(today.month / 3) local first_month = (q - 1) * 3 + 1 - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = first_month, day = 1 })) --[[@as string]], - time_suffix - ) + return os.date('%Y-%m-%d', os.time({ year = today.year, month = first_month, day = 1 })) --[[@as string]] end if lower == 'eoq' then local q = math.ceil(today.month / 3) local last_month = q * 3 - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 })) --[[@as string]], - time_suffix - ) + return os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 })) --[[@as string]] end if lower == 'soy' then - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = 1, day = 1 })) --[[@as string]], - time_suffix - ) + return os.date('%Y-%m-%d', os.time({ year = today.year, month = 1, day = 1 })) --[[@as string]] end if lower == 'eoy' then - return append_time( - os.date('%Y-%m-%d', os.time({ year = today.year, month = 12, day = 31 })) --[[@as string]], - time_suffix - ) + return os.date('%Y-%m-%d', os.time({ year = today.year, month = 12, day = 31 })) --[[@as string]] end if lower == 'later' or lower == 'someday' then - return append_time(config.get().someday_date, time_suffix) + return config.get().someday_date end local n = lower:match('^%+(%d+)d$') if n then - 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 - ) + return os.date( + '%Y-%m-%d', + os.time({ + year = today.year, + month = today.month, + day = today.day + ( + tonumber(n) --[[@as integer]] + ), + }) + ) --[[@as string]] end n = lower:match('^%+(%d+)w$') if n then - 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 - ) + return os.date( + '%Y-%m-%d', + os.time({ + year = today.year, + month = today.month, + day = today.day + ( + tonumber(n) --[[@as integer]] + ) * 7, + }) + ) --[[@as string]] end n = lower:match('^%+(%d+)m$') if n then - 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 - ) + return os.date( + '%Y-%m-%d', + os.time({ + year = today.year, + month = today.month + ( + tonumber(n) --[[@as integer]] + ), + day = today.day, + }) + ) --[[@as string]] end n = lower:match('^%-(%d+)d$') if n then - 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 - ) + return os.date( + '%Y-%m-%d', + os.time({ + year = today.year, + month = today.month, + day = today.day - ( + tonumber(n) --[[@as integer]] + ), + }) + ) --[[@as string]] end n = lower:match('^%-(%d+)w$') if n then - 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 - ) + return os.date( + '%Y-%m-%d', + os.time({ + year = today.year, + month = today.month, + day = today.day - ( + tonumber(n) --[[@as integer]] + ) * 7, + }) + ) --[[@as string]] end local ord = lower:match('^(%d+)[snrt][tdh]$') @@ -370,7 +222,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 append_time(os.date('%Y-%m-%d', t) --[[@as string]], time_suffix) + return os.date('%Y-%m-%d', t) --[[@as string]] end m = m + 1 if m > 12 then @@ -380,7 +232,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 append_time(os.date('%Y-%m-%d', t) --[[@as string]], time_suffix) + return os.date('%Y-%m-%d', t) --[[@as string]] end return nil end @@ -392,23 +244,17 @@ function M.resolve_date(text) if today.month >= target_month then y = y + 1 end - return append_time( - os.date('%Y-%m-%d', os.time({ year = y, month = target_month, day = 1 })) --[[@as string]], - time_suffix - ) + return os.date('%Y-%m-%d', os.time({ year = y, month = target_month, day = 1 })) --[[@as string]] end local target_wday = weekday_map[lower] if target_wday then local current_wday = today.wday local delta = (target_wday - current_wday) % 7 - return append_time( - os.date( - '%Y-%m-%d', - os.time({ year = today.year, month = today.month, day = today.day + delta }) - ) --[[@as string]], - time_suffix - ) + return os.date( + '%Y-%m-%d', + os.time({ year = today.year, month = today.month, day = today.day + delta }) + ) --[[@as string]] end return nil @@ -427,7 +273,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[T%d:]*)$' + local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d)$' local date_pattern_any = '^' .. vim.pesc(dk) .. ':(.+)$' local rec_pattern = '^' .. vim.pesc(rk) .. ':(%S+)$' @@ -438,7 +284,7 @@ function M.body(text) if metadata.due then break end - if not is_valid_datetime(due_val) then + if not is_valid_date(due_val) then break end metadata.due = due_val diff --git a/lua/pending/recur.lua b/lua/pending/recur.lua index 9c647aa..c0a2091 100644 --- a/lua/pending/recur.lua +++ b/lua/pending/recur.lua @@ -80,33 +80,20 @@ 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 date_part, time_part = split_datetime(base_date) - local y, m, d = date_part:match('^(%d+)-(%d+)-(%d+)$') + local y, m, d = base_date: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 - result = os.date('%Y-%m-%d', os.time({ year = yn, month = mn, day = dn + interval })) --[[@as string]] + return os.date('%Y-%m-%d', os.time({ year = yn, month = mn, day = dn + interval })) --[[@as string]] elseif freq == 'weekly' then - result = os.date('%Y-%m-%d', os.time({ year = yn, month = mn, day = dn + interval * 7 })) --[[@as string]] + return 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 @@ -116,20 +103,14 @@ 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]]) - result = os.date('%Y-%m-%d', os.time({ year = new_y, month = new_m, day = clamped_d })) --[[@as string]] + return 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]]) - result = os.date('%Y-%m-%d', os.time({ year = new_y, month = mn, day = clamped_d })) --[[@as string]] - else - return base_date + return os.date('%Y-%m-%d', os.time({ year = new_y, month = mn, day = clamped_d })) --[[@as string]] end - - if time_part then - return result .. 'T' .. time_part - end - return result + return base_date end ---@param base_date string @@ -143,16 +124,13 @@ 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 - local base = time_part and (today .. 'T' .. time_part) or today - return advance_date(base, parsed.freq, parsed.interval) + return advance_date(today, parsed.freq, parsed.interval) end local next_date = advance_date(base_date, parsed.freq, parsed.interval) - local compare_today = time_part and (today .. 'T' .. time_part) or today - while next_date <= compare_today do + while next_date <= today do next_date = advance_date(next_date, parsed.freq, parsed.interval) end return next_date diff --git a/lua/pending/store.lua b/lua/pending/store.lua index c9e9b45..4f3d2f1 100644 --- a/lua/pending/store.lua +++ b/lua/pending/store.lua @@ -19,7 +19,6 @@ 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 = {} @@ -35,7 +34,6 @@ local function empty_data() version = SUPPORTED_VERSION, next_id = 1, tasks = {}, - undo = {}, } end @@ -167,24 +165,13 @@ 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 @@ -195,18 +182,10 @@ 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') @@ -321,7 +300,6 @@ function M.find_index(id) end ---@param tasks pending.Task[] ----@return nil function M.replace_tasks(tasks) M.data().tasks = tasks end @@ -347,24 +325,11 @@ 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 diff --git a/lua/pending/views.lua b/lua/pending/views.lua index a9f56bf..17a7a37 100644 --- a/lua/pending/views.lua +++ b/lua/pending/views.lua @@ -21,10 +21,7 @@ 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 + local y, m, d = due:match('^(%d%d%d%d)-(%d%d)-(%d%d)$') if not y then return due end @@ -33,30 +30,7 @@ local function format_due(due) 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 + return os.date(config.get().date_format, t) --[[@as string]] end ---@param tasks pending.Task[] @@ -100,6 +74,7 @@ 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 = {} @@ -174,7 +149,7 @@ function M.category_view(tasks) raw_due = task.due, status = task.status, category = cat, - overdue = task.status == 'pending' and task.due ~= nil and is_overdue(task.due) or nil, + overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil, recur = task.recur, }) end @@ -187,6 +162,7 @@ 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 = {} @@ -224,7 +200,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 is_overdue(task.due) or nil, + overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil, show_category = true, recur = task.recur, }) diff --git a/spec/parse_spec.lua b/spec/parse_spec.lua index fd36d68..edeffcd 100644 --- a/spec/parse_spec.lua +++ b/spec/parse_spec.lua @@ -323,75 +323,6 @@ describe('parse', function() end) end) - describe('resolve_date with time suffix', function() - local today = os.date('*t') --[[@as osdate]] - local tomorrow_str = os.date( - '%Y-%m-%d', - os.time({ year = today.year, month = today.month, day = today.day + 1 }) - ) --[[@as string]] - - it('resolves bare hour to T09:00', function() - local result = parse.resolve_date('tomorrow@9') - assert.are.equal(tomorrow_str .. 'T09:00', result) - end) - - it('resolves bare military hour to T14:00', function() - local result = parse.resolve_date('tomorrow@14') - assert.are.equal(tomorrow_str .. 'T14:00', result) - end) - - it('resolves H:MM to T09:30', function() - local result = parse.resolve_date('tomorrow@9:30') - assert.are.equal(tomorrow_str .. 'T09:30', result) - end) - - it('resolves HH:MM (existing format) to T09:30', function() - local result = parse.resolve_date('tomorrow@09:30') - assert.are.equal(tomorrow_str .. 'T09:30', result) - end) - - it('resolves 2pm to T14:00', function() - local result = parse.resolve_date('tomorrow@2pm') - assert.are.equal(tomorrow_str .. 'T14:00', result) - end) - - it('resolves 9am to T09:00', function() - local result = parse.resolve_date('tomorrow@9am') - assert.are.equal(tomorrow_str .. 'T09:00', result) - end) - - it('resolves 9:30pm to T21:30', function() - local result = parse.resolve_date('tomorrow@9:30pm') - assert.are.equal(tomorrow_str .. 'T21:30', result) - end) - - it('resolves 12am to T00:00', function() - local result = parse.resolve_date('tomorrow@12am') - assert.are.equal(tomorrow_str .. 'T00:00', result) - end) - - it('resolves 12pm to T12:00', function() - local result = parse.resolve_date('tomorrow@12pm') - assert.are.equal(tomorrow_str .. 'T12:00', result) - end) - - it('rejects hour 24', function() - assert.is_nil(parse.resolve_date('tomorrow@24')) - end) - - it('rejects 13am', function() - assert.is_nil(parse.resolve_date('tomorrow@13am')) - end) - - it('rejects minute 60', function() - assert.is_nil(parse.resolve_date('tomorrow@9:60')) - end) - - it('rejects alphabetic garbage', function() - assert.is_nil(parse.resolve_date('tomorrow@abc')) - end) - end) - describe('command_add', function() it('parses simple text', function() local desc, meta = parse.command_add('Buy milk')