Commit graph

47 commits

Author SHA1 Message Date
11df8407a1 feat(config): add keymaps table for buffer-local bindings
Problem: Users had no way to customize or disable buffer-local key
bindings in the pending buffer.

Solution: Add a pending.Keymaps class and keymaps field to
pending.Config with defaults for all eight buffer actions. Setting any
key to false disables that binding.
2026-02-25 13:24:09 -05:00
c038130a8b refactor(init): remove help popup, use config-driven keymaps
Problem: Buffer-local keymaps were hardcoded with no way for users to
customize them. The g? help popup duplicated information already in the
vimdoc.

Solution: Remove show_help() and the g? mapping. Refactor
_setup_buf_mappings to read from cfg.keymaps, letting users override or
disable any buffer-local binding via vim.g.pending.
2026-02-25 13:24:03 -05:00
39fd4ef17a fix(recur): resolve LuaLS type errors
Problem: LuaLS reported undefined-field for `_raw` on RecurSpec and
param-type-mismatch for `last_day.day` in `advance_date` because
`osdate.day` infers as `string|integer`.

Solution: Add `_raw` to the RecurSpec class annotation and cast
`last_day.day` to integer in both `math.min` call sites.
2026-02-25 13:23:55 -05:00
9c5d57bece ci: fix 2026-02-25 13:06:20 -05:00
7ebfcc63c3 docs: document recurrence, expanded dates, omnifunc, new config
Problem: the vimdoc does not cover recurrence, expanded date syntax,
omnifunc completion, or the new config fields.

Solution: add DATE INPUT and RECURRENCE sections, update INLINE
METADATA, COMMANDS, CONFIGURATION, HIGHLIGHT GROUPS, HEALTH CHECK,
and DATA FORMAT. Expand the help popup with recurrence patterns and
new date tokens. Add recurrence validation to healthcheck.
2026-02-25 13:04:46 -05:00
153f614990 feat(complete): add omnifunc for cat:, due:, and rec: tokens
Problem: the pending buffer has no completion source, requiring
users to type metadata tokens from memory.

Solution: add complete.lua with an omnifunc that completes cat:
tokens from existing categories, due: tokens from the named date
vocabulary, and rec: tokens from recurrence shorthands.
2026-02-25 13:04:37 -05:00
a1a8d1db3b feat(buffer): add PendingRecur highlight and recurrence virtual text
Problem: recurring tasks have no visual indicator in the buffer,
and the extmark logic uses a rigid if/elseif chain that does not
compose well with additional virtual text fields.

Solution: add PendingRecur highlight group linking to DiagnosticInfo.
Refactor apply_extmarks() to build virtual text parts dynamically,
appending category, recurrence indicator, and due date as separate
composable segments. Set omnifunc on the pending buffer.
2026-02-25 13:04:29 -05:00
1e2d72914c feat(views): add recurrence to LineMeta
Problem: LineMeta does not carry recurrence info, so the buffer
layer cannot display recurrence indicators.

Solution: add recur field to LineMeta and populate it in both
category_view() and priority_view().
2026-02-25 13:04:17 -05:00
b08d01fe78 feat(init): spawn next task on recurring task completion
Problem: completing a recurring task does not create the next
occurrence, and :Pending add does not pass recurrence fields.

Solution: in toggle_complete(), detect recurrence and spawn a new
pending task with the next due date. Wire rec/rec_mode through the
add() command path.
2026-02-25 13:04:10 -05:00
597ec6bb2a feat(diff): propagate recurrence through buffer reconciliation
Problem: the diff layer does not extract or apply recurrence fields,
so rec: tokens written in the buffer are silently ignored on :w.

Solution: add rec and rec_mode to ParsedEntry, extract them in
parse_buffer(), and pass them through create and update paths in
apply().
2026-02-25 13:03:58 -05:00
e19cfdda18 feat(parse): add rec: inline token parsing
Problem: the buffer parser does not recognize recurrence tokens,
so users cannot set recurrence rules inline.

