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 ck = cfg.category_syntax or 'cat' local dk = cfg.date_syntax or 'due' local rk = cfg.recur_syntax or 'rec' return { dk .. ':', ck .. ':', rk .. ':', '+!', '+!!', '+!!!', '-!', '-' .. dk, '-' .. ck, '-' .. 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 ---@return string[] local function complete_add(arg_lead) local cfg = require('pending.config').get() local dk = cfg.date_syntax or 'due' local rk = cfg.recur_syntax or 'rec' local ck = cfg.category_syntax or 'cat' local prefix = arg_lead:match('^(' .. vim.pesc(dk) .. ':)(.*)$') if prefix then local after_colon = arg_lead:sub(#prefix + 1) local result = {} for _, d in ipairs(edit_date_values()) 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 result = {} for _, p in ipairs(edit_recur_values()) 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('^(' .. vim.pesc(ck) .. ':)(.*)$') if cat_prefix then local after_colon = arg_lead:sub(#cat_prefix + 1) local store = require('pending.store') local s = store.new(store.resolve_path()) s:load() local seen = {} local cats = {} for _, task in ipairs(s: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 {} 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') local s = store.new(store.resolve_path()) s:load() local ids = {} for _, task in ipairs(s: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 ck = cfg.category_syntax or 'cat' local cat_prefix = arg_lead:match('^(' .. vim.pesc(ck) .. ':)(.*)$') if cat_prefix then local after_colon = arg_lead:sub(#cat_prefix + 1) local store = require('pending.store') local s = store.new(store.resolve_path()) s:load() local seen = {} local cats = {} for _, task in ipairs(s: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 pending = require('pending') local subcmds = { 'add', 'archive', 'auth', 'done', 'due', 'edit', 'filter', 'undo' } for _, b in ipairs(pending.sync_backends()) do table.insert(subcmds, b) end table.sort(subcmds) if not cmd_line:match('^Pending%s+%S') then return filter_candidates(arg_lead, subcmds) end if cmd_line:match('^Pending%s+filter') then local after_filter = cmd_line:match('^Pending%s+filter%s+(.*)') or '' local used = {} for word in after_filter:gmatch('%S+') do used[word] = true end local candidates = { 'clear', 'overdue', 'today', 'priority', 'done', 'pending', 'wip', 'blocked', 'cancelled', } local store = require('pending.store') local s = store.new(store.resolve_path()) s:load() local seen = {} for _, task in ipairs(s:active_tasks()) do if task.category and not seen[task.category] then seen[task.category] = true local ck = (require('pending.config').get().category_syntax or 'cat') table.insert(candidates, ck .. ':' .. task.category) end end local filtered = {} for _, c in ipairs(candidates) do if not used[c] and (arg_lead == '' or c:find(arg_lead, 1, true) == 1) then table.insert(filtered, c) end end return filtered end if cmd_line:match('^Pending%s+add%s') then return complete_add(arg_lead) end if cmd_line:match('^Pending%s+archive%s') then return filter_candidates(arg_lead, { '7d', '2w', '30d', '3m', '6m', '1y' }) end if cmd_line:match('^Pending%s+done%s') then local store = require('pending.store') local s = store.new(store.resolve_path()) s:load() local ids = {} for _, task in ipairs(s:active_tasks()) do table.insert(ids, tostring(task.id)) end return filter_candidates(arg_lead, ids) end if cmd_line:match('^Pending%s+edit') then return complete_edit(arg_lead, cmd_line) end if cmd_line:match('^Pending%s+auth') then local after_auth = cmd_line:match('^Pending%s+auth%s+(.*)') or '' local parts = {} for w in after_auth:gmatch('%S+') do table.insert(parts, w) end local trailing = after_auth:match('%s$') if #parts == 0 or (#parts == 1 and not trailing) then local auth_names = {} for _, b in ipairs(pending.sync_backends()) do local ok, mod = pcall(require, 'pending.sync.' .. b) if ok and type(mod.auth) == 'function' then table.insert(auth_names, b) end end return filter_candidates(arg_lead, auth_names) end local backend_name = parts[1] if #parts == 1 or (#parts == 2 and not trailing) then local ok, mod = pcall(require, 'pending.sync.' .. backend_name) if ok and type(mod.auth_complete) == 'function' then return filter_candidates(arg_lead, mod.auth_complete()) end return {} end return {} end local backend_set = pending.sync_backend_set() local matched_backend = cmd_line:match('^Pending%s+(%S+)') if matched_backend and backend_set[matched_backend] then local after_backend = cmd_line:match('^Pending%s+%S+%s+(.*)') if not after_backend then return {} end local ok, mod = pcall(require, 'pending.sync.' .. matched_backend) if not ok then return {} end local actions = {} for k, v in pairs(mod) do if type(v) == 'function' and k:sub(1, 1) ~= '_' and k ~= 'health' and k ~= 'auth' and k ~= 'auth_complete' then table.insert(actions, k) end end table.sort(actions) return filter_candidates(arg_lead, actions) end return {} end, }) vim.keymap.set('n', '(pending-open)', function() require('pending').open() end) vim.keymap.set('n', '(pending-close)', function() require('pending.buffer').close() end) vim.keymap.set('n', '(pending-toggle)', function() require('pending').toggle_complete() end) vim.keymap.set('n', '(pending-view)', function() require('pending.buffer').toggle_view() end) vim.keymap.set('n', '(pending-priority)', function() require('pending').toggle_priority() end) vim.keymap.set('n', '(pending-date)', function() require('pending').prompt_date() end) vim.keymap.set('n', '(pending-undo)', function() require('pending').undo_write() end) vim.keymap.set('n', '(pending-category)', function() require('pending').prompt_category() end) vim.keymap.set('n', '(pending-recur)', function() require('pending').prompt_recur() end) vim.keymap.set('n', '(pending-move-down)', function() require('pending').move_task('down') end) vim.keymap.set('n', '(pending-move-up)', function() require('pending').move_task('up') end) vim.keymap.set('n', '(pending-wip)', function() require('pending').toggle_status('wip') end) vim.keymap.set('n', '(pending-blocked)', function() require('pending').toggle_status('blocked') end) vim.keymap.set('n', '(pending-cancelled)', function() require('pending').toggle_status('cancelled') end) vim.keymap.set('n', '(pending-edit-notes)', function() require('pending').open_detail() end) vim.keymap.set('n', '(pending-priority-up)', function() require('pending').increment_priority() end) vim.keymap.set('n', '(pending-priority-down)', function() require('pending').decrement_priority() end) vim.keymap.set('x', '(pending-priority-up-visual)', function() vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, false, true), 'nx', false) require('pending').increment_priority_visual() end) vim.keymap.set('x', '(pending-priority-down-visual)', function() vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, false, true), 'nx', false) require('pending').decrement_priority_visual() end) vim.keymap.set('n', '(pending-filter)', function() vim.ui.input({ prompt = 'Filter: ' }, function(input) if input then require('pending').filter(input) end end) end) vim.keymap.set('n', '(pending-open-line)', function() require('pending.buffer').open_line(false) end) vim.keymap.set('n', '(pending-open-line-above)', function() require('pending.buffer').open_line(true) end) vim.keymap.set({ 'o', 'x' }, '(pending-a-task)', function() require('pending.textobj').a_task(vim.v.count1) end) vim.keymap.set({ 'o', 'x' }, '(pending-i-task)', function() require('pending.textobj').i_task(vim.v.count1) end) vim.keymap.set({ 'o', 'x' }, '(pending-a-category)', function() require('pending.textobj').a_category(vim.v.count1) end) vim.keymap.set({ 'o', 'x' }, '(pending-i-category)', function() require('pending.textobj').i_category(vim.v.count1) end) vim.keymap.set({ 'n', 'x', 'o' }, '(pending-next-header)', function() require('pending.textobj').next_header(vim.v.count1) end) vim.keymap.set({ 'n', 'x', 'o' }, '(pending-prev-header)', function() require('pending.textobj').prev_header(vim.v.count1) end) vim.keymap.set({ 'n', 'x', 'o' }, '(pending-next-task)', function() require('pending.textobj').next_task(vim.v.count1) end) vim.keymap.set({ 'n', 'x', 'o' }, '(pending-prev-task)', function() require('pending.textobj').prev_task(vim.v.count1) end) vim.keymap.set('n', '(pending-tab)', function() vim.cmd.tabnew() require('pending').open() end) vim.api.nvim_create_user_command('PendingTab', function() vim.cmd.tabnew() require('pending').open() end, {})