ci: some fixes
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

This commit is contained in:
Barrett Ruth 2026-03-13 20:38:29 -04:00
parent 2b75843dab
commit e816e6fb7e
3 changed files with 111 additions and 77 deletions

View file

@ -140,9 +140,9 @@ COMMANDS *pending-commands*
:Pending add Work: standup due:tomorrow rec:weekdays
:Pending add Buy milk due:fri +!!
<
Trailing `+!`, `+!!`, or `+!!!` tokens set the priority level (capped
at `max_priority`). If the buffer is currently open it is re-rendered
after the add.
`+!`, `+!!`, or `+!!!` tokens anywhere in the text set the priority
level (capped at `max_priority`). If the buffer is currently open it
is re-rendered after the add.
*:Pending-archive*
:Pending archive [{duration}]
@ -638,8 +638,8 @@ task data.
==============================================================================
INLINE METADATA *pending-metadata*
Metadata tokens may be appended to any task line before saving. Tokens are
parsed from the right and consumed until a non-metadata token is reached.
Metadata tokens may appear anywhere in a task line. On save, tokens are
extracted from any position and the remaining words form the description.
Supported tokens: ~
@ -663,9 +663,8 @@ On `:w`, the description becomes `Buy milk`, the due date is stored as
`2026-03-15` and rendered as right-aligned virtual text, and the task is
placed under the `Errands` category header.
Parsing stops at the first token that is not a recognised metadata token.
Repeated tokens of the same type also stop parsing — only one `due:`, one
`cat:`, and one `rec:` per task line are consumed.
Only the first occurrence of each metadata type is consumed — duplicate
tokens are silently dropped.
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

@ -536,7 +536,6 @@ function M.body(text)
end
local metadata = {}
local i = #tokens
local ck = category_key()
local dk = date_key()
local rk = recur_key()
@ -544,84 +543,82 @@ function M.body(text)
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d[T%d:]*)$'
local date_pattern_any = '^' .. vim.pesc(dk) .. ':(.+)$'
local rec_pattern = '^' .. vim.pesc(rk) .. ':(%S+)$'
local forge_indices = {}
local desc_tokens = {}
local forge_tokens = {}
for _, token in ipairs(tokens) do
local consumed = false
while i >= 1 do
local token = tokens[i]
local due_val = token:match(date_pattern_strict)
if due_val then
if metadata.due then
break
if due_val and is_valid_datetime(due_val) then
if not metadata.due then
metadata.due = due_val
end
if not is_valid_datetime(due_val) then
break
end
metadata.due = due_val
i = i - 1
else
consumed = true
end
if not consumed then
local raw_val = token:match(date_pattern_any)
if raw_val then
if metadata.due then
break
end
local resolved = M.resolve_date(raw_val)
if not resolved then
break
end
metadata.due = resolved
i = i - 1
else
local cat_val = token:match(cat_pattern)
if cat_val then
if metadata.category then
break
if resolved then
if not metadata.due then
metadata.due = resolved
end
consumed = true
end
end
end
if not consumed then
local cat_val = token:match(cat_pattern)
if cat_val then
if not metadata.category then
metadata.category = cat_val
i = i - 1
else
local pri_bangs = token:match('^%+(!+)$')
if pri_bangs then
if metadata.priority then
break
end
local max = config.get().max_priority or 3
metadata.priority = math.min(#pri_bangs, max)
i = i - 1
else
local rec_val = token:match(rec_pattern)
if rec_val then
if metadata.recur then
break
end
local recur = require('pending.recur')
local raw_spec = rec_val
if raw_spec:sub(1, 1) == '!' then
metadata.recur_mode = 'completion'
raw_spec = raw_spec:sub(2)
end
if not recur.validate(raw_spec) then
break
end
metadata.recur = raw_spec
i = i - 1
elseif forge.parse_ref(token) then
table.insert(forge_indices, i)
i = i - 1
else
break
end
end
end
consumed = true
end
end
if not consumed then
local pri_bangs = token:match('^%+(!+)$')
if pri_bangs then
if not metadata.priority then
local max = config.get().max_priority or 3
metadata.priority = math.min(#pri_bangs, max)
end
consumed = true
end
end
if not consumed then
local rec_val = token:match(rec_pattern)
if rec_val then
local recur = require('pending.recur')
local raw_spec = rec_val
if raw_spec:sub(1, 1) == '!' then
raw_spec = raw_spec:sub(2)
end
if recur.validate(raw_spec) then
if not metadata.recur then
metadata.recur_mode = rec_val:sub(1, 1) == '!' and 'completion' or nil
metadata.recur = raw_spec
end
consumed = true
end
end
end
if not consumed then
if forge.parse_ref(token) then
table.insert(forge_tokens, token)
else
table.insert(desc_tokens, token)
end
end
end
local desc_tokens = {}
for j = 1, i do
table.insert(desc_tokens, tokens[j])
end
for fi = #forge_indices, 1, -1 do
table.insert(desc_tokens, tokens[forge_indices[fi]])
for _, ft in ipairs(forge_tokens) do
table.insert(desc_tokens, ft)
end
local description = table.concat(desc_tokens, ' ')

View file

@ -48,10 +48,16 @@ describe('parse', function()
assert.are.equal('Errands', meta.category)
end)
it('stops at duplicate key', function()
it('first occurrence wins for duplicate keys', function()
local desc, meta = parse.body('Buy milk due:2026-03-15 due:2026-04-01')
assert.are.equal('Buy milk due:2026-03-15', desc)
assert.are.equal('2026-04-01', meta.due)
assert.are.equal('Buy milk', desc)
assert.are.equal('2026-03-15', meta.due)
end)
it('drops identical duplicate metadata tokens', function()
local desc, meta = parse.body('Buy milk due:tomorrow due:tomorrow')
assert.are.equal('Buy milk', desc)
assert.are.equal(os.date('%Y-%m-%d', os.time() + 86400), meta.due)
end)
it('stops at non-meta token', function()
@ -138,6 +144,38 @@ describe('parse', function()
assert.are.equal('Work', meta.category)
assert.truthy(desc:find('gl:a/b#12', 1, true))
end)
it('extracts leading metadata', function()
local desc, meta = parse.body('due:2026-03-15 Fix the bug')
assert.are.equal('Fix the bug', desc)
assert.are.equal('2026-03-15', meta.due)
end)
it('extracts metadata from the middle', function()
local desc, meta = parse.body('Fix due:2026-03-15 the bug')
assert.are.equal('Fix the bug', desc)
assert.are.equal('2026-03-15', meta.due)
end)
it('extracts multiple metadata from any position', function()
local desc, meta = parse.body('cat:Work Fix due:2026-03-15 the bug')
assert.are.equal('Fix the bug', desc)
assert.are.equal('2026-03-15', meta.due)
assert.are.equal('Work', meta.category)
end)
it('extracts all metadata types from mixed positions', function()
local today = os.date('*t') --[[@as osdate]]
local tomorrow = os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = today.day + 1 })
)
local desc, meta = parse.body('due:tomorrow cat:Work Fix the bug +!')
assert.are.equal('Fix the bug', desc)
assert.are.equal(tomorrow, meta.due)
assert.are.equal('Work', meta.category)
assert.are.equal(1, meta.priority)
end)
end)
describe('parse.resolve_date', function()