Compare commits

..

No commits in common. "be81abfe48e59d9d6ff6b7acda6afbd351c4cbc7" and "b08d01fe787adac9e9af7a865c7191dcc0661028" have entirely different histories.

14 changed files with 183 additions and 675 deletions

View file

@ -30,16 +30,13 @@ concealed tokens and are never visible during editing.
Features: ~
- Oil-style buffer editing: standard Vim motions for all task operations
- Inline metadata syntax: `due:`, `cat:`, and `rec:` tokens parsed on `:w`
- Relative date input: `today`, `tomorrow`, `+Nd`, `+Nw`, `+Nm`, weekday
names, month names, ordinals, and more
- Recurring tasks with automatic next-date spawning on completion
- Inline metadata syntax: `due:` and `cat:` tokens parsed on `:w`
- Relative date input: `today`, `tomorrow`, `+Nd`, weekday names
- Two views: category (default) and priority flat list
- Multi-level undo (up to 20 `:w` saves, session-only)
- Quick-add from the command line with `:Pending add`
- Quickfix list of overdue/due-today tasks via `:Pending due`
- Foldable category sections (`zc`/`zo`) in category view
- Omnifunc completion for `cat:`, `due:`, and `rec:` tokens (`<C-x><C-o>`)
- Google Calendar one-way push via OAuth PKCE
==============================================================================
@ -98,18 +95,20 @@ parsed from the right and consumed until a non-metadata token is reached.
Supported tokens: ~
`due:YYYY-MM-DD` Set a due date using an absolute date.
`due:<name>` Resolve a named date (see |pending-dates| below).
`due:today` Resolve to today's date.
`due:tomorrow` Resolve to tomorrow's date.
`due:+Nd` Resolve to N days from today (e.g. `due:+3d`).
`due:mon` Resolve to the next occurrence of that weekday.
Supported: `sun` `mon` `tue` `wed` `thu` `fri` `sat`
`cat:Name` Move the task to the named category on save.
`rec:<pattern>` Set a recurrence rule (see |pending-recurrence|).
The token name for due dates defaults to `due` and is configurable via
`date_syntax` in |pending-config|. The token name for recurrence defaults to
`rec` and is configurable via `recur_syntax`.
`date_syntax` in |pending-config|. If `date_syntax` is set to `by`, write
`by:2026-03-15` instead.
Example: >
Buy milk due:2026-03-15 cat:Errands
Take out trash due:monday rec:weekly
<
On `:w`, the description becomes `Buy milk`, the due date is stored as
@ -117,87 +116,8 @@ On `:w`, the description becomes `Buy milk`, the due date is stored as
placed under the `Errands` category header.
Parsing stops at the first token that is not a recognised metadata token.
Repeated tokens of the same type also stop parsing — only one `due:`, one
`cat:`, and one `rec:` per task line are consumed.
Omnifunc completion is available for all three token types. In insert mode,
type `due:`, `cat:`, or `rec:` and press `<C-x><C-o>` to see suggestions.
==============================================================================
DATE INPUT *pending-dates*
Named dates can be used anywhere a date is accepted: the `due:` inline
token, the `D` prompt, and `:Pending add`.
Token Resolves to ~
----- -----------
`today` Today's date
`tomorrow` Tomorrow's date
`yesterday` Yesterday's date
`eod` Today (end of day semantics)
`+Nd` N days from today (e.g. `+3d`)
`+Nw` N weeks from today (e.g. `+2w`)
`+Nm` N months from today (e.g. `+1m`)
`-Nd` N days ago (e.g. `-2d`)
`-Nw` N weeks ago (e.g. `-1w`)
`mon``sun` Next occurrence of that weekday
`jan``dec` 1st of next occurrence of that month
`1st``31st` Next occurrence of that day-of-month
`sow` / `eow` Monday / Sunday of current week
`som` / `eom` First / last day of current month
`soq` / `eoq` First / last day of current quarter
`soy` / `eoy` January 1 / December 31 of current year
`later` / `someday` Sentinel date (default: `9999-12-30`)
==============================================================================
RECURRENCE *pending-recurrence*
Tasks can recur on a schedule. Add a `rec:` token to set recurrence: >
- [ ] Take out trash due:monday rec:weekly
- [ ] Pay rent due:2026-03-01 rec:monthly
- [ ] Standup due:tomorrow rec:weekdays
<
When a recurring task is marked done with `<CR>`:
1. The current task stays as done (preserving history).
2. A new pending task is created with the same description, category,
priority, and recurrence — with the due date advanced to the next
occurrence.
Shorthand patterns: ~
Pattern Meaning ~
------- -------
`daily` Every day
`weekdays` Monday through Friday
`weekly` Every week
`biweekly` Every 2 weeks (alias: `2w`)
`monthly` Every month
`quarterly` Every 3 months (alias: `3m`)
`yearly` Every year (alias: `annual`)
`Nd` Every N days (e.g. `3d`)
`Nw` Every N weeks (e.g. `2w`)
`Nm` Every N months (e.g. `6m`)
`Ny` Every N years (e.g. `2y`)
For patterns the shorthand cannot express, use a raw RRULE fragment: >
rec:FREQ=MONTHLY;BYDAY=1MO
<
Completion-based recurrence: ~ *pending-recur-completion*
By default, recurrence is schedule-based: the next due date advances from the
original schedule, skipping to the next future occurrence. Prefix the pattern
with `!` for completion-based mode, where the next due date advances from the
completion date: >
rec:!weekly
<
Schedule-based is like org-mode `++`; completion-based is like `.+`.
Google Calendar: ~
Recurrence patterns map directly to iCalendar RRULE strings for future GCal
sync support. Completion-based recurrence cannot be synced (it is inherently
local).
Repeated tokens of the same type also stop parsing — only one `due:` and one
`cat:` per task line are consumed.
==============================================================================
COMMANDS *pending-commands*
@ -215,7 +135,6 @@ COMMANDS *pending-commands*
:Pending add Buy groceries due:2026-03-15
:Pending add School: Submit homework
:Pending add Errands: Pick up dry cleaning due:fri
:Pending add Work: standup due:tomorrow rec:weekdays
<
If the buffer is currently open it is re-rendered after the add.
@ -250,34 +169,27 @@ MAPPINGS *pending-mappings*
The following keys are set buffer-locally when the task buffer opens. They
are active only in the `pending://` buffer.
Buffer-local keys are configured via the `keymaps` table in |pending-config|.
The defaults are shown below. Set any key to `false` to disable it.
Default buffer-local keys: ~
Buffer-local keys: ~
Key Action ~
------- ------------------------------------------------
`q` Close the task buffer (`close`)
`<CR>` Toggle complete / uncomplete (`toggle`)
`!` Toggle the priority flag (`priority`)
`D` Prompt for a due date (`date`)
`<Tab>` Switch between category / priority view (`view`)
`U` Undo the last `:w` save (`undo`)
`o` Insert a new task line below (`open_line`)
`O` Insert a new task line above (`open_line_above`)
`<CR>` Toggle complete / uncomplete the task at cursor
`!` Toggle the priority flag on the task at cursor
`D` Prompt for a due date on the task at cursor
`<Tab>` Switch between category view and priority view
`U` Undo the last `:w` save
`g?` Show a help popup with available keys
`zc` Fold the current category section (category view only)
`zo` Unfold the current category section (category view only)
`dd`, `p`, `P`, and `:w` work as standard Vim operations.
`o` and `O` are overridden to insert a correctly-formatted blank task line
at the position below or above the cursor rather than using standard Vim
indentation. `dd`, `p`, `P`, and `:w` work as expected.
*<Plug>(pending-open)*
<Plug>(pending-open)
Open the task buffer. Maps to |:Pending| with no arguments.
*<Plug>(pending-close)*
<Plug>(pending-close)
Close the task buffer window.
*<Plug>(pending-toggle)*
<Plug>(pending-toggle)
Toggle complete / uncomplete for the task under the cursor.
@ -294,18 +206,6 @@ Default buffer-local keys: ~
<Plug>(pending-view)
Switch between category view and priority view.
*<Plug>(pending-undo)*
<Plug>(pending-undo)
Undo the last `:w` save.
*<Plug>(pending-open-line)*
<Plug>(pending-open-line)
Insert a correctly-formatted blank task line below the cursor.
*<Plug>(pending-open-line-above)*
<Plug>(pending-open-line-above)
Insert a correctly-formatted blank task line above the cursor.
Example configuration: >lua
vim.keymap.set('n', '<leader>t', '<Plug>(pending-open)')
vim.keymap.set('n', '<leader>T', '<Plug>(pending-toggle)')
@ -342,19 +242,7 @@ loads: >lua
default_category = 'Inbox',
date_format = '%b %d',
date_syntax = 'due',
recur_syntax = 'rec',
someday_date = '9999-12-30',
category_order = {},
keymaps = {
close = 'q',
toggle = '<CR>',
view = '<Tab>',
priority = '!',
date = 'D',
undo = 'U',
open_line = 'o',
open_line_above = 'O',
},
gcal = {
calendar = 'Tasks',
credentials_path = '/path/to/client_secret.json',
@ -390,28 +278,12 @@ Fields: ~
this to use a different keyword, for example `'by'`
to write `by:2026-03-15` instead of `due:2026-03-15`.
{recur_syntax} (string, default: 'rec')
The token name for inline recurrence metadata. Change
this to use a different keyword, for example
`'repeat'` to write `repeat:weekly`.
{someday_date} (string, default: '9999-12-30')
The date that `later` and `someday` resolve to. This
acts as a "no date" sentinel for GTD-style workflows.
{category_order} (string[], default: {})
Ordered list of category names. In category view,
categories that appear in this list are shown in the
given order. Categories not in the list are appended
after the ordered ones in their natural order.
{keymaps} (table, default: see below) *pending.Keymaps*
Buffer-local key bindings. Each field maps an action
name to a key string. Set a field to `false` to
disable that binding. Unset fields use the default.
See |pending-mappings| for the full list of actions
and their default keys.
{gcal} (table, default: nil)
Google Calendar sync configuration. See
|pending.GcalConfig|. Omit this field entirely to
@ -499,11 +371,6 @@ PendingDone Applied to the text of completed tasks.
PendingPriority Applied to the `! ` priority marker on priority tasks.
Default: links to `DiagnosticWarn`.
*PendingRecur*
PendingRecur Applied to the recurrence indicator virtual text shown
alongside due dates for recurring tasks.
Default: links to `DiagnosticInfo`.
To override a group in your colorscheme or config: >lua
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
<
@ -521,7 +388,6 @@ Checks performed: ~
category, date format, date syntax)
- Whether the data directory exists (warning if not yet created)
- Whether the data file exists and can be parsed; reports total task count
- Validates recurrence specs on stored tasks
- Whether `curl` is available (required for Google Calendar sync)
- Whether `openssl` is available (required for OAuth PKCE)
@ -548,8 +414,6 @@ Task fields: ~
{category} (string) Category name. Defaults to `default_category`.
{priority} (integer) `1` for priority tasks, `0` otherwise.
{due} (string) ISO date string `YYYY-MM-DD`, or absent.
{recur} (string) Recurrence shorthand (e.g. `weekly`), or absent.
{recur_mode} (string) `'scheduled'` or `'completion'`, or absent.
{entry} (string) ISO 8601 UTC timestamp of creation.
{modified} (string) ISO 8601 UTC timestamp of last modification.
{end} (string) ISO 8601 UTC timestamp of completion or deletion.

