refactor: tighten LuaCATS annotations and canonicalize metadata fields (#141)
* refactor: tighten LuaCATS annotations across modules Problem: type annotations repeated inline unions with no aliases, used `table<string, any>` where structured types exist, and had loose `string` where union types should be used. Solution: add `pending.TaskStatus`, `pending.RecurMode`, `pending.TaskExtra`, `pending.ForgeType`, `pending.ForgeState`, `pending.ForgeAuthStatus` aliases and `pending.SyncBackend` interface. Replace inline unions and loose types with the new aliases in `store.lua`, `forge.lua`, `config.lua`, `diff.lua`, `views.lua`, `parse.lua`, `init.lua`, and `oauth.lua`. * refactor: canonicalize internal metadata field names Problem: `pending.Metadata` used shorthand field names (`cat`, `rec`, `rec_mode`) matching user-facing token syntax, coupling internal representation to config. `RecurSpec.from_completion` used a boolean where a `pending.RecurMode` alias exists. `category_syntax` was hardcoded to `'cat'` with no config option. Solution: rename `Metadata` fields to `category`/`recur`/`recur_mode`, add `category_syntax` config option (default `'cat'`), rename `ParsedEntry` fields to match, replace `RecurSpec.from_completion` with `mode: pending.RecurMode`, and restore `[string]` indexer on `pending.ForgeConfig` alongside explicit fields.
This commit is contained in:
parent
5c53adb3ec
commit
d26bdcb3a8
16 changed files with 144 additions and 80 deletions
|
|
@ -605,9 +605,10 @@ Supported tokens: ~
|
||||||
`cat:Name` Move the task to the named category on save.
|
`cat:Name` Move the task to the named category on save.
|
||||||
`rec:<pattern>` Set a recurrence rule (see |pending-recurrence|).
|
`rec:<pattern>` Set a recurrence rule (see |pending-recurrence|).
|
||||||
|
|
||||||
The token name for due dates defaults to `due` and is configurable via
|
The token name for categories defaults to `cat` and is configurable via
|
||||||
`date_syntax` in |pending-config|. The token name for recurrence defaults to
|
`category_syntax` in |pending-config|. The token name for due dates defaults
|
||||||
`rec` and is configurable via `recur_syntax`.
|
to `due` and is configurable via `date_syntax`. The token name for recurrence
|
||||||
|
defaults to `rec` and is configurable via `recur_syntax`.
|
||||||
|
|
||||||
Example: >
|
Example: >
|
||||||
|
|
||||||
|
|
@ -734,6 +735,7 @@ loads: >lua
|
||||||
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
|
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
|
||||||
default_category = 'Todo',
|
default_category = 'Todo',
|
||||||
date_format = '%b %d',
|
date_format = '%b %d',
|
||||||
|
category_syntax = 'cat',
|
||||||
date_syntax = 'due',
|
date_syntax = 'due',
|
||||||
recur_syntax = 'rec',
|
recur_syntax = 'rec',
|
||||||
someday_date = '9999-12-30',
|
someday_date = '9999-12-30',
|
||||||
|
|
@ -817,6 +819,12 @@ Fields: ~
|
||||||
'%m/%d', -- 03/15 (year inferred)
|
'%m/%d', -- 03/15 (year inferred)
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
{category_syntax} (string, default: 'cat')
|
||||||
|
The token name for inline category metadata. Change
|
||||||
|
this to use a different keyword, for example
|
||||||
|
`'category'` to write `category:Work` instead of
|
||||||
|
`cat:Work`.
|
||||||
|
|
||||||
{date_syntax} (string, default: 'due')
|
{date_syntax} (string, default: 'due')
|
||||||
The token name for inline due-date metadata. Change
|
The token name for inline due-date metadata. Change
|
||||||
this to use a different keyword, for example `'by'`
|
this to use a different keyword, for example `'by'`
|
||||||
|
|
|
||||||
|
|
@ -136,9 +136,11 @@ function M.omnifunc(findstart, base)
|
||||||
local dk = date_key()
|
local dk = date_key()
|
||||||
local rk = recur_key()
|
local rk = recur_key()
|
||||||
|
|
||||||
|
local ck = config.get().category_syntax or 'cat'
|
||||||
|
|
||||||
local checks = {
|
local checks = {
|
||||||
{ vim.pesc(dk) .. ':([%S]*)$', dk },
|
{ vim.pesc(dk) .. ':([%S]*)$', dk },
|
||||||
{ 'cat:([%S]*)$', 'cat' },
|
{ vim.pesc(ck) .. ':([%S]*)$', ck },
|
||||||
{ vim.pesc(rk) .. ':([%S]*)$', rk },
|
{ vim.pesc(rk) .. ':([%S]*)$', rk },
|
||||||
}
|
}
|
||||||
for _, b in ipairs(forge.backends()) do
|
for _, b in ipairs(forge.backends()) do
|
||||||
|
|
@ -172,10 +174,10 @@ function M.omnifunc(findstart, base)
|
||||||
table.insert(matches, { word = c.word, menu = '[' .. source .. ']', info = c.info })
|
table.insert(matches, { word = c.word, menu = '[' .. source .. ']', info = c.info })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif source == 'cat' then
|
elseif source == (config.get().category_syntax or 'cat') then
|
||||||
for _, c in ipairs(get_categories()) do
|
for _, c in ipairs(get_categories()) do
|
||||||
if base == '' or c:sub(1, #base) == base then
|
if base == '' or c:sub(1, #base) == base then
|
||||||
table.insert(matches, { word = c, menu = '[cat]' })
|
table.insert(matches, { word = c, menu = '[' .. source .. ']' })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif source == rk then
|
elseif source == rk then
|
||||||
|
|
@ -190,7 +192,7 @@ function M.omnifunc(findstart, base)
|
||||||
local seen = {}
|
local seen = {}
|
||||||
for _, task in ipairs(s:tasks()) do
|
for _, task in ipairs(s:tasks()) do
|
||||||
if task._extra and task._extra._forge_ref then
|
if task._extra and task._extra._forge_ref then
|
||||||
local ref = task._extra._forge_ref
|
local ref = task._extra._forge_ref --[[@as pending.ForgeRef]]
|
||||||
local key = ref.owner .. '/' .. ref.repo
|
local key = ref.owner .. '/' .. ref.repo
|
||||||
if not seen[key] then
|
if not seen[key] then
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@
|
||||||
---@field close? boolean
|
---@field close? boolean
|
||||||
---@field validate? boolean
|
---@field validate? boolean
|
||||||
---@field warn_missing_cli? boolean
|
---@field warn_missing_cli? boolean
|
||||||
|
---@field github? pending.ForgeInstanceConfig
|
||||||
|
---@field gitlab? pending.ForgeInstanceConfig
|
||||||
|
---@field codeberg? pending.ForgeInstanceConfig
|
||||||
---@field [string] pending.ForgeInstanceConfig
|
---@field [string] pending.ForgeInstanceConfig
|
||||||
|
|
||||||
---@class pending.SyncConfig
|
---@class pending.SyncConfig
|
||||||
|
|
@ -92,6 +95,7 @@
|
||||||
---@field data_path string
|
---@field data_path string
|
||||||
---@field default_category string
|
---@field default_category string
|
||||||
---@field date_format string
|
---@field date_format string
|
||||||
|
---@field category_syntax string
|
||||||
---@field date_syntax string
|
---@field date_syntax string
|
||||||
---@field recur_syntax string
|
---@field recur_syntax string
|
||||||
---@field someday_date string
|
---@field someday_date string
|
||||||
|
|
@ -113,6 +117,7 @@ local defaults = {
|
||||||
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
|
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
|
||||||
default_category = 'Todo',
|
default_category = 'Todo',
|
||||||
date_format = '%b %d',
|
date_format = '%b %d',
|
||||||
|
category_syntax = 'cat',
|
||||||
date_syntax = 'due',
|
date_syntax = 'due',
|
||||||
recur_syntax = 'rec',
|
recur_syntax = 'rec',
|
||||||
someday_date = '9999-12-30',
|
someday_date = '9999-12-30',
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ local parse = require('pending.parse')
|
||||||
---@field id? integer
|
---@field id? integer
|
||||||
---@field description? string
|
---@field description? string
|
||||||
---@field priority? integer
|
---@field priority? integer
|
||||||
---@field status? string
|
---@field status? pending.TaskStatus
|
||||||
---@field category? string
|
---@field category? string
|
||||||
---@field due? string
|
---@field due? string
|
||||||
---@field rec? string
|
---@field recur? string
|
||||||
---@field rec_mode? string
|
---@field recur_mode? pending.RecurMode
|
||||||
---@field forge_ref? pending.ForgeRef
|
---@field forge_ref? pending.ForgeRef
|
||||||
---@field lnum integer
|
---@field lnum integer
|
||||||
|
|
||||||
|
|
@ -65,10 +65,10 @@ function M.parse_buffer(lines)
|
||||||
description = description,
|
description = description,
|
||||||
priority = priority,
|
priority = priority,
|
||||||
status = status,
|
status = status,
|
||||||
category = metadata.cat or current_category or config.get().default_category,
|
category = metadata.category or current_category or config.get().default_category,
|
||||||
due = metadata.due,
|
due = metadata.due,
|
||||||
rec = metadata.rec,
|
recur = metadata.recur,
|
||||||
rec_mode = metadata.rec_mode,
|
recur_mode = metadata.recur_mode,
|
||||||
forge_ref = forge_ref,
|
forge_ref = forge_ref,
|
||||||
lnum = i,
|
lnum = i,
|
||||||
})
|
})
|
||||||
|
|
@ -126,8 +126,8 @@ function M.apply(lines, s, hidden_ids)
|
||||||
category = entry.category,
|
category = entry.category,
|
||||||
priority = entry.priority,
|
priority = entry.priority,
|
||||||
due = entry.due,
|
due = entry.due,
|
||||||
recur = entry.rec,
|
recur = entry.recur,
|
||||||
recur_mode = entry.rec_mode,
|
recur_mode = entry.recur_mode,
|
||||||
order = order_counter,
|
order = order_counter,
|
||||||
_extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil,
|
_extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil,
|
||||||
})
|
})
|
||||||
|
|
@ -157,13 +157,13 @@ function M.apply(lines, s, hidden_ids)
|
||||||
task.due = entry.due
|
task.due = entry.due
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
if entry.rec ~= nil then
|
if entry.recur ~= nil then
|
||||||
if task.recur ~= entry.rec then
|
if task.recur ~= entry.recur then
|
||||||
task.recur = entry.rec
|
task.recur = entry.recur
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
if task.recur_mode ~= entry.rec_mode then
|
if task.recur_mode ~= entry.recur_mode then
|
||||||
task.recur_mode = entry.rec_mode
|
task.recur_mode = entry.recur_mode
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -201,8 +201,8 @@ function M.apply(lines, s, hidden_ids)
|
||||||
category = entry.category,
|
category = entry.category,
|
||||||
priority = entry.priority,
|
priority = entry.priority,
|
||||||
due = entry.due,
|
due = entry.due,
|
||||||
recur = entry.rec,
|
recur = entry.recur,
|
||||||
recur_mode = entry.rec_mode,
|
recur_mode = entry.recur_mode,
|
||||||
order = order_counter,
|
order = order_counter,
|
||||||
_extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil,
|
_extra = entry.forge_ref and { _forge_ref = entry.forge_ref } or nil,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
local config = require('pending.config')
|
local config = require('pending.config')
|
||||||
local log = require('pending.log')
|
local log = require('pending.log')
|
||||||
|
|
||||||
|
---@alias pending.ForgeType 'issue'|'pull_request'|'merge_request'|'repo'
|
||||||
|
---@alias pending.ForgeState 'open'|'closed'|'merged'
|
||||||
|
---@alias pending.ForgeAuthStatus 'unknown'|'ok'|'failed'
|
||||||
|
|
||||||
---@class pending.ForgeRef
|
---@class pending.ForgeRef
|
||||||
---@field forge string
|
---@field forge string
|
||||||
---@field owner string
|
---@field owner string
|
||||||
---@field repo string
|
---@field repo string
|
||||||
---@field type 'issue'|'pull_request'|'merge_request'|'repo'
|
---@field type pending.ForgeType
|
||||||
---@field number? integer
|
---@field number? integer
|
||||||
---@field url string
|
---@field url string
|
||||||
|
|
||||||
---@class pending.ForgeCache
|
---@class pending.ForgeCache
|
||||||
---@field title? string
|
---@field title? string
|
||||||
---@field state 'open'|'closed'|'merged'
|
---@field state pending.ForgeState
|
||||||
---@field labels? string[]
|
---@field labels? string[]
|
||||||
---@field fetched_at string
|
---@field fetched_at string
|
||||||
|
|
||||||
|
|
@ -27,10 +31,10 @@ local log = require('pending.log')
|
||||||
---@field auth_status_args string[]
|
---@field auth_status_args string[]
|
||||||
---@field default_icon string
|
---@field default_icon string
|
||||||
---@field default_issue_format string
|
---@field default_issue_format string
|
||||||
---@field _auth? 'unknown'|'ok'|'failed'
|
---@field _auth? pending.ForgeAuthStatus
|
||||||
---@field parse_url fun(self: pending.ForgeBackend, url: string): pending.ForgeRef?
|
---@field parse_url fun(self: pending.ForgeBackend, url: string): pending.ForgeRef?
|
||||||
---@field api_args fun(self: pending.ForgeBackend, ref: pending.ForgeRef): string[]
|
---@field api_args fun(self: pending.ForgeBackend, ref: pending.ForgeRef): string[]
|
||||||
---@field parse_state fun(self: pending.ForgeBackend, decoded: table): 'open'|'closed'|'merged'
|
---@field parse_state fun(self: pending.ForgeBackend, decoded: table): pending.ForgeState
|
||||||
|
|
||||||
---@class pending.forge
|
---@class pending.forge
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
|
||||||
|
|
@ -984,10 +984,10 @@ function M.add(text)
|
||||||
end
|
end
|
||||||
s:add({
|
s:add({
|
||||||
description = description,
|
description = description,
|
||||||
category = metadata.cat,
|
category = metadata.category,
|
||||||
due = metadata.due,
|
due = metadata.due,
|
||||||
recur = metadata.rec,
|
recur = metadata.recur,
|
||||||
recur_mode = metadata.rec_mode,
|
recur_mode = metadata.recur_mode,
|
||||||
priority = metadata.priority,
|
priority = metadata.priority,
|
||||||
})
|
})
|
||||||
_save_and_notify()
|
_save_and_notify()
|
||||||
|
|
@ -998,6 +998,14 @@ function M.add(text)
|
||||||
log.info('Task added: ' .. description)
|
log.info('Task added: ' .. description)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class pending.SyncBackend
|
||||||
|
---@field name string
|
||||||
|
---@field auth fun(): nil
|
||||||
|
---@field push? fun(): nil
|
||||||
|
---@field pull? fun(): nil
|
||||||
|
---@field sync? fun(): nil
|
||||||
|
---@field health? fun(): nil
|
||||||
|
|
||||||
---@type string[]?
|
---@type string[]?
|
||||||
local _sync_backends = nil
|
local _sync_backends = nil
|
||||||
|
|
||||||
|
|
@ -1186,6 +1194,7 @@ end
|
||||||
local function parse_edit_token(token)
|
local function parse_edit_token(token)
|
||||||
local recur = require('pending.recur')
|
local recur = require('pending.recur')
|
||||||
local cfg = require('pending.config').get()
|
local cfg = require('pending.config').get()
|
||||||
|
local ck = cfg.category_syntax or 'cat'
|
||||||
local dk = cfg.date_syntax or 'due'
|
local dk = cfg.date_syntax or 'due'
|
||||||
local rk = cfg.recur_syntax or 'rec'
|
local rk = cfg.recur_syntax or 'rec'
|
||||||
|
|
||||||
|
|
@ -1201,7 +1210,7 @@ local function parse_edit_token(token)
|
||||||
if token == '-due' or token == '-' .. dk then
|
if token == '-due' or token == '-' .. dk then
|
||||||
return 'due', vim.NIL, nil
|
return 'due', vim.NIL, nil
|
||||||
end
|
end
|
||||||
if token == '-cat' then
|
if token == '-' .. ck then
|
||||||
return 'category', vim.NIL, nil
|
return 'category', vim.NIL, nil
|
||||||
end
|
end
|
||||||
if token == '-rec' or token == '-' .. rk then
|
if token == '-rec' or token == '-' .. rk then
|
||||||
|
|
@ -1223,7 +1232,7 @@ local function parse_edit_token(token)
|
||||||
'Invalid date: ' .. due_val .. '. Use YYYY-MM-DD, today, tomorrow, +Nd, weekday names, etc.'
|
'Invalid date: ' .. due_val .. '. Use YYYY-MM-DD, today, tomorrow, +Nd, weekday names, etc.'
|
||||||
end
|
end
|
||||||
|
|
||||||
local cat_val = token:match('^cat:(.+)$')
|
local cat_val = token:match('^' .. vim.pesc(ck) .. ':(.+)$')
|
||||||
if cat_val then
|
if cat_val then
|
||||||
return 'category', cat_val, nil
|
return 'category', cat_val, nil
|
||||||
end
|
end
|
||||||
|
|
@ -1248,11 +1257,15 @@ local function parse_edit_token(token)
|
||||||
.. token
|
.. token
|
||||||
.. '. Valid: '
|
.. '. Valid: '
|
||||||
.. dk
|
.. dk
|
||||||
.. ':<date>, cat:<name>, '
|
.. ':<date>, '
|
||||||
|
.. ck
|
||||||
|
.. ':<name>, '
|
||||||
.. rk
|
.. rk
|
||||||
.. ':<pattern>, +!, -!, -'
|
.. ':<pattern>, +!, -!, -'
|
||||||
.. dk
|
.. dk
|
||||||
.. ', -cat, -'
|
.. ', -'
|
||||||
|
.. ck
|
||||||
|
.. ', -'
|
||||||
.. rk
|
.. rk
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ local config = require('pending.config')
|
||||||
|
|
||||||
---@class pending.Metadata
|
---@class pending.Metadata
|
||||||
---@field due? string
|
---@field due? string
|
||||||
---@field cat? string
|
---@field category? string
|
||||||
---@field rec? string
|
---@field recur? string
|
||||||
---@field rec_mode? 'scheduled'|'completion'
|
---@field recur_mode? pending.RecurMode
|
||||||
---@field priority? integer
|
---@field priority? integer
|
||||||
|
|
||||||
---@class pending.parse
|
---@class pending.parse
|
||||||
|
|
@ -107,6 +107,11 @@ local function is_valid_datetime(s)
|
||||||
return is_valid_date(date_part) and is_valid_time(time_part)
|
return is_valid_date(date_part) and is_valid_time(time_part)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
local function category_key()
|
||||||
|
return config.get().category_syntax or 'cat'
|
||||||
|
end
|
||||||
|
|
||||||
---@return string
|
---@return string
|
||||||
local function date_key()
|
local function date_key()
|
||||||
return config.get().date_syntax or 'due'
|
return config.get().date_syntax or 'due'
|
||||||
|
|
@ -531,8 +536,10 @@ function M.body(text)
|
||||||
|
|
||||||
local metadata = {}
|
local metadata = {}
|
||||||
local i = #tokens
|
local i = #tokens
|
||||||
|
local ck = category_key()
|
||||||
local dk = date_key()
|
local dk = date_key()
|
||||||
local rk = recur_key()
|
local rk = recur_key()
|
||||||
|
local cat_pattern = '^' .. vim.pesc(ck) .. ':(%S+)$'
|
||||||
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d[T%d:]*)$'
|
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 date_pattern_any = '^' .. vim.pesc(dk) .. ':(.+)$'
|
||||||
local rec_pattern = '^' .. vim.pesc(rk) .. ':(%S+)$'
|
local rec_pattern = '^' .. vim.pesc(rk) .. ':(%S+)$'
|
||||||
|
|
@ -562,12 +569,12 @@ function M.body(text)
|
||||||
metadata.due = resolved
|
metadata.due = resolved
|
||||||
i = i - 1
|
i = i - 1
|
||||||
else
|
else
|
||||||
local cat_val = token:match('^cat:(%S+)$')
|
local cat_val = token:match(cat_pattern)
|
||||||
if cat_val then
|
if cat_val then
|
||||||
if metadata.cat then
|
if metadata.category then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
metadata.cat = cat_val
|
metadata.category = cat_val
|
||||||
i = i - 1
|
i = i - 1
|
||||||
else
|
else
|
||||||
local pri_bangs = token:match('^%+(!+)$')
|
local pri_bangs = token:match('^%+(!+)$')
|
||||||
|
|
@ -581,19 +588,19 @@ function M.body(text)
|
||||||
else
|
else
|
||||||
local rec_val = token:match(rec_pattern)
|
local rec_val = token:match(rec_pattern)
|
||||||
if rec_val then
|
if rec_val then
|
||||||
if metadata.rec then
|
if metadata.recur then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
local recur = require('pending.recur')
|
local recur = require('pending.recur')
|
||||||
local raw_spec = rec_val
|
local raw_spec = rec_val
|
||||||
if raw_spec:sub(1, 1) == '!' then
|
if raw_spec:sub(1, 1) == '!' then
|
||||||
metadata.rec_mode = 'completion'
|
metadata.recur_mode = 'completion'
|
||||||
raw_spec = raw_spec:sub(2)
|
raw_spec = raw_spec:sub(2)
|
||||||
end
|
end
|
||||||
if not recur.validate(raw_spec) then
|
if not recur.validate(raw_spec) then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
metadata.rec = raw_spec
|
metadata.recur = raw_spec
|
||||||
i = i - 1
|
i = i - 1
|
||||||
else
|
else
|
||||||
break
|
break
|
||||||
|
|
@ -624,7 +631,7 @@ function M.command_add(text)
|
||||||
local rest = text:sub(#cat_prefix + 2):match('^%s*(.+)$')
|
local rest = text:sub(#cat_prefix + 2):match('^%s*(.+)$')
|
||||||
if rest then
|
if rest then
|
||||||
local desc, meta = M.body(rest)
|
local desc, meta = M.body(rest)
|
||||||
meta.cat = meta.cat or cat_prefix
|
meta.category = meta.category or cat_prefix
|
||||||
return desc, meta
|
return desc, meta
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
---@field freq 'daily'|'weekly'|'monthly'|'yearly'
|
---@field freq 'daily'|'weekly'|'monthly'|'yearly'
|
||||||
---@field interval integer
|
---@field interval integer
|
||||||
---@field byday? string[]
|
---@field byday? string[]
|
||||||
---@field from_completion boolean
|
---@field mode pending.RecurMode
|
||||||
---@field _raw? string
|
---@field _raw? string
|
||||||
|
|
||||||
---@class pending.recur
|
---@class pending.recur
|
||||||
|
|
@ -10,29 +10,29 @@ local M = {}
|
||||||
|
|
||||||
---@type table<string, pending.RecurSpec>
|
---@type table<string, pending.RecurSpec>
|
||||||
local named = {
|
local named = {
|
||||||
daily = { freq = 'daily', interval = 1, from_completion = false },
|
daily = { freq = 'daily', interval = 1, mode = 'scheduled' },
|
||||||
weekdays = {
|
weekdays = {
|
||||||
freq = 'weekly',
|
freq = 'weekly',
|
||||||
interval = 1,
|
interval = 1,
|
||||||
byday = { 'MO', 'TU', 'WE', 'TH', 'FR' },
|
byday = { 'MO', 'TU', 'WE', 'TH', 'FR' },
|
||||||
from_completion = false,
|
mode = 'scheduled',
|
||||||
},
|
},
|
||||||
weekly = { freq = 'weekly', interval = 1, from_completion = false },
|
weekly = { freq = 'weekly', interval = 1, mode = 'scheduled' },
|
||||||
biweekly = { freq = 'weekly', interval = 2, from_completion = false },
|
biweekly = { freq = 'weekly', interval = 2, mode = 'scheduled' },
|
||||||
monthly = { freq = 'monthly', interval = 1, from_completion = false },
|
monthly = { freq = 'monthly', interval = 1, mode = 'scheduled' },
|
||||||
quarterly = { freq = 'monthly', interval = 3, from_completion = false },
|
quarterly = { freq = 'monthly', interval = 3, mode = 'scheduled' },
|
||||||
yearly = { freq = 'yearly', interval = 1, from_completion = false },
|
yearly = { freq = 'yearly', interval = 1, mode = 'scheduled' },
|
||||||
annual = { freq = 'yearly', interval = 1, from_completion = false },
|
annual = { freq = 'yearly', interval = 1, mode = 'scheduled' },
|
||||||
}
|
}
|
||||||
|
|
||||||
---@param spec string
|
---@param spec string
|
||||||
---@return pending.RecurSpec?
|
---@return pending.RecurSpec?
|
||||||
function M.parse(spec)
|
function M.parse(spec)
|
||||||
local from_completion = false
|
local mode = 'scheduled' ---@type pending.RecurMode
|
||||||
local s = spec
|
local s = spec
|
||||||
|
|
||||||
if s:sub(1, 1) == '!' then
|
if s:sub(1, 1) == '!' then
|
||||||
from_completion = true
|
mode = 'completion'
|
||||||
s = s:sub(2)
|
s = s:sub(2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ function M.parse(spec)
|
||||||
freq = base.freq,
|
freq = base.freq,
|
||||||
interval = base.interval,
|
interval = base.interval,
|
||||||
byday = base.byday,
|
byday = base.byday,
|
||||||
from_completion = from_completion,
|
mode = mode,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ function M.parse(spec)
|
||||||
return {
|
return {
|
||||||
freq = freq_map[unit],
|
freq = freq_map[unit],
|
||||||
interval = num,
|
interval = num,
|
||||||
from_completion = from_completion,
|
mode = mode,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -66,7 +66,7 @@ function M.parse(spec)
|
||||||
return {
|
return {
|
||||||
freq = 'daily',
|
freq = 'daily',
|
||||||
interval = 1,
|
interval = 1,
|
||||||
from_completion = from_completion,
|
mode = mode,
|
||||||
_raw = s,
|
_raw = s,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
@ -134,7 +134,7 @@ end
|
||||||
|
|
||||||
---@param base_date string
|
---@param base_date string
|
||||||
---@param spec string
|
---@param spec string
|
||||||
---@param mode 'scheduled'|'completion'
|
---@param mode pending.RecurMode
|
||||||
---@return string
|
---@return string
|
||||||
function M.next_due(base_date, spec, mode)
|
function M.next_due(base_date, spec, mode)
|
||||||
local parsed = M.parse(spec)
|
local parsed = M.parse(spec)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,31 @@
|
||||||
local config = require('pending.config')
|
local config = require('pending.config')
|
||||||
|
|
||||||
|
---@alias pending.TaskStatus 'pending'|'done'|'deleted'|'wip'|'blocked'
|
||||||
|
---@alias pending.RecurMode 'scheduled'|'completion'
|
||||||
|
|
||||||
|
---@class pending.TaskExtra
|
||||||
|
---@field _forge_ref? pending.ForgeRef
|
||||||
|
---@field _forge_cache? pending.ForgeCache
|
||||||
|
---@field _gtasks_task_id? string
|
||||||
|
---@field _gtasks_list_id? string
|
||||||
|
---@field _gcal_event_id? string
|
||||||
|
---@field _gcal_calendar_id? string
|
||||||
|
---@field [string] any
|
||||||
|
|
||||||
---@class pending.Task
|
---@class pending.Task
|
||||||
---@field id integer
|
---@field id integer
|
||||||
---@field description string
|
---@field description string
|
||||||
---@field status 'pending'|'done'|'deleted'|'wip'|'blocked'
|
---@field status pending.TaskStatus
|
||||||
---@field category? string
|
---@field category? string
|
||||||
---@field priority integer
|
---@field priority integer
|
||||||
---@field due? string
|
---@field due? string
|
||||||
---@field recur? string
|
---@field recur? string
|
||||||
---@field recur_mode? 'scheduled'|'completion'
|
---@field recur_mode? pending.RecurMode
|
||||||
---@field entry string
|
---@field entry string
|
||||||
---@field modified string
|
---@field modified string
|
||||||
---@field end? string
|
---@field end? string
|
||||||
---@field order integer
|
---@field order integer
|
||||||
---@field _extra? table<string, any>
|
---@field _extra? pending.TaskExtra
|
||||||
|
|
||||||
---@class pending.Data
|
---@class pending.Data
|
||||||
---@field version integer
|
---@field version integer
|
||||||
|
|
@ -24,14 +36,14 @@ local config = require('pending.config')
|
||||||
|
|
||||||
---@class pending.TaskFields
|
---@class pending.TaskFields
|
||||||
---@field description string
|
---@field description string
|
||||||
---@field status? string
|
---@field status? pending.TaskStatus
|
||||||
---@field category? string
|
---@field category? string
|
||||||
---@field priority? integer
|
---@field priority? integer
|
||||||
---@field due? string
|
---@field due? string
|
||||||
---@field recur? string
|
---@field recur? string
|
||||||
---@field recur_mode? string
|
---@field recur_mode? pending.RecurMode
|
||||||
---@field order? integer
|
---@field order? integer
|
||||||
---@field _extra? table
|
---@field _extra? pending.TaskExtra
|
||||||
|
|
||||||
---@class pending.Store
|
---@class pending.Store
|
||||||
---@field path string
|
---@field path string
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ local BUNDLED_CLIENT_SECRET = 'PLACEHOLDER'
|
||||||
---@field config_key string
|
---@field config_key string
|
||||||
|
|
||||||
---@class pending.OAuthClient : pending.OAuthClientOpts
|
---@class pending.OAuthClient : pending.OAuthClientOpts
|
||||||
|
---@field token_path fun(self: pending.OAuthClient): string
|
||||||
|
---@field resolve_credentials fun(self: pending.OAuthClient): pending.OAuthCredentials
|
||||||
|
---@field load_tokens fun(self: pending.OAuthClient): pending.OAuthTokens?
|
||||||
|
---@field save_tokens fun(self: pending.OAuthClient, tokens: pending.OAuthTokens): boolean
|
||||||
|
---@field refresh_access_token fun(self: pending.OAuthClient, creds: pending.OAuthCredentials, tokens: pending.OAuthTokens): pending.OAuthTokens?
|
||||||
|
---@field get_access_token fun(self: pending.OAuthClient): string?
|
||||||
|
---@field setup fun(self: pending.OAuthClient): nil
|
||||||
|
---@field auth fun(self: pending.OAuthClient, on_complete?: fun(ok: boolean): nil): nil
|
||||||
|
---@field clear_tokens fun(self: pending.OAuthClient): nil
|
||||||
local OAuthClient = {}
|
local OAuthClient = {}
|
||||||
OAuthClient.__index = OAuthClient
|
OAuthClient.__index = OAuthClient
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ local parse = require('pending.parse')
|
||||||
---@field id? integer
|
---@field id? integer
|
||||||
---@field due? string
|
---@field due? string
|
||||||
---@field raw_due? string
|
---@field raw_due? string
|
||||||
---@field status? string
|
---@field status? pending.TaskStatus
|
||||||
---@field category? string
|
---@field category? string
|
||||||
---@field overdue? boolean
|
---@field overdue? boolean
|
||||||
---@field show_category? boolean
|
---@field show_category? boolean
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,19 @@ vim.g.loaded_pending = true
|
||||||
---@return string[]
|
---@return string[]
|
||||||
local function edit_field_candidates()
|
local function edit_field_candidates()
|
||||||
local cfg = require('pending.config').get()
|
local cfg = require('pending.config').get()
|
||||||
|
local ck = cfg.category_syntax or 'cat'
|
||||||
local dk = cfg.date_syntax or 'due'
|
local dk = cfg.date_syntax or 'due'
|
||||||
local rk = cfg.recur_syntax or 'rec'
|
local rk = cfg.recur_syntax or 'rec'
|
||||||
return {
|
return {
|
||||||
dk .. ':',
|
dk .. ':',
|
||||||
'cat:',
|
ck .. ':',
|
||||||
rk .. ':',
|
rk .. ':',
|
||||||
'+!',
|
'+!',
|
||||||
'+!!',
|
'+!!',
|
||||||
'+!!!',
|
'+!!!',
|
||||||
'-!',
|
'-!',
|
||||||
'-' .. dk,
|
'-' .. dk,
|
||||||
'-cat',
|
'-' .. ck,
|
||||||
'-' .. rk,
|
'-' .. rk,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
@ -135,7 +136,9 @@ local function complete_edit(arg_lead, cmd_line)
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
local cat_prefix = arg_lead:match('^(cat:)(.*)$')
|
local ck = cfg.category_syntax or 'cat'
|
||||||
|
|
||||||
|
local cat_prefix = arg_lead:match('^(' .. vim.pesc(ck) .. ':)(.*)$')
|
||||||
if cat_prefix then
|
if cat_prefix then
|
||||||
local after_colon = arg_lead:sub(#cat_prefix + 1)
|
local after_colon = arg_lead:sub(#cat_prefix + 1)
|
||||||
local store = require('pending.store')
|
local store = require('pending.store')
|
||||||
|
|
@ -192,7 +195,8 @@ end, {
|
||||||
for _, task in ipairs(s:active_tasks()) do
|
for _, task in ipairs(s:active_tasks()) do
|
||||||
if task.category and not seen[task.category] then
|
if task.category and not seen[task.category] then
|
||||||
seen[task.category] = true
|
seen[task.category] = true
|
||||||
table.insert(candidates, 'cat:' .. task.category)
|
local ck = (require('pending.config').get().category_syntax or 'cat')
|
||||||
|
table.insert(candidates, ck .. ':' .. task.category)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local filtered = {}
|
local filtered = {}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ describe('diff', function()
|
||||||
'/1/- [ ] Take trash out rec:weekly',
|
'/1/- [ ] Take trash out rec:weekly',
|
||||||
}
|
}
|
||||||
local result = diff.parse_buffer(lines)
|
local result = diff.parse_buffer(lines)
|
||||||
assert.are.equal('weekly', result[2].rec)
|
assert.are.equal('weekly', result[2].recur)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('extracts rec: with completion mode', function()
|
it('extracts rec: with completion mode', function()
|
||||||
|
|
@ -80,8 +80,8 @@ describe('diff', function()
|
||||||
'/1/- [ ] Water plants rec:!daily',
|
'/1/- [ ] Water plants rec:!daily',
|
||||||
}
|
}
|
||||||
local result = diff.parse_buffer(lines)
|
local result = diff.parse_buffer(lines)
|
||||||
assert.are.equal('daily', result[2].rec)
|
assert.are.equal('daily', result[2].recur)
|
||||||
assert.are.equal('completion', result[2].rec_mode)
|
assert.are.equal('completion', result[2].recur_mode)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('inline due: token is parsed', function()
|
it('inline due: token is parsed', function()
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,7 @@ describe('forge parse.body integration', function()
|
||||||
it('extracts category but keeps forge ref in description', function()
|
it('extracts category but keeps forge ref in description', function()
|
||||||
local desc, meta = parse.body('Fix bug gh:user/repo#42 cat:Work')
|
local desc, meta = parse.body('Fix bug gh:user/repo#42 cat:Work')
|
||||||
assert.equals('Fix bug gh:user/repo#42', desc)
|
assert.equals('Fix bug gh:user/repo#42', desc)
|
||||||
assert.equals('Work', meta.cat)
|
assert.equals('Work', meta.category)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('leaves non-forge tokens as description', function()
|
it('leaves non-forge tokens as description', function()
|
||||||
|
|
|
||||||
|
|
@ -31,21 +31,21 @@ describe('parse', function()
|
||||||
it('extracts category', function()
|
it('extracts category', function()
|
||||||
local desc, meta = parse.body('Buy groceries cat:Errands')
|
local desc, meta = parse.body('Buy groceries cat:Errands')
|
||||||
assert.are.equal('Buy groceries', desc)
|
assert.are.equal('Buy groceries', desc)
|
||||||
assert.are.equal('Errands', meta.cat)
|
assert.are.equal('Errands', meta.category)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('extracts both due and cat', function()
|
it('extracts both due and cat', function()
|
||||||
local desc, meta = parse.body('Buy milk due:2026-03-15 cat:Errands')
|
local desc, meta = parse.body('Buy milk due:2026-03-15 cat:Errands')
|
||||||
assert.are.equal('Buy milk', desc)
|
assert.are.equal('Buy milk', desc)
|
||||||
assert.are.equal('2026-03-15', meta.due)
|
assert.are.equal('2026-03-15', meta.due)
|
||||||
assert.are.equal('Errands', meta.cat)
|
assert.are.equal('Errands', meta.category)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('extracts metadata in any order', function()
|
it('extracts metadata in any order', function()
|
||||||
local desc, meta = parse.body('Buy milk cat:Errands due:2026-03-15')
|
local desc, meta = parse.body('Buy milk cat:Errands due:2026-03-15')
|
||||||
assert.are.equal('Buy milk', desc)
|
assert.are.equal('Buy milk', desc)
|
||||||
assert.are.equal('2026-03-15', meta.due)
|
assert.are.equal('2026-03-15', meta.due)
|
||||||
assert.are.equal('Errands', meta.cat)
|
assert.are.equal('Errands', meta.category)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('stops at duplicate key', function()
|
it('stops at duplicate key', function()
|
||||||
|
|
@ -400,7 +400,7 @@ describe('parse', function()
|
||||||
it('detects category prefix', function()
|
it('detects category prefix', function()
|
||||||
local desc, meta = parse.command_add('School: Do homework')
|
local desc, meta = parse.command_add('School: Do homework')
|
||||||
assert.are.equal('Do homework', desc)
|
assert.are.equal('Do homework', desc)
|
||||||
assert.are.equal('School', meta.cat)
|
assert.are.equal('School', meta.category)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('ignores lowercase prefix', function()
|
it('ignores lowercase prefix', function()
|
||||||
|
|
@ -411,7 +411,7 @@ describe('parse', function()
|
||||||
it('combines category prefix with inline metadata', function()
|
it('combines category prefix with inline metadata', function()
|
||||||
local desc, meta = parse.command_add('School: Do homework due:2026-03-15')
|
local desc, meta = parse.command_add('School: Do homework due:2026-03-15')
|
||||||
assert.are.equal('Do homework', desc)
|
assert.are.equal('Do homework', desc)
|
||||||
assert.are.equal('School', meta.cat)
|
assert.are.equal('School', meta.category)
|
||||||
assert.are.equal('2026-03-15', meta.due)
|
assert.are.equal('2026-03-15', meta.due)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ describe('recur', function()
|
||||||
local r = recur.parse('daily')
|
local r = recur.parse('daily')
|
||||||
assert.are.equal('daily', r.freq)
|
assert.are.equal('daily', r.freq)
|
||||||
assert.are.equal(1, r.interval)
|
assert.are.equal(1, r.interval)
|
||||||
assert.is_false(r.from_completion)
|
assert.are.equal('scheduled', r.mode)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('parses weekdays', function()
|
it('parses weekdays', function()
|
||||||
|
|
@ -79,7 +79,7 @@ describe('recur', function()
|
||||||
it('parses ! prefix as completion-based', function()
|
it('parses ! prefix as completion-based', function()
|
||||||
local r = recur.parse('!weekly')
|
local r = recur.parse('!weekly')
|
||||||
assert.are.equal('weekly', r.freq)
|
assert.are.equal('weekly', r.freq)
|
||||||
assert.is_true(r.from_completion)
|
assert.are.equal('completion', r.mode)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('parses raw RRULE fragment', function()
|
it('parses raw RRULE fragment', function()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue