diff --git a/doc/pending.txt b/doc/pending.txt index 7026922..3052d15 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -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 `` to see diff --git a/lua/pending/parse.lua b/lua/pending/parse.lua index 1b36578..9fd179e 100644 --- a/lua/pending/parse.lua +++ b/lua/pending/parse.lua @@ -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, ' ') diff --git a/spec/parse_spec.lua b/spec/parse_spec.lua index aebe0c7..e02f1dc 100644 --- a/spec/parse_spec.lua +++ b/spec/parse_spec.lua @@ -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()