View file

@ -55,7 +55,6 @@ local function set_buf_options(bufnr)
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].filetype = 'pending'
vim.bo[bufnr].modifiable = true
vim.bo[bufnr].omnifunc = 'v:lua.require("pending.complete").omnifunc'
end
---@param winid integer
@ -123,22 +122,24 @@ local function apply_extmarks(bufnr, line_meta)
local row = i - 1
if m.type == 'task' then
local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue'
local virt_parts = {}
if m.show_category and m.category then
table.insert(virt_parts, { m.category, 'PendingHeader' })
end
if m.recur then
table.insert(virt_parts, { '\u{21bb} ' .. m.recur, 'PendingRecur' })
end
if m.due then
table.insert(virt_parts, { m.due, due_hl })
end
if #virt_parts > 0 then
for p = 1, #virt_parts - 1 do
virt_parts[p][1] = virt_parts[p][1] .. ' '
if m.show_category then
local virt_text
if m.category and m.due then
virt_text = { { m.category .. ' ', 'PendingHeader' }, { m.due, due_hl } }
elseif m.category then
virt_text = { { m.category, 'PendingHeader' } }
elseif m.due then
virt_text = { { m.due, due_hl } }
end
if virt_text then
vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, {
virt_text = virt_text,
virt_text_pos = 'eol',
})
end
elseif m.due then
vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, {
virt_text = virt_parts,
virt_text = { { m.due, due_hl } },
virt_text_pos = 'eol',
})
end
@ -166,7 +167,6 @@ local function setup_highlights()
vim.api.nvim_set_hl(0, 'PendingOverdue', { link = 'DiagnosticError', default = true })
vim.api.nvim_set_hl(0, 'PendingDone', { link = 'Comment', default = true })
vim.api.nvim_set_hl(0, 'PendingPriority', { link = 'DiagnosticWarn', default = true })
vim.api.nvim_set_hl(0, 'PendingRecur', { link = 'DiagnosticInfo', default = true })
end
local function snapshot_folds(bufnr)

