fix(diff): preserve due/rec when absent from buffer line (#68)

* 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.
This commit is contained in:
Barrett Ruth 2026-03-05 12:46:54 -05:00 committed by GitHub
parent b7ce1c05ec
commit 7fb3289b21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 300 additions and 109 deletions

View file

@ -1,4 +1,5 @@
local config = require('pending.config')
local log = require('pending.log')
local oauth = require('pending.sync.oauth')
local M = {}
@ -248,7 +249,7 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
if task.status == 'deleted' and gtid and list_id then
local err = delete_gtask(access_token, list_id, gtid)
if err then
vim.notify('pending.nvim: gtasks delete failed: ' .. err, vim.log.levels.WARN)
log.warn('gtasks delete failed: ' .. err)
else
if not task._extra then
task._extra = {}
@ -265,7 +266,7 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
if gtid and list_id then
local err = update_gtask(access_token, list_id, gtid, task_to_gtask(task))
if err then
vim.notify('pending.nvim: gtasks update failed: ' .. err, vim.log.levels.WARN)
log.warn('gtasks update failed: ' .. err)
else
updated = updated + 1
end
@ -275,7 +276,7 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
if not err and lid then
local new_id, create_err = create_gtask(access_token, lid, task_to_gtask(task))
if create_err then
vim.notify('pending.nvim: gtasks create failed: ' .. create_err, vim.log.levels.WARN)
log.warn('gtasks create failed: ' .. create_err)
elseif new_id then
if not task._extra then
task._extra = {}
@ -305,10 +306,7 @@ local function pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
for list_name, list_id in pairs(tasklists) do
local items, err = list_gtasks(access_token, list_id)
if err then
vim.notify(
'pending.nvim: error fetching list ' .. list_name .. ': ' .. err,
vim.log.levels.WARN
)
log.warn('error fetching list ' .. list_name .. ': ' .. err)
else
for _, gtask in ipairs(items or {}) do
local local_task = by_gtasks_id[gtask.id]
@ -350,7 +348,7 @@ local function sync_setup()
end
local tasklists, tl_err = get_all_tasklists(access_token)
if tl_err or not tasklists then
vim.notify('pending.nvim: ' .. (tl_err or 'failed to fetch task lists'), vim.log.levels.ERROR)
log.error(tl_err or 'failed to fetch task lists')
return nil
end
local s = require('pending').store()
@ -379,9 +377,7 @@ function M.push()
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
buffer.render(buffer.bufnr())
end
vim.notify(
string.format('pending.nvim: Google Tasks pushed — +%d ~%d -%d', created, updated, deleted)
)
log.info(string.format('Google Tasks pushed — +%d ~%d -%d', created, updated, deleted))
end)
end
@ -402,7 +398,7 @@ function M.pull()
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
buffer.render(buffer.bufnr())
end
vim.notify(string.format('pending.nvim: Google Tasks pulled — +%d ~%d', created, updated))
log.info(string.format('Google Tasks pulled — +%d ~%d', created, updated))
end)
end
@ -425,9 +421,9 @@ function M.sync()
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
buffer.render(buffer.bufnr())
end
vim.notify(
log.info(
string.format(
'pending.nvim: Google Tasks synced — push: +%d ~%d -%d, pull: +%d ~%d',
'Google Tasks synced — push: +%d ~%d -%d, pull: +%d ~%d',
pushed_create,
pushed_update,
pushed_delete,