Solution: add recur_key() helper and rec: token parsing in body()
and command_add(), with ! prefix handling for completion-based mode
and validation via recur.validate().
2026-02-25 13:03:49 -05:00
e69e93f536 feat(store): add recur and recur_mode task fields
Problem: the task schema has no fields for storing recurrence rules.

Solution: add recur and recur_mode to the Task class, known_fields,
task_to_table, table_to_task, and the add() signature.
2026-02-25 13:03:40 -05:00
c2310f2659 feat(recur): add recurrence parsing and next-date computation
Problem: the plugin has no concept of recurring tasks, which is
needed for habits and repeating deadlines.

Solution: add recur.lua with parse(), validate(), next_due(),
to_rrule(), and shorthand_list(). Supports named shorthands (daily,
weekdays, weekly, etc.), interval notation (Nd, Nw, Nm, Ny), raw
RRULE passthrough, and ! prefix for completion-based mode. Includes
day-clamping for month/year advancement.
2026-02-25 13:03:19 -05:00
fed3e7d78d feat(parse): expand date vocabulary with named dates
Problem: the date input only supports today, tomorrow, +Nd, and
weekday names, lacking relative offsets like weeks/months, period
boundaries, ordinals, month names, and backdating.

Solution: add yesterday, eod, sow/eow, som/eom, soq/eoq, soy/eoy,
+Nw, +Nm, -Nd, -Nw, ordinals (1st-31st), month names (jan-dec),
and later/someday to resolve_date(). Add tests for all new tokens.
2026-02-25 13:03:06 -05:00
4703b154fc feat(config): add recur_syntax and someday_date fields
Problem: the plugin needs configuration for the recurrence token name
and the sentinel date used by the `later`/`someday` named dates.