View file

@ -1,138 +0,0 @@
local config = require('pending.config')
---@class pending.complete
local M = {}
---@return string
local function date_key()
return config.get().date_syntax or 'due'
end
---@return string
local function recur_key()
return config.get().recur_syntax or 'rec'
end
---@return string[]
local function get_categories()
local store = require('pending.store')
local seen = {}
local result = {}
for _, task in ipairs(store.active_tasks()) do
local cat = task.category
if cat and not seen[cat] then
seen[cat] = true
table.insert(result, cat)
end
end
table.sort(result)
return result
end
---@return string[]
local function date_completions()
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 recur_completions()
local recur = require('pending.recur')
local list = recur.shorthand_list()
local result = {}
for _, s in ipairs(list) do
table.insert(result, s)
end
for _, s in ipairs(list) do
table.insert(result, '!' .. s)
end
return result
end
---@type string?
local _complete_source = nil
---@param findstart integer
---@param base string
---@return integer|table[]
function M.omnifunc(findstart, base)
if findstart == 1 then
local line = vim.api.nvim_get_current_line()
local col = vim.api.nvim_win_get_cursor(0)[2]
local before = line:sub(1, col)
local dk = date_key()
local rk = recur_key()
local checks = {
{ vim.pesc(dk) .. ':([%S]*)$', dk },
{ 'cat:([%S]*)$', 'cat' },
{ vim.pesc(rk) .. ':([%S]*)$', rk },
}
for _, check in ipairs(checks) do
local start = before:find(check[1])
if start then
local colon_pos = before:find(':', start, true)
if colon_pos then
_complete_source = check[2]
return colon_pos
end
end
end
_complete_source = nil
return -1
end
local candidates = {}
local source = _complete_source or ''
local dk = date_key()
local rk = recur_key()
if source == dk then
candidates = date_completions()
elseif source == 'cat' then
candidates = get_categories()
elseif source == rk then
candidates = recur_completions()
end
local matches = {}
for _, c in ipairs(candidates) do
if base == '' or c:sub(1, #base) == base then
table.insert(matches, { word = c, menu = '[' .. source .. ']' })
end
end
return matches
end
return M

View file

@ -2,16 +2,6 @@
---@field calendar? string
---@field credentials_path? string
---@class pending.Keymaps
---@field close? string|false
---@field toggle? string|false
---@field view? string|false
---@field priority? string|false
---@field date? string|false
---@field undo? string|false
---@field open_line? string|false
---@field open_line_above? string|false
---@class pending.Config
---@field data_path string
---@field default_view 'category'|'priority'
@ -22,7 +12,6 @@
---@field someday_date string
---@field category_order? string[]
---@field drawer_height? integer
---@field keymaps pending.Keymaps
---@field gcal? pending.GcalConfig
---@class pending.config
@ -38,16 +27,6 @@ local defaults = {
recur_syntax = 'rec',
someday_date = '9999-12-30',
category_order = {},
keymaps = {
close = 'q',
toggle = '<CR>',
view = '<Tab>',
priority = '!',
date = 'D',
undo = 'U',
open_line = 'o',
open_line_above = 'O',
},
}
---@type pending.Config?

View file

@ -27,17 +27,6 @@ function M.check()
if load_ok then
local tasks = store.tasks()
vim.health.ok('Data file loaded: ' .. #tasks .. ' tasks')
local recur = require('pending.recur')
local invalid_count = 0
for _, task in ipairs(tasks) do
if task.recur and not recur.validate(task.recur) then
invalid_count = invalid_count + 1
vim.health.warn('Task ' .. task.id .. ' has invalid recurrence spec: ' .. task.recur)
end
end
if invalid_count == 0 then
vim.health.ok('All recurrence specs are valid')
end
else
vim.health.error('Failed to load data file: ' .. tostring(err))
end

View file

@ -50,44 +50,37 @@ end
---@param bufnr integer
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
vim.keymap.set('n', 'q', function()
buffer.close()
end, opts)
vim.keymap.set('n', '<Esc>', function()
buffer.close()
end, opts)
vim.keymap.set('n', '<CR>', function()
M.toggle_complete()
end, opts)
vim.keymap.set('n', '<Tab>', function()
buffer.toggle_view()
end, opts)
vim.keymap.set('n', 'g?', function()
M.show_help()
end, opts)
vim.keymap.set('n', '!', function()
M.toggle_priority()
end, opts)
vim.keymap.set('n', 'D', function()
M.prompt_date()
end, opts)
vim.keymap.set('n', 'U', function()
M.undo_write()
end, opts)
vim.keymap.set('n', 'o', function()
buffer.open_line(false)
end, opts)
vim.keymap.set('n', 'O', function()
buffer.open_line(true)
end, opts)
end
---@param bufnr integer
@ -137,8 +130,7 @@ function M.toggle_complete()
if task.recur and task.due then
local recur = require('pending.recur')
local mode = task.recur_mode or 'scheduled'
local base = mode == 'completion' and os.date('%Y-%m-%d') --[[@as string]]
or task.due
local base = mode == 'completion' and os.date('%Y-%m-%d') --[[@as string]] or task.due
local next_date = recur.next_due(base, task.recur, mode)
store.add({
description = task.description,
@ -341,6 +333,75 @@ function M.due()
vim.cmd('copen')
end
function M.show_help()
local cfg = require('pending.config').get()
local dk = cfg.date_syntax or 'due'
local rk = cfg.recur_syntax or 'rec'
local lines = {
'pending.nvim keybindings',
'',
'<CR> Toggle complete/uncomplete',
'<Tab> Switch category/priority view',
'! Toggle urgent',
'D Set due date',
'U Undo last write',
'o / O Add new task line',
'dd Delete task line (on :w)',
'p / P Paste (duplicates get new IDs)',
'zc / zo Fold/unfold category (category view)',
':w Save all changes',
'',
':Pending add <text> Quick-add task',
':Pending add Cat: <text> Quick-add with category',
':Pending due Show overdue/due qflist',
':Pending sync Push to Google Calendar',
':Pending archive [days] Purge old done tasks',
':Pending undo Undo last write',
'',
'Inline metadata (on new lines before :w):',
' ' .. dk .. ':YYYY-MM-DD Set due date',
' cat:Name Set category',
' ' .. rk .. ':pattern Set recurrence',
'',
'Due date input:',
' today, tomorrow, yesterday, +Nd, +Nw, +Nm',
' -Nd, -Nw, mon-sun, jan-dec, 1st-31st',
' eod, eow, eom, eoq, eoy, sow, som, soq, soy',
' later, someday',
' Empty input clears due date',
'',
'Recurrence patterns:',
' daily, weekdays, weekly, biweekly',
' monthly, quarterly, yearly, Nd, Nw, Nm, Ny',
' Prefix ! for completion-based (e.g. !weekly)',
'',
'Completion: <C-x><C-o> after due:, cat:, rec:',
'',
'Press q or <Esc> to close',
}
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
vim.bo[buf].bufhidden = 'wipe'
local width = 54
local height = #lines
local win = vim.api.nvim_open_win(buf, true, {
relative = 'editor',
width = width,
height = height,
col = math.floor((vim.o.columns - width) / 2),
row = math.floor((vim.o.lines - height) / 2),
style = 'minimal',
border = 'rounded',
})
vim.keymap.set('n', 'q', function()
vim.api.nvim_win_close(win, true)
end, { buffer = buf, silent = true })
vim.keymap.set('n', '<Esc>', function()
vim.api.nvim_win_close(win, true)
end, { buffer = buf, silent = true })
end
---@param args string
function M.command(args)
if not args or args == '' then

View file

@ -45,18 +45,9 @@ local weekday_map = {
}
local month_map = {
jan = 1,
feb = 2,
mar = 3,
apr = 4,
may = 5,
jun = 6,
jul = 7,
aug = 8,
sep = 9,
oct = 10,
nov = 11,
dec = 12,
jan = 1, feb = 2, mar = 3, apr = 4,
may = 5, jun = 6, jul = 7, aug = 8,
sep = 9, oct = 10, nov = 11, dec = 12,
}
---@param today osdate
@ -106,31 +97,49 @@ function M.resolve_date(text)
end
if lower == 'som' then
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = 1 })) --[[@as string]]
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month, day = 1 })
) --[[@as string]]
end
if lower == 'eom' then
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 })) --[[@as string]]
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = today.month + 1, day = 0 })
) --[[@as string]]
end
if lower == 'soq' then
local q = math.ceil(today.month / 3)
local first_month = (q - 1) * 3 + 1
return os.date('%Y-%m-%d', os.time({ year = today.year, month = first_month, day = 1 })) --[[@as string]]
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = first_month, day = 1 })
) --[[@as string]]
end
if lower == 'eoq' then
local q = math.ceil(today.month / 3)
local last_month = q * 3
return os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 })) --[[@as string]]
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = last_month + 1, day = 0 })
) --[[@as string]]
end
if lower == 'soy' then
return os.date('%Y-%m-%d', os.time({ year = today.year, month = 1, day = 1 })) --[[@as string]]
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = 1, day = 1 })
) --[[@as string]]
end
if lower == 'eoy' then
return os.date('%Y-%m-%d', os.time({ year = today.year, month = 12, day = 31 })) --[[@as string]]
return os.date(
'%Y-%m-%d',
os.time({ year = today.year, month = 12, day = 31 })
) --[[@as string]]
end
if lower == 'later' or lower == 'someday' then
@ -144,9 +153,7 @@ function M.resolve_date(text)
os.time({
year = today.year,
month = today.month,
day = today.day + (
tonumber(n) --[[@as integer]]
),
day = today.day + (tonumber(n) --[[@as integer]]),
})
) --[[@as string]]
end
@ -158,9 +165,7 @@ function M.resolve_date(text)
os.time({
year = today.year,
month = today.month,
day = today.day + (
tonumber(n) --[[@as integer]]
) * 7,
day = today.day + (tonumber(n) --[[@as integer]]) * 7,
})
) --[[@as string]]
end
@ -171,9 +176,7 @@ function M.resolve_date(text)
'%Y-%m-%d',
os.time({
year = today.year,
month = today.month + (
tonumber(n) --[[@as integer]]
),
month = today.month + (tonumber(n) --[[@as integer]]),
day = today.day,
})
) --[[@as string]]
@ -186,9 +189,7 @@ function M.resolve_date(text)
os.time({
year = today.year,
month = today.month,
day = today.day - (
tonumber(n) --[[@as integer]]
),
day = today.day - (tonumber(n) --[[@as integer]]),
})
) --[[@as string]]
end
@ -200,9 +201,7 @@ function M.resolve_date(text)
os.time({
year = today.year,
month = today.month,
day = today.day - (
tonumber(n) --[[@as integer]]
) * 7,
day = today.day - (tonumber(n) --[[@as integer]]) * 7,
})
) --[[@as string]]
end
@ -244,7 +243,10 @@ function M.resolve_date(text)
if today.month >= target_month then
y = y + 1
end
return os.date('%Y-%m-%d', os.time({ year = y, month = target_month, day = 1 })) --[[@as string]]
return os.date(
'%Y-%m-%d',
os.time({ year = y, month = target_month, day = 1 })
) --[[@as string]]
end
local target_wday = weekday_map[lower]

