From 87eedc8610151205f2a33f515019babf146229ba Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 24 Feb 2026 18:30:00 -0500 Subject: [PATCH] feat(init): multi-level undo, quickfix due, BufEnter reload Problem: undo was single-level with shallow references; no way to query due/overdue tasks via quickfix; two instances sharing tasks.json would diverge silently. Solution: replace _undo_state with _undo_states[] (cap 20, deep copies via store.snapshot()); add M.due() which populates the quickfix list with overdue/due-today tasks; add BufEnter autocmd that reloads from disk when the buffer is unmodified; expand show_help() with folds, :Pending due, relative date syntax, PendingOverdue, and empty-input date clearing. --- lua/pending/init.lua | 89 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/lua/pending/init.lua b/lua/pending/init.lua index cb930d0..5db6a0c 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -3,11 +3,12 @@ local diff = require('pending.diff') local parse = require('pending.parse') local store = require('pending.store') ----@class task +---@class pending.init local M = {} ----@type pending.Task[]? -local _undo_state = nil +---@type pending.Task[][] +local _undo_states = {} +local UNDO_MAX = 20 ---@return integer bufnr function M.open() @@ -27,6 +28,16 @@ function M._setup_autocmds(bufnr) M._on_write(bufnr) end, }) + vim.api.nvim_create_autocmd('BufEnter', { + group = group, + buffer = bufnr, + callback = function() + if not vim.bo[bufnr].modified then + store.load() + buffer.render(bufnr) + end + end, + }) end ---@param bufnr integer @@ -55,19 +66,23 @@ end ---@param bufnr integer function M._on_write(bufnr) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - _undo_state = store.active_tasks() + local snapshot = store.snapshot() + table.insert(_undo_states, snapshot) + if #_undo_states > UNDO_MAX then + table.remove(_undo_states, 1) + end diff.apply(lines) buffer.render(bufnr) end function M.undo_write() - if not _undo_state then + if #_undo_states == 0 then vim.notify('Nothing to undo.', vim.log.levels.WARN) return end - store.replace_tasks(_undo_state) + local state = table.remove(_undo_states) + store.replace_tasks(state) store.save() - _undo_state = nil buffer.render(buffer.bufnr()) end @@ -227,6 +242,49 @@ function M.archive(days) end end +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 + local qf_items = {} + + 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 + local task = store.get(m.id or 0) + local label = m.raw_due < today and '[OVERDUE] ' or '[DUE] ' + table.insert(qf_items, { + bufnr = bufnr, + lnum = lnum, + col = 1, + text = label .. (task and task.description or ''), + }) + end + end + 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] ' + local text = label .. task.description + if task.category then + text = text .. ' [' .. task.category .. ']' + end + table.insert(qf_items, { text = text }) + end + end + end + + if #qf_items == 0 then + vim.notify('No due or overdue tasks.') + return + end + + vim.fn.setqflist(qf_items, 'r') + vim.cmd('copen') +end + function M.show_help() local cfg = require('pending.config').get() local dk = cfg.date_syntax or 'due' @@ -239,27 +297,36 @@ function M.show_help() 'd Set due date', 'U Undo last write', 'o / O Add new task line', - 'dd Delete task (on :w)', + 'dd Delete task line (on :w)', 'p / P Paste (duplicates get new IDs)', + 'zc / zo Fold/unfold category (category view)', ':w Save all changes', '', ':Pending add Quick-add task', ':Pending add Cat: Quick-add with category', + ':Pending due Show overdue/due qflist', ':Pending sync Push to Google Calendar', ':Pending archive [days] Purge old done tasks', - ':Pending undo Undo last write', + ':Pending undo Undo last write', '', 'Inline metadata (on new lines before :w):', ' ' .. dk .. ':YYYY-MM-DD Set due date', ' cat:Name Set category', '', + 'Due date input:', + ' today, tomorrow, +Nd, mon-sun', + ' Empty input clears due date', + '', + 'Highlights:', + ' PendingOverdue overdue tasks (red)', + '', 'Press q or to close', } local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) vim.bo[buf].modifiable = false vim.bo[buf].bufhidden = 'wipe' - local width = 50 + local width = 54 local height = #lines local win = vim.api.nvim_open_win(buf, true, { relative = 'editor', @@ -292,6 +359,8 @@ function M.command(args) elseif cmd == 'archive' then local d = rest ~= '' and tonumber(rest) or nil M.archive(d) + elseif cmd == 'due' then + M.due() elseif cmd == 'undo' then M.undo_write() else