feat: warn on dirty buffer before store-dependent actions (#83)
* fix(buffer): use `default_category` config for empty placeholder
Problem: The empty-buffer fallback hardcoded the category name `TODO`,
ignoring the user's `default_category` config value (default: `Todo`).
Solution: Read `config.get().default_category` at render time and use
that value for both the header line and `LineMeta` category field.
* fix(diff): match optional checkbox char in `parse_buffer` patterns
Problem: `parse_buffer` used `%[.%]` which requires exactly one
character between brackets, failing to parse empty `[]` checkboxes.
Solution: Change to `%[.?%]` so the character is optional, matching
`[]`, `[ ]`, `[x]`, and `[!]` uniformly.
* fix(init): add `nowait` to buffer keymap opts
Problem: Buffer-local mappings like `!` could be swallowed by Neovim's
operator-pending machinery or by global maps sharing a prefix, since
the keymap opts did not include `nowait`.
Solution: Add `nowait = true` to the shared `opts` table used for all
buffer-local mappings in `_setup_buf_mappings`.
* feat(init): allow `:Pending done` with no args to use cursor line
Problem: `:Pending done` required an explicit task ID, making it
awkward to mark the current task done while inside the pending buffer.
Solution: When called with no ID, `M.done()` reads the cursor row from
`buffer.meta()` to resolve the task ID, erroring if the cursor is not
on a saved task line.
* fix(views): populate `priority` field in `LineMeta`
Problem: Both `category_view` and `priority_view` omitted `priority`
from the `LineMeta` they produced. `apply_extmarks` checks `m.priority`
to decide whether to render the priority icon, so it was always nil,
causing the `[ ]` pending-icon overlay to replace the `[!]` buffer text.
Solution: Add `priority = task.priority` to both LineMeta constructors.
* fix(buffer): keep `_meta` in sync when `open_line` inserts a new line
Problem: `open_line` inserted a buffer line without updating `_meta`,
leaving the entry at that row pointing to the task that was shifted
down. Pressing `<CR>` (toggle_complete) would read the stale meta,
find a real task ID, toggle it, and re-render — destroying the unsaved
new line.
Solution: Insert a `{ type = 'blank' }` sentinel into `_meta` at the
new line's position so buffer-local actions see no task there.
* fix(buffer): use task sentinel in `open_line` for better unsaved-task errors
* feat(init): warn on dirty buffer before store-dependent actions
Problem: `toggle_complete`, `toggle_priority`, `prompt_date`, and
`done` (no-args) all read from `buffer.meta()` which is stale whenever
the buffer has unsaved edits, leading to silent no-ops or acting on the
wrong task.
Solution: Add a `require_saved()` guard that emits a `log.warn` and
returns false when the buffer is modified. Each store-dependent action
calls it before touching meta or the store.
* fix(init): guard `view`, `undo`, and `filter` against dirty buffer
Problem: `toggle_view`, `undo_write`, and `filter` all call
`buffer.render()` which rewrites the buffer from the store, silently
discarding any unsaved edits. The previous `require_saved()` change
missed these three entry points.
Solution: Add `require_saved()` to the `view` and `filter` keymap
lambdas and to `M.undo_write()`. Also guard `M.filter()` directly so
`:Pending filter` from the command line is covered too.
* fix(init): improve dirty-buffer warning message
* fix(init): tighten dirty-buffer warning message
This commit is contained in:
parent
7ad27f6fca
commit
2929b4d8fa
3 changed files with 57 additions and 6 deletions
|
|
@ -133,6 +133,7 @@ function M.open_line(above)
|
|||
local insert_row = above and (row - 1) or row
|
||||
vim.bo[bufnr].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(bufnr, insert_row, insert_row, false, { '- [ ] ' })
|
||||
table.insert(_meta, insert_row + 1, { type = 'task' })
|
||||
vim.api.nvim_win_set_cursor(0, { insert_row + 1, 6 })
|
||||
vim.cmd('startinsert!')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -83,6 +83,16 @@ local function _save_and_notify()
|
|||
M._recompute_counts()
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
local function require_saved()
|
||||
local bufnr = buffer.bufnr()
|
||||
if bufnr and vim.bo[bufnr].modified then
|
||||
log.warn('save changes first (:w)')
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@return pending.Counts
|
||||
function M.counts()
|
||||
if not _counts then
|
||||
|
|
@ -175,6 +185,9 @@ end
|
|||
---@param pred_str string
|
||||
---@return nil
|
||||
function M.filter(pred_str)
|
||||
if not require_saved() then
|
||||
return
|
||||
end
|
||||
if pred_str == 'clear' or pred_str == '' then
|
||||
buffer.set_filter({}, {})
|
||||
local bufnr = buffer.bufnr()
|
||||
|
|
@ -232,7 +245,7 @@ end
|
|||
function M._setup_buf_mappings(bufnr)
|
||||
local cfg = require('pending.config').get()
|
||||
local km = cfg.keymaps
|
||||
local opts = { buffer = bufnr, silent = true }
|
||||
local opts = { buffer = bufnr, silent = true, nowait = true }
|
||||
|
||||
---@type table<string, fun()>
|
||||
local actions = {
|
||||
|
|
@ -243,6 +256,9 @@ function M._setup_buf_mappings(bufnr)
|
|||
M.toggle_complete()
|
||||
end,
|
||||
view = function()
|
||||
if not require_saved() then
|
||||
return
|
||||
end
|
||||
buffer.toggle_view()
|
||||
end,
|
||||
priority = function()
|
||||
|
|
@ -255,6 +271,9 @@ function M._setup_buf_mappings(bufnr)
|
|||
M.undo_write()
|
||||
end,
|
||||
filter = function()
|
||||
if not require_saved() then
|
||||
return
|
||||
end
|
||||
vim.ui.input({ prompt = 'Filter: ' }, function(input)
|
||||
if input then
|
||||
M.filter(input)
|
||||
|
|
@ -370,6 +389,9 @@ end
|
|||
|
||||
---@return nil
|
||||
function M.undo_write()
|
||||
if not require_saved() then
|
||||
return
|
||||
end
|
||||
local s = get_store()
|
||||
local stack = s:undo_stack()
|
||||
if #stack == 0 then
|
||||
|
|
@ -388,6 +410,9 @@ function M.toggle_complete()
|
|||
if not bufnr then
|
||||
return
|
||||
end
|
||||
if not require_saved() 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
|
||||
|
|
@ -430,13 +455,30 @@ function M.toggle_complete()
|
|||
end
|
||||
end
|
||||
|
||||
---@param id_str string
|
||||
---@param id_str? string
|
||||
---@return nil
|
||||
function M.done(id_str)
|
||||
local id = tonumber(id_str)
|
||||
if not id then
|
||||
log.error('Invalid task ID: ' .. tostring(id_str))
|
||||
return
|
||||
local id
|
||||
if not id_str or id_str == '' then
|
||||
if not require_saved() 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
|
||||
log.error('Cursor is not on a task line.')
|
||||
return
|
||||
end
|
||||
id = meta[row].id
|
||||
if not id then
|
||||
return
|
||||
end
|
||||
else
|
||||
id = tonumber(id_str)
|
||||
if not id then
|
||||
log.error('Invalid task ID: ' .. tostring(id_str))
|
||||
return
|
||||
end
|
||||
end
|
||||
local s = get_store()
|
||||
s:load()
|
||||
|
|
@ -478,6 +520,9 @@ function M.toggle_priority()
|
|||
if not bufnr then
|
||||
return
|
||||
end
|
||||
if not require_saved() 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
|
||||
|
|
@ -510,6 +555,9 @@ function M.prompt_date()
|
|||
if not bufnr then
|
||||
return
|
||||
end
|
||||
if not require_saved() 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
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ function M.category_view(tasks)
|
|||
raw_due = task.due,
|
||||
status = task.status,
|
||||
category = cat,
|
||||
priority = task.priority,
|
||||
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due)
|
||||
or nil,
|
||||
recur = task.recur,
|
||||
|
|
@ -207,6 +208,7 @@ function M.priority_view(tasks)
|
|||
raw_due = task.due,
|
||||
status = task.status,
|
||||
category = task.category,
|
||||
priority = task.priority,
|
||||
overdue = task.status == 'pending' and task.due ~= nil and parse.is_overdue(task.due) or nil,
|
||||
show_category = true,
|
||||
recur = task.recur,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue