Problem: `<C-a>` and `<C-x>` only worked in normal mode for single-task priority adjustment. In visual mode (`v`, `V`, `<C-v>`), they fell through to Vim's native number increment. Only `g<C-a>`/`g<C-x>` were mapped for visual batch priority. Solution: add `priority_up` and `priority_down` to the visual actions table so `<C-a>`/`<C-x>` call the batch function in all visual modes. Add corresponding `x`-mode `<Plug>` mappings. Document that both mappings work in normal and visual mode.
481 lines
13 KiB
Lua
481 lines
13 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 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' }
|
|
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', '<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-category)', function()
|
|
require('pending').prompt_category()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-recur)', function()
|
|
require('pending').prompt_recur()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-move-down)', function()
|
|
require('pending').move_task('down')
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-move-up)', function()
|
|
require('pending').move_task('up')
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-wip)', function()
|
|
require('pending').toggle_status('wip')
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-blocked)', function()
|
|
require('pending').toggle_status('blocked')
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-priority-up)', function()
|
|
require('pending').increment_priority()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-priority-down)', function()
|
|
require('pending').decrement_priority()
|
|
end)
|
|
|
|
vim.keymap.set('x', '<Plug>(pending-priority-up)', function()
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'nx', false)
|
|
require('pending').increment_priority_visual()
|
|
end)
|
|
|
|
vim.keymap.set('x', '<Plug>(pending-priority-down)', function()
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'nx', false)
|
|
require('pending').decrement_priority_visual()
|
|
end)
|
|
|
|
vim.keymap.set('x', '<Plug>(pending-priority-up-visual)', function()
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'nx', false)
|
|
require('pending').increment_priority_visual()
|
|
end)
|
|
|
|
vim.keymap.set('x', '<Plug>(pending-priority-down-visual)', function()
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, false, true), 'nx', false)
|
|
require('pending').decrement_priority_visual()
|
|
end)
|
|
|
|
vim.keymap.set('n', '<Plug>(pending-filter)', function()
|
|
vim.ui.input({ prompt = 'Filter: ' }, function(input)
|
|
if input then
|
|
require('pending').filter(input)
|
|
end
|
|
end)
|
|
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)
|
|
|
|
vim.keymap.set('n', '<Plug>(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, {})
|