View file

@ -3,7 +3,6 @@
---@field interval integer
---@field byday? string[]
---@field from_completion boolean
---@field _raw? string
---@class pending.recur
local M = {}
@ -11,12 +10,7 @@ local M = {}
---@type table<string, pending.RecurSpec>
local named = {
daily = { freq = 'daily', interval = 1, from_completion = false },
weekdays = {
freq = 'weekly',
interval = 1,
byday = { 'MO', 'TU', 'WE', 'TH', 'FR' },
from_completion = false,
},
weekdays = { freq = 'weekly', interval = 1, byday = { 'MO', 'TU', 'WE', 'TH', 'FR' }, from_completion = false },
weekly = { freq = 'weekly', interval = 1, from_completion = false },
biweekly = { freq = 'weekly', interval = 2, from_completion = false },
monthly = { freq = 'monthly', interval = 1, from_completion = false },
@ -102,12 +96,12 @@ local function advance_date(base_date, freq, interval)
new_y = new_y + 1
end
local last_day = os.date('*t', os.time({ year = new_y, month = new_m + 1, day = 0 })) --[[@as osdate]]
local clamped_d = math.min(dn, last_day.day --[[@as integer]])
local clamped_d = math.min(dn, last_day.day)
return os.date('%Y-%m-%d', os.time({ year = new_y, month = new_m, day = clamped_d })) --[[@as string]]
elseif freq == 'yearly' then
local new_y = yn + interval
local last_day = os.date('*t', os.time({ year = new_y, month = mn + 1, day = 0 })) --[[@as osdate]]
local clamped_d = math.min(dn, last_day.day --[[@as integer]])
local clamped_d = math.min(dn, last_day.day)
return os.date('%Y-%m-%d', os.time({ year = new_y, month = mn, day = clamped_d })) --[[@as string]]
end
return base_date

View file

@ -10,7 +10,6 @@ local config = require('pending.config')
---@field overdue? boolean
---@field show_category? boolean
---@field priority? integer
---@field recur? string
---@class pending.views
local M = {}
@ -150,7 +149,6 @@ function M.category_view(tasks)
status = task.status,
category = cat,
overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil,
recur = task.recur,
})
end
end
@ -202,7 +200,6 @@ function M.priority_view(tasks)
category = task.category,
overdue = task.status == 'pending' and task.due ~= nil and task.due < today or nil,
show_category = true,
recur = task.recur,
})
end

