Compare commits
No commits in common. "ca4f9a384cb7f8c3d9429fddcbea8570638f2592" and "0e0568769d1967a5cee9a2cde0f4c9d50f100464" have entirely different histories.
ca4f9a384c
...
0e0568769d
12 changed files with 646 additions and 15 deletions
|
|
@ -30,7 +30,7 @@ concealed tokens and are never visible during editing.
|
||||||
|
|
||||||
Features: ~
|
Features: ~
|
||||||
- Oil-style buffer editing: standard Vim motions for all task operations
|
- Oil-style buffer editing: standard Vim motions for all task operations
|
||||||
- Inline metadata syntax: `due:`, `cat:`, and `rec:` tokens parsed on `:w`
|
- Inline metadata syntax: `due:`, `cat:`, `rec:`, and `file:` tokens parsed on `:w`
|
||||||
- Relative date input: `today`, `tomorrow`, `+Nd`, `+Nw`, `+Nm`, weekday
|
- Relative date input: `today`, `tomorrow`, `+Nd`, `+Nw`, `+Nm`, weekday
|
||||||
names, month names, ordinals, and more
|
names, month names, ordinals, and more
|
||||||
- Recurring tasks with automatic next-date spawning on completion
|
- Recurring tasks with automatic next-date spawning on completion
|
||||||
|
|
@ -101,6 +101,7 @@ Supported tokens: ~
|
||||||
`due:<name>` Resolve a named date (see |pending-dates| below).
|
`due:<name>` Resolve a named date (see |pending-dates| below).
|
||||||
`cat:Name` Move the task to the named category on save.
|
`cat:Name` Move the task to the named category on save.
|
||||||
`rec:<pattern>` Set a recurrence rule (see |pending-recurrence|).
|
`rec:<pattern>` Set a recurrence rule (see |pending-recurrence|).
|
||||||
|
`file:<path>:<n>` Attach a file reference (see |pending-file-token|).
|
||||||
|
|
||||||
The token name for due dates defaults to `due` and is configurable via
|
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
|
`date_syntax` in |pending-config|. The token name for recurrence defaults to
|
||||||
|
|
@ -118,12 +119,45 @@ placed under the `Errands` category header.
|
||||||
|
|
||||||
Parsing stops at the first token that is not a recognised metadata token.
|
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
|
Repeated tokens of the same type also stop parsing — only one `due:`, one
|
||||||
`cat:`, and one `rec:` per task line are consumed.
|
`cat:`, one `rec:`, and one `file:` per task line are consumed.
|
||||||
|
|
||||||
Omnifunc completion is available for `due:`, `cat:`, and `rec:` token types.
|
Omnifunc completion is available for `due:`, `cat:`, and `rec:` token types.
|
||||||
In insert mode, type the token prefix and press `<C-x><C-o>` to see
|
In insert mode, type the token prefix and press `<C-x><C-o>` to see
|
||||||
suggestions.
|
suggestions.
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
FILE TOKEN *pending-file-token*
|
||||||
|
|
||||||
|
The `file:` inline token attaches a source file reference to a task. The
|
||||||
|
syntax is: >
|
||||||
|
|
||||||
|
file:<relative-path>:<line-number>
|
||||||
|
<
|
||||||
|
|
||||||
|
The path is stored relative to the directory containing the data file. The
|
||||||
|
token is rendered as virtual text at the end of the task line, showing only
|
||||||
|
the basename and line number (e.g. `auth.lua:42`) using the |PendingFile|
|
||||||
|
highlight group.
|
||||||
|
|
||||||
|
Example: >
|
||||||
|
|
||||||
|
Fix null pointer file:src/auth.lua:42
|
||||||
|
Update tests file:spec/parse_spec.lua:100
|
||||||
|
<
|
||||||
|
|
||||||
|
`gf` in normal mode in the task buffer follows the file reference, opening
|
||||||
|
the file and jumping to the specified line. The default key is `gf` and can
|
||||||
|
be changed via the `goto_file` keymap in |pending-config|. Set it to `false`
|
||||||
|
to disable.
|
||||||
|
|
||||||
|
To attach the current file and cursor position to an existing task, invoke
|
||||||
|
|<Plug>(pending-add-here)| from any source file. A `vim.ui.select()` picker
|
||||||
|
lists all active tasks; selecting one records the current file and line.
|
||||||
|
|
||||||
|
To clear a file reference with `:Pending edit`: >vim
|
||||||
|
:Pending edit 5 -file
|
||||||
|
<
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
DATE INPUT *pending-dates*
|
DATE INPUT *pending-dates*
|
||||||
|
|
||||||
|
|
@ -300,6 +334,7 @@ COMMANDS *pending-commands*
|
||||||
:Pending edit 5 due:tomorrow cat:Work +!
|
:Pending edit 5 due:tomorrow cat:Work +!
|
||||||
:Pending edit 5 -due -cat -rec
|
:Pending edit 5 -due -cat -rec
|
||||||
:Pending edit 5 rec:!weekly due:fri
|
:Pending edit 5 rec:!weekly due:fri
|
||||||
|
:Pending edit 5 -file
|
||||||
<
|
<
|
||||||
Operations: ~
|
Operations: ~
|
||||||
`due:<date>` Set due date (accepts all |pending-dates| vocabulary).
|
`due:<date>` Set due date (accepts all |pending-dates| vocabulary).
|
||||||
|
|
@ -310,6 +345,7 @@ COMMANDS *pending-commands*
|
||||||
`-due` Clear due date.
|
`-due` Clear due date.
|
||||||
`-cat` Clear category.
|
`-cat` Clear category.
|
||||||
`-rec` Clear recurrence.
|
`-rec` Clear recurrence.
|
||||||
|
`-file` Clear the attached file reference (see |pending-file-token|).
|
||||||
|
|
||||||
Tab completion is available for IDs, field names, date values, categories,
|
Tab completion is available for IDs, field names, date values, categories,
|
||||||
and recurrence patterns.
|
and recurrence patterns.
|
||||||
|
|
@ -352,6 +388,7 @@ Default buffer-local keys: ~
|
||||||
`U` Undo the last `:w` save (`undo`)
|
`U` Undo the last `:w` save (`undo`)
|
||||||
`o` Insert a new task line below (`open_line`)
|
`o` Insert a new task line below (`open_line`)
|
||||||
`O` Insert a new task line above (`open_line_above`)
|
`O` Insert a new task line above (`open_line_above`)
|
||||||
|
`gf` Open the file attached to the task under the cursor (`goto_file`)
|
||||||
`zc` Fold the current category section (category view only)
|
`zc` Fold the current category section (category view only)
|
||||||
`zo` Unfold the current category section (category view only)
|
`zo` Unfold the current category section (category view only)
|
||||||
|
|
||||||
|
|
@ -453,6 +490,21 @@ All motions support count: `3]]` jumps three headers forward. `]]` and
|
||||||
<Plug>(pending-prev-task)
|
<Plug>(pending-prev-task)
|
||||||
Jump to the previous task line, skipping headers and blanks.
|
Jump to the previous task line, skipping headers and blanks.
|
||||||
|
|
||||||
|
*<Plug>(pending-goto-file)*
|
||||||
|
<Plug>(pending-goto-file)
|
||||||
|
Open the file attached to the task under the cursor. If the cursor is not
|
||||||
|
on a task line, or the task has no file reference, a warning is shown. If
|
||||||
|
the referenced file cannot be read, an error is shown.
|
||||||
|
See |pending-file-token|.
|
||||||
|
|
||||||
|
*<Plug>(pending-add-here)*
|
||||||
|
<Plug>(pending-add-here)
|
||||||
|
Attach the current file and cursor line to an existing task. Invoke from
|
||||||
|
any source file (not the pending buffer itself) to open a picker listing
|
||||||
|
all active tasks. The selected task receives a `file:` reference pointing
|
||||||
|
to the current buffer's file and the cursor's line number.
|
||||||
|
See |pending-file-token|.
|
||||||
|
|
||||||
<Plug>(pending-tab) *<Plug>(pending-tab)*
|
<Plug>(pending-tab) *<Plug>(pending-tab)*
|
||||||
Open the task buffer in a new tab. See |:PendingTab|.
|
Open the task buffer in a new tab. See |:PendingTab|.
|
||||||
|
|
||||||
|
|
@ -553,6 +605,7 @@ loads: >lua
|
||||||
prev_header = '[[',
|
prev_header = '[[',
|
||||||
next_task = ']t',
|
next_task = ']t',
|
||||||
prev_task = '[t',
|
prev_task = '[t',
|
||||||
|
goto_file = 'gf',
|
||||||
},
|
},
|
||||||
sync = {
|
sync = {
|
||||||
gcal = {
|
gcal = {
|
||||||
|
|
@ -615,6 +668,11 @@ Fields: ~
|
||||||
See |pending-mappings| for the full list of actions
|
See |pending-mappings| for the full list of actions
|
||||||
and their default keys.
|
and their default keys.
|
||||||
|
|
||||||
|
{goto_file} (string|false, default: 'gf')
|
||||||
|
Open the file attached to the task under the
|
||||||
|
cursor. Set to `false` to disable. See
|
||||||
|
|pending-file-token|.
|
||||||
|
|
||||||
{debug} (boolean, default: false)
|
{debug} (boolean, default: false)
|
||||||
Enable diagnostic logging. When `true`, textobj
|
Enable diagnostic logging. When `true`, textobj
|
||||||
motions, mapping registration, and cursor jumps
|
motions, mapping registration, and cursor jumps
|
||||||
|
|
@ -631,6 +689,12 @@ Fields: ~
|
||||||
name and the value is the backend-specific config
|
name and the value is the backend-specific config
|
||||||
table. Currently only `gcal` is built-in.
|
table. Currently only `gcal` is built-in.
|
||||||
|
|
||||||
|
{gcal} (table, default: nil)
|
||||||
|
Legacy shorthand for `sync.gcal`. If `gcal` is set
|
||||||
|
but `sync.gcal` is not, the value is migrated
|
||||||
|
automatically. New configs should use `sync.gcal`
|
||||||
|
instead. See |pending.GcalConfig|.
|
||||||
|
|
||||||
{icons} (table) *pending.Icons*
|
{icons} (table) *pending.Icons*
|
||||||
Icon characters displayed in the buffer. Fields:
|
Icon characters displayed in the buffer. Fields:
|
||||||
{pending} Uncompleted task icon. Default: '○'
|
{pending} Uncompleted task icon. Default: '○'
|
||||||
|
|
@ -875,6 +939,9 @@ Configuration: >lua
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
||||||
|
The legacy `gcal` top-level key is still accepted and migrated automatically.
|
||||||
|
New configurations should use `sync.gcal`.
|
||||||
|
|
||||||
*pending.GcalConfig*
|
*pending.GcalConfig*
|
||||||
Fields: ~
|
Fields: ~
|
||||||
{calendar} (string, default: 'Pendings')
|
{calendar} (string, default: 'Pendings')
|
||||||
|
|
@ -975,6 +1042,12 @@ PendingFilter Applied to the `FILTER:` header line shown at the top of
|
||||||
the buffer when a filter is active.
|
the buffer when a filter is active.
|
||||||
Default: links to `DiagnosticWarn`.
|
Default: links to `DiagnosticWarn`.
|
||||||
|
|
||||||
|
*PendingFile*
|
||||||
|
PendingFile Applied to the file reference virtual text shown for tasks
|
||||||
|
that have a `file:` token attached (see |pending-file-token|).
|
||||||
|
Displays the basename and line number (e.g. `auth.lua:42`).
|
||||||
|
Default: links to `Directory`.
|
||||||
|
|
||||||
To override a group in your colorscheme or config: >lua
|
To override a group in your colorscheme or config: >lua
|
||||||
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
|
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
|
||||||
<
|
<
|
||||||
|
|
@ -986,6 +1059,16 @@ Run |:checkhealth| pending to verify your setup: >vim
|
||||||
:checkhealth pending
|
:checkhealth pending
|
||||||
<
|
<
|
||||||
|
|
||||||
|
Checks performed: ~
|
||||||
|
- Config loads without error
|
||||||
|
- Reports active configuration values (data path, default view, default
|
||||||
|
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
|
||||||
|
- Discovers sync backends under `lua/pending/sync/` and runs each backend's
|
||||||
|
`health()` function if it exists (e.g. gcal checks for `curl` and `openssl`)
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
STORE RESOLUTION *pending-store-resolution*
|
STORE RESOLUTION *pending-store-resolution*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,10 @@ local function apply_extmarks(bufnr, line_meta)
|
||||||
if m.due then
|
if m.due then
|
||||||
table.insert(virt_parts, { icons.due .. ' ' .. m.due, due_hl })
|
table.insert(virt_parts, { icons.due .. ' ' .. m.due, due_hl })
|
||||||
end
|
end
|
||||||
|
if m.file then
|
||||||
|
local display = m.file:match('([^/]+:%d+)$') or m.file
|
||||||
|
table.insert(virt_parts, { display, 'PendingFile' })
|
||||||
|
end
|
||||||
if #virt_parts > 0 then
|
if #virt_parts > 0 then
|
||||||
for p = 1, #virt_parts - 1 do
|
for p = 1, #virt_parts - 1 do
|
||||||
virt_parts[p][1] = virt_parts[p][1] .. ' '
|
virt_parts[p][1] = virt_parts[p][1] .. ' '
|
||||||
|
|
@ -234,6 +238,7 @@ local function setup_highlights()
|
||||||
vim.api.nvim_set_hl(0, 'PendingPriority', { link = 'DiagnosticWarn', default = true })
|
vim.api.nvim_set_hl(0, 'PendingPriority', { link = 'DiagnosticWarn', default = true })
|
||||||
vim.api.nvim_set_hl(0, 'PendingRecur', { link = 'DiagnosticInfo', default = true })
|
vim.api.nvim_set_hl(0, 'PendingRecur', { link = 'DiagnosticInfo', default = true })
|
||||||
vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true })
|
vim.api.nvim_set_hl(0, 'PendingFilter', { link = 'DiagnosticWarn', default = true })
|
||||||
|
vim.api.nvim_set_hl(0, 'PendingFile', { link = 'Directory', default = true })
|
||||||
end
|
end
|
||||||
|
|
||||||
local function snapshot_folds(bufnr)
|
local function snapshot_folds(bufnr)
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ function M.omnifunc(findstart, base)
|
||||||
{ vim.pesc(dk) .. ':([%S]*)$', dk },
|
{ vim.pesc(dk) .. ':([%S]*)$', dk },
|
||||||
{ 'cat:([%S]*)$', 'cat' },
|
{ 'cat:([%S]*)$', 'cat' },
|
||||||
{ vim.pesc(rk) .. ':([%S]*)$', rk },
|
{ vim.pesc(rk) .. ':([%S]*)$', rk },
|
||||||
|
{ 'file:([%S]*)$', 'file' },
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, check in ipairs(checks) do
|
for _, check in ipairs(checks) do
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
---@field prev_header? string|false
|
---@field prev_header? string|false
|
||||||
---@field next_task? string|false
|
---@field next_task? string|false
|
||||||
---@field prev_task? string|false
|
---@field prev_task? string|false
|
||||||
|
---@field goto_file? string|false
|
||||||
|
|
||||||
---@class pending.Config
|
---@class pending.Config
|
||||||
---@field data_path string
|
---@field data_path string
|
||||||
|
|
@ -45,6 +46,7 @@
|
||||||
---@field debug? boolean
|
---@field debug? boolean
|
||||||
---@field keymaps pending.Keymaps
|
---@field keymaps pending.Keymaps
|
||||||
---@field sync? pending.SyncConfig
|
---@field sync? pending.SyncConfig
|
||||||
|
---@field gcal? pending.GcalConfig
|
||||||
---@field icons pending.Icons
|
---@field icons pending.Icons
|
||||||
|
|
||||||
---@class pending.config
|
---@class pending.config
|
||||||
|
|
@ -100,6 +102,10 @@ function M.get()
|
||||||
end
|
end
|
||||||
local user = vim.g.pending or {}
|
local user = vim.g.pending or {}
|
||||||
_resolved = vim.tbl_deep_extend('force', defaults, user)
|
_resolved = vim.tbl_deep_extend('force', defaults, user)
|
||||||
|
if _resolved.gcal and not (_resolved.sync and _resolved.sync.gcal) then
|
||||||
|
_resolved.sync = _resolved.sync or {}
|
||||||
|
_resolved.sync.gcal = _resolved.gcal
|
||||||
|
end
|
||||||
return _resolved
|
return _resolved
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ local parse = require('pending.parse')
|
||||||
---@field due? string
|
---@field due? string
|
||||||
---@field rec? string
|
---@field rec? string
|
||||||
---@field rec_mode? string
|
---@field rec_mode? string
|
||||||
|
---@field file? string
|
||||||
---@field lnum integer
|
---@field lnum integer
|
||||||
|
|
||||||
---@class pending.diff
|
---@class pending.diff
|
||||||
|
|
@ -56,6 +57,7 @@ function M.parse_buffer(lines)
|
||||||
due = metadata.due,
|
due = metadata.due,
|
||||||
rec = metadata.rec,
|
rec = metadata.rec,
|
||||||
rec_mode = metadata.rec_mode,
|
rec_mode = metadata.rec_mode,
|
||||||
|
file = metadata.file,
|
||||||
lnum = i,
|
lnum = i,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
@ -133,6 +135,19 @@ function M.apply(lines, s, hidden_ids)
|
||||||
task.recur_mode = entry.rec_mode
|
task.recur_mode = entry.rec_mode
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
|
local old_file = (task._extra and task._extra.file) or nil
|
||||||
|
if entry.file ~= old_file then
|
||||||
|
task._extra = task._extra or {}
|
||||||
|
if entry.file then
|
||||||
|
task._extra.file = entry.file
|
||||||
|
else
|
||||||
|
task._extra.file = nil
|
||||||
|
if next(task._extra) == nil then
|
||||||
|
task._extra = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
if entry.status and task.status ~= entry.status then
|
if entry.status and task.status ~= entry.status then
|
||||||
task.status = entry.status
|
task.status = entry.status
|
||||||
if entry.status == 'done' then
|
if entry.status == 'done' then
|
||||||
|
|
|
||||||
|
|
@ -323,6 +323,16 @@ function M._setup_buf_mappings(bufnr)
|
||||||
end, opts)
|
end, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local goto_key = km.goto_file
|
||||||
|
if goto_key == nil then
|
||||||
|
goto_key = 'gf'
|
||||||
|
end
|
||||||
|
if goto_key and goto_key ~= false then
|
||||||
|
vim.keymap.set('n', goto_key --[[@as string]], function()
|
||||||
|
M.goto_file()
|
||||||
|
end, opts)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
|
|
@ -654,6 +664,10 @@ local function parse_edit_token(token)
|
||||||
if token == '-rec' or token == '-' .. rk then
|
if token == '-rec' or token == '-' .. rk then
|
||||||
return 'recur', vim.NIL, nil
|
return 'recur', vim.NIL, nil
|
||||||
end
|
end
|
||||||
|
if token == '-file' then
|
||||||
|
return 'file_clear', true, nil
|
||||||
|
end
|
||||||
|
|
||||||
local due_val = token:match('^' .. vim.pesc(dk) .. ':(.+)$')
|
local due_val = token:match('^' .. vim.pesc(dk) .. ':(.+)$')
|
||||||
if due_val then
|
if due_val then
|
||||||
local resolved = parse.resolve_date(due_val)
|
local resolved = parse.resolve_date(due_val)
|
||||||
|
|
@ -697,10 +711,11 @@ local function parse_edit_token(token)
|
||||||
.. dk
|
.. dk
|
||||||
.. ':<date>, cat:<name>, '
|
.. ':<date>, cat:<name>, '
|
||||||
.. rk
|
.. rk
|
||||||
.. ':<pattern>, +!, -!, -'
|
.. ':<pattern>, file:<path>:<line>, +!, -!, -'
|
||||||
.. dk
|
.. dk
|
||||||
.. ', -cat, -'
|
.. ', -cat, -'
|
||||||
.. rk
|
.. rk
|
||||||
|
.. ', -file'
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param id_str string
|
---@param id_str string
|
||||||
|
|
@ -780,6 +795,9 @@ function M.edit(id_str, rest)
|
||||||
elseif field == 'priority' then
|
elseif field == 'priority' then
|
||||||
updates.priority = value
|
updates.priority = value
|
||||||
table.insert(feedback, value == 1 and 'priority added' or 'priority removed')
|
table.insert(feedback, value == 1 and 'priority added' or 'priority removed')
|
||||||
|
elseif field == 'file_clear' then
|
||||||
|
updates.file_clear = true
|
||||||
|
table.insert(feedback, 'file reference removed')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -792,6 +810,17 @@ function M.edit(id_str, rest)
|
||||||
|
|
||||||
s:update(id, updates)
|
s:update(id, updates)
|
||||||
|
|
||||||
|
if updates.file_clear then
|
||||||
|
local t = s:get(id)
|
||||||
|
if t and t._extra then
|
||||||
|
t._extra.file = nil
|
||||||
|
if next(t._extra) == nil then
|
||||||
|
t._extra = nil
|
||||||
|
end
|
||||||
|
t.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
s:save()
|
s:save()
|
||||||
|
|
||||||
local bufnr = buffer.bufnr()
|
local bufnr = buffer.bufnr()
|
||||||
|
|
@ -802,6 +831,91 @@ function M.edit(id_str, rest)
|
||||||
vim.notify('Task #' .. id .. ' updated: ' .. table.concat(feedback, ', '))
|
vim.notify('Task #' .. id .. ' updated: ' .. table.concat(feedback, ', '))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
|
function M.goto_file()
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
if vim.bo[bufnr].filetype ~= 'pending' then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
local meta = buffer.meta()
|
||||||
|
local m = meta and meta[lnum]
|
||||||
|
if not m or m.type ~= 'task' then
|
||||||
|
vim.notify('No task on this line', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local task = get_store():get(m.id)
|
||||||
|
if not task or not task._extra or not task._extra.file then
|
||||||
|
vim.notify('No file attached to this task', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local file_spec = task._extra.file
|
||||||
|
local rel_path, line_str = file_spec:match('^(.+):(%d+)$')
|
||||||
|
if not rel_path then
|
||||||
|
vim.notify('Invalid file spec: ' .. file_spec, vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local data_dir = vim.fn.fnamemodify(get_store().path, ':h')
|
||||||
|
local abs_path = data_dir .. '/' .. rel_path
|
||||||
|
if vim.fn.filereadable(abs_path) == 0 then
|
||||||
|
vim.notify('File not found: ' .. abs_path, vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.cmd.edit(abs_path)
|
||||||
|
local lnum_target = tonumber(line_str) or 1
|
||||||
|
vim.api.nvim_win_set_cursor(0, { lnum_target, 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
|
function M.add_here()
|
||||||
|
local cur_bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
if vim.bo[cur_bufnr].filetype == 'pending' then
|
||||||
|
vim.notify('Already in pending buffer', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cur_file = vim.api.nvim_buf_get_name(cur_bufnr)
|
||||||
|
if cur_file == '' or vim.fn.filereadable(cur_file) == 0 then
|
||||||
|
vim.notify('Not editing a readable file', vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cur_lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
local s = get_store()
|
||||||
|
local data_dir = vim.fn.fnamemodify(s.path, ':h')
|
||||||
|
local abs_file = vim.fn.fnamemodify(cur_file, ':p')
|
||||||
|
local rel_file
|
||||||
|
if abs_file:sub(1, #data_dir + 1) == data_dir .. '/' then
|
||||||
|
rel_file = abs_file:sub(#data_dir + 2)
|
||||||
|
else
|
||||||
|
rel_file = abs_file
|
||||||
|
end
|
||||||
|
local file_spec = rel_file .. ':' .. cur_lnum
|
||||||
|
s:load()
|
||||||
|
local tasks = s:active_tasks()
|
||||||
|
if #tasks == 0 then
|
||||||
|
vim.notify('No active tasks', vim.log.levels.INFO)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local items = {}
|
||||||
|
for _, task in ipairs(tasks) do
|
||||||
|
table.insert(items, task)
|
||||||
|
end
|
||||||
|
vim.ui.select(items, {
|
||||||
|
prompt = 'Attach file to task:',
|
||||||
|
format_item = function(task)
|
||||||
|
return '[' .. task.id .. '] ' .. task.description
|
||||||
|
end,
|
||||||
|
}, function(task)
|
||||||
|
if not task then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
task._extra = task._extra or {}
|
||||||
|
task._extra.file = file_spec
|
||||||
|
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
||||||
|
s:save()
|
||||||
|
vim.notify('Attached ' .. file_spec .. ' to task ' .. task.id)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.init()
|
function M.init()
|
||||||
local path = vim.fn.getcwd() .. '/.pending.json'
|
local path = vim.fn.getcwd() .. '/.pending.json'
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,7 @@ end
|
||||||
|
|
||||||
---@param text string
|
---@param text string
|
||||||
---@return string description
|
---@return string description
|
||||||
---@return { due?: string, cat?: string, rec?: string, rec_mode?: 'scheduled'|'completion' } metadata
|
---@return { due?: string, cat?: string, rec?: string, rec_mode?: 'scheduled'|'completion', file?: string? } metadata
|
||||||
function M.body(text)
|
function M.body(text)
|
||||||
local tokens = {}
|
local tokens = {}
|
||||||
for token in text:gmatch('%S+') do
|
for token in text:gmatch('%S+') do
|
||||||
|
|
@ -481,7 +481,18 @@ function M.body(text)
|
||||||
metadata.rec = raw_spec
|
metadata.rec = raw_spec
|
||||||
i = i - 1
|
i = i - 1
|
||||||
else
|
else
|
||||||
break
|
local file_path_val, file_line_val = token:match('^file:(.+):(%d+)$')
|
||||||
|
if file_path_val and file_line_val then
|
||||||
|
if metadata.file then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
metadata.file = file_path_val .. ':' .. file_line_val
|
||||||
|
i = i - 1
|
||||||
|
elseif token:match('^file:') then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -499,7 +510,7 @@ end
|
||||||
|
|
||||||
---@param text string
|
---@param text string
|
||||||
---@return string description
|
---@return string description
|
||||||
---@return { due?: string, cat?: string, rec?: string, rec_mode?: 'scheduled'|'completion' } metadata
|
---@return { due?: string, cat?: string, rec?: string, rec_mode?: 'scheduled'|'completion', file?: string? } metadata
|
||||||
function M.command_add(text)
|
function M.command_add(text)
|
||||||
local cat_prefix = text:match('^(%S.-):%s')
|
local cat_prefix = text:match('^(%S.-):%s')
|
||||||
if cat_prefix then
|
if cat_prefix then
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ local SCOPE = 'https://www.googleapis.com/auth/calendar'
|
||||||
---@return table<string, any>
|
---@return table<string, any>
|
||||||
local function gcal_config()
|
local function gcal_config()
|
||||||
local cfg = config.get()
|
local cfg = config.get()
|
||||||
return (cfg.sync and cfg.sync.gcal) or {}
|
return (cfg.sync and cfg.sync.gcal) or cfg.gcal or {}
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return string
|
---@return string
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ local parse = require('pending.parse')
|
||||||
---@field show_category? boolean
|
---@field show_category? boolean
|
||||||
---@field priority? integer
|
---@field priority? integer
|
||||||
---@field recur? string
|
---@field recur? string
|
||||||
|
---@field file? string
|
||||||
|
|
||||||
---@class pending.views
|
---@class pending.views
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
@ -159,6 +160,7 @@ function M.category_view(tasks)
|
||||||
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due)
|
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due)
|
||||||
or nil,
|
or nil,
|
||||||
recur = task.recur,
|
recur = task.recur,
|
||||||
|
file = task._extra and task._extra.file or nil,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -210,6 +212,7 @@ function M.priority_view(tasks)
|
||||||
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due) or nil,
|
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due) or nil,
|
||||||
show_category = true,
|
show_category = true,
|
||||||
recur = task.recur,
|
recur = task.recur,
|
||||||
|
file = task._extra and task._extra.file or nil,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,13 @@ local function edit_field_candidates()
|
||||||
dk .. ':',
|
dk .. ':',
|
||||||
'cat:',
|
'cat:',
|
||||||
rk .. ':',
|
rk .. ':',
|
||||||
|
'file:',
|
||||||
'+!',
|
'+!',
|
||||||
'-!',
|
'-!',
|
||||||
'-' .. dk,
|
'-' .. dk,
|
||||||
'-cat',
|
'-cat',
|
||||||
'-' .. rk,
|
'-' .. rk,
|
||||||
|
'-file',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -298,6 +300,14 @@ vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-prev-task)', function()
|
||||||
require('pending.textobj').prev_task(vim.v.count1)
|
require('pending.textobj').prev_task(vim.v.count1)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
vim.keymap.set('n', '<Plug>(pending-goto-file)', function()
|
||||||
|
require('pending').goto_file()
|
||||||
|
end)
|
||||||
|
|
||||||
|
vim.keymap.set('n', '<Plug>(pending-add-here)', function()
|
||||||
|
require('pending').add_here()
|
||||||
|
end)
|
||||||
|
|
||||||
vim.keymap.set('n', '<Plug>(pending-tab)', function()
|
vim.keymap.set('n', '<Plug>(pending-tab)', function()
|
||||||
vim.cmd.tabnew()
|
vim.cmd.tabnew()
|
||||||
require('pending').open()
|
require('pending').open()
|
||||||
|
|
|
||||||
358
spec/file_spec.lua
Normal file
358
spec/file_spec.lua
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
require('spec.helpers')
|
||||||
|
|
||||||
|
local config = require('pending.config')
|
||||||
|
local diff = require('pending.diff')
|
||||||
|
local parse = require('pending.parse')
|
||||||
|
local store = require('pending.store')
|
||||||
|
local views = require('pending.views')
|
||||||
|
|
||||||
|
describe('file token', function()
|
||||||
|
local tmpdir
|
||||||
|
local s
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
tmpdir = vim.fn.tempname()
|
||||||
|
vim.fn.mkdir(tmpdir, 'p')
|
||||||
|
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||||
|
config.reset()
|
||||||
|
package.loaded['pending'] = nil
|
||||||
|
package.loaded['pending.buffer'] = nil
|
||||||
|
s = store.new(tmpdir .. '/tasks.json')
|
||||||
|
s:load()
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
vim.fn.delete(tmpdir, 'rf')
|
||||||
|
vim.g.pending = nil
|
||||||
|
config.reset()
|
||||||
|
package.loaded['pending'] = nil
|
||||||
|
package.loaded['pending.buffer'] = nil
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('parse.body', function()
|
||||||
|
it('extracts file token with path and line number', function()
|
||||||
|
local desc, meta = parse.body('Fix the bug file:src/auth.lua:42')
|
||||||
|
assert.are.equal('Fix the bug', desc)
|
||||||
|
assert.are.equal('src/auth.lua:42', meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('extracts file token with nested path', function()
|
||||||
|
local desc, meta = parse.body('Do something file:lua/pending/init.lua:100')
|
||||||
|
assert.are.equal('Do something', desc)
|
||||||
|
assert.are.equal('lua/pending/init.lua:100', meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('strips file token from description', function()
|
||||||
|
local desc, meta = parse.body('Task description file:foo.lua:1')
|
||||||
|
assert.are.equal('Task description', desc)
|
||||||
|
assert.are.equal('foo.lua:1', meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('stops parsing on duplicate file token', function()
|
||||||
|
local desc, meta = parse.body('Task file:b.lua:2 file:a.lua:1')
|
||||||
|
assert.are.equal('Task file:b.lua:2', desc)
|
||||||
|
assert.are.equal('a.lua:1', meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('treats malformed file token (no line number) as non-metadata', function()
|
||||||
|
local desc, meta = parse.body('Task file:nolineno')
|
||||||
|
assert.are.equal('Task file:nolineno', desc)
|
||||||
|
assert.is_nil(meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('treats file: prefix with no path as non-metadata', function()
|
||||||
|
local desc, meta = parse.body('Task file:')
|
||||||
|
assert.are.equal('Task file:', desc)
|
||||||
|
assert.is_nil(meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('handles file token alongside other metadata tokens', function()
|
||||||
|
local desc, meta = parse.body('Task cat:Work file:src/main.lua:10')
|
||||||
|
assert.are.equal('Task', desc)
|
||||||
|
assert.are.equal('Work', meta.cat)
|
||||||
|
assert.are.equal('src/main.lua:10', meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not extract file token when line number is not numeric', function()
|
||||||
|
local desc, meta = parse.body('Task file:src/foo.lua:abc')
|
||||||
|
assert.are.equal('Task file:src/foo.lua:abc', desc)
|
||||||
|
assert.is_nil(meta.file)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('diff reconciliation', function()
|
||||||
|
it('stores file field in _extra on write', function()
|
||||||
|
local t = s:add({ description = 'Task one' })
|
||||||
|
s:save()
|
||||||
|
local lines = {
|
||||||
|
'/' .. t.id .. '/- [ ] Task one file:src/auth.lua:42',
|
||||||
|
}
|
||||||
|
diff.apply(lines, s)
|
||||||
|
local updated = s:get(t.id)
|
||||||
|
assert.is_not_nil(updated._extra)
|
||||||
|
assert.are.equal('src/auth.lua:42', updated._extra.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('updates file field when token changes', function()
|
||||||
|
local t = s:add({ description = 'Task one', _extra = { file = 'old.lua:1' } })
|
||||||
|
s:save()
|
||||||
|
local lines = {
|
||||||
|
'/' .. t.id .. '/- [ ] Task one file:new.lua:99',
|
||||||
|
}
|
||||||
|
diff.apply(lines, s)
|
||||||
|
local updated = s:get(t.id)
|
||||||
|
assert.are.equal('new.lua:99', updated._extra.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('clears file field when token is removed from line', function()
|
||||||
|
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||||
|
s:save()
|
||||||
|
local lines = {
|
||||||
|
'/' .. t.id .. '/- [ ] Task one',
|
||||||
|
}
|
||||||
|
diff.apply(lines, s)
|
||||||
|
local updated = s:get(t.id)
|
||||||
|
assert.is_nil(updated._extra)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('preserves other _extra fields when file is cleared', function()
|
||||||
|
local t = s:add({
|
||||||
|
description = 'Task one',
|
||||||
|
_extra = { file = 'src/auth.lua:42', _gcal_event_id = 'abc123' },
|
||||||
|
})
|
||||||
|
s:save()
|
||||||
|
local lines = {
|
||||||
|
'/' .. t.id .. '/- [ ] Task one',
|
||||||
|
}
|
||||||
|
diff.apply(lines, s)
|
||||||
|
local updated = s:get(t.id)
|
||||||
|
assert.is_not_nil(updated._extra)
|
||||||
|
assert.is_nil(updated._extra.file)
|
||||||
|
assert.are.equal('abc123', updated._extra._gcal_event_id)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('round-trips file field through JSON', function()
|
||||||
|
local t = s:add({ description = 'Task one' })
|
||||||
|
s:save()
|
||||||
|
local lines = {
|
||||||
|
'/' .. t.id .. '/- [ ] Task one file:src/auth.lua:42',
|
||||||
|
}
|
||||||
|
diff.apply(lines, s)
|
||||||
|
s:load()
|
||||||
|
local loaded = s:get(t.id)
|
||||||
|
assert.is_not_nil(loaded._extra)
|
||||||
|
assert.are.equal('src/auth.lua:42', loaded._extra.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('accepts optional hidden_ids parameter without error', function()
|
||||||
|
local t = s:add({ description = 'Task one' })
|
||||||
|
s:save()
|
||||||
|
local lines = {
|
||||||
|
'/' .. t.id .. '/- [ ] Task one',
|
||||||
|
}
|
||||||
|
assert.has_no_error(function()
|
||||||
|
diff.apply(lines, s, {})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('LineMeta', function()
|
||||||
|
it('category_view populates file field in LineMeta', function()
|
||||||
|
local t = s:add({
|
||||||
|
description = 'Task one',
|
||||||
|
_extra = { file = 'src/auth.lua:42' },
|
||||||
|
})
|
||||||
|
s:save()
|
||||||
|
local tasks = s:active_tasks()
|
||||||
|
local _, meta = views.category_view(tasks)
|
||||||
|
local task_meta = nil
|
||||||
|
for _, m in ipairs(meta) do
|
||||||
|
if m.type == 'task' and m.id == t.id then
|
||||||
|
task_meta = m
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(task_meta)
|
||||||
|
assert.are.equal('src/auth.lua:42', task_meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('priority_view populates file field in LineMeta', function()
|
||||||
|
local t = s:add({
|
||||||
|
description = 'Task one',
|
||||||
|
_extra = { file = 'src/auth.lua:42' },
|
||||||
|
})
|
||||||
|
s:save()
|
||||||
|
local tasks = s:active_tasks()
|
||||||
|
local _, meta = views.priority_view(tasks)
|
||||||
|
local task_meta = nil
|
||||||
|
for _, m in ipairs(meta) do
|
||||||
|
if m.type == 'task' and m.id == t.id then
|
||||||
|
task_meta = m
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(task_meta)
|
||||||
|
assert.are.equal('src/auth.lua:42', task_meta.file)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('file field is nil in LineMeta when task has no file', function()
|
||||||
|
local t = s:add({ description = 'Task one' })
|
||||||
|
s:save()
|
||||||
|
local tasks = s:active_tasks()
|
||||||
|
local _, meta = views.category_view(tasks)
|
||||||
|
local task_meta = nil
|
||||||
|
for _, m in ipairs(meta) do
|
||||||
|
if m.type == 'task' and m.id == t.id then
|
||||||
|
task_meta = m
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(task_meta)
|
||||||
|
assert.is_nil(task_meta.file)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe(':Pending edit -file', function()
|
||||||
|
it('clears file reference from task', function()
|
||||||
|
local pending = require('pending')
|
||||||
|
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||||
|
s:save()
|
||||||
|
pending.edit(tostring(t.id), '-file')
|
||||||
|
s:load()
|
||||||
|
local updated = s:get(t.id)
|
||||||
|
assert.is_nil(updated._extra)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('shows feedback when file reference is removed', function()
|
||||||
|
local pending = require('pending')
|
||||||
|
local t = s:add({ description = 'Task one', _extra = { file = 'src/auth.lua:42' } })
|
||||||
|
s:save()
|
||||||
|
local messages = {}
|
||||||
|
local orig_notify = vim.notify
|
||||||
|
vim.notify = function(msg, level)
|
||||||
|
table.insert(messages, { msg = msg, level = level })
|
||||||
|
end
|
||||||
|
pending.edit(tostring(t.id), '-file')
|
||||||
|
vim.notify = orig_notify
|
||||||
|
assert.are.equal(1, #messages)
|
||||||
|
assert.truthy(messages[1].msg:find('file reference removed'))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not error when task has no file', function()
|
||||||
|
local pending = require('pending')
|
||||||
|
local t = s:add({ description = 'Task one' })
|
||||||
|
s:save()
|
||||||
|
assert.has_no_error(function()
|
||||||
|
pending.edit(tostring(t.id), '-file')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('preserves other _extra fields when -file is used', function()
|
||||||
|
local pending = require('pending')
|
||||||
|
local t = s:add({
|
||||||
|
description = 'Task one',
|
||||||
|
_extra = { file = 'src/auth.lua:42', _gcal_event_id = 'abc' },
|
||||||
|
})
|
||||||
|
s:save()
|
||||||
|
pending.edit(tostring(t.id), '-file')
|
||||||
|
s:load()
|
||||||
|
local updated = s:get(t.id)
|
||||||
|
assert.is_not_nil(updated._extra)
|
||||||
|
assert.is_nil(updated._extra.file)
|
||||||
|
assert.are.equal('abc', updated._extra._gcal_event_id)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('goto_file', function()
|
||||||
|
it('notifies warn when task has no file attached', function()
|
||||||
|
local pending = require('pending')
|
||||||
|
local buffer = require('pending.buffer')
|
||||||
|
|
||||||
|
local t = s:add({ description = 'Task one' })
|
||||||
|
s:save()
|
||||||
|
|
||||||
|
buffer.set_store(s)
|
||||||
|
local bufnr = buffer.open()
|
||||||
|
vim.bo[bufnr].filetype = 'pending'
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
local meta = buffer.meta()
|
||||||
|
local task_lnum = nil
|
||||||
|
for lnum, m in ipairs(meta) do
|
||||||
|
if m.type == 'task' and m.id == t.id then
|
||||||
|
task_lnum = lnum
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(task_lnum)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { task_lnum, 0 })
|
||||||
|
|
||||||
|
local messages = {}
|
||||||
|
local orig_notify = vim.notify
|
||||||
|
vim.notify = function(msg, level)
|
||||||
|
table.insert(messages, { msg = msg, level = level })
|
||||||
|
end
|
||||||
|
|
||||||
|
pending.goto_file()
|
||||||
|
|
||||||
|
vim.notify = orig_notify
|
||||||
|
|
||||||
|
local warned = false
|
||||||
|
for _, m in ipairs(messages) do
|
||||||
|
if m.level == vim.log.levels.WARN and m.msg:find('No file attached') then
|
||||||
|
warned = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(warned)
|
||||||
|
|
||||||
|
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('notifies error when file spec is unreadable', function()
|
||||||
|
local pending = require('pending')
|
||||||
|
local buffer = require('pending.buffer')
|
||||||
|
|
||||||
|
local t = s:add({
|
||||||
|
description = 'Task one',
|
||||||
|
_extra = { file = 'nonexistent/path.lua:1' },
|
||||||
|
})
|
||||||
|
s:save()
|
||||||
|
|
||||||
|
buffer.set_store(s)
|
||||||
|
local bufnr = buffer.open()
|
||||||
|
vim.bo[bufnr].filetype = 'pending'
|
||||||
|
vim.api.nvim_set_current_buf(bufnr)
|
||||||
|
|
||||||
|
local meta = buffer.meta()
|
||||||
|
local task_lnum = nil
|
||||||
|
for lnum, m in ipairs(meta) do
|
||||||
|
if m.type == 'task' and m.id == t.id then
|
||||||
|
task_lnum = lnum
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_not_nil(task_lnum)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { task_lnum, 0 })
|
||||||
|
|
||||||
|
local messages = {}
|
||||||
|
local orig_notify = vim.notify
|
||||||
|
vim.notify = function(msg, level)
|
||||||
|
table.insert(messages, { msg = msg, level = level })
|
||||||
|
end
|
||||||
|
|
||||||
|
pending.goto_file()
|
||||||
|
|
||||||
|
vim.notify = orig_notify
|
||||||
|
|
||||||
|
local errored = false
|
||||||
|
for _, m in ipairs(messages) do
|
||||||
|
if m.level == vim.log.levels.ERROR and m.msg:find('File not found') then
|
||||||
|
errored = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert.is_true(errored)
|
||||||
|
|
||||||
|
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
@ -112,14 +112,39 @@ describe('sync', function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('works with sync.gcal config', function()
|
describe('config migration', function()
|
||||||
config.reset()
|
it('migrates legacy gcal to sync.gcal', function()
|
||||||
vim.g.pending = {
|
config.reset()
|
||||||
data_path = tmpdir .. '/tasks.json',
|
vim.g.pending = {
|
||||||
sync = { gcal = { calendar = 'NewStyle' } },
|
data_path = tmpdir .. '/tasks.json',
|
||||||
}
|
gcal = { calendar = 'MyCalendar' },
|
||||||
local cfg = config.get()
|
}
|
||||||
assert.are.equal('NewStyle', cfg.sync.gcal.calendar)
|
local cfg = config.get()
|
||||||
|
assert.is_not_nil(cfg.sync)
|
||||||
|
assert.is_not_nil(cfg.sync.gcal)
|
||||||
|
assert.are.equal('MyCalendar', cfg.sync.gcal.calendar)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('does not overwrite explicit sync.gcal with legacy gcal', function()
|
||||||
|
config.reset()
|
||||||
|
vim.g.pending = {
|
||||||
|
data_path = tmpdir .. '/tasks.json',
|
||||||
|
gcal = { calendar = 'Legacy' },
|
||||||
|
sync = { gcal = { calendar = 'Explicit' } },
|
||||||
|
}
|
||||||
|
local cfg = config.get()
|
||||||
|
assert.are.equal('Explicit', cfg.sync.gcal.calendar)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('works with sync.gcal and no legacy gcal', function()
|
||||||
|
config.reset()
|
||||||
|
vim.g.pending = {
|
||||||
|
data_path = tmpdir .. '/tasks.json',
|
||||||
|
sync = { gcal = { calendar = 'NewStyle' } },
|
||||||
|
}
|
||||||
|
local cfg = config.get()
|
||||||
|
assert.are.equal('NewStyle', cfg.sync.gcal.calendar)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('gcal module', function()
|
describe('gcal module', function()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue