Problem: :Pending sync hardcodes Google Calendar — M.sync() does
pcall(require, 'pending.sync.gcal') and calls gcal.sync() directly.
The config has a flat gcal field. This prevents adding new sync backends
without modifying init.lua.
Solution: Define a backend interface contract (name, auth, sync, health
fields), refactor :Pending sync to dispatch via require('pending.sync.'
.. backend_name), add sync table to config with legacy gcal migration,
rename gcal.authorize to gcal.auth, add gcal.health for checkhealth,
and add tab completion for backend names and actions.
272 lines
6.9 KiB
Lua
272 lines
6.9 KiB
Lua
if vim.g.loaded_pending then
|
|
return
|
|
end
|
|
vim.g.loaded_pending = true
|
|
|
|
---@return string[]
|
|
local function edit_field_candidates()
|
|
local cfg = require('pending.config').get()
|
|
local dk = cfg.date_syntax or 'due'
|
|
local rk = cfg.recur_syntax or 'rec'
|
|
return {
|
|
dk .. ':',
|
|
'cat:',
|
|
rk .. ':',
|
|
'+!',
|
|
'-!',
|
|
'-' .. dk,
|
|
'-cat',
|
|
'-' .. rk,
|
|
}
|
|
end
|
|
|
|
---@return string[]
|
|
local function edit_date_values()
|
|
return {
|
|
'today',
|
|
'tomorrow',
|
|
'yesterday',
|
|
'+1d',
|
|
'+2d',
|
|
'+3d',
|
|
'+1w',
|
|
'+2w',
|
|
'+1m',
|
|
'mon',
|
|
'tue',
|
|
'wed',
|
|
'thu',
|
|
'fri',
|
|
'sat',
|
|
'sun',
|
|
'eod',
|
|
'eow',
|
|
'eom',
|
|
'eoq',
|
|
'eoy',
|
|
'sow',
|
|
'som',
|
|
'soq',
|
|
'soy',
|
|
'later',
|
|
}
|
|
end
|
|
|
|
---@return string[]
|
|
local function edit_recur_values()
|
|
local ok, recur = pcall(require, 'pending.recur')
|
|
if not ok then
|
|
return {}
|
|
end
|
|
local result = {}
|
|
for _, s in ipairs(recur.shorthand_list()) do
|
|
table.insert(result, s)
|
|
end
|
|
for _, s in ipairs(recur.shorthand_list()) do
|
|
table.insert(result, '!' .. s)
|
|
end
|
|
return result
|
|
end
|
|
|
|
---@param lead string
|
|
---@param candidates string[]
|
|
---@return string[]
|
|
local function filter_candidates(lead, candidates)
|
|
return vim.tbl_filter(function(s)
|
|
return s:find(lead, 1, true) == 1
|
|
end, candidates)
|
|
end
|
|
|
|
---@param arg_lead string
|
|
---@param cmd_line string
|
|
---@return string[]
|
|
local function complete_edit(arg_lead, cmd_line)
|
|
local cfg = require('pending.config').get()
|
|
local dk = cfg.date_syntax or 'due'
|
|
local rk = cfg.recur_syntax or 'rec'
|
|
|
|
local after_edit = cmd_line:match('^Pending%s+edit%s+(.*)')
|
|
if not after_edit then
|
|
return {}
|
|
end
|
|
|
|
local parts = {}
|
|
for part in after_edit:gmatch('%S+') do
|
|
table.insert(parts, part)
|
|
end
|
|
|
|
local trailing_space = after_edit:match('%s$')
|
|
if #parts == 0 or (#parts == 1 and not trailing_space) then
|
|
local store = require('pending.store')
|
|
store.load()
|
|
local ids = {}
|
|
for _, task in ipairs(store.active_tasks()) do
|
|
table.insert(ids, tostring(task.id))
|
|
end
|
|
return filter_candidates(arg_lead, ids)
|
|
end
|
|
|
|
local prefix = arg_lead:match('^(' .. vim.pesc(dk) .. ':)(.*)$')
|
|
if prefix then
|
|
local after_colon = arg_lead:sub(#prefix + 1)
|
|
local dates = edit_date_values()
|
|
local result = {}
|
|
for _, d in ipairs(dates) do
|
|
if d:find(after_colon, 1, true) == 1 then
|
|
table.insert(result, prefix .. d)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
local rec_prefix = arg_lead:match('^(' .. vim.pesc(rk) .. ':)(.*)$')
|
|
if rec_prefix then
|
|
local after_colon = arg_lead:sub(#rec_prefix + 1)
|
|
local pats = edit_recur_values()
|
|
local result = {}
|
|
for _, p in ipairs(pats) do
|
|
if p:find(after_colon, 1, true) == 1 then
|
|
table.insert(result, rec_prefix .. p)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
local cat_prefix = arg_lead:match('^(cat:)(.*)$')
|
|
if cat_prefix then
|
|
local after_colon = arg_lead:sub(#cat_prefix + 1)
|
|
local store = require('pending.store')
|
|
store.load()
|
|
local seen = {}
|
|
local cats = {}
|
|
for _, task in ipairs(store.active_tasks()) do
|
|
if task.category and not seen[task.category] then
|
|
seen[task.category] = true
|
|
table.insert(cats, task.category)
|
|
end
|
|
end
|
|
table.sort(cats)
|
|
local result = {}
|
|
for _, c in ipairs(cats) do
|
|
if c:find(after_colon, 1, true) == 1 then
|
|
table.insert(result, cat_prefix .. c)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
return filter_candidates(arg_lead, edit_field_candidates())
|
|
end
|
|
|
|
vim.api.nvim_create_user_command('Pending', function(opts)
|
|
require('pending').command(opts.args)
|
|
end, {
|
|
bar = true,
|
|
nargs = '*',
|
|
complete = function(arg_lead, cmd_line)
|
|
local subcmds = { 'add', 'archive', 'due', 'edit', 'sync', 'undo' }
|
|
if not cmd_line:match('^Pending%s+%S') then
|
|
return filter_candidates(arg_lead, subcmds)
|
|
end
|
|
if cmd_line:match('^Pending%s+edit') then
|
|
return complete_edit(arg_lead, cmd_line)
|
|
end
|
|
if cmd_line:match('^Pending%s+sync') then
|
|
local after_sync = cmd_line:match('^Pending%s+sync%s+(.*)')
|
|
if not after_sync then
|
|
return {}
|
|
end
|
|
local parts = {}
|
|
for part in after_sync:gmatch('%S+') do
|
|
table.insert(parts, part)
|
|
end
|
|
local trailing_space = after_sync:match('%s$')
|
|
if #parts == 0 or (#parts == 1 and not trailing_space) then
|
|
local backends = {}
|
|
local pattern = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true)
|
|
for _, path in ipairs(pattern) do
|
|
local name = vim.fn.fnamemodify(path, ':t:r')
|
|
table.insert(backends, name)
|
|
end
|
|
table.sort(backends)
|
|
return filter_candidates(arg_lead, backends)
|
|
end
|
|
if #parts == 1 and trailing_space then
|
|
return filter_candidates(arg_lead, { 'auth', 'sync' })
|
|
end
|
|
if #parts >= 2 and not trailing_space then
|
|
return filter_candidates(arg_lead, { 'auth', 'sync' })
|
|
end
|
|
return {}
|
|
end
|
|
return {}
|
|
end,
|
|
})
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-open)', function()
|
|
require('pending').open()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-close)', function()
|
|
require('pending.buffer').close()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-toggle)', function()
|
|
require('pending').toggle_complete()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-view)', function()
|
|
require('pending.buffer').toggle_view()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-priority)', function()
|
|
require('pending').toggle_priority()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-date)', function()
|
|
require('pending').prompt_date()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-undo)', function()
|
|
require('pending').undo_write()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-open-line)', function()
|
|
require('pending.buffer').open_line(false)
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-open-line-above)', function()
|
|
require('pending.buffer').open_line(true)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-a-task)', function()
|
|
require('pending.textobj').a_task(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-i-task)', function()
|
|
require('pending.textobj').i_task(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-a-category)', function()
|
|
require('pending.textobj').a_category(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-i-category)', function()
|
|
require('pending.textobj').i_category(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-next-header)', function()
|
|
require('pending.textobj').next_header(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-prev-header)', function()
|
|
require('pending.textobj').prev_header(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-next-task)', function()
|
|
require('pending.textobj').next_task(vim.v.count1)
|
|
end)
|
|
|
|
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-prev-task)', function()
|
|
require('pending.textobj').prev_task(vim.v.count1)
|
|
end)
|