* refactor(config): change default category from Inbox to Todo * refactor(views): adopt markdown checkbox line format Problem: task lines used an opaque /ID/ [N] prefix format that was hard to read and inconsistent between category and priority views. Header lines had no visual marker distinguishing them from tasks. Solution: render headers as '## Cat', task lines as '/ID/- [x|!| ] description'. State encoding: [x]=done, [!]=urgent, [ ]=pending. Both views use the same construction. * refactor(diff): parse and reconcile markdown checkbox format Problem: parse_buffer matched the old ' text' indent pattern and detected headers via '^%S'. Priority was read from a '[N] ' prefix. apply() never reconciled status changes written into the buffer. Solution: match '- [.] text' for tasks and '^## ' for headers. Extract state char to derive priority (! -> 1) and status (x -> done). apply() now reconciles status from the buffer, setting/clearing 'end' timestamps — enabling the oil-style edit-checkbox-then-:w workflow. * refactor(buffer): update syntax, extmarks, and render for checkbox format Problem: syntax patterns matched the old indent/[N] format; right_align virtual text produced a broken layout in narrow windows; the done strikethrough skipped past the ' ' indent leaving '- [x] ' unstyled; render() added undo history entries so 'u' could undo a re-render. Solution: update taskHeader/taskLine patterns for '## '/'- [.]'; rename taskPriority -> taskCheckbox matching '[!]'; switch virt_text_pos to 'eol'; drop the +2 col_start offset so strikethrough covers '- [x] '; guard nvim_buf_set_lines with undolevels=-1 so renders are not undoable. Also fix open_line to insert '- [ ] ' and position cursor at col 6. * refactor(init): replace multi-level priority with binary toggle Problem: <C-a>/<C-x> overrode Vim's native number increment and the visual g<C-a>/g<C-x> variants added complexity for marginal value. toggle_complete() left the cursor on the wrong line after re-render. Solution: remove change_priority/change_priority_visual; add toggle_priority() (0<->1) mapped to '!', with cursor-follow after render matching the pattern already used in priority toggle. Add cursor-follow to toggle_complete() for the same reason. Update plugin plugs (priority-up/down -> priority) and add 'due'/'undo' to the :Pending completion list. Update help text accordingly. * feat(buffer): reflect current view in buffer name Problem: no way to tell at a glance which view (category vs priority) is active — the buffer was always named 'pending://'. Solution: update the buffer name to 'pending://category' or 'pending://priority' on every render, so the view is visible in the statusline/tabline without any extra UI.
156 lines
4 KiB
Lua
156 lines
4 KiB
Lua
local config = require('pending.config')
|
|
local parse = require('pending.parse')
|
|
local store = require('pending.store')
|
|
|
|
---@class pending.ParsedEntry
|
|
---@field type 'task'|'header'|'blank'
|
|
---@field id? integer
|
|
---@field description? string
|
|
---@field priority? integer
|
|
---@field status? string
|
|
---@field category? string
|
|
---@field due? string
|
|
---@field lnum integer
|
|
|
|
---@class pending.diff
|
|
local M = {}
|
|
|
|
---@return string
|
|
local function timestamp()
|
|
return os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
|
end
|
|
|
|
---@param lines string[]
|
|
---@return pending.ParsedEntry[]
|
|
function M.parse_buffer(lines)
|
|
local result = {}
|
|
local current_category = nil
|
|
|
|
for i, line in ipairs(lines) do
|
|
local id, body = line:match('^/(%d+)/(- %[.%] .*)$')
|
|
if not id then
|
|
body = line:match('^(- %[.%] .*)$')
|
|
end
|
|
if line == '' then
|
|
table.insert(result, { type = 'blank', lnum = i })
|
|
elseif id or body then
|
|
local stripped = body:match('^- %[.%] (.*)$') or body
|
|
local state_char = body:match('^- %[(.-)%]') or ' '
|
|
local priority = state_char == '!' and 1 or 0
|
|
local status = state_char == 'x' and 'done' or 'pending'
|
|
local description, metadata = parse.body(stripped)
|
|
if description and description ~= '' then
|
|
table.insert(result, {
|
|
type = 'task',
|
|
id = id and tonumber(id) or nil,
|
|
description = description,
|
|
priority = priority,
|
|
status = status,
|
|
category = metadata.cat or current_category or config.get().default_category,
|
|
due = metadata.due,
|
|
lnum = i,
|
|
})
|
|
end
|
|
elseif line:match('^## (.+)$') then
|
|
current_category = line:match('^## (.+)$')
|
|
table.insert(result, { type = 'header', category = current_category, lnum = i })
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
---@param lines string[]
|
|
function M.apply(lines)
|
|
local parsed = M.parse_buffer(lines)
|
|
local now = timestamp()
|
|
local data = store.data()
|
|
|
|
local old_by_id = {}
|
|
for _, task in ipairs(data.tasks) do
|
|
if task.status ~= 'deleted' then
|
|
old_by_id[task.id] = task
|
|
end
|
|
end
|
|
|
|
local seen_ids = {}
|
|
local order_counter = 0
|
|
|
|
for _, entry in ipairs(parsed) do
|
|
if entry.type ~= 'task' then
|
|
goto continue
|
|
end
|
|
|
|
order_counter = order_counter + 1
|
|
|
|
if entry.id and old_by_id[entry.id] then
|
|
if seen_ids[entry.id] then
|
|
store.add({
|
|
description = entry.description,
|
|
category = entry.category,
|
|
priority = entry.priority,
|
|
due = entry.due,
|
|
order = order_counter,
|
|
})
|
|
else
|
|
seen_ids[entry.id] = true
|
|
local task = old_by_id[entry.id]
|
|
local changed = false
|
|
if task.description ~= entry.description then
|
|
task.description = entry.description
|
|
changed = true
|
|
end
|
|
if task.category ~= entry.category then
|
|
task.category = entry.category
|
|
changed = true
|
|
end
|
|
if task.priority ~= entry.priority then
|
|
task.priority = entry.priority
|
|
changed = true
|
|
end
|
|
if task.due ~= entry.due then
|
|
task.due = entry.due
|
|
changed = true
|
|
end
|
|
if entry.status and task.status ~= entry.status then
|
|
task.status = entry.status
|
|
if entry.status == 'done' then
|
|
task['end'] = now
|
|
else
|
|
task['end'] = nil
|
|
end
|
|
changed = true
|
|
end
|
|
if task.order ~= order_counter then
|
|
task.order = order_counter
|
|
changed = true
|
|
end
|
|
if changed then
|
|
task.modified = now
|
|
end
|
|
end
|
|
else
|
|
store.add({
|
|
description = entry.description,
|
|
category = entry.category,
|
|
priority = entry.priority,
|
|
due = entry.due,
|
|
order = order_counter,
|
|
})
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
|
|
for id, task in pairs(old_by_id) do
|
|
if not seen_ids[id] then
|
|
task.status = 'deleted'
|
|
task['end'] = now
|
|
task.modified = now
|
|
end
|
|
end
|
|
|
|
store.save()
|
|
end
|
|
|
|
return M
|