From 979c4339b8820329c1c4179dd68425005bf69b72 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:28:58 -0400 Subject: [PATCH] fix(buffer): correct extmark drift on `open_line` for done tasks (#118) * fix(config): update default keymaps to match vimdoc Problem: four keymap defaults in `config.lua` still used the old deprecated keys (`!`, `D`, `U`, `F`) while `doc/pending.txt` documents the `g`-prefixed replacements (`g!`, `gd`, `gz`, `gf`). Solution: update `priority`, `date`, `undo`, and `filter` defaults to `g!`, `gd`, `gz`, and `gf` respectively. * fix(buffer): correct extmark drift on `open_line` above/below done tasks Problem: `open_line` used `nvim_buf_set_lines` which triggered `on_bytes` with a `start_row` offset designed for native `o`/`O` keypresses. The `_meta` entry was inserted one position too late, causing the done task's `PendingDone` highlight to attach to the new blank line instead. Solution: suppress `on_bytes` during `open_line` by reusing the `_rendering` guard, insert the meta entry at the correct position, and immediately reapply inline extmarks for the affected rows. * fix(buffer): infer task status from line text in `reapply_dirty_inline` Problem: `on_bytes` inserts bare `{ type = 'task' }` meta entries with no `status` field for any new lines (paste, undo, native edits). When meta positions also shift incorrectly (e.g. `P` paste above), existing meta with the wrong status ends up on the wrong row. This causes done tasks to lose their `PendingDone` highlight and pending tasks to appear greyed out. Solution: always re-infer `status` from the actual buffer line text for dirty task rows before applying extmarks. The checkbox character (`[x]`, `[>]`, `[=]`, `[ ]`) is the source of truth, with fallback to the existing meta status if the line doesn't match a task pattern. --- lua/pending/buffer.lua | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index 5d18e1f..250ed8e 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -182,6 +182,23 @@ local function apply_inline_row(bufnr, row, m, icons) end end +---@param line string +---@return string? +local function infer_status(line) + local ch = line:match('^/%d+/- %[(.)%]') or line:match('^- %[(.)%]') + if not ch then + return nil + end + if ch == 'x' then + return 'done' + elseif ch == '>' then + return 'wip' + elseif ch == '=' then + return 'blocked' + end + return 'pending' +end + ---@param bufnr integer ---@return nil function M.reapply_dirty_inline(bufnr) @@ -191,6 +208,10 @@ function M.reapply_dirty_inline(bufnr) local icons = config.get().icons for row in pairs(_dirty_rows) do local m = _meta[row] + if m and m.type == 'task' then + local line = vim.api.nvim_buf_get_lines(bufnr, row - 1, row, false)[1] or '' + m.status = infer_status(line) or m.status + end if m then vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, row - 1, row) apply_inline_row(bufnr, row - 1, m, icons) @@ -345,8 +366,25 @@ function M.open_line(above) end local row = vim.api.nvim_win_get_cursor(0)[1] local insert_row = above and (row - 1) or row + local meta_pos = insert_row + 1 + + _rendering = true vim.bo[bufnr].modifiable = true vim.api.nvim_buf_set_lines(bufnr, insert_row, insert_row, false, { '- [ ] ' }) + _rendering = false + + table.insert(_meta, meta_pos, { type = 'task' }) + + local icons = config.get().icons + local total = vim.api.nvim_buf_line_count(bufnr) + for r = meta_pos, math.min(meta_pos + 1, total) do + vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, r - 1, r) + local m = _meta[r] + if m then + apply_inline_row(bufnr, r - 1, m, icons) + end + end + vim.api.nvim_win_set_cursor(0, { insert_row + 1, 6 }) vim.cmd('startinsert!') end