* fix(oauth): resolve re-auth deadlock and improve flow robustness
Problem: in-flight TCP server held port 18392 for up to 120 seconds.
Calling `auth()` again caused `bind()` to fail silently — the browser
opened but no listener could receive the OAuth callback. `_wipe()` on
exchange failure also destroyed credentials, forcing full re-setup.
Solution: `_active_close` at module scope cancels any in-flight server
when `auth()` or `clear_tokens()` is called. Binding is guarded with
`pcall`; the browser only opens after the server is listening. Swapped
`_wipe()` for `clear_tokens()` in `_exchange_code` to preserve
credentials on failure. Added `select_account` to `prompt` so Google
always shows the account picker on re-auth.
* test(oauth): isolate bundled-credentials fallback from real filesystem
Problem: `resolve_credentials` reads from `vim.fn.stdpath('data')`,
the real Neovim data dir. The test passed only because `_wipe()` was
incidentally deleting the user's credential file mid-run.
Solution: stub `oauth.load_json_file` for the duration of the test so
real credential files cannot interfere with the fallback assertion.
* ci: format
* fix(sync): replace cryptic sigil counters with readable output
Problem: sync summaries used unexplained sigils (`+/-/~` and `!`) that
conveyed no meaning, mixed symbol and prose formats across operations,
and `gcal push` silently swallowed failures with no aggregate counter.
Solution: replace all summary `log.info` calls with a shared
`fmt_counts` helper that formats `N label` pairs separated by ` | `,
suppresses zero counts, and falls back to "nothing to do". Add a
`failed` counter to `gcal.push` to surface errors previously only
emitted as individual warnings.
* ci: format
Problem: push/sync permanently deleted remote Google Calendar events and
Google Tasks entries whenever a local task was marked deleted, done, or
de-due'd. There was no opt-out, so a misfire could silently cause
irreversible data loss on the remote side.
Solution: add a `remote_delete` boolean to the config (default `false`).
A unified flag at `sync.remote_delete` sets the base; per-backend
overrides at `sync.gcal.remote_delete` / `sync.gtasks.remote_delete`
take precedence when non-nil. When disabled, `_extra` remote IDs are
cleared silently (unlinking) so stale IDs don't accumulate.
* fix(buffer): use `default_category` config for empty placeholder
Problem: The empty-buffer fallback hardcoded the category name `TODO`,
ignoring the user's `default_category` config value (default: `Todo`).
Solution: Read `config.get().default_category` at render time and use
that value for both the header line and `LineMeta` category field.
* fix(diff): match optional checkbox char in `parse_buffer` patterns
Problem: `parse_buffer` used `%[.%]` which requires exactly one
character between brackets, failing to parse empty `[]` checkboxes.
Solution: Change to `%[.?%]` so the character is optional, matching
`[]`, `[ ]`, `[x]`, and `[!]` uniformly.
* fix(init): add `nowait` to buffer keymap opts
Problem: Buffer-local mappings like `!` could be swallowed by Neovim's
operator-pending machinery or by global maps sharing a prefix, since
the keymap opts did not include `nowait`.
Solution: Add `nowait = true` to the shared `opts` table used for all
buffer-local mappings in `_setup_buf_mappings`.
* feat(init): allow `:Pending done` with no args to use cursor line
Problem: `:Pending done` required an explicit task ID, making it
awkward to mark the current task done while inside the pending buffer.
Solution: When called with no ID, `M.done()` reads the cursor row from
`buffer.meta()` to resolve the task ID, erroring if the cursor is not
on a saved task line.
* fix(views): populate `priority` field in `LineMeta`
Problem: Both `category_view` and `priority_view` omitted `priority`
from the `LineMeta` they produced. `apply_extmarks` checks `m.priority`
to decide whether to render the priority icon, so it was always nil,
causing the `[ ]` pending-icon overlay to replace the `[!]` buffer text.
Solution: Add `priority = task.priority` to both LineMeta constructors.
* fix(buffer): keep `_meta` in sync when `open_line` inserts a new line
Problem: `open_line` inserted a buffer line without updating `_meta`,
leaving the entry at that row pointing to the task that was shifted
down. Pressing `<CR>` (toggle_complete) would read the stale meta,
find a real task ID, toggle it, and re-render — destroying the unsaved
new line.
Solution: Insert a `{ type = 'blank' }` sentinel into `_meta` at the
new line's position so buffer-local actions see no task there.
* fix(buffer): use task sentinel in `open_line` for better unsaved-task errors
* feat(init): warn on dirty buffer before store-dependent actions
Problem: `toggle_complete`, `toggle_priority`, `prompt_date`, and
`done` (no-args) all read from `buffer.meta()` which is stale whenever
the buffer has unsaved edits, leading to silent no-ops or acting on the
wrong task.
Solution: Add a `require_saved()` guard that emits a `log.warn` and
returns false when the buffer is modified. Each store-dependent action
calls it before touching meta or the store.
* fix(init): guard `view`, `undo`, and `filter` against dirty buffer
Problem: `toggle_view`, `undo_write`, and `filter` all call
`buffer.render()` which rewrites the buffer from the store, silently
discarding any unsaved edits. The previous `require_saved()` change
missed these three entry points.
Solution: Add `require_saved()` to the `view` and `filter` keymap
lambdas and to `M.undo_write()`. Also guard `M.filter()` directly so
`:Pending filter` from the command line is covered too.
* fix(init): improve dirty-buffer warning message
* fix(init): tighten dirty-buffer warning message
* feat(oauth): add `OAuthClient:clear_tokens()` method
Problem: no way to wipe just the token file while keeping credentials
intact — `_wipe()` removed both.
Solution: add `clear_tokens()` that removes only the token file.
* fix(sync): warn instead of auto-reauth when token is missing
Problem: `with_token` silently triggered an OAuth browser flow when no
tokens existed, with no user-facing explanation.
Solution: replace the auto-reauth branch with a `log.warn` directing
the user to run `:Pending auth`.
* feat(init): add `clear` and `reset` actions to `:Pending auth`
Problem: no CLI path existed to wipe stale tokens or reset credentials,
and the `vim.ui.select` backend picker was misleading given shared tokens.
Solution: accept an args string in `M.auth()`, dispatching `clear` to
`clear_tokens()`, `reset` to `_wipe()`, and bare backend names to the
existing auth flow. Remove the picker.
* feat(plugin): add tab completion for `:Pending auth` subcommands
`:Pending auth <Tab>` completes `gcal gtasks clear reset`;
`:Pending auth <backend> <Tab>` completes `clear reset`.
* fix(buffer): use `default_category` config for empty placeholder
Problem: The empty-buffer fallback hardcoded the category name `TODO`,
ignoring the user's `default_category` config value (default: `Todo`).
Solution: Read `config.get().default_category` at render time and use
that value for both the header line and `LineMeta` category field.
* fix(diff): match optional checkbox char in `parse_buffer` patterns
Problem: `parse_buffer` used `%[.%]` which requires exactly one
character between brackets, failing to parse empty `[]` checkboxes.
Solution: Change to `%[.?%]` so the character is optional, matching
`[]`, `[ ]`, `[x]`, and `[!]` uniformly.
* fix(init): add `nowait` to buffer keymap opts
Problem: Buffer-local mappings like `!` could be swallowed by Neovim's
operator-pending machinery or by global maps sharing a prefix, since
the keymap opts did not include `nowait`.
Solution: Add `nowait = true` to the shared `opts` table used for all
buffer-local mappings in `_setup_buf_mappings`.
* feat(init): allow `:Pending done` with no args to use cursor line
Problem: `:Pending done` required an explicit task ID, making it
awkward to mark the current task done while inside the pending buffer.
Solution: When called with no ID, `M.done()` reads the cursor row from
`buffer.meta()` to resolve the task ID, erroring if the cursor is not
on a saved task line.
* fix(views): populate `priority` field in `LineMeta`
Problem: Both `category_view` and `priority_view` omitted `priority`
from the `LineMeta` they produced. `apply_extmarks` checks `m.priority`
to decide whether to render the priority icon, so it was always nil,
causing the `[ ]` pending-icon overlay to replace the `[!]` buffer text.
Solution: Add `priority = task.priority` to both LineMeta constructors.
* fix(buffer): keep `_meta` in sync when `open_line` inserts a new line
Problem: `open_line` inserted a buffer line without updating `_meta`,
leaving the entry at that row pointing to the task that was shifted
down. Pressing `<CR>` (toggle_complete) would read the stale meta,
find a real task ID, toggle it, and re-render — destroying the unsaved
new line.
Solution: Insert a `{ type = 'blank' }` sentinel into `_meta` at the
new line's position so buffer-local actions see no task there.
* fix(buffer): use task sentinel in `open_line` for better unsaved-task errors
* feat(init): warn on dirty buffer before store-dependent actions
Problem: `toggle_complete`, `toggle_priority`, `prompt_date`, and
`done` (no-args) all read from `buffer.meta()` which is stale whenever
the buffer has unsaved edits, leading to silent no-ops or acting on the
wrong task.
Solution: Add a `require_saved()` guard that emits a `log.warn` and
returns false when the buffer is modified. Each store-dependent action
calls it before touching meta or the store.
* fix(init): guard `view`, `undo`, and `filter` against dirty buffer
Problem: `toggle_view`, `undo_write`, and `filter` all call
`buffer.render()` which rewrites the buffer from the store, silently
discarding any unsaved edits. The previous `require_saved()` change
missed these three entry points.
Solution: Add `require_saved()` to the `view` and `filter` keymap
lambdas and to `M.undo_write()`. Also guard `M.filter()` directly so
`:Pending filter` from the command line is covered too.
* fix(init): improve dirty-buffer warning message
* fix(init): tighten dirty-buffer warning message
* fix(buffer): use `default_category` config for empty placeholder
Problem: The empty-buffer fallback hardcoded the category name `TODO`,
ignoring the user's `default_category` config value (default: `Todo`).
Solution: Read `config.get().default_category` at render time and use
that value for both the header line and `LineMeta` category field.
* fix(diff): match optional checkbox char in `parse_buffer` patterns
Problem: `parse_buffer` used `%[.%]` which requires exactly one
character between brackets, failing to parse empty `[]` checkboxes.
Solution: Change to `%[.?%]` so the character is optional, matching
`[]`, `[ ]`, `[x]`, and `[!]` uniformly.
Toggles a task's done/pending status by ID from the command line,
matching the buffer \`<CR>\` behaviour including recurrence spawning.
Tab-completes active task IDs.
Problem: Failed token exchange left credential files on disk, trapping
users in a broken auth loop with no way back to setup. The `auth`
prompt used raw backend names and a terse prompt string. The `health`
action appeared in `:Pending gcal health` tab completion but silently
no-oped outside `:checkhealth`. gcal health omitted the token check
that gtasks had.
Solution: `_exchange_code` now calls `_wipe()` on both failure paths,
clearing the token and credentials files so the next `:Pending auth`
routes back through `setup()`. Prompt uses full service names and
"Authenticate with:". `health` is filtered from sync subcommand
completion and dispatch — its home is `:checkhealth pending`. gcal
health now checks for tokens.
* feat(sync): unify Google auth under :Pending auth
Problem: users had to run `:Pending gtasks auth` and `:Pending gcal
auth` separately, producing two token files and two browser consents
for the same Google account.
Solution: introduce `oauth.google_client` with combined tasks +
calendar scopes and a single `google_tokens.json`. Remove per-backend
`auth`/`setup` from `gcal` and `gtasks`; add top-level `:Pending auth`
that prompts with `vim.ui.select` and delegates to the shared client's
`setup()` or `auth()` based on credential availability.
* docs: update vimdoc for unified Google auth
Problem: `doc/pending.txt` still documented per-backend `:Pending gtasks
auth` / `:Pending gcal auth` commands and separate token files, which no
longer exist after the auth unification.
Solution: add `:Pending auth` entry to COMMANDS and a new
`*pending-google-auth*` section covering the shared PKCE flow, combined
scopes, and `google_tokens.json`. Remove `auth` from gcal/gtasks action
tables and update all cross-references to use `:Pending auth`.
* ci: format
* feat(sync): selective push, remote deletion detection, and gcal fix
Problem: `push_pass` updated all remote-linked tasks unconditionally,
causing unnecessary API calls and potential clobbering of remote edits
made between syncs. `pull`/`sync` never noticed when a task disappeared
from remote. `update_event` omitted `transparency` that `create_event`
set. Failure counts were absent from sync log summaries.
Solution: Introduce `_gtasks_synced_at` in `_extra` — stamped after
every successful push/pull create or update — so `push_pass` skips
tasks unchanged since last sync. Add `detect_remote_deletions` to
unlink local tasks whose remote entry disappeared from a successfully
fetched list. Surface failures as `!N` in all sync logs and
`unlinked: N` for pull/sync. Add `transparency = 'transparent'` to
`update_event`. Cover new behaviour with 7 tests in `gtasks_spec.lua`.
* ci: formt
* feat(sync): unify Google auth under :Pending auth
Problem: users had to run `:Pending gtasks auth` and `:Pending gcal
auth` separately, producing two token files and two browser consents
for the same Google account.
Solution: introduce `oauth.google_client` with combined tasks +
calendar scopes and a single `google_tokens.json`. Remove per-backend
`auth`/`setup` from `gcal` and `gtasks`; add top-level `:Pending auth`
that prompts with `vim.ui.select` and delegates to the shared client's
`setup()` or `auth()` based on credential availability.
* docs: update vimdoc for unified Google auth
Problem: `doc/pending.txt` still documented per-backend `:Pending gtasks
auth` / `:Pending gcal auth` commands and separate token files, which no
longer exist after the auth unification.
Solution: add `:Pending auth` entry to COMMANDS and a new
`*pending-google-auth*` section covering the shared PKCE flow, combined
scopes, and `google_tokens.json`. Remove `auth` from gcal/gtasks action
tables and update all cross-references to use `:Pending auth`.
* ci: format
* feat(sync): add `setup` command to configure credentials interactively
Problem: users had to manually create a JSON credentials file at the
correct path before authenticating, with no guidance from the plugin.
Solution: add `OAuthClient:setup()` that prompts for client ID and
secret via `vim.ui.input`, writes to the shared
`google_credentials.json`, then immediately starts the OAuth flow.
Expose as `:Pending {gtasks,gcal} setup`. Also extend
`resolve_credentials()` to fall back to a shared `google_credentials.json`
so one file covers both backends.
* fix(sync): improve `setup` input loop with validation and masking
Problem: `setup()` used async `vim.ui.input` for both prompts, causing
newline and re-prompt issues when validation failed. The secret was also
echoed in plain text.
Solution: switch to synchronous `vim.fn.input` / `vim.fn.inputsecret`
loops with `vim.cmd.redraw()` + `nvim_echo` for inline error display and
re-prompting. Validate client ID format and `GOCSPX-` secret prefix
before saving.
* fix(oauth): fix `ipairs` nil truncation in `resolve_credentials` and add file-path setup option
Problem: `resolve_credentials` built `cred_paths` with a potentially nil
first element (`credentials_path`), causing `ipairs` to stop immediately
and always fall through to bundled placeholder credentials.
Solution: build `cred_paths` without nil entries using `table.insert`.
Also add a `2. Load from JSON file path` option to `setup()` via
`vim.fn.inputlist`, with `vim.fn.expand` for `~`/`$HOME` support and
the `installed` wrapper unwrap.
* doc: cleanup
* ci: format
* fix(sync): surface auth failures and detect missing credentials
Problem: three silent failure paths remained in the sync auth flow —
`with_token` gave no feedback when auth was cancelled or failed,
`get_access_token` logged a generic message on refresh failure, and
`auth()` opened a browser with `PLACEHOLDER` credentials with no
Neovim-side error.
Solution: add `log.error` in `with_token` when `get_access_token`
returns nil after auth, improve the refresh-failure message to name
the backend and hint at re-auth, and guard `auth()` with a pre-flight
check that errors immediately when bundled placeholder credentials are
detected.
* feat(sync): add `setup` command to configure credentials interactively
Problem: users had to manually create a JSON credentials file at the
correct path before authenticating, with no guidance from the plugin.
Solution: add `OAuthClient:setup()` that prompts for client ID and
secret via `vim.ui.input`, writes to the shared
`google_credentials.json`, then immediately starts the OAuth flow.
Expose as `:Pending {gtasks,gcal} setup`. Also extend
`resolve_credentials()` to fall back to a shared `google_credentials.json`
so one file covers both backends.
* fix(sync): improve `setup` input loop with validation and masking
Problem: `setup()` used async `vim.ui.input` for both prompts, causing
newline and re-prompt issues when validation failed. The secret was also
echoed in plain text.
Solution: switch to synchronous `vim.fn.input` / `vim.fn.inputsecret`
loops with `vim.cmd.redraw()` + `nvim_echo` for inline error display and
re-prompting. Validate client ID format and `GOCSPX-` secret prefix
before saving.
* fix(oauth): fix `ipairs` nil truncation in `resolve_credentials` and add file-path setup option
Problem: `resolve_credentials` built `cred_paths` with a potentially nil
first element (`credentials_path`), causing `ipairs` to stop immediately
and always fall through to bundled placeholder credentials.
Solution: build `cred_paths` without nil entries using `table.insert`.
Also add a `2. Load from JSON file path` option to `setup()` via
`vim.fn.inputlist`, with `vim.fn.expand` for `~`/`$HOME` support and
the `installed` wrapper unwrap.
* doc: cleanup
* ci: format
* fix(sync): trigger auth then resume operation when not authenticated
Problem: `get_access_token()` called `auth()` then immediately tried to
load tokens, but `auth()` is async (TCP server + browser redirect), so
tokens were never present at that point. All sync operations silently
aborted when unauthenticated.
Solution: Remove the inline auth attempt from `get_access_token()` and
add an `on_complete` callback to `auth()` / `_exchange_code()`. Add a
`with_token(callback)` helper in `gtasks.lua` and `gcal.lua` that
triggers auth with the sync operation as the continuation, so
`push`/`pull`/`sync` resume automatically after the OAuth flow
completes.
* ci: format
* fix(diff): preserve due/rec when absent from buffer line
Problem: `diff.apply` overwrites `task.due` and `task.recur` with `nil`
whenever those fields aren't present as inline tokens in the buffer line.
Because metadata is rendered as virtual text (never in the line text),
every description edit silently clears due dates and recurrence rules.
Solution: Only update `due`, `recur`, and `recur_mode` in the existing-
task branch when the parsed entry actually contains them (non-nil). Users
can still set/change these inline by typing `due:<date>` or `rec:<rule>`;
clearing them requires `:Pending edit <id> -due`.
* refactor: remove project-local store discovery
Problem: `store.resolve_path()` searched upward for `.pending.json`,
silently splitting task data across multiple files depending on CWD.
Solution: `resolve_path()` now always returns `config.get().data_path`.
Remove `M.init()` and the `:Pending init` command and tab-completion
entry. Remove the project-local health message.
* refactor: extract log.lua, standardise [pending.nvim]: prefix
Problem: Notifications were scattered across files using bare
`vim.notify` with inconsistent `pending.nvim: ` prefixes, and the
`debug` guard in `textobj.lua` and `init.lua` was duplicated inline.
Solution: Add `lua/pending/log.lua` with `info`, `warn`, `error`, and
`debug` functions (prefix `[pending.nvim]: `). `log.debug` only fires
when `config.debug = true` or the optional `override` param is `true`.
Replace all `vim.notify` callsites and remove inline debug guards.
* feat(parse): configurable input date formats
Problem: `due:` only accepted ISO `YYYY-MM-DD` and built-in keywords;
users expecting locale-style dates like `03/15/2026` or `15-Mar-2026`
had no way to configure alternative input formats.
Solution: Add `input_date_formats` config field (string[]). Each entry
is a strftime-like format string supporting `%Y`, `%y`, `%m`, `%d`,
`%e`, `%b`, `%B`. Formats are tried in order after built-in keywords
fail. When no year specifier is present the current or next year is
inferred. Update vimdoc and add 8 parse_spec tests.
* refactor(oauth): async coroutine support, pure-Lua PKCE, server hardening
Problem: OAuth module shelled out to openssl for PKCE, used blocking
`vim.system():wait()`, had a weak `os.time()` PRNG seed, and the TCP
callback server leaked on read errors with no timeout.
Solution: Add `M.system()` coroutine wrapper and `M.async()` helper,
replace openssl with `vim.fn.sha256` + `vim.base64.encode`, seed from
`vim.uv.hrtime()`, add `close_server()` guard with 120s timeout, and
close the server on read errors.
* fix(gtasks): async operations, error notifications, buffer refresh
Problem: Sync operations blocked the editor, `push_pass` silently
dropped delete/update/create API errors, and the buffer was not
re-rendered after push/pull/sync.
Solution: Wrap `push`, `pull`, `sync` in `oauth.async()`, add
`vim.notify` for all `push_pass` failure paths, and re-render the
pending buffer after each operation.
* fix(init): edit recompute, filter predicates, sync action listing
Problem: `M.edit()` skipped `_recompute_counts()` after saving,
`compute_hidden_ids` lacked `done`/`pending` predicates, and
`run_sync` defaulted to `sync` instead of listing available actions.
Solution: Replace `s:save()` with `_save_and_notify()` in `M.edit()`,
add `done` and `pending` filter predicates, and list backend actions
when no action is specified.
* refactor(gcal): per-category calendars, async push, error notifications
Problem: gcal used a single hardcoded calendar name, ran synchronously
blocking the editor, and silently dropped some API errors.
Solution: Fetch all calendars and map categories to calendars (creating
on demand), wrap push in `oauth.async()`, notify on individual API
failures, track `_gcal_calendar_id` in `_extra`, and remove the `$`
anchor from `next_day` pattern.
* refactor: formatting fixes, config cleanup, health simplification
Problem: Formatter disagreements in `init.lua` and `gtasks.lua`,
stale `calendar` field in gcal config, and redundant health checks
for data directory existence.
Solution: Apply stylua formatting, remove `calendar` field from
`pending.GcalConfig`, drop data-dir and no-file health messages,
add `done`/`pending` to filter tab-completion candidates.
* docs: update vimdoc for sync refactor, remove demo scripts
Problem: Docs still referenced openssl dependency, defaulting to `sync`
action, and the `calendar` config field. Demo scripts used the old
singleton `store` API.
Solution: Update vimdoc and README to reflect explicit actions, per-
category calendars, and pure-Lua PKCE. Remove stale demo scripts and
update sync specs to match new behavior.
* fix(types): correct LuaLS annotations in oauth and gcal
* feat(gtasks): add Google Tasks bidirectional sync
Problem: pending.nvim only supported one-way push to Google Calendar.
Users who use Google Tasks had no way to sync tasks bidirectionally.
Solution: add `lua/pending/sync/gtasks.lua` backend with OAuth PKCE
auth, push/pull/sync actions, and field mapping between pending tasks
and Google Tasks (category↔tasklist, `priority`/`recur` via notes).
* refactor(cli): promote sync backends to top-level subcommands
Problem: `:Pending sync gtasks auth` required an extra `sync` keyword
that added no value and made the command unnecessarily verbose.
Solution: route `gtasks` and `gcal` as top-level `:Pending` subcommands
via `SYNC_BACKEND_SET` lookup. Tab completion introspects backend
modules for available actions instead of hardcoding `{ 'auth', 'sync' }`.
* docs(gtasks): document Google Tasks backend and CLI changes
Problem: vimdoc had no coverage for the gtasks backend and still
referenced the old `:Pending sync <backend>` command form.
Solution: add `:Pending-gtasks` and `:Pending-gcal` command sections
with per-action docs, update sync backend interface, and add gtasks
config example.
* ci: format
* refactor(sync): extract shared OAuth into `oauth.lua`
Problem: `gcal.lua` and `gtasks.lua` duplicated ~250 lines of identical
OAuth code (token management, PKCE flow, credential loading, curl
helpers, url encoding).
Solution: Extract a shared `OAuthClient` metatable in `oauth.lua` with
module-level utilities and instance methods. Both backends now delegate
all OAuth to `oauth.new()`. Skip `oauth` in `health.lua` backend
discovery by checking for a `name` field.
* feat(sync): ship bundled OAuth credentials
Problem: Users must manually create a Google Cloud project and place a
credentials JSON file before sync works — terrible onboarding.
Solution: Add `client_id`/`client_secret` fields to `GcalConfig` and
`GtasksConfig`. `oauth.lua` resolves credentials in three tiers: config
fields, credentials file, then bundled defaults (placeholders for now).
* docs(sync): document bundled credentials and config fields
* ci: format
* feat(gtasks): add Google Tasks bidirectional sync
Problem: pending.nvim only supported one-way push to Google Calendar.
Users who use Google Tasks had no way to sync tasks bidirectionally.
Solution: add `lua/pending/sync/gtasks.lua` backend with OAuth PKCE
auth, push/pull/sync actions, and field mapping between pending tasks
and Google Tasks (category↔tasklist, `priority`/`recur` via notes).
* refactor(cli): promote sync backends to top-level subcommands
Problem: `:Pending sync gtasks auth` required an extra `sync` keyword
that added no value and made the command unnecessarily verbose.
Solution: route `gtasks` and `gcal` as top-level `:Pending` subcommands
via `SYNC_BACKEND_SET` lookup. Tab completion introspects backend
modules for available actions instead of hardcoding `{ 'auth', 'sync' }`.
* docs(gtasks): document Google Tasks backend and CLI changes
Problem: vimdoc had no coverage for the gtasks backend and still
referenced the old `:Pending sync <backend>` command form.
Solution: add `:Pending-gtasks` and `:Pending-gcal` command sections
with per-action docs, update sync backend interface, and add gtasks
config example.
* ci: format
* docs: remove unnecessary mini.ai recipe from vimdoc
Problem: the `*pending-mini-ai*` section assumed mini.ai intercepts
buffer-local `at`/`it`/`aC`/`iC` mappings, requiring a manual
`vim.b.miniai_config` workaround.
Solution: remove the section. Neovim's keymap resolver already
prioritizes longer buffer-local mappings over mini.ai's global
`a`/`i` handlers — no recipe needed.
* refactor(icons): unify category/header icon and use checkbox overlays
Problem: `header` and `category` were separate icons for the same
concept. The icon overlay replaced `[ ]` with a bare character,
hiding the markdown checkbox syntax. Header format `## ` produced
a double-space with single-char icons.
Solution: merge `header` into `category` (one icon for both header
lines and EOL labels). Overlay renders `[icon]` preserving bracket
syntax. Change header line format from `## ` to `# ` so the
2-char overlay (`# `) maps cleanly.
* ci: remove empty `assets/` placeholder
* refactor(config): default icons to ascii
Problem: default icons used unicode characters (○, ✓, ●, ▸, ·, ↺)
which render poorly in some terminals and font configurations.
Solution: replace defaults with ascii equivalents (-, x, !, >, ., ~).
Users can still override to unicode or nerd font icons via config.
* ci: ignore library type checking
* ci: cleanup ci script
* refactor(config): default icons to ascii
Problem: default icons used unicode characters (○, ✓, ●, ▸, ·, ↺)
which render poorly in some terminals and font configurations.
Solution: replace defaults with ascii equivalents (-, x, !, >, ., ~).
Users can still override to unicode or nerd font icons via config.
* ci: ignore library type checking
* refactor(config): remove legacy gcal top-level config key
Problem: the gcal migration shim silently accepted vim.g.pending = { gcal
= {...} } and copied it to sync.gcal, adding complexity and a deprecated
API surface.
Solution: remove the migration block in config.get(), drop the cfg.gcal
fallback in gcal_config(), delete the two migration tests, and clean up
the vimdoc references. Callers must now use sync.gcal directly.
* ci: fix
* fix(spec): remove duplicate buffer require in complete_spec
* docs(pending): reorganize vimdoc and fix incorrect defaults
Problem: sections were out of logical order — inline metadata appeared
before commands, GCal before its own backend framework, store resolution
duplicated and buried after health check. Two defaults were wrong:
default_category documented as 'Inbox' (should be 'Todo') and the gcal
calendar example used 'Tasks' (should be 'Pendings').
Solution: reorder all 21 sections into onboarding-first flow, add a
CONTENTS table with hyperlinks, fix both incorrect defaults in every
location they appeared, and remove the duplicate STORE RESOLUTION
section.
* feat(filter): wire F key and <Plug>(pending-filter) mapping
Problem: the filter predicate logic, diff guard, _on_write handling,
:Pending filter command, and filter_spec were already implemented, but
there was no buffer-local key to invoke filtering interactively.
Solution: add filter = 'F' to keymaps config and defaults, wire the
filter action in _setup_buf_mappings via vim.ui.input, add
<Plug>(pending-filter), and update the vimdoc (mappings table, Plug
section, config example, and FILTERS section).
* refactor(config): remove legacy gcal top-level config key
Problem: the gcal migration shim silently accepted vim.g.pending = { gcal
= {...} } and copied it to sync.gcal, adding complexity and a deprecated
API surface.
Solution: remove the migration block in config.get(), drop the cfg.gcal
fallback in gcal_config(), delete the two migration tests, and clean up
the vimdoc references. Callers must now use sync.gcal directly.
* ci: fix
* fix(spec): remove duplicate buffer require in complete_spec
* docs(pending): reorganize vimdoc and fix incorrect defaults
Problem: sections were out of logical order — inline metadata appeared
before commands, GCal before its own backend framework, store resolution
duplicated and buried after health check. Two defaults were wrong:
default_category documented as 'Inbox' (should be 'Todo') and the gcal
calendar example used 'Tasks' (should be 'Pendings').
Solution: reorder all 21 sections into onboarding-first flow, add a
CONTENTS table with hyperlinks, fix both incorrect defaults in every
location they appeared, and remove the duplicate STORE RESOLUTION
section.
* refactor(config): remove legacy gcal top-level config key
Problem: the gcal migration shim silently accepted vim.g.pending = { gcal
= {...} } and copied it to sync.gcal, adding complexity and a deprecated
API surface.
Solution: remove the migration block in config.get(), drop the cfg.gcal
fallback in gcal_config(), delete the two migration tests, and clean up
the vimdoc references. Callers must now use sync.gcal directly.
* ci: fix
* fix(spec): remove duplicate buffer require in complete_spec
Add *pending-store-resolution* section explaining upward .pending.json
discovery and fallback to the global data_path. Document :Pending init
under COMMANDS. Add a cross-reference from the data_path config field.
Problem: every spec used the old singleton API (store.unload(),
store.load(), store.add(), etc.) and diff.apply(lines, hidden).
Solution: lower-level specs (store, diff, views, complete, file) use
s = store.new(path); s:load() directly. Higher-level specs (archive,
edit, filter, status, sync) reset package.loaded['pending'] in
before_each and use pending.store() to access the live instance.
diff.apply calls updated to diff.apply(lines, s, hidden_ids).
* refactor(store): convert singleton to Store.new() factory
Problem: store.lua used module-level _data singleton, making
project-local stores impossible and creating hidden global state.
Solution: introduce Store metatable with all operations as instance
methods. M.new(path) constructs an instance; M.resolve_path()
searches upward for .pending.json and falls back to
config.get().data_path. Singleton module API is removed.
* refactor(diff): accept store instance as parameter
Problem: diff.apply called store singleton methods directly, coupling
it to global state and preventing use with project-local stores.
Solution: change signature to apply(lines, s, hidden_ids?) where s is
a pending.Store instance. All store operations now go through s.
* refactor(buffer): add set_store/store accessors, drop singleton dep
Problem: buffer.lua imported store directly and called singleton
methods, preventing it from working with per-project store instances.
Solution: add module-level _store, M.set_store(s), and M.store()
accessors. open() and render() use _store instead of the singleton.
init.lua will call buffer.set_store(s) before buffer.open().
* refactor(complete,health,sync,plugin): update callers to store instance API
Problem: complete.lua, health.lua, sync/gcal.lua, and plugin/pending.lua
all called singleton store methods directly.
Solution: complete.lua uses buffer.store() for category lookups;
health.lua uses store.new(store.resolve_path()) and reports the
resolved path; gcal.lua calls require('pending').store() for task
access; plugin tab-completion creates ephemeral store instances via
store.new(store.resolve_path()). Add 'init' to the subcommands list.
* feat(init): thread Store instance through init, add :Pending init
Problem: init.lua called singleton store methods throughout, and there
was no way to create a project-local .pending.json file.
Solution: add module-level _store and private get_store() that
lazy-constructs via store.new(store.resolve_path()). Add public
M.store() accessor used by specs and sync backends. M.open() calls
buffer.set_store(get_store()) before buffer.open(). All store
callsites converted to get_store():method(). goto_file() and
add_here() derive the data directory from get_store().path.
Add M.init() which creates .pending.json in cwd and dispatches from
M.command() as ':Pending init'.
* test: update all specs for Store instance API
Problem: every spec used the old singleton API (store.unload(),
store.load(), store.add(), etc.) and diff.apply(lines, hidden).
Solution: lower-level specs (store, diff, views, complete, file) use
s = store.new(path); s:load() directly. Higher-level specs (archive,
edit, filter, status, sync) reset package.loaded['pending'] in
before_each and use pending.store() to access the live instance.
diff.apply calls updated to diff.apply(lines, s, hidden_ids).
* docs(pending): document :Pending init and store resolution
Add *pending-store-resolution* section explaining upward .pending.json
discovery and fallback to the global data_path. Document :Pending init
under COMMANDS. Add a cross-reference from the data_path config field.
* ci: format
* ci: remove unused variable
* feat(config): add icons table with unicode defaults
* feat(buffer): render icon overlays from config.icons
Problem: status characters ([ ], [x], [!]) and metadata prefixes are
hardcoded literals with no user customization.
Solution: read config.icons in apply_extmarks and apply overlay
extmarks for checkboxes/headers, replace hardcoded recur ↺ with
icons.recur, and prefix due/category virt_text with configurable
icon characters.
* feat(plugin): add PendingTab command and <Plug>(pending-tab)
* docs: add icons config, PendingTab recipes, and demo infrastructure
Problem: icon customization and auto-start workflow are undocumented;
no demo asset exists for the README.
Solution: document pending.Icons in vimdoc with nerd font and ASCII
recipes, add PendingTab to commands and mappings, add open-on-startup
recipe, add demo-init.lua and demo.tape for VHS screenshot generation,
add assets/ directory, add README icons section and demo placeholder.
* ci: format
* feat(file-token): add file: inline metadata token with gf navigation
Problem: there was no way to link a task to a specific location in a
source file, or to quickly jump from a task to the relevant code.
Solution: add a file:<path>:<line> inline token that stores a relative
file reference in task._extra.file. Virtual text renders basename:line
in a new PendingFile highlight group. A buffer-local gf mapping
(configurable via keymaps.goto_file) opens the file at the given line.
M.add_here() lets users attach the current cursor position to any task
via vim.ui.select(). M.edit() gains -file support to clear the
reference. <Plug>(pending-goto-file) and <Plug>(pending-add-here) are
exposed for custom mappings.
* test(file-token): add parse, diff, views, edit, and navigation tests
Problem: the file: token implementation had no test coverage.
Solution: add spec/file_spec.lua covering parse.body extraction,
malformed token handling, duplicate token stop-parsing, diff
reconciliation (store/update/clear/round-trip), LineMeta population
in both views, :Pending edit -file, and goto_file notify paths for
no-file and unreadable-file cases. All 292 tests pass.
* style: apply stylua formatting
* fix(types): remove empty elseif block, fix file? annotation nullability
Problem: users with mini.ai installed find that buffer-local `at`, `it`,
`aC`, `iC` text objects never fire because mini.ai intercepts `a`/`i` as
single-key handlers in operator-pending/visual modes before Neovim's
mapping system can route them to buffer-local maps.
Solution: add a *pending-mini-ai* recipe section to the RECIPES block in
pending.txt. The recipe explains the conflict, describes mini.ai's
custom_textobjects spec (`{ from = {line,col}, to = {line,col} }`), and
shows how to wrap `textobj.inner_task_range` and `textobj.category_bounds`
(the two functions that return positional data) into the shape mini.ai
expects, registered via `vim.b.miniai_config` in a FileType autocmd. Notes
that `aC` cannot be expressed this way due to its linewise selection, and
that the built-in keymaps work fine for users without mini.ai.
* feat(filter): oil-like editable filter line with predicate dispatch
Problem: no way to narrow the pending buffer to a subset of tasks
without manual scrolling; filtered-out tasks would be silently deleted
on :w because diff.apply() marks unseen IDs as deleted.
Solution: add a FILTER: line rendered at the top of the buffer when a
filter is active. The line is editable — :w re-parses it and updates
the hidden set. diff.apply() gains a hidden_ids param that prevents
filtered-out tasks from being marked deleted. Predicates: cat:X,
overdue, today, priority (space-separated AND). :Pending filter sets
it programmatically; :Pending filter clear removes it.
* ci: format
* refactor(sync): extract backend interface, adapt gcal module
Problem: :Pending sync hardcodes Google Calendar — M.sync() does
pcall(require, 'pending.sync.gcal') and calls gcal.sync() directly.
The config has a flat gcal field. This prevents adding new sync backends
without modifying init.lua.
Solution: Define a backend interface contract (name, auth, sync, health
fields), refactor :Pending sync to dispatch via require('pending.sync.'
.. backend_name), add sync table to config with legacy gcal migration,
rename gcal.authorize to gcal.auth, add gcal.health for checkhealth,
and add tab completion for backend names and actions.
* docs(sync): update vimdoc for backend interface
Problem: Vimdoc documents :Pending sync as a bare command that pushes
to Google Calendar, with no mention of backends or the sync table config.
Solution: Update :Pending sync section to show {backend} [{action}]
syntax with examples, add SYNC BACKENDS section documenting the interface
contract, update config example to use sync.gcal, document legacy gcal
migration, and update health check description.
* test(sync): add backend dispatch tests
Problem: No test coverage for sync dispatch logic, config migration,
or gcal module interface conformance.
Solution: Add spec/sync_spec.lua with tests for: bare sync errors,
empty backend errors, unknown backend errors, unknown action errors,
default-to-sync routing, explicit sync/auth routing, legacy gcal config
migration, explicit sync.gcal precedence, and gcal module interface
fields (name, auth, sync, health).
* feat: :Pending edit command for CLI metadata editing
Problem: editing task metadata (due date, category, priority,
recurrence) requires opening the buffer and editing inline. No way
to make quick metadata changes from the command line.
Solution: add :Pending edit {id} [operations...] command that applies
metadata changes by numeric task ID. Supports due:<date>, cat:<name>,
rec:<pattern>, +!, -!, -due, -cat, -rec operations with full date
vocabulary and recurrence validation. Pushes to undo stack, re-renders
the buffer if open, and provides feedback messages. Tab completion for
IDs, field names, date vocabulary, categories, and recurrence patterns.
Also fixes store.update() to properly clear fields set to vim.NIL.
* ci: formt
Problem: no way to know about overdue or due-today tasks without
opening :Pending. No ambient awareness for statusline plugins.
Solution: add counts(), statusline(), and has_due() public API
functions backed by a module-local cache that recomputes after every
store.save() and store.load(). Fire a User PendingStatusChanged event
on every recompute. Extract is_overdue() and is_today() from duplicate
locals into parse.lua as public functions. Refactor views.lua and
init.lua to use the shared date logic. Add vimdoc API section and
integration recipes for lualine, heirline, manual statusline, startup
notification, and event-driven refresh.
* feat: text objects and motions for the pending buffer
Problem: the pending buffer has action-button mappings but no Vim
grammar. You cannot dat to delete a task, cit to change a description,
or ]] to jump to the next category header.
Solution: add textobj.lua with at/it (a task / inner task), aC/iC
(a category / inner category), ]]/[[ (next/prev header), and ]t/[t
(next/prev task). All text objects work in operator-pending and visual
modes; motions work in normal, visual, and operator-pending. Mappings
are configurable via the keymaps table and exposed as <Plug> mappings.
* fix(textobj): escape Lua pattern hyphen, fix test expectations
Problem: inner_task_range used unescaped '-' in Lua patterns, which
acts as a lazy quantifier instead of matching a literal hyphen. The
metadata-stripping logic also tokenized the full line including the
prefix, so the rebuilt string could never be found after the prefix.
All test column expectations were off by one.
Solution: escape hyphens with %-, rewrite metadata stripping to
tokenize only the description portion after the prefix, and correct
all test assertions to match actual rendered column positions.
* feat(textobj): add debug mode, rename priority view buffer
Problem: the ]] motion reportedly lands one line past the header in
some environments, and ]t/[t may not override Neovim defaults. No
way to diagnose these at runtime. Also, pending://priority is a poor
buffer name for the flat ranked view.
Solution: add a debug config option (vim.g.pending = { debug = true })
that logs meta state, cursor positions, and mapping registration to
:messages at DEBUG level. Rename the buffer from pending://priority to
pending://queue. Internal view identifier stays 'priority'.
* docs: text objects, motions, debug mode, queue view rename
Problem: vimdoc had no documentation for the new text objects, motions,
debug config, or the pending://queue buffer rename.
Solution: add text object and motion tables to the mappings section,
document all eight <Plug> mappings, add debug field to the config
reference, update config example with new keymap defaults, rename
priority view references to queue throughout the vimdoc.
* fix(textobj): use correct config variable, raise log level
Problem: motion keymaps (]], [[, ]t, [t) were never set because
`config.get().debug` referenced an undefined `config` variable,
crashing _setup_buf_mappings before the motion loop. Debug logging
also used vim.log.levels.DEBUG which is filtered by default.
Solution: replace `config` with `cfg` (already in scope) and raise
both debug notify calls from DEBUG to INFO.
* ci: formt
* fix(plugin): allow command chaining with bar separator
Problem: :Pending|only failed because the command definition lacked the
bar attribute, causing | to be consumed as an argument.
Solution: Add bar = true to nvim_create_user_command so | is treated as
a command separator, matching fugitive's :Git behavior.
* refactor(buffer): remove opinionated window options
Problem: The plugin hardcoded number, relativenumber, wrap, spell,
signcolumn, foldcolumn, and cursorline in set_win_options, overriding
user preferences with no way to opt out.
Solution: Remove all cosmetic window options. Users who want them can
set them in after/ftplugin/pending.lua. Only conceallevel,
concealcursor, and winfixheight remain as functionally required.
* 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.
* feat(parse): flexible time parsing for @ suffix
Problem: the @HH:MM time suffix required zero-padded 24-hour format,
forcing users to write due:tomorrow@14:00 instead of due:tomorrow@2pm.
Solution: add normalize_time() that accepts bare hours (9, 14),
H:MM (9:30), am/pm (2pm, 9:30am, 12am), and existing HH:MM format,
normalizing all to canonical HH:MM on save.
* feat(complete): add info descriptions to omnifunc items
Problem: completion menu items had no description, making it hard to
distinguish between similar entries like date shorthands and recurrence
patterns.
Solution: return { word, info } tables from date_completions() and
recur_completions(), surfacing human-readable descriptions in the
completion popup.
* ci: format
* fix(plugin): allow command chaining with bar separator
Problem: :Pending|only failed because the command definition lacked the
bar attribute, causing | to be consumed as an argument.
Solution: Add bar = true to nvim_create_user_command so | is treated as
a command separator, matching fugitive's :Git behavior.
* refactor(buffer): remove opinionated window options
Problem: The plugin hardcoded number, relativenumber, wrap, spell,
signcolumn, foldcolumn, and cursorline in set_win_options, overriding
user preferences with no way to opt out.
Solution: Remove all cosmetic window options. Users who want them can
set them in after/ftplugin/pending.lua. Only conceallevel,
concealcursor, and winfixheight remain as functionally required.
* fix(plugin): allow command chaining with bar separator
Problem: :Pending|only failed because the command definition lacked the
bar attribute, causing | to be consumed as an argument.
Solution: Add bar = true to nvim_create_user_command so | is treated as
a command separator, matching fugitive's :Git behavior.
* fix: last window
Problem: :Pending|only failed because the command definition lacked the
bar attribute, causing | to be consumed as an argument.
Solution: Add bar = true to nvim_create_user_command so | is treated as
a command separator, matching fugitive's :Git behavior.
* 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.
* 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.
* 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.
* 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.
* 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().
* 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().
* 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.
* 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().
* 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.
* 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.
* 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.
* ci: fix
* 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.
* 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.
* 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.
* feat(plugin): add Plug mappings for all buffer actions
Problem: Only five of nine buffer actions had <Plug> mappings, so users
could not bind close, undo, open-line, or open-line-above globally.
Solution: Add <Plug>(pending-close), <Plug>(pending-undo),
<Plug>(pending-open-line), and <Plug>(pending-open-line-above).
* docs: update mappings and config for keymaps and new Plug entries
Problem: Vimdoc still listed g? help popup, lacked documentation for
the four new <Plug> mappings, and had no keymaps config section.
Solution: Remove g? from mappings table, document all nine <Plug>
mappings, add keymaps table to the config example and field reference,
and note that buffer-local keys are configurable.
* 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.
* 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.
* test: add top-priority missing test coverage
Problem: several critical code paths had zero test coverage —
parse.resolve_date (relative date resolution), store.snapshot
(foundation of the undo stack), and the diff.apply invariant that
unchanged tasks do not get their modified timestamp bumped. The
diff.apply due/priority clearing paths were also untested.
Solution: add six targeted test blocks across parse_spec, store_spec,
and diff_spec: resolve_date happy/failure paths, parse.body with
relative due tokens, snapshot copy-semantics and deleted-task
exclusion, diff unchanged-modified invariant, due cleared on removal,
priority cleared on ! removal.
* test: add second batch of missing test coverage
Problem: six more gaps from the audit remained after the first batch —
archive persistence verification, diff modified-on-rename, parse_buffer
inline cat:/due: token parsing, and store.update immutability invariants.
Solution: add six it() blocks across archive_spec, diff_spec, and
store_spec: archive unload/reload persistence check, modified timestamp
updated on description change, inline cat: overrides header category,
inline due: token parsed from buffer line, id/entry fields immutable
under store.update, and end timestamp not overwritten on second
completion.
Problem: fc4a47a changed the priority display format from '! ' to
'[N] ' in views.lua and diff.lua but left two existing test
assertions and their descriptions using the old format, causing
both to fail.
Solution: update the input line in diff parse_buffer test, update
the expected string and description names in views category_view
test, and rename the diff.apply description to match the new idiom.
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.
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.