View file

@ -22,10 +22,6 @@ 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)
@ -41,15 +37,3 @@ 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)

View file

@ -1,171 +0,0 @@
require('spec.helpers')
local config = require('pending.config')
local store = require('pending.store')
describe('complete', function()
local tmpdir
local complete = require('pending.complete')
before_each(function()
tmpdir = vim.fn.tempname()
vim.fn.mkdir(tmpdir, 'p')
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
config.reset()
store.unload()
store.load()
end)
after_each(function()
vim.fn.delete(tmpdir, 'rf')
vim.g.pending = nil
config.reset()
end)
describe('findstart', function()
it('returns column after colon for cat: prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat:Wo' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 })
local result = complete.omnifunc(1, '')
assert.are.equal(15, result)
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('returns column after colon for due: prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task due:to' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 })
local result = complete.omnifunc(1, '')
assert.are.equal(15, result)
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('returns column after colon for rec: prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task rec:we' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 })
local result = complete.omnifunc(1, '')
assert.are.equal(15, result)
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('returns -1 for non-token position', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] some task ' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 14 })
local result = complete.omnifunc(1, '')
assert.are.equal(-1, result)
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
end)
describe('completions', function()
it('returns existing categories for cat:', function()
store.add({ description = 'A', category = 'Work' })
store.add({ description = 'B', category = 'Home' })
store.add({ description = 'C', category = 'Work' })
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat: x' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 })
complete.omnifunc(1, '')
local result = complete.omnifunc(0, '')
local words = {}
for _, item in ipairs(result) do
table.insert(words, item.word)
end
assert.is_true(vim.tbl_contains(words, 'Work'))
assert.is_true(vim.tbl_contains(words, 'Home'))
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('filters categories by base', function()
store.add({ description = 'A', category = 'Work' })
store.add({ description = 'B', category = 'Home' })
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat:W' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 })
complete.omnifunc(1, '')
local result = complete.omnifunc(0, 'W')
assert.are.equal(1, #result)
assert.are.equal('Work', result[1].word)
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('returns named dates for due:', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task due: x' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 })
complete.omnifunc(1, '')
local result = complete.omnifunc(0, '')
assert.is_true(#result > 0)
local words = {}
for _, item in ipairs(result) do
table.insert(words, item.word)
end
assert.is_true(vim.tbl_contains(words, 'today'))
assert.is_true(vim.tbl_contains(words, 'tomorrow'))
assert.is_true(vim.tbl_contains(words, 'eom'))
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('filters dates by base prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task due:to' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 })
complete.omnifunc(1, '')
local result = complete.omnifunc(0, 'to')
local words = {}
for _, item in ipairs(result) do
table.insert(words, item.word)
end
assert.is_true(vim.tbl_contains(words, 'today'))
assert.is_true(vim.tbl_contains(words, 'tomorrow'))
assert.is_false(vim.tbl_contains(words, 'eom'))
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('returns recurrence shorthands for rec:', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task rec: x' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 })
complete.omnifunc(1, '')
local result = complete.omnifunc(0, '')
assert.is_true(#result > 0)
local words = {}
for _, item in ipairs(result) do
table.insert(words, item.word)
end
assert.is_true(vim.tbl_contains(words, 'daily'))
assert.is_true(vim.tbl_contains(words, 'weekly'))
assert.is_true(vim.tbl_contains(words, '!weekly'))
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
it('filters recurrence by base prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task rec:we' })
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 })
complete.omnifunc(1, '')
local result = complete.omnifunc(0, 'we')
local words = {}
for _, item in ipairs(result) do
table.insert(words, item.word)
end
assert.is_true(vim.tbl_contains(words, 'weekly'))
assert.is_true(vim.tbl_contains(words, 'weekdays'))
assert.is_false(vim.tbl_contains(words, 'daily'))
vim.api.nvim_buf_delete(bufnr, { force = true })
end)
end)
end)

View file

@ -193,8 +193,7 @@ describe('parse', function()
it('returns last day of current month for eom', function()
local today = os.date('*t') --[[@as osdate]]
local expected =
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 }))
local expected = os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 }))
local result = parse.resolve_date('eom')
assert.are.equal(expected, result)
end)
@ -212,8 +211,7 @@ describe('parse', function()
local today = os.date('*t') --[[@as osdate]]
local q = math.ceil(today.month / 3)
local last_month = q * 3
local expected =
os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 }))
local expected = os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 }))
local result = parse.resolve_date('eoq')
assert.are.equal(expected, result)
end)

