* feat: :Pending edit command for CLI metadata editing
Problem: editing task metadata (due date, category, priority,
recurrence) requires opening the buffer and editing inline. No way
to make quick metadata changes from the command line.
Solution: add :Pending edit {id} [operations...] command that applies
metadata changes by numeric task ID. Supports due:<date>, cat:<name>,
rec:<pattern>, +!, -!, -due, -cat, -rec operations with full date
vocabulary and recurrence validation. Pushes to undo stack, re-renders
the buffer if open, and provides feedback messages. Tab completion for
IDs, field names, date vocabulary, categories, and recurrence patterns.
Also fixes store.update() to properly clear fields set to vim.NIL.
* ci: formt
716 lines
17 KiB
Lua
716 lines
17 KiB
Lua
local buffer = require('pending.buffer')
|
|
local diff = require('pending.diff')
|
|
local parse = require('pending.parse')
|
|
local store = require('pending.store')
|
|
|
|
---@class pending.Counts
|
|
---@field overdue integer
|
|
---@field today integer
|
|
---@field pending integer
|
|
---@field priority integer
|
|
---@field next_due? string
|
|
|
|
---@class pending.init
|
|
local M = {}
|
|
|
|
local UNDO_MAX = 20
|
|
|
|
---@type pending.Counts?
|
|
local _counts = nil
|
|
|
|
---@return nil
|
|
function M._recompute_counts()
|
|
local cfg = require('pending.config').get()
|
|
local someday = cfg.someday_date
|
|
local overdue = 0
|
|
local today = 0
|
|
local pending = 0
|
|
local priority = 0
|
|
local next_due = nil ---@type string?
|
|
local today_str = os.date('%Y-%m-%d') --[[@as string]]
|
|
|
|
for _, task in ipairs(store.active_tasks()) do
|
|
if task.status == 'pending' then
|
|
pending = pending + 1
|
|
if task.priority > 0 then
|
|
priority = priority + 1
|
|
end
|
|
if task.due and task.due ~= someday then
|
|
if parse.is_overdue(task.due) then
|
|
overdue = overdue + 1
|
|
elseif parse.is_today(task.due) then
|
|
today = today + 1
|
|
end
|
|
local date_part = task.due:match('^(%d%d%d%d%-%d%d%-%d%d)') or task.due
|
|
if date_part >= today_str and (not next_due or task.due < next_due) then
|
|
next_due = task.due
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
_counts = {
|
|
overdue = overdue,
|
|
today = today,
|
|
pending = pending,
|
|
priority = priority,
|
|
next_due = next_due,
|
|
}
|
|
|
|
vim.api.nvim_exec_autocmds('User', { pattern = 'PendingStatusChanged' })
|
|
end
|
|
|
|
---@return nil
|
|
local function _save_and_notify()
|
|
store.save()
|
|
M._recompute_counts()
|
|
end
|
|
|
|
---@return pending.Counts
|
|
function M.counts()
|
|
if not _counts then
|
|
store.load()
|
|
M._recompute_counts()
|
|
end
|
|
return _counts --[[@as pending.Counts]]
|
|
end
|
|
|
|
---@return string
|
|
function M.statusline()
|
|
local c = M.counts()
|
|
if c.overdue > 0 and c.today > 0 then
|
|
return c.overdue .. ' overdue, ' .. c.today .. ' today'
|
|
elseif c.overdue > 0 then
|
|
return c.overdue .. ' overdue'
|
|
elseif c.today > 0 then
|
|
return c.today .. ' today'
|
|
end
|
|
return ''
|
|
end
|
|
|
|
---@return boolean
|
|
function M.has_due()
|
|
local c = M.counts()
|
|
return c.overdue > 0 or c.today > 0
|
|
end
|
|
|
|
---@return integer bufnr
|
|
function M.open()
|
|
local bufnr = buffer.open()
|
|
M._setup_autocmds(bufnr)
|
|
M._setup_buf_mappings(bufnr)
|
|
return bufnr
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return nil
|
|
function M._setup_autocmds(bufnr)
|
|
local group = vim.api.nvim_create_augroup('PendingBuffer', { clear = true })
|
|
vim.api.nvim_create_autocmd('BufWriteCmd', {
|
|
group = group,
|
|
buffer = bufnr,
|
|
callback = function()
|
|
M._on_write(bufnr)
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('BufEnter', {
|
|
group = group,
|
|
buffer = bufnr,
|
|
callback = function()
|
|
if not vim.bo[bufnr].modified then
|
|
store.load()
|
|
buffer.render(bufnr)
|
|
end
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('WinClosed', {
|
|
group = group,
|
|
callback = function(ev)
|
|
if tonumber(ev.match) == buffer.winid() then
|
|
buffer.clear_winid()
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return nil
|
|
function M._setup_buf_mappings(bufnr)
|
|
local cfg = require('pending.config').get()
|
|
local km = cfg.keymaps
|
|
local opts = { buffer = bufnr, silent = true }
|
|
|
|
---@type table<string, fun()>
|
|
local actions = {
|
|
close = function()
|
|
buffer.close()
|
|
end,
|
|
toggle = function()
|
|
M.toggle_complete()
|
|
end,
|
|
view = function()
|
|
buffer.toggle_view()
|
|
end,
|
|
priority = function()
|
|
M.toggle_priority()
|
|
end,
|
|
date = function()
|
|
M.prompt_date()
|
|
end,
|
|
undo = function()
|
|
M.undo_write()
|
|
end,
|
|
open_line = function()
|
|
buffer.open_line(false)
|
|
end,
|
|
open_line_above = function()
|
|
buffer.open_line(true)
|
|
end,
|
|
}
|
|
|
|
for name, fn in pairs(actions) do
|
|
local key = km[name]
|
|
if key and key ~= false then
|
|
vim.keymap.set('n', key --[[@as string]], fn, opts)
|
|
end
|
|
end
|
|
|
|
local textobj = require('pending.textobj')
|
|
|
|
---@type table<string, { modes: string[], fn: fun(count: integer), visual_fn?: fun(count: integer) }>
|
|
local textobjs = {
|
|
a_task = {
|
|
modes = { 'o', 'x' },
|
|
fn = textobj.a_task,
|
|
visual_fn = textobj.a_task_visual,
|
|
},
|
|
i_task = {
|
|
modes = { 'o', 'x' },
|
|
fn = textobj.i_task,
|
|
visual_fn = textobj.i_task_visual,
|
|
},
|
|
a_category = {
|
|
modes = { 'o', 'x' },
|
|
fn = textobj.a_category,
|
|
visual_fn = textobj.a_category_visual,
|
|
},
|
|
i_category = {
|
|
modes = { 'o', 'x' },
|
|
fn = textobj.i_category,
|
|
visual_fn = textobj.i_category_visual,
|
|
},
|
|
}
|
|
|
|
for name, spec in pairs(textobjs) do
|
|
local key = km[name]
|
|
if key and key ~= false then
|
|
for _, mode in ipairs(spec.modes) do
|
|
if mode == 'x' and spec.visual_fn then
|
|
vim.keymap.set(mode, key --[[@as string]], function()
|
|
spec.visual_fn(vim.v.count1)
|
|
end, opts)
|
|
else
|
|
vim.keymap.set(mode, key --[[@as string]], function()
|
|
spec.fn(vim.v.count1)
|
|
end, opts)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@type table<string, fun(count: integer)>
|
|
local motions = {
|
|
next_header = textobj.next_header,
|
|
prev_header = textobj.prev_header,
|
|
next_task = textobj.next_task,
|
|
prev_task = textobj.prev_task,
|
|
}
|
|
|
|
for name, fn in pairs(motions) do
|
|
local key = km[name]
|
|
if cfg.debug then
|
|
vim.notify(
|
|
('[pending] mapping motion %s → %s (buf=%d)'):format(name, key or 'nil', bufnr),
|
|
vim.log.levels.INFO
|
|
)
|
|
end
|
|
if key and key ~= false then
|
|
vim.keymap.set({ 'n', 'x', 'o' }, key --[[@as string]], function()
|
|
fn(vim.v.count1)
|
|
end, opts)
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return nil
|
|
function M._on_write(bufnr)
|
|
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
|
local snapshot = store.snapshot()
|
|
local stack = store.undo_stack()
|
|
table.insert(stack, snapshot)
|
|
if #stack > UNDO_MAX then
|
|
table.remove(stack, 1)
|
|
end
|
|
diff.apply(lines)
|
|
M._recompute_counts()
|
|
buffer.render(bufnr)
|
|
end
|
|
|
|
---@return nil
|
|
function M.undo_write()
|
|
local stack = store.undo_stack()
|
|
if #stack == 0 then
|
|
vim.notify('Nothing to undo.', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
local state = table.remove(stack)
|
|
store.replace_tasks(state)
|
|
_save_and_notify()
|
|
buffer.render(buffer.bufnr())
|
|
end
|
|
|
|
---@return nil
|
|
function M.toggle_complete()
|
|
local bufnr = buffer.bufnr()
|
|
if not bufnr then
|
|
return
|
|
end
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local meta = buffer.meta()
|
|
if not meta[row] or meta[row].type ~= 'task' then
|
|
return
|
|
end
|
|
local id = meta[row].id
|
|
if not id then
|
|
return
|
|
end
|
|
local task = store.get(id)
|
|
if not task then
|
|
return
|
|
end
|
|
if task.status == 'done' then
|
|
store.update(id, { status = 'pending', ['end'] = vim.NIL })
|
|
else
|
|
if task.recur and task.due then
|
|
local recur = require('pending.recur')
|
|
local mode = task.recur_mode or 'scheduled'
|
|
local next_date = recur.next_due(task.due, task.recur, mode)
|
|
store.add({
|
|
description = task.description,
|
|
category = task.category,
|
|
priority = task.priority,
|
|
due = next_date,
|
|
recur = task.recur,
|
|
recur_mode = task.recur_mode,
|
|
})
|
|
end
|
|
store.update(id, { status = 'done' })
|
|
end
|
|
_save_and_notify()
|
|
buffer.render(bufnr)
|
|
for lnum, m in ipairs(buffer.meta()) do
|
|
if m.id == id then
|
|
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
---@return nil
|
|
function M.toggle_priority()
|
|
local bufnr = buffer.bufnr()
|
|
if not bufnr then
|
|
return
|
|
end
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local meta = buffer.meta()
|
|
if not meta[row] or meta[row].type ~= 'task' then
|
|
return
|
|
end
|
|
local id = meta[row].id
|
|
if not id then
|
|
return
|
|
end
|
|
local task = store.get(id)
|
|
if not task then
|
|
return
|
|
end
|
|
local new_priority = task.priority > 0 and 0 or 1
|
|
store.update(id, { priority = new_priority })
|
|
_save_and_notify()
|
|
buffer.render(bufnr)
|
|
for lnum, m in ipairs(buffer.meta()) do
|
|
if m.id == id then
|
|
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
---@return nil
|
|
function M.prompt_date()
|
|
local bufnr = buffer.bufnr()
|
|
if not bufnr then
|
|
return
|
|
end
|
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
|
local meta = buffer.meta()
|
|
if not meta[row] or meta[row].type ~= 'task' then
|
|
return
|
|
end
|
|
local id = meta[row].id
|
|
if not id then
|
|
return
|
|
end
|
|
vim.ui.input({ prompt = 'Due date (today, +3d, fri@2pm, etc.): ' }, function(input)
|
|
if not input then
|
|
return
|
|
end
|
|
local due = input ~= '' and input or nil
|
|
if due then
|
|
local resolved = parse.resolve_date(due)
|
|
if resolved then
|
|
due = resolved
|
|
elseif
|
|
not due:match('^%d%d%d%d%-%d%d%-%d%d$')
|
|
and not due:match('^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d$')
|
|
then
|
|
vim.notify('Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDThh:mm.', vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
end
|
|
store.update(id, { due = due })
|
|
_save_and_notify()
|
|
buffer.render(bufnr)
|
|
end)
|
|
end
|
|
|
|
---@param text string
|
|
---@return nil
|
|
function M.add(text)
|
|
if not text or text == '' then
|
|
vim.notify('Usage: :Pending add <description>', vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
store.load()
|
|
local description, metadata = parse.command_add(text)
|
|
if not description or description == '' then
|
|
vim.notify('Pending must have a description.', vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
store.add({
|
|
description = description,
|
|
category = metadata.cat,
|
|
due = metadata.due,
|
|
recur = metadata.rec,
|
|
recur_mode = metadata.rec_mode,
|
|
})
|
|
_save_and_notify()
|
|
local bufnr = buffer.bufnr()
|
|
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
|
buffer.render(bufnr)
|
|
end
|
|
vim.notify('Pending added: ' .. description)
|
|
end
|
|
|
|
---@return nil
|
|
function M.sync()
|
|
local ok, gcal = pcall(require, 'pending.sync.gcal')
|
|
if not ok then
|
|
vim.notify('Google Calendar sync module not available.', vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
gcal.sync()
|
|
end
|
|
|
|
---@param days? integer
|
|
---@return nil
|
|
function M.archive(days)
|
|
days = days or 30
|
|
local cutoff = os.time() - (days * 86400)
|
|
local tasks = store.tasks()
|
|
local archived = 0
|
|
local kept = {}
|
|
for _, task in ipairs(tasks) do
|
|
if (task.status == 'done' or task.status == 'deleted') and task['end'] then
|
|
local y, mo, d, h, mi, s = task['end']:match('^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z$')
|
|
if y then
|
|
local t = os.time({
|
|
year = tonumber(y) --[[@as integer]],
|
|
month = tonumber(mo) --[[@as integer]],
|
|
day = tonumber(d) --[[@as integer]],
|
|
hour = tonumber(h) --[[@as integer]],
|
|
min = tonumber(mi) --[[@as integer]],
|
|
sec = tonumber(s) --[[@as integer]],
|
|
})
|
|
if t < cutoff then
|
|
archived = archived + 1
|
|
goto skip
|
|
end
|
|
end
|
|
end
|
|
table.insert(kept, task)
|
|
::skip::
|
|
end
|
|
store.replace_tasks(kept)
|
|
_save_and_notify()
|
|
vim.notify('Archived ' .. archived .. ' tasks.')
|
|
local bufnr = buffer.bufnr()
|
|
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
|
buffer.render(bufnr)
|
|
end
|
|
end
|
|
|
|
---@return nil
|
|
function M.due()
|
|
local bufnr = buffer.bufnr()
|
|
local is_valid = bufnr ~= nil and vim.api.nvim_buf_is_valid(bufnr)
|
|
local meta = is_valid and buffer.meta() or nil
|
|
local qf_items = {}
|
|
|
|
if meta and bufnr then
|
|
for lnum, m in ipairs(meta) do
|
|
if
|
|
m.type == 'task'
|
|
and m.raw_due
|
|
and m.status ~= 'done'
|
|
and (parse.is_overdue(m.raw_due) or parse.is_today(m.raw_due))
|
|
then
|
|
local task = store.get(m.id or 0)
|
|
local label = parse.is_overdue(m.raw_due) and '[OVERDUE] ' or '[DUE] '
|
|
table.insert(qf_items, {
|
|
bufnr = bufnr,
|
|
lnum = lnum,
|
|
col = 1,
|
|
text = label .. (task and task.description or ''),
|
|
})
|
|
end
|
|
end
|
|
else
|
|
store.load()
|
|
for _, task in ipairs(store.active_tasks()) do
|
|
if
|
|
task.status == 'pending'
|
|
and task.due
|
|
and (parse.is_overdue(task.due) or parse.is_today(task.due))
|
|
then
|
|
local label = parse.is_overdue(task.due) and '[OVERDUE] ' or '[DUE] '
|
|
local text = label .. task.description
|
|
if task.category then
|
|
text = text .. ' [' .. task.category .. ']'
|
|
end
|
|
table.insert(qf_items, { text = text })
|
|
end
|
|
end
|
|
end
|
|
|
|
if #qf_items == 0 then
|
|
vim.notify('No due or overdue tasks.')
|
|
return
|
|
end
|
|
|
|
vim.fn.setqflist(qf_items, 'r')
|
|
vim.cmd('copen')
|
|
end
|
|
|
|
---@param token string
|
|
---@return string|nil field
|
|
---@return any value
|
|
---@return string|nil err
|
|
local function parse_edit_token(token)
|
|
local recur = require('pending.recur')
|
|
local cfg = require('pending.config').get()
|
|
local dk = cfg.date_syntax or 'due'
|
|
local rk = cfg.recur_syntax or 'rec'
|
|
|
|
if token == '+!' then
|
|
return 'priority', 1, nil
|
|
end
|
|
if token == '-!' then
|
|
return 'priority', 0, nil
|
|
end
|
|
if token == '-due' or token == '-' .. dk then
|
|
return 'due', vim.NIL, nil
|
|
end
|
|
if token == '-cat' then
|
|
return 'category', vim.NIL, nil
|
|
end
|
|
if token == '-rec' or token == '-' .. rk then
|
|
return 'recur', vim.NIL, nil
|
|
end
|
|
|
|
local due_val = token:match('^' .. vim.pesc(dk) .. ':(.+)$')
|
|
if due_val then
|
|
local resolved = parse.resolve_date(due_val)
|
|
if resolved then
|
|
return 'due', resolved, nil
|
|
end
|
|
if
|
|
due_val:match('^%d%d%d%d%-%d%d%-%d%d$') or due_val:match('^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d$')
|
|
then
|
|
return 'due', due_val, nil
|
|
end
|
|
return nil,
|
|
nil,
|
|
'Invalid date: ' .. due_val .. '. Use YYYY-MM-DD, today, tomorrow, +Nd, weekday names, etc.'
|
|
end
|
|
|
|
local cat_val = token:match('^cat:(.+)$')
|
|
if cat_val then
|
|
return 'category', cat_val, nil
|
|
end
|
|
|
|
local rec_val = token:match('^' .. vim.pesc(rk) .. ':(.+)$')
|
|
if rec_val then
|
|
local raw_spec = rec_val
|
|
local rec_mode = nil
|
|
if raw_spec:sub(1, 1) == '!' then
|
|
rec_mode = 'completion'
|
|
raw_spec = raw_spec:sub(2)
|
|
end
|
|
if not recur.validate(raw_spec) then
|
|
return nil, nil, 'Invalid recurrence pattern: ' .. rec_val
|
|
end
|
|
return 'recur', { spec = raw_spec, mode = rec_mode }, nil
|
|
end
|
|
|
|
return nil,
|
|
nil,
|
|
'Unknown operation: '
|
|
.. token
|
|
.. '. Valid: '
|
|
.. dk
|
|
.. ':<date>, cat:<name>, '
|
|
.. rk
|
|
.. ':<pattern>, +!, -!, -'
|
|
.. dk
|
|
.. ', -cat, -'
|
|
.. rk
|
|
end
|
|
|
|
---@param id_str string
|
|
---@param rest string
|
|
---@return nil
|
|
function M.edit(id_str, rest)
|
|
if not id_str or id_str == '' then
|
|
vim.notify(
|
|
'Usage: :Pending edit <id> [due:<date>] [cat:<name>] [rec:<pattern>] [+!] [-!] [-due] [-cat] [-rec]',
|
|
vim.log.levels.ERROR
|
|
)
|
|
return
|
|
end
|
|
|
|
local id = tonumber(id_str)
|
|
if not id then
|
|
vim.notify('Invalid task ID: ' .. id_str, vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
store.load()
|
|
local task = store.get(id)
|
|
if not task then
|
|
vim.notify('No task with ID ' .. id .. '.', vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
if not rest or rest == '' then
|
|
vim.notify(
|
|
'Usage: :Pending edit <id> [due:<date>] [cat:<name>] [rec:<pattern>] [+!] [-!] [-due] [-cat] [-rec]',
|
|
vim.log.levels.ERROR
|
|
)
|
|
return
|
|
end
|
|
|
|
local tokens = {}
|
|
for tok in rest:gmatch('%S+') do
|
|
table.insert(tokens, tok)
|
|
end
|
|
|
|
local updates = {}
|
|
local feedback = {}
|
|
|
|
for _, tok in ipairs(tokens) do
|
|
local field, value, err = parse_edit_token(tok)
|
|
if err then
|
|
vim.notify(err, vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
if field == 'recur' then
|
|
if value == vim.NIL then
|
|
updates.recur = vim.NIL
|
|
updates.recur_mode = vim.NIL
|
|
table.insert(feedback, 'recurrence removed')
|
|
else
|
|
updates.recur = value.spec
|
|
updates.recur_mode = value.mode
|
|
table.insert(feedback, 'recurrence set to ' .. value.spec)
|
|
end
|
|
elseif field == 'due' then
|
|
if value == vim.NIL then
|
|
updates.due = vim.NIL
|
|
table.insert(feedback, 'due date removed')
|
|
else
|
|
updates.due = value
|
|
table.insert(feedback, 'due date set to ' .. tostring(value))
|
|
end
|
|
elseif field == 'category' then
|
|
if value == vim.NIL then
|
|
updates.category = vim.NIL
|
|
table.insert(feedback, 'category removed')
|
|
else
|
|
updates.category = value
|
|
table.insert(feedback, 'category set to ' .. tostring(value))
|
|
end
|
|
elseif field == 'priority' then
|
|
updates.priority = value
|
|
table.insert(feedback, value == 1 and 'priority added' or 'priority removed')
|
|
end
|
|
end
|
|
|
|
local snapshot = store.snapshot()
|
|
local stack = store.undo_stack()
|
|
table.insert(stack, snapshot)
|
|
if #stack > UNDO_MAX then
|
|
table.remove(stack, 1)
|
|
end
|
|
|
|
store.update(id, updates)
|
|
store.save()
|
|
|
|
local bufnr = buffer.bufnr()
|
|
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
|
buffer.render(bufnr)
|
|
end
|
|
|
|
vim.notify('Task #' .. id .. ' updated: ' .. table.concat(feedback, ', '))
|
|
end
|
|
|
|
---@param args string
|
|
---@return nil
|
|
function M.command(args)
|
|
if not args or args == '' then
|
|
M.open()
|
|
return
|
|
end
|
|
local cmd, rest = args:match('^(%S+)%s*(.*)')
|
|
if cmd == 'add' then
|
|
M.add(rest)
|
|
elseif cmd == 'edit' then
|
|
local id_str, edit_rest = rest:match('^(%S+)%s*(.*)')
|
|
M.edit(id_str, edit_rest)
|
|
elseif cmd == 'sync' then
|
|
M.sync()
|
|
elseif cmd == 'archive' then
|
|
local d = rest ~= '' and tonumber(rest) or nil
|
|
M.archive(d)
|
|
elseif cmd == 'due' then
|
|
M.due()
|
|
elseif cmd == 'undo' then
|
|
M.undo_write()
|
|
else
|
|
vim.notify('Unknown Pending subcommand: ' .. cmd, vim.log.levels.ERROR)
|
|
end
|
|
end
|
|
|
|
return M
|