Problem: extmarks ghosted after `cc`/undo on task lines, auth warnings
fired even when CLIs were authenticated, and `refresh()` hit forge APIs
on every buffer open regardless of `auto_close`.
Solution: add `invalidate = true` to all extmarks so Neovim cleans them
up on text deletion. Run `auth status` before warning to verify the CLI
is actually unauthenticated. Gate `refresh()` behind `auto_close` config.
Problem: adding a new forge required touching 5 lookup tables
(`FORGE_HOSTS`, `FORGE_CLI`, `FORGE_AUTH_CMD`, `SHORTHAND_PREFIX`,
`_warned_forges`) and every branching site in `_api_args`,
`fetch_metadata`, and `parse_ref`.
Solution: introduce a `ForgeBackend` class with `parse_url`,
`api_args`, and `parse_state` methods, plus a `register()` /
`backends()` registry. New forges (Gitea, Forgejo) are a single
`register()` call via the `gitea_backend()` convenience constructor.
Problem: Forge metadata fetching required manual token management —
config fields, CLI token extraction, and curl with auth headers. Each
forge had a different auth path, and Codeberg had no CLI support at all.
Solution: Delete `get_token()` and `_api_url()`, replace with
`_api_args()` that builds `gh api`, `glab api`, or `tea api` arg
arrays. The CLIs handle auth internally. Add `warn_missing_cli` config
(default true) that warns once per forge per session on failure. Add
forge CLI checks to `:checkhealth`. Remove `token` from config/docs.
* docs: document S3 backend, auto-auth, and `:Pending done` command
Problem: The S3 backend had no `:Pending s3` entry in the COMMANDS
section, `:Pending auth` only mentioned Google, the `sync` config
field omitted `s3`, `_s3_sync_id` was missing from the data format
section, `:Pending done` was implemented but undocumented, and the
README lacked a features overview.
Solution: Add `:Pending s3` and `:Pending done` command docs, rewrite
`:Pending auth` to cover all backends and sub-actions, update config
and data format references, add `aws` CLI to requirements, and add a
Features section to `README.md`.
* feat(forge): add forge link parser and metadata fetcher
Problem: no way to associate tasks with GitHub, GitLab, or Codeberg
issues/PRs, or to track their remote state.
Solution: add `forge.lua` with shorthand (`gh:user/repo#42`) and full
URL parsing, async metadata fetching via `curl`, label formatting,
conceal pattern generation, token resolution, and `refresh()` for
state pull (closed/merged -> done).
* feat(config): add forge config defaults and `%l` eol specifier
Problem: no configuration surface for forge link rendering, icons,
issue format, or self-hosted instances.
Solution: add `pending.ForgeConfig` class with per-forge `token`,
`icon`, `issue_format`, and `instances` fields. Add `%l` to the
default `eol_format` so forge labels render in virtual text.
* feat(parse): extract forge refs from task body
Problem: `parse.body()` had no awareness of forge link tokens, so
`gh:user/repo#42` stayed in the description instead of metadata.
Solution: add `forge_ref` field to `pending.Metadata` and extend the
right-to-left token loop in `body()` to call `forge.parse_ref()` as
the final fallback before breaking.
* feat(diff): persist forge refs in store on write
Problem: forge refs parsed from buffer lines were discarded during
diff reconciliation and never stored in the JSON.
Solution: thread `forge_ref` through `parse_buffer` entries into
`diff.apply`, storing it in `task._extra._forge_ref` for both new
and existing tasks.
* feat(views): pass forge ref and cache to line metadata
Problem: `LineMeta` had no forge fields, so `buffer.lua` could not
render forge labels or apply forge-specific highlights.
Solution: add `forge_ref` and `forge_cache` fields to `LineMeta`,
populated from `task._extra` in both `category_view` and
`priority_view`.
* feat(buffer): render forge links as concealed text with eol virt text
Problem: forge tokens were visible as raw text with no virtual text
labels, and the eol separator logic collapsed all gaps when
non-adjacent specifiers were absent.
Solution: add forge conceal syntax patterns in `setup_syntax()`, add
`PendingForge`/`PendingForgeClosed` highlight groups, handle the
`%l` specifier in `build_eol_virt()`, fix separator collapsing to
buffer one separator between present segments, and change
`concealcursor` to `nc` (reveal in visual and insert mode).
* feat(complete): add forge shorthand omnifunc completions
Problem: no completion support for `gh:`, `gl:`, or `cb:` tokens,
requiring users to type owner/repo from memory.
Solution: extend `omnifunc` to detect `gh:`/`gl:`/`cb:` prefixes and
complete with `owner/repo#` candidates from existing forge refs in
the store.
* feat: trigger forge refresh on buffer open
Problem: forge metadata was never fetched, so virt text highlights
could not reflect remote issue/PR state.
Solution: call `forge.refresh()` in `M.open()` so metadata is
fetched once per `:Pending` invocation rather than on every render.
* test(forge): add forge parsing spec
Problem: no test coverage for forge link shorthand parsing, URL
parsing, label formatting, or API URL generation.
Solution: add `spec/forge_spec.lua` covering `_parse_shorthand`,
`parse_ref` for all three forges, full URL parsing including nested
GitLab groups, `format_label`, and `_api_url`.
* docs: document forge links feature
Problem: no user-facing documentation for forge link syntax,
configuration, or behavior.
Solution: add forge links section to `README.md` and `pending.txt`
covering shorthand/URL syntax, config options, virtual text
rendering, state pull, and auth resolution.
* feat(forge): add `find_refs()` inline token scanner
Problem: forge tokens were extracted by `parse.body()` which stripped
them from the description, making editing awkward and multi-ref lines
impossible.
Solution: add `find_refs(text)` that scans a string for all forge
tokens by whitespace tokenization, returning byte offsets and parsed
refs without modifying the input. Remove unused `conceal_patterns()`.
* refactor: move forge ref detection from `parse.body()` to `diff`
Problem: `parse.body()` stripped forge tokens from the description,
losing the raw text. This made inline overlay rendering impossible
since the token no longer existed in the buffer.
Solution: remove the `forge.parse_ref()` branch from `parse.body()`
and call `forge.find_refs()` in `diff.parse_buffer()` instead. The
description now retains forge tokens verbatim; `_extra._forge_ref`
is still populated from the first matched ref.
* feat(buffer): render forge links as inline conceal overlays
Problem: forge tokens were stripped from the buffer and shown as EOL
virtual text via `%l`. The token disappeared from the editable line,
and multi-ref tasks broke.
Solution: compute `forge_spans` in `views.lua` with byte offsets for
each forge token in the rendered line. In `apply_inline_row()`, place
extmarks with `conceal=''` and `virt_text_pos='inline'` to visually
replace each raw token with its formatted label. Clear stale
`forge_spans` on dirty rows to prevent `end_col` out-of-range errors
after edits like `dd`.
* fix(config): remove `%l` from default `eol_format`
Problem: forge links are now rendered inline, making the `%l` EOL
specifier redundant in the default format.
Solution: change default `eol_format` from `'%l %c %r %d'` to
`'%c %r %d'`. The `%l` specifier remains functional for users who
explicitly set it.
* test(forge): update specs for inline forge refs
Problem: existing tests asserted that `parse.body()` stripped forge
tokens from the description and populated `meta.forge_ref`. The
`conceal_patterns` test referenced a removed function.
Solution: update `parse.body` integration tests to assert tokens stay
in the description. Add `find_refs()` tests covering single/multiple
refs, URLs, byte offsets, and empty cases. Remove `conceal_patterns`
test. Update diff tests to assert description includes the token.
* docs: update forge links for inline overlay rendering
Problem: documentation described forge tokens as stripped from the
description and rendered via EOL `%l` specifier by default.
Solution: update forge links section to describe inline conceal
overlay rendering. Update default `eol_format` reference. Change
`issue_format` field description from "EOL label" to "inline overlay
label".
* ci: format
* refactor(forge): remove `%l` eol specifier, add `auto_close` config, fix icons
Problem: `%l` was dead code after inline overlays replaced EOL
rendering. Auto-close was always on with no opt-out. Forge icon
defaults were empty strings.
Solution: remove `%l` from the eol format parser and renderer. Add
`forge.auto_close` (default `false`) to gate state-pull. Set nerd
font icons: `` (GitHub), `` (GitLab), `` (Codeberg). Keep
conceal active in insert mode via `concealcursor = 'nic'`.
* fix(config): set correct nerd font icons for forge defaults
Problem: `%l` was dead code after inline overlays replaced EOL
rendering. Auto-close was always on with no opt-out. Forge icon
defaults were empty strings.
Solution: remove `%l` from the eol format parser and renderer. Add
`forge.auto_close` (default `false`) to gate state-pull. Set nerd
font icons: `` (GitHub), `` (GitLab), `` (Codeberg). Keep
conceal active in insert mode via `concealcursor = 'nic'`.
* docs: document S3 backend, auto-auth, and `:Pending done` command
Problem: The S3 backend had no `:Pending s3` entry in the COMMANDS
section, `:Pending auth` only mentioned Google, the `sync` config
field omitted `s3`, `_s3_sync_id` was missing from the data format
section, `:Pending done` was implemented but undocumented, and the
README lacked a features overview.
Solution: Add `:Pending s3` and `:Pending done` command docs, rewrite
`:Pending auth` to cover all backends and sub-actions, update config
and data format references, add `aws` CLI to requirements, and add a
Features section to `README.md`.
* feat(forge): add forge link parser and metadata fetcher
Problem: no way to associate tasks with GitHub, GitLab, or Codeberg
issues/PRs, or to track their remote state.
Solution: add `forge.lua` with shorthand (`gh:user/repo#42`) and full
URL parsing, async metadata fetching via `curl`, label formatting,
conceal pattern generation, token resolution, and `refresh()` for
state pull (closed/merged -> done).
* feat(config): add forge config defaults and `%l` eol specifier
Problem: no configuration surface for forge link rendering, icons,
issue format, or self-hosted instances.
Solution: add `pending.ForgeConfig` class with per-forge `token`,
`icon`, `issue_format`, and `instances` fields. Add `%l` to the
default `eol_format` so forge labels render in virtual text.
* feat(parse): extract forge refs from task body
Problem: `parse.body()` had no awareness of forge link tokens, so
`gh:user/repo#42` stayed in the description instead of metadata.
Solution: add `forge_ref` field to `pending.Metadata` and extend the
right-to-left token loop in `body()` to call `forge.parse_ref()` as
the final fallback before breaking.
* feat(diff): persist forge refs in store on write
Problem: forge refs parsed from buffer lines were discarded during
diff reconciliation and never stored in the JSON.
Solution: thread `forge_ref` through `parse_buffer` entries into
`diff.apply`, storing it in `task._extra._forge_ref` for both new
and existing tasks.
* feat(views): pass forge ref and cache to line metadata
Problem: `LineMeta` had no forge fields, so `buffer.lua` could not
render forge labels or apply forge-specific highlights.
Solution: add `forge_ref` and `forge_cache` fields to `LineMeta`,
populated from `task._extra` in both `category_view` and
`priority_view`.
* feat(buffer): render forge links as concealed text with eol virt text
Problem: forge tokens were visible as raw text with no virtual text
labels, and the eol separator logic collapsed all gaps when
non-adjacent specifiers were absent.
Solution: add forge conceal syntax patterns in `setup_syntax()`, add
`PendingForge`/`PendingForgeClosed` highlight groups, handle the
`%l` specifier in `build_eol_virt()`, fix separator collapsing to
buffer one separator between present segments, and change
`concealcursor` to `nc` (reveal in visual and insert mode).
* feat(complete): add forge shorthand omnifunc completions
Problem: no completion support for `gh:`, `gl:`, or `cb:` tokens,
requiring users to type owner/repo from memory.
Solution: extend `omnifunc` to detect `gh:`/`gl:`/`cb:` prefixes and
complete with `owner/repo#` candidates from existing forge refs in
the store.
* feat: trigger forge refresh on buffer open
Problem: forge metadata was never fetched, so virt text highlights
could not reflect remote issue/PR state.
Solution: call `forge.refresh()` in `M.open()` so metadata is
fetched once per `:Pending` invocation rather than on every render.
* test(forge): add forge parsing spec
Problem: no test coverage for forge link shorthand parsing, URL
parsing, label formatting, or API URL generation.
Solution: add `spec/forge_spec.lua` covering `_parse_shorthand`,
`parse_ref` for all three forges, full URL parsing including nested
GitLab groups, `format_label`, and `_api_url`.
* docs: document forge links feature
Problem: no user-facing documentation for forge link syntax,
configuration, or behavior.
Solution: add forge links section to `README.md` and `pending.txt`
covering shorthand/URL syntax, config options, virtual text
rendering, state pull, and auth resolution.
* feat(forge): add `find_refs()` inline token scanner
Problem: forge tokens were extracted by `parse.body()` which stripped
them from the description, making editing awkward and multi-ref lines
impossible.
Solution: add `find_refs(text)` that scans a string for all forge
tokens by whitespace tokenization, returning byte offsets and parsed
refs without modifying the input. Remove unused `conceal_patterns()`.
* refactor: move forge ref detection from `parse.body()` to `diff`
Problem: `parse.body()` stripped forge tokens from the description,
losing the raw text. This made inline overlay rendering impossible
since the token no longer existed in the buffer.
Solution: remove the `forge.parse_ref()` branch from `parse.body()`
and call `forge.find_refs()` in `diff.parse_buffer()` instead. The
description now retains forge tokens verbatim; `_extra._forge_ref`
is still populated from the first matched ref.
* feat(buffer): render forge links as inline conceal overlays
Problem: forge tokens were stripped from the buffer and shown as EOL
virtual text via `%l`. The token disappeared from the editable line,
and multi-ref tasks broke.
Solution: compute `forge_spans` in `views.lua` with byte offsets for
each forge token in the rendered line. In `apply_inline_row()`, place
extmarks with `conceal=''` and `virt_text_pos='inline'` to visually
replace each raw token with its formatted label. Clear stale
`forge_spans` on dirty rows to prevent `end_col` out-of-range errors
after edits like `dd`.
* fix(config): remove `%l` from default `eol_format`
Problem: forge links are now rendered inline, making the `%l` EOL
specifier redundant in the default format.
Solution: change default `eol_format` from `'%l %c %r %d'` to
`'%c %r %d'`. The `%l` specifier remains functional for users who
explicitly set it.
* test(forge): update specs for inline forge refs
Problem: existing tests asserted that `parse.body()` stripped forge
tokens from the description and populated `meta.forge_ref`. The
`conceal_patterns` test referenced a removed function.
Solution: update `parse.body` integration tests to assert tokens stay
in the description. Add `find_refs()` tests covering single/multiple
refs, URLs, byte offsets, and empty cases. Remove `conceal_patterns`
test. Update diff tests to assert description includes the token.
* docs: update forge links for inline overlay rendering
Problem: documentation described forge tokens as stripped from the
description and rendered via EOL `%l` specifier by default.
Solution: update forge links section to describe inline conceal
overlay rendering. Update default `eol_format` reference. Change
`issue_format` field description from "EOL label" to "inline overlay
label".
* ci: format
Problem: documentation described forge tokens as stripped from the
description and rendered via EOL `%l` specifier by default.
Solution: update forge links section to describe inline conceal
overlay rendering. Update default `eol_format` reference. Change
`issue_format` field description from "EOL label" to "inline overlay
label".
Problem: existing tests asserted that `parse.body()` stripped forge
tokens from the description and populated `meta.forge_ref`. The
`conceal_patterns` test referenced a removed function.
Solution: update `parse.body` integration tests to assert tokens stay
in the description. Add `find_refs()` tests covering single/multiple
refs, URLs, byte offsets, and empty cases. Remove `conceal_patterns`
test. Update diff tests to assert description includes the token.
Problem: forge links are now rendered inline, making the `%l` EOL
specifier redundant in the default format.
Solution: change default `eol_format` from `'%l %c %r %d'` to
`'%c %r %d'`. The `%l` specifier remains functional for users who
explicitly set it.
Problem: forge tokens were stripped from the buffer and shown as EOL
virtual text via `%l`. The token disappeared from the editable line,
and multi-ref tasks broke.
Solution: compute `forge_spans` in `views.lua` with byte offsets for
each forge token in the rendered line. In `apply_inline_row()`, place
extmarks with `conceal=''` and `virt_text_pos='inline'` to visually
replace each raw token with its formatted label. Clear stale
`forge_spans` on dirty rows to prevent `end_col` out-of-range errors
after edits like `dd`.
Problem: `parse.body()` stripped forge tokens from the description,
losing the raw text. This made inline overlay rendering impossible
since the token no longer existed in the buffer.
Solution: remove the `forge.parse_ref()` branch from `parse.body()`
and call `forge.find_refs()` in `diff.parse_buffer()` instead. The
description now retains forge tokens verbatim; `_extra._forge_ref`
is still populated from the first matched ref.
Problem: forge tokens were extracted by `parse.body()` which stripped
them from the description, making editing awkward and multi-ref lines
impossible.
Solution: add `find_refs(text)` that scans a string for all forge
tokens by whitespace tokenization, returning byte offsets and parsed
refs without modifying the input. Remove unused `conceal_patterns()`.
Problem: no user-facing documentation for forge link syntax,
configuration, or behavior.
Solution: add forge links section to `README.md` and `pending.txt`
covering shorthand/URL syntax, config options, virtual text
rendering, state pull, and auth resolution.
Problem: no test coverage for forge link shorthand parsing, URL
parsing, label formatting, or API URL generation.
Solution: add `spec/forge_spec.lua` covering `_parse_shorthand`,
`parse_ref` for all three forges, full URL parsing including nested
GitLab groups, `format_label`, and `_api_url`.
Problem: forge metadata was never fetched, so virt text highlights
could not reflect remote issue/PR state.
Solution: call `forge.refresh()` in `M.open()` so metadata is
fetched once per `:Pending` invocation rather than on every render.
Problem: no completion support for `gh:`, `gl:`, or `cb:` tokens,
requiring users to type owner/repo from memory.
Solution: extend `omnifunc` to detect `gh:`/`gl:`/`cb:` prefixes and
complete with `owner/repo#` candidates from existing forge refs in
the store.
Problem: forge tokens were visible as raw text with no virtual text
labels, and the eol separator logic collapsed all gaps when
non-adjacent specifiers were absent.
Solution: add forge conceal syntax patterns in `setup_syntax()`, add
`PendingForge`/`PendingForgeClosed` highlight groups, handle the
`%l` specifier in `build_eol_virt()`, fix separator collapsing to
buffer one separator between present segments, and change
`concealcursor` to `nc` (reveal in visual and insert mode).
Problem: `LineMeta` had no forge fields, so `buffer.lua` could not
render forge labels or apply forge-specific highlights.
Solution: add `forge_ref` and `forge_cache` fields to `LineMeta`,
populated from `task._extra` in both `category_view` and
`priority_view`.
Problem: forge refs parsed from buffer lines were discarded during
diff reconciliation and never stored in the JSON.
Solution: thread `forge_ref` through `parse_buffer` entries into
`diff.apply`, storing it in `task._extra._forge_ref` for both new
and existing tasks.
Problem: `parse.body()` had no awareness of forge link tokens, so
`gh:user/repo#42` stayed in the description instead of metadata.
Solution: add `forge_ref` field to `pending.Metadata` and extend the
right-to-left token loop in `body()` to call `forge.parse_ref()` as
the final fallback before breaking.
Problem: no configuration surface for forge link rendering, icons,
issue format, or self-hosted instances.
Solution: add `pending.ForgeConfig` class with per-forge `token`,
`icon`, `issue_format`, and `instances` fields. Add `%l` to the
default `eol_format` so forge labels render in virtual text.
Problem: no way to associate tasks with GitHub, GitLab, or Codeberg
issues/PRs, or to track their remote state.
Solution: add `forge.lua` with shorthand (`gh:user/repo#42`) and full
URL parsing, async metadata fetching via `curl`, label formatting,
conceal pattern generation, token resolution, and `refresh()` for
state pull (closed/merged -> done).
Problem: The S3 backend had no `:Pending s3` entry in the COMMANDS
section, `:Pending auth` only mentioned Google, the `sync` config
field omitted `s3`, `_s3_sync_id` was missing from the data format
section, `:Pending done` was implemented but undocumented, and the
README lacked a features overview.
Solution: Add `:Pending s3` and `:Pending done` command docs, rewrite
`:Pending auth` to cover all backends and sub-actions, update config
and data format references, add `aws` CLI to requirements, and add a
Features section to `README.md`.
Problem: The S3 backend had no `:Pending s3` entry in the COMMANDS
section, `:Pending auth` only mentioned Google, the `sync` config
field omitted `s3`, `_s3_sync_id` was missing from the data format
section, `:Pending done` was implemented but undocumented, and the
README lacked a features overview.
Solution: Add `:Pending s3` and `:Pending done` command docs, rewrite
`:Pending auth` to cover all backends and sub-actions, update config
and data format references, add `aws` CLI to requirements, and add a
Features section to `README.md`.
Problem: `with_token()` recommended the generic `:Pending auth` when
credentials were missing, even though the backend was already known.
Solution: append the backend name so the message reads e.g.
`:Pending auth gtasks` instead of `:Pending auth`.
Problem: running a sync action (e.g. `:Pending gtasks push`) without
being authenticated would silently abort with a warning, requiring
the user to manually run `:Pending auth` first.
Solution: `oauth.with_token()` now auto-triggers the browser auth flow
when no token exists (for non-bundled credentials) and resumes the
original action on success. `auth()` and `_exchange_code()` now call
`on_complete(ok)` on all exit paths. S3 backends run
`aws sts get-caller-identity` before every sync action, auto-triggering
SSO login on expired sessions.
* fix(sync): add backend name prefix to all OAuth log messages (#121)
Problem: four log messages in `oauth.lua` lacked the `self.name` backend
prefix, producing generic notifications instead of identifying which
backend (`gcal`/`gtasks`) triggered the message.
Solution: prepend `self.name .. ': '` to the four unprefixed messages
and drop the hardcoded "Google" from the browser prompt since `self.name`
already identifies the service.
* fix(sync): canonicalize all log prefixes across sync backends (#121)
Problem: log messages in `oauth.lua`, `gcal.lua`, `gtasks.lua`, and
`s3.lua` were inconsistent — some lacked a backend prefix, others used
sentence-case or bare error strings without identifying the source.
Solution: prefix all user-facing log messages with their backend name
(`gcal:`, `gtasks:`, `S3:`, `Google:`). Capitalize `S3` and `Google`
display names. Normalize casing and separator style (em dash) across
all sync log sites.
* fix(buffer): escape hyphens in `infer_status` Lua patterns
Problem: `infer_status` used `/-` in its Lua patterns, which is a lazy
quantifier on `/` rather than a literal hyphen. This caused the function
to always return `nil` for lines with an `/id/` prefix, so status was
never inferred from buffer text during `reapply_dirty_inline`.
Solution: escape hyphens as `%-` in both patterns. Also add debug
logging to `on_bytes`, `reapply_dirty_inline`, `apply_extmarks`, and
the `TextChanged`/`TextChangedI`/`InsertLeave` autocmds.
* ci: format
* fix(config): update default keymaps to match vimdoc
Problem: four keymap defaults in `config.lua` still used the old
deprecated keys (`!`, `D`, `U`, `F`) while `doc/pending.txt` documents
the `g`-prefixed replacements (`g!`, `gd`, `gz`, `gf`).
Solution: update `priority`, `date`, `undo`, and `filter` defaults to
`g!`, `gd`, `gz`, and `gf` respectively.
* fix(buffer): correct extmark drift on `open_line` above/below done tasks
Problem: `open_line` used `nvim_buf_set_lines` which triggered `on_bytes`
with a `start_row` offset designed for native `o`/`O` keypresses. The
`_meta` entry was inserted one position too late, causing the done task's
`PendingDone` highlight to attach to the new blank line instead.
Solution: suppress `on_bytes` during `open_line` by reusing the
`_rendering` guard, insert the meta entry at the correct position, and
immediately reapply inline extmarks for the affected rows.
* fix(buffer): infer task status from line text in `reapply_dirty_inline`
Problem: `on_bytes` inserts bare `{ type = 'task' }` meta entries with
no `status` field for any new lines (paste, undo, native edits). When
meta positions also shift incorrectly (e.g. `P` paste above), existing
meta with the wrong status ends up on the wrong row. This causes done
tasks to lose their `PendingDone` highlight and pending tasks to appear
greyed out.
Solution: always re-infer `status` from the actual buffer line text for
dirty task rows before applying extmarks. The checkbox character (`[x]`,
`[>]`, `[=]`, `[ ]`) is the source of truth, with fallback to the
existing meta status if the line doesn't match a task pattern.
Problem: four keymap defaults in `config.lua` still used the old
deprecated keys (`!`, `D`, `U`, `F`) while `doc/pending.txt` documents
the `g`-prefixed replacements (`g!`, `gd`, `gz`, `gf`).
Solution: update `priority`, `date`, `undo`, and `filter` defaults to
`g!`, `gd`, `gz`, and `gf` respectively.
* feat(s3): create bucket interactively during auth when unconfigured
Problem: when a user runs `:Pending s3 auth` with no bucket configured,
auth succeeds but offers no way to create the bucket. The user must
manually run `aws s3api create-bucket` and update their config.
Solution: add `util.input()` coroutine-aware prompt wrapper and a
`create_bucket()` flow in `s3.lua` that prompts for bucket name and
region, handles the `us-east-1` LocationConstraint quirk, and logs a
config snippet on success. Called automatically from `auth()` when
`sync.s3.bucket` is absent.
* ci: typing
* feat(parse): add `parse_duration_to_days` for duration string conversion
Problem: The archive command accepted only a bare integer for days,
inconsistent with the `+Nd`/`+Nw`/`+Nm` duration syntax used elsewhere.
Solution: Add `parse_duration_to_days()` supporting `Nd`, `Nw`, `Nm`,
and bare integers. Returns nil on invalid input for caller error handling.
* feat(archive): duration syntax and confirmation prompt
Problem: `:Pending archive` accepted only a bare integer for days and
silently deleted tasks with no confirmation, risking accidental data loss.
Solution: Accept duration strings (`7d`, `3w`, `2m`) via
`parse.parse_duration_to_days()`, show a `vim.ui.input` confirmation
prompt before removing tasks, and skip the prompt when zero tasks match.
* feat: add `<C-a>` / `<C-x>` keymaps for priority increment/decrement
Problem: Priority could only be cycled with `g!` (0→1→2→3→0), with no
way to directly increment or decrement.
Solution: Add `adjust_priority()` with clamping at 0 and `max_priority`,
exposed as `increment_priority()` / `decrement_priority()` on `<C-a>` /
`<C-x>`. Includes `<Plug>` mappings and vimdoc.
* fix(s3): use parenthetical defaults in bucket creation prompts
Problem: `util.input` with `default` pre-filled the input field, and
the success message said "Add to your config" ambiguously.
Solution: Show defaults in prompt text as `(default)` instead of
pre-filling, and clarify the message to "Add to your pending.nvim
config".
* ci: format
* ci(sync): normalize log prefix to `backend:` across all sync backends
Problem: Sync log messages used inconsistent prefixes like `s3 push:`,
`gtasks pull:`, `gtasks sync —` instead of the `backend: action` pattern
used by auth messages.
Solution: Normalize all sync backend logs to `backend: action ...` format
across `s3.lua`, `gcal.lua`, and `gtasks.lua`.
* ci: fix linter warnings in archive spec and s3 bucket creation
* feat(s3): create bucket interactively during auth when unconfigured
Problem: when a user runs `:Pending s3 auth` with no bucket configured,
auth succeeds but offers no way to create the bucket. The user must
manually run `aws s3api create-bucket` and update their config.
Solution: add `util.input()` coroutine-aware prompt wrapper and a
`create_bucket()` flow in `s3.lua` that prompts for bucket name and
region, handles the `us-east-1` LocationConstraint quirk, and logs a
config snippet on success. Called automatically from `auth()` when
`sync.s3.bucket` is absent.
* ci: typing
* feat(parse): add `parse_duration_to_days` for duration string conversion
Problem: The archive command accepted only a bare integer for days,
inconsistent with the `+Nd`/`+Nw`/`+Nm` duration syntax used elsewhere.
Solution: Add `parse_duration_to_days()` supporting `Nd`, `Nw`, `Nm`,
and bare integers. Returns nil on invalid input for caller error handling.
* feat(archive): duration syntax and confirmation prompt
Problem: `:Pending archive` accepted only a bare integer for days and
silently deleted tasks with no confirmation, risking accidental data loss.
Solution: Accept duration strings (`7d`, `3w`, `2m`) via
`parse.parse_duration_to_days()`, show a `vim.ui.input` confirmation
prompt before removing tasks, and skip the prompt when zero tasks match.
* feat: add `<C-a>` / `<C-x>` keymaps for priority increment/decrement
Problem: Priority could only be cycled with `g!` (0→1→2→3→0), with no
way to directly increment or decrement.
Solution: Add `adjust_priority()` with clamping at 0 and `max_priority`,
exposed as `increment_priority()` / `decrement_priority()` on `<C-a>` /
`<C-x>`. Includes `<Plug>` mappings and vimdoc.
* fix(s3): use parenthetical defaults in bucket creation prompts
Problem: `util.input` with `default` pre-filled the input field, and
the success message said "Add to your config" ambiguously.
Solution: Show defaults in prompt text as `(default)` instead of
pre-filling, and clarify the message to "Add to your pending.nvim
config".
* ci: format
* feat(s3): create bucket interactively during auth when unconfigured
Problem: when a user runs `:Pending s3 auth` with no bucket configured,
auth succeeds but offers no way to create the bucket. The user must
manually run `aws s3api create-bucket` and update their config.
Solution: add `util.input()` coroutine-aware prompt wrapper and a
`create_bucket()` flow in `s3.lua` that prompts for bucket name and
region, handles the `us-east-1` LocationConstraint quirk, and logs a
config snippet on success. Called automatically from `auth()` when
`sync.s3.bucket` is absent.
* ci: typing
* feat(parse): add `parse_duration_to_days` for duration string conversion
Problem: The archive command accepted only a bare integer for days,
inconsistent with the `+Nd`/`+Nw`/`+Nm` duration syntax used elsewhere.
Solution: Add `parse_duration_to_days()` supporting `Nd`, `Nw`, `Nm`,
and bare integers. Returns nil on invalid input for caller error handling.
* feat(archive): duration syntax and confirmation prompt
Problem: `:Pending archive` accepted only a bare integer for days and
silently deleted tasks with no confirmation, risking accidental data loss.
Solution: Accept duration strings (`7d`, `3w`, `2m`) via
`parse.parse_duration_to_days()`, show a `vim.ui.input` confirmation
prompt before removing tasks, and skip the prompt when zero tasks match.
* feat(s3): create bucket interactively during auth when unconfigured
Problem: when a user runs `:Pending s3 auth` with no bucket configured,
auth succeeds but offers no way to create the bucket. The user must
manually run `aws s3api create-bucket` and update their config.
Solution: add `util.input()` coroutine-aware prompt wrapper and a
`create_bucket()` flow in `s3.lua` that prompts for bucket name and
region, handles the `us-east-1` LocationConstraint quirk, and logs a
config snippet on success. Called automatically from `auth()` when
`sync.s3.bucket` is absent.
* ci: typing
* refactor(types): extract inline anonymous types into named classes
Problem: several functions used inline `{...}` table types in their
`@param` and `@return` annotations, making them hard to read and
impossible to reference from other modules.
Solution: extract each into a named `---@class`: `pending.Metadata`,
`pending.TaskFields`, `pending.CompletionItem`, `pending.SystemResult`,
and `pending.OAuthClientOpts`.
* refactor(sync): extract shared utilities into `sync/util.lua`
Problem: sync epilogue code (`s:save()`, `_recompute_counts()`,
`buffer.render()`) and `fmt_counts` were duplicated across `gcal.lua`
and `gtasks.lua`. The concurrency guard lived in `oauth.lua`, coupling
non-OAuth backends to the OAuth module.
Solution: create `sync/util.lua` with `async`, `system`, `with_guard`,
`finish`, and `fmt_counts`. Delegate from `oauth.lua` and replace
duplicated code in both backends. Add per-backend `auth()` and
`auth_complete()` methods to `gcal.lua` and `gtasks.lua`.
* feat(sync): auto-discover backends, per-backend auth, S3 backend
Problem: sync backends were hardcoded in `SYNC_BACKENDS` list in
`init.lua`, auth routed directly through `oauth.google_client`, and
adding a non-OAuth backend required editing multiple files.
Solution: replace hardcoded list with `discover_backends()` that globs
`lua/pending/sync/*.lua` at runtime. Rewrite `M.auth()` to dispatch
to per-backend `auth()` methods with `vim.ui.select` fallback. Add
`lua/pending/sync/s3.lua` with push/pull/sync via AWS CLI, per-task
merge by `_s3_sync_id` (UUID), and `pending.S3Config` type.
Problem: several functions used inline `{...}` table types in their
`@param` and `@return` annotations, making them hard to read and
impossible to reference from other modules.
Solution: extract each into a named `---@class`: `pending.Metadata`,
`pending.TaskFields`, `pending.CompletionItem`, `pending.SystemResult`,
and `pending.OAuthClientOpts`.
Problem: the task editing surface had gaps — category and recurrence had
no keymaps, `:Pending edit` required knowing the task ID, tasks couldn't
be reordered with a keymap, priority was binary (0/1), and `wip`/`blocked`
states were documented but unimplemented.
Solution: fill every cell so every property is editable in every way.
- `gc`/`gr` keymaps for category select and recurrence prompt
- cursor-aware `:Pending edit` (omit ID to use task under cursor)
- `J`/`K` keymaps to reorder tasks within a category
- multi-level priorities (`max_priority` config, `g!` cycles 0→1→2→3→0)
- `+!!`/`+!!!` tokens in `:Pending edit`, `:Pending add`, `parse.body()`
- `PendingPriority2`/`PendingPriority3` highlight groups
- `gw`/`gb` keymaps toggle `wip`/`blocked` status
- `>`/`=` state chars in buffer rendering and diff parsing
- `PendingWip`/`PendingBlocked` highlight groups
- sort order: wip → pending → blocked → done
- `wip`/`blocked` filter predicates and icons
Problem: The queue view sort order (priority → due → order) is hardcoded
with no documentation of a configurable alternative.
Solution: Document `queue_sort` and `category_sort` config fields with
named presets, sort key syntax, `-` direction prefix, and the `status`
key opt-in for disabling the pending/done split. Update the views
section to reference the new `pending-sort` tag.
Problem: The vimdoc only describes `pending`/`done`/`deleted` statuses
with no mention of work-in-progress or blocked states.
Solution: Document new `wip` and `blocked` statuses across views (sort
order), filters (new predicates), icons (`>` and `=`), highlight groups
(`PendingWip`, `PendingBlocked`), and the data format schema.
Problem: Default buffer-local keys `!`, `D`, `F`, `U` shadow common Vim
builtins (`!` filter, `D` delete-to-eol, `F` reverse-find, `U` line-undo).
Solution: Document new defaults `g!`, `gd`, `gf`, `gz` in the mappings
table, config example, and command references. Add a deprecated-keys
section listing the old-to-new mapping with removal timeline.
Problem: View-related config fields (`default_view`, `eol_format`,
`category_order`, `folding`) are scattered as top-level siblings
alongside unrelated fields like `data_path` and `date_syntax`.
Solution: Group them under a `view` table with per-view sub-tables:
`view.default`, `view.eol_format`, `view.category.order`,
`view.category.folding`, and `view.queue` (empty, ready for #100).
Update all call sites, tests, and vimdoc.
* refactor(buffer): split extmark namespace into `ns_eol` and `ns_inline`
Problem: all extmarks shared a single `pending` namespace, making it
impossible to selectively clear position-sensitive extmarks (overlays,
highlights) while preserving stable EOL virtual text (due dates,
recurrence).
Solution: introduce `ns_eol` for end-of-line virtual text and
`ns_inline` for overlays and highlights. `clear_marks()` and
`apply_extmarks()` operate on both namespaces independently.
* feat(buffer): track line changes via `on_bytes` to keep `_meta` aligned
Problem: `_meta` is a positional array keyed by line number. Line
insertions and deletions during editing desync it from actual buffer
content, breaking `get_fold()`, cursor-based task lookups, and extmark
re-application.
Solution: attach an `on_bytes` callback that adjusts `_meta` on line
insertions/deletions and tracks dirty rows. Remove the manual
`_meta` insert from `open_line()` since `on_bytes` now handles it.
Reset dirty rows on each full render.
* feat(buffer): clear only inline extmarks on dirty rows during edits
Problem: `TextChanged` cleared all extmarks (both namespaces) on every
edit, causing EOL virtual text (due dates, recurrence) to vanish while
the user types.
Solution: replace blanket `clear_marks()` with per-row
`clear_inline_row()` that only removes `ns_inline` extmarks on rows
flagged dirty by `on_bytes`. EOL virtual text is preserved untouched.
* feat(buffer): re-apply inline extmarks after edits
Problem: inline extmarks (checkbox overlays, strikethrough, header
highlights) were cleared during edits and only restored on `:w`,
leaving the buffer visually bare while editing.
Solution: extract `apply_inline_row()` from `apply_extmarks()` and
call it via `reapply_dirty_inline()` on `InsertLeave` and normal-mode
`TextChanged`. Insert-mode `TextChangedI` still only clears inline
marks on dirty rows to avoid overlay flicker while typing.
* fix(buffer): suppress `on_bytes` during render and fix definition order
Problem: `on_bytes` fired during `render()`'s `nvim_buf_set_lines`,
corrupting `_meta` with duplicate entries and causing out-of-range
extmark errors. Also, `apply_inline_row` was defined after its first
caller `reapply_dirty_inline`.
Solution: add `_rendering` guard flag around `nvim_buf_set_lines` in
`render()` so `on_bytes` is a no-op during authoritative renders.
Move `apply_inline_row` above `reapply_dirty_inline` to satisfy Lua
local scoping rules.
* feat(buffer): add configurable `eol_format` for EOL virtual text
Problem: EOL virtual text order (category → recurrence → due) and the
double-space separator are hardcoded in `apply_extmarks()`. Users cannot
reorder, omit, or restyle metadata fields.
Solution: Add `eol_format` config field (default `'%c %r %d'`) with
`%c`, `%r`, `%d` specifiers. `parse_eol_format()` tokenizes the format
string; `build_eol_virt()` resolves specifiers against `LineMeta` and
collapses literals around absent fields.
* ci: format