View file

@ -151,14 +151,11 @@ describe('recur', function()
it('completion mode advances from today', function()
local today = os.date('*t') --[[@as osdate]]
local expected = os.date(
'%Y-%m-%d',
os.time({
year = today.year,
month = today.month,
day = today.day + 7,
})
)
local expected = os.date('%Y-%m-%d', os.time({
year = today.year,
month = today.month,
day = today.day + 7,
}))
local result = recur.next_due('2020-01-01', 'weekly', 'completion')
assert.are.equal(expected, result)
end)

View file

@ -204,30 +204,6 @@ describe('views', function()
assert.is_falsy(task_meta.overdue)
end)
it('includes recur in LineMeta for recurring tasks', function()
store.add({ description = 'Recurring', category = 'Inbox', recur = 'weekly' })
local _, meta = views.category_view(store.active_tasks())
local task_meta
for _, m in ipairs(meta) do
if m.type == 'task' then
task_meta = m
end
end
assert.are.equal('weekly', task_meta.recur)
end)
it('has nil recur in LineMeta for non-recurring tasks', function()
store.add({ description = 'Normal', category = 'Inbox' })
local _, meta = views.category_view(store.active_tasks())
local task_meta
for _, m in ipairs(meta) do
if m.type == 'task' then
task_meta = m
end
end
assert.is_nil(task_meta.recur)
end)
it('respects category_order when set', function()
vim.g.pending = { data_path = tmpdir .. '/tasks.json', category_order = { 'Work', 'Inbox' } }
config.reset()
@ -423,29 +399,5 @@ describe('views', function()
end
assert.is_falsy(task_meta.overdue)
end)
it('includes recur in LineMeta for recurring tasks', function()
store.add({ description = 'Recurring', category = 'Inbox', recur = 'daily' })
local _, meta = views.priority_view(store.active_tasks())
local task_meta
for _, m in ipairs(meta) do
if m.type == 'task' then
task_meta = m
end
end
assert.are.equal('daily', task_meta.recur)
end)
it('has nil recur in LineMeta for non-recurring tasks', function()
store.add({ description = 'Normal', category = 'Inbox' })
local _, meta = views.priority_view(store.active_tasks())
local task_meta
for _, m in ipairs(meta) do
if m.type == 'task' then
task_meta = m
end
end
assert.is_nil(task_meta.recur)
end)
end)
end)