fix(parse): position-independent inline metadata parsing (#164)
Some checks are pending
quality / changes (push) Waiting to run
quality / Lua Format Check (push) Blocked by required conditions
quality / Lua Lint Check (push) Blocked by required conditions
quality / Lua Type Check (push) Blocked by required conditions
quality / Markdown Format Check (push) Blocked by required conditions
test / Test (Neovim nightly) (push) Waiting to run
test / Test (Neovim stable) (push) Waiting to run

Problem: `parse.body()` scanned tokens right-to-left and broke on the
first non-metadata token, so metadata only worked at the trailing end
of a line. `due:tomorrow Fix the bug` silently failed to parse the
due date.

Solution: Replace the right-to-left `while` loop with a single
left-to-right pass that extracts metadata tokens from any position.
Duplicate metadata tokens are dropped with a `log.warn`. Update docs
and tests accordingly.
This commit is contained in:
Barrett Ruth 2026-03-13 20:48:18 -04:00
parent 6e220797b9
commit f3ef1ca0db
3 changed files with 33 additions and 3 deletions

View file

@ -664,7 +664,7 @@ On `:w`, the description becomes `Buy milk`, the due date is stored as
placed under the `Errands` category header.
Only the first occurrence of each metadata type is consumed — duplicate
tokens are silently dropped.
tokens are dropped with a warning.
Omnifunc completion is available for `due:`, `cat:`, and `rec:` token types.
In insert mode, type the token prefix and press `<C-x><C-o>` to see

View file

@ -1,5 +1,6 @@
local config = require('pending.config')
local forge = require('pending.forge')
local log = require('pending.log')
---@class pending.Metadata
---@field due? string
@ -553,6 +554,8 @@ function M.body(text)
if due_val and is_valid_datetime(due_val) then
if not metadata.due then
metadata.due = due_val
else
log.warn('duplicate ' .. dk .. ': token ignored: ' .. token)
end
consumed = true
end
@ -563,6 +566,8 @@ function M.body(text)
if resolved then
if not metadata.due then
metadata.due = resolved
else
log.warn('duplicate ' .. dk .. ': token ignored: ' .. token)
end
consumed = true
end
@ -574,6 +579,8 @@ function M.body(text)
if cat_val then
if not metadata.category then
metadata.category = cat_val
else
log.warn('duplicate ' .. ck .. ': token ignored: ' .. token)
end
consumed = true
end
@ -585,6 +592,8 @@ function M.body(text)
if not metadata.priority then
local max = config.get().max_priority or 3
metadata.priority = math.min(#pri_bangs, max)
else
log.warn('duplicate priority token ignored: ' .. token)
end
consumed = true
end
@ -602,6 +611,8 @@ function M.body(text)
if not metadata.recur then
metadata.recur_mode = rec_val:sub(1, 1) == '!' and 'completion' or nil
metadata.recur = raw_spec
else
log.warn('duplicate ' .. rk .. ': token ignored: ' .. token)
end
consumed = true
end

View file

@ -48,16 +48,35 @@ describe('parse', function()
assert.are.equal('Errands', meta.category)
end)
it('first occurrence wins for duplicate keys', function()
it('first occurrence wins for duplicate keys and warns', function()
local warnings = {}
local orig = vim.notify
vim.notify = function(m, level)
if level == vim.log.levels.WARN then
table.insert(warnings, m)
end
end
local desc, meta = parse.body('Buy milk due:2026-03-15 due:2026-04-01')
vim.notify = orig
assert.are.equal('Buy milk', desc)
assert.are.equal('2026-03-15', meta.due)
assert.are.equal(1, #warnings)
assert.truthy(warnings[1]:find('duplicate', 1, true))
end)
it('drops identical duplicate metadata tokens', function()
it('drops identical duplicate metadata tokens and warns', function()
local warnings = {}
local orig = vim.notify
vim.notify = function(m, level)
if level == vim.log.levels.WARN then
table.insert(warnings, m)
end
end
local desc, meta = parse.body('Buy milk due:tomorrow due:tomorrow')
vim.notify = orig
assert.are.equal('Buy milk', desc)
assert.are.equal(os.date('%Y-%m-%d', os.time() + 86400), meta.due)
assert.are.equal(1, #warnings)
end)
it('stops at non-meta token', function()