Solution: add `recur_syntax` (default 'rec') and `someday_date`
(default '9999-12-30') to pending.Config and the defaults table.
2026-02-25 13:01:29 -05:00
Barrett Ruth
fbeb0e2bee
feat(buffer): open as bottom-drawer split like fugitive (#23)
* feat(buffer): open as bottom-drawer split like fugitive

Problem: :Pending replaced the current buffer, making it impossible to
view tasks alongside the file being edited. No way to close the drawer
without :q or switching buffers manually.

Solution: open the task buffer in a botright horizontal split instead of
replacing the current buffer. Track the drawer window ID so re-opening
focuses it rather than creating a second split. Set winfixheight so the
drawer keeps its height when other windows open or close. Add q/<Esc>
mappings to close the drawer, and a WinClosed autocmd to clear the
tracked window ID when the user closes it manually. Add drawer_height
config option (default 15).

* fix(buffer): default to natural split height like fugitive

Problem: hardcoded drawer_height=15 was too small and diverged from
fugitive's model. Fugitive issues a plain botright split and lets Vim's
own split rules (equalalways, winheight) divide the available space.

Solution: remove the default height so the split sizes naturally. Only
call nvim_win_set_height when the user sets drawer_height to a positive
value, preserving the opt-in customization path.
2026-02-25 09:34:17 -05:00
Barrett Ruth
5db242a9cf
refactor: adopt markdown-style checkbox buffer format (#20)
* refactor(config): change default category from Inbox to Todo

* refactor(views): adopt markdown checkbox line format

Problem: task lines used an opaque /ID/  [N] prefix format that was
hard to read and inconsistent between category and priority views.
Header lines had no visual marker distinguishing them from tasks.

Solution: render headers as '## Cat', task lines as
'/ID/- [x|!| ] description'. State encoding: [x]=done, [!]=urgent,
[ ]=pending. Both views use the same construction.

* refactor(diff): parse and reconcile markdown checkbox format

Problem: parse_buffer matched the old '  text' indent pattern and
detected headers via '^%S'. Priority was read from a '[N] ' prefix.
apply() never reconciled status changes written into the buffer.

Solution: match '- [.] text' for tasks and '^## ' for headers.
Extract state char to derive priority (! -> 1) and status (x -> done).
apply() now reconciles status from the buffer, setting/clearing 'end'
timestamps — enabling the oil-style edit-checkbox-then-:w workflow.

* refactor(buffer): update syntax, extmarks, and render for checkbox format

Problem: syntax patterns matched the old indent/[N] format; right_align
virtual text produced a broken layout in narrow windows; the done
strikethrough skipped past the '  ' indent leaving '- [x] ' unstyled;
render() added undo history entries so 'u' could undo a re-render.

Solution: update taskHeader/taskLine patterns for '## '/'- [.]'; rename
taskPriority -> taskCheckbox matching '[!]'; switch virt_text_pos to
'eol'; drop the +2 col_start offset so strikethrough covers '- [x] ';
guard nvim_buf_set_lines with undolevels=-1 so renders are not undoable.
Also fix open_line to insert '- [ ] ' and position cursor at col 6.

* refactor(init): replace multi-level priority with binary toggle

Problem: <C-a>/<C-x> overrode Vim's native number increment and the
visual g<C-a>/g<C-x> variants added complexity for marginal value.
toggle_complete() left the cursor on the wrong line after re-render.

Solution: remove change_priority/change_priority_visual; add
toggle_priority() (0<->1) mapped to '!', with cursor-follow after
render matching the pattern already used in priority toggle. Add
cursor-follow to toggle_complete() for the same reason. Update plugin
plugs (priority-up/down -> priority) and add 'due'/'undo' to the
:Pending completion list. Update help text accordingly.

* feat(buffer): reflect current view in buffer name

Problem: no way to tell at a glance which view (category vs priority)
is active — the buffer was always named 'pending://'.

Solution: update the buffer name to 'pending://category' or
'pending://priority' on every render, so the view is visible in
the statusline/tabline without any extra UI.
2026-02-24 23:21:55 -05:00
437944d441 fix(diff): cast tonumber result to integer
Problem: LuaLS infers priority as integer from the = 0 initialiser
but tonumber returns number?, causing a cast-local-type diagnostic.

Solution: inline --[[@as integer]] cast after the tonumber call.
2026-02-24 22:32:14 -05:00
8f9052bad1 ci: format 2026-02-24 22:32:14 -05:00
7f0bd43b34 feat(buffer): preserve category fold state across re-renders
Problem: pressing :w, toggling priority, or any other operation that
calls buffer.render() reset foldlevel = 99, causing all manually
collapsed category sections to snap back open.

Solution: snapshot which categories are folded (per window) before
nvim_buf_set_lines destroys the fold tree, then restore them after
fold options are re-applied by calling normal! zc on each previously
closed header line. State persists across all render call sites
within a session.
2026-02-24 22:32:14 -05:00
fc4a47a1ec feat(init): multi-level priority with <C-a>/<C-x>
Problem: priority was binary (0 or 1), toggled with !, with no way
to express finer gradations or use Vim's native increment idiom.

Solution: replace toggle_priority with change_priority(delta) which
clamps to floor 0. Display format changes from '! ' to '[N] ' so any
integer level is representable. Parser updated to extract numeric
level from the [N] prefix. Visual g<C-a>/g<C-x> apply the delta to
all tasks in the selection. <Plug>(pending-priority) replaced with
<Plug>(pending-priority-up) and <Plug>(pending-priority-down).
2026-02-24 22:20:18 -05:00
cee0560341 ci: format 2026-02-24 19:52:48 -05:00
3919e3f88f fix(buffer): replace indentexpr with dedicated o/O mappings
Problem: setup_indentexpr always returned 0 because no task line
starts with whitespace (the /ID/ prefix begins with /), so the
return 2 branch was dead code. Pressing o or O opened a blank line
at column 0 with no ID prefix, which the diff parser cannot
recognise as a task.

Solution: remove setup_indentexpr and M.get_indent() entirely; add
M.open_line(above) which inserts a two-space stub line and enters
insert mode at the end so the user types directly into the new task
body. The diff layer already handles lines matching ^  .+ as new
tasks. Add o and O buffer-local mappings in init.lua.
2026-02-24 19:52:48 -05:00
d2c9eb1808 fix(init): reposition cursor after priority toggle re-render
Problem: pressing ! re-sorts the view so the toggled task moves to
the top of its category, but the cursor stays on the original line
number and lands on a different task.

Solution: after buffer.render(), iterate buffer.meta() to find the
new line number for the toggled task's id and call
nvim_win_set_cursor to follow it.
2026-02-24 19:51:04 -05:00
6b14a6bf90 fix(buffer): link highlight groups to colorscheme via default = true
Problem: highlight groups used hardcoded hex colours and a bespoke
hlexists guard, ignoring the user's colorscheme and preventing
overrides from working naturally.

Solution: replace the guard wrapper with direct nvim_set_hl calls
using default = true and link, so each group falls back to a
semantically appropriate built-in group (Title, DiagnosticHint,
DiagnosticError, Comment, DiagnosticWarn) unless the user has
already defined them.
2026-02-24 19:50:27 -05:00
f0b58df317 fix(init): remap date prompt from d to D
Problem: mapping d for the date prompt intercepts dd before Vim can
recognize it as a motion, so dd never deletes a line.

Solution: move the date prompt to D, restoring full d-operator
behaviour (dd, dw, d$, etc.) and updating the help popup to match.
2026-02-24 18:56:12 -05:00
bef6c4ca17 fix(health): remove superfluous config info lines 2026-02-24 18:56:12 -05:00
2b73ab1cd0 fix: resolve remaining LuaLS type errors
Problem: CI lua-typecheck-action reported three categories of errors:
1. parse.lua - multi-assignment of tonumber() results left y/m/d typed
   as number? rather than integer, failing os.time()'s field types
2. gcal.lua - url_encode returned str:gsub() which yields string+integer
   but the annotation declared @return string (redundant-return-value)
3. gcal.lua - calendar_id typed string? from find_or_create_calendar was
   passed to functions expecting string; the existing `if err` guard did
   not narrow the type for LuaLS

Solution: replace the y/m/d multi-assignment with yn/mn/dn locals whose
types resolve cleanly to integer; wrap the gsub return in parentheses to
discard the count; add `or not calendar_id` to the error guard so LuaLS
narrows calendar_id to string for the rest of the scope.
2026-02-24 18:50:28 -05:00
87eedc8610 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.
2026-02-24 18:44:13 -05:00
40ebd0ebb2 feat(buffer): add category folds via foldexpr
Problem: category_view had no fold support, making it harder to
focus on one category in large lists.

Solution: add M.get_fold() returning '>1' for headers, '1' for task
lines, and '0' for blanks. M.render() now sets foldmethod=expr
(foldlevel=99) in category view and foldmethod=manual in priority.
2026-02-24 18:44:13 -05:00
4e9ce2bc8a feat(store): add snapshot() and atomic json write
Problem: the single-level undo used shallow references so mutations
during diff.apply() corrupted the saved state. JSON writes were also
non-atomic, risking partial writes on crash.

Solution: add M.snapshot() which deep-copies active tasks (including
_extra). Change M.save() to write a .tmp file then rename atomically.
2026-02-24 18:44:13 -05:00
fc45ca3fcd fix(gcal): add LuaCATS annotations and resolve type errors
Problem: gcal.lua had ~10 LuaLS errors from untyped credential and
token tables, string|osdate casts, and untyped _gcal_event_id
field access.

Solution: add pending.GcalCredentials and pending.GcalTokens class
definitions, annotate all local functions with @param/@return, add
--[[@as string]] casts on os.date returns, and fix _gcal_event_id
access to use bracket notation with casts.
2026-02-24 18:44:13 -05:00
Barrett Ruth
68dbea7d52
fix(parse): cast os.date('*t') return to osdate (#3)
Problem: LuaLS types os.date('*t') as string|osdate, causing type
errors when accessing .year, .month, .day, .wday fields in
is_valid_date and resolve_date.

Solution: add --[[@as osdate]] casts on both os.date('*t') calls.
2026-02-24 18:40:46 -05:00
Barrett Ruth
f21658f138
feat: overdue highlighting, relative dates, undo write, buffer mappings (#1)
* feat(config): add category_order field

Problem: category display order was always insertion order with no way
to configure it.

Solution: add category_order to config defaults so users can declare a
preferred category ordering; unspecified categories append after.

* feat(parse): add relative date resolution

Problem: due dates required full YYYY-MM-DD input, adding friction for
common cases like "today" or "next monday".

Solution: add resolve_date() supporting today, tomorrow, +Nd, and
weekday abbreviations; extend inline token parsing to resolve relative
values before falling back to strict date validation.

* feat(views): overdue flag, category in priority view, category ordering

Problem: overdue tasks were visually indistinct from upcoming ones;
priority view had no category context; category display order was not
configurable.

Solution: compute overdue meta flag for pending tasks past their due
date; set show_category on priority view task meta; reorder categories
according to config.category_order when present.

* feat(buffer): overdue highlight, category virt text in priority view

Problem: overdue tasks had no visual distinction; priority view showed
no category context alongside due dates.

Solution: add PendingOverdue highlight group; render category name as
right-aligned virtual text in priority view, composited with the due
date when both are present.

* feat(init): undo write and buffer-local default mappings

Problem: _undo_state was captured on every save but never consumed;
toggle_priority and prompt_date had no buffer-local defaults, requiring
manual <Plug> configuration.

Solution: implement undo_write() to restore pre-save task state; add !,
d, and U as buffer-local defaults following fugitive's philosophy of
owning the buffer; expose :Pending undo as a command alias.

* test(views): add views spec

Problem: views.lua had no test coverage.

Solution: add 26 tests covering category_view and priority_view
including sort order, line format, overdue detection, show_category
meta, and category_order config behavior.

* test(archive): add archive spec

Problem: archive had no test coverage.

Solution: add 9 tests covering cutoff logic, custom day counts, pending
task preservation, deleted task cleanup, and notify output.

* docs: add vimdoc

Problem: no :help documentation existed.

Solution: add doc/pending.txt covering all features — commands,
mappings, views, configuration, Google Calendar sync, highlight groups,
data format, and health check — following standard vimdoc conventions.

* ci: format

* fix: resolve lint and type check errors

Problem: selene flagged unused variables in new spec files; LuaLS
flagged os.date/os.time return type mismatches, integer? assignments,
and stale task.Task/task.GcalConfig type references.

Solution: prefix unused spec variables with _ or drop unnecessary
assignments; add --[[@as string/integer]] casts for os.date and
os.time calls; add category_order field to pending.Config annotation;
fix task.GcalConfig -> pending.GcalConfig and task.Task[] ->
pending.Task[]; add nil guards on meta[row].id before store calls;
cast store.data() return to non-optional.

* ci: format

* fix: sync

* ci: format
2026-02-24 18:33:07 -05:00
78a275d096
feat: rename 2026-02-24 15:21:44 -05:00
b00a4f01d4 fix: resolve selene lint warnings and errors
Problem: unused variables (undo_state, sha_result, date match
captures) and duplicate if/else branches in gcal sync triggered
selene warnings and errors.

Solution: prefix unused state with underscore, simplify date
validation to a single pattern match, remove dead sha_result
call, and merge duplicate event deletion branches into a single
condition.
2026-02-24 15:20:06 -05:00
edd16f6ecf fix: correct buffer parser and timestamp test
Problem: parse_buffer classified /id/-prefixed task lines as headers
because '/' matches the '^%S' header pattern. Store timestamp test
was flaky when add and update ran within the same second.

Solution: check for task line patterns (id prefix or 2-space indent)
before falling through to the header branch. Backdate the initial
modified timestamp in the store update test.
2026-02-24 15:19:59 -05:00
6cb5ae9dda
ci: format 2026-02-24 15:17:24 -05:00
126724b939 feat(health): add checkhealth integration
Problem: need a way to diagnose config, data file, and
dependency issues.

Solution: add health module reporting config values, data
directory status, task count, and curl/openssl availability.
2026-02-24 15:09:58 -05:00
9eb29f8fe1 feat(gcal): add one-way Google Calendar sync
Problem: need to push tasks with due dates to Google Calendar
as all-day events.

Solution: add gcal module with OAuth 2.0 loopback flow, PKCE,
token refresh, calendar creation, and event CRUD. Maps pending
tasks to all-day events, deletes events on completion, and
stores event IDs in task _extra for round-trip.
2026-02-24 15:09:50 -05:00
5284ef6047 feat: add commands, mappings, and plugin entry point
Problem: need user-facing :Todo command, buffer-local keymaps,
Plug mappings, completion toggle, help float, archive, and
syntax highlighting.

Solution: add init.lua with command dispatcher, toggle complete/
priority, date prompt, archive purge, and help float. Add
plugin/todo.lua entry point with :Todo command and Plug mappings.
Add syntax/todo.vim for conceal and priority highlighting.
2026-02-24 15:09:43 -05:00
c03412837d feat(diff): add buffer-to-store diff algorithm
Problem: need to reconcile buffer edits against the JSON store
on :w, handling creates, deletes, updates, reorders, and
duplicate IDs from yank/paste.

Solution: add diff module that parses buffer lines, matches
against stored tasks by ID, creates new tasks for unknown or
duplicate IDs, marks removed tasks as deleted, and updates
changed fields.
2026-02-24 15:09:36 -05:00
049e77a4fb feat(buffer): add scratch buffer with concealment and extmarks
Problem: need a buffer to display tasks with concealed IDs,
virtual text due dates, strikethrough for done items, and
auto-indent.

Solution: add buffer module with acwrite scratch buffer, syntax
conceal for /id/ prefixes, right-aligned due date extmarks,
header/done highlighting, indentexpr, and view toggling.
2026-02-24 15:09:29 -05:00
a0cbca6db5 feat(views): add category and priority view renderers
Problem: need to render task lists grouped by category or sorted
by priority with concealed IDs and metadata.

Solution: add category_view and priority_view functions that
produce buffer lines with hidden /id/ prefixes, priority flags,
and line metadata for extmark placement.
2026-02-24 15:09:22 -05:00
a8eaa27106 feat(parse): add inline metadata parser
Problem: need to extract due dates and categories from task
descriptions typed in the buffer.

Solution: add right-to-left token parser for configurable date
syntax (default 'due:') and cat: metadata keys. Supports
Category: prefix syntax for :Todo add commands.
2026-02-24 15:09:15 -05:00
607a9868d9 feat(store): add JSON data store with CRUD operations
Problem: need persistent task storage with forward-compatible
schema and unknown field preservation.

Solution: add store module with load/save, add/update/delete,
ID allocation, and UDA round-trip preservation. Data stored at
stdpath('data')/todo/tasks.json with version 1 schema.
2026-02-24 15:09:07 -05:00
116e3c5b25 feat(config): add configuration module
Problem: need user-configurable settings with sensible defaults.

Solution: add config module that merges vim.g.todo with defaults
for data_path, default_view, default_category, date_format, and
date_syntax.
2026-02-24 15:08:59 -05:00