From e71d6cdff612f4976db4ad4230e2da76851f3755 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 8 Mar 2026 14:12:03 -0400 Subject: [PATCH] feat(buffer): re-apply inline extmarks after edits Problem: inline extmarks (checkbox overlays, strikethrough, header highlights) were cleared during edits and only restored on `:w`, leaving the buffer visually bare while editing. Solution: extract `apply_inline_row()` from `apply_extmarks()` and call it via `reapply_dirty_inline()` on `InsertLeave` and normal-mode `TextChanged`. Insert-mode `TextChangedI` still only clears inline marks on dirty rows to avoid overlay flicker while typing. --- lua/pending/buffer.lua | 109 +++++++++++++++++++++++++---------------- lua/pending/init.lua | 21 +++++++- 2 files changed, 88 insertions(+), 42 deletions(-) diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index 79cd2c6..a387104 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -116,6 +116,23 @@ function M.clear_dirty_rows() _dirty_rows = {} end +---@param bufnr integer +---@return nil +function M.reapply_dirty_inline(bufnr) + if not next(_dirty_rows) then + return + end + local icons = config.get().icons + for row in pairs(_dirty_rows) do + local m = _meta[row] + if m then + vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, row - 1, row) + apply_inline_row(bufnr, row - 1, m, icons) + end + end + _dirty_rows = {} +end + ---@param bufnr integer ---@return nil function M.attach_bytes(bufnr) @@ -281,6 +298,55 @@ function M.get_fold() end end +---@param bufnr integer +---@param row integer +---@param m pending.LineMeta +---@param icons table +local function apply_inline_row(bufnr, row, m, icons) + if m.type == 'filter' then + local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' + vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, 0, { + end_col = #line, + hl_group = 'PendingFilter', + }) + elseif m.type == 'task' then + if m.status == 'done' then + local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' + local col_start = line:find('/%d+/') and select(2, line:find('/%d+/')) or 0 + vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, col_start, { + end_col = #line, + hl_group = 'PendingDone', + }) + end + local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' + local bracket_col = (line:find('%[') or 1) - 1 + local icon, icon_hl + if m.status == 'done' then + icon, icon_hl = icons.done, 'PendingDone' + elseif m.priority and m.priority > 0 then + icon, icon_hl = icons.priority, 'PendingPriority' + else + icon, icon_hl = icons.pending, 'Normal' + end + vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, bracket_col, { + virt_text = { { '[' .. icon .. ']', icon_hl } }, + virt_text_pos = 'overlay', + priority = 100, + }) + elseif m.type == 'header' then + local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' + vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, 0, { + end_col = #line, + hl_group = 'PendingHeader', + }) + vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, 0, { + virt_text = { { icons.category .. ' ', 'PendingHeader' } }, + virt_text_pos = 'overlay', + priority = 100, + }) + end +end + ---@param bufnr integer ---@param line_meta pending.LineMeta[] local function apply_extmarks(bufnr, line_meta) @@ -289,13 +355,7 @@ local function apply_extmarks(bufnr, line_meta) vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) for i, m in ipairs(line_meta) do local row = i - 1 - if m.type == 'filter' then - local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' - vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, 0, { - end_col = #line, - hl_group = 'PendingFilter', - }) - elseif m.type == 'task' then + 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 @@ -316,41 +376,8 @@ local function apply_extmarks(bufnr, line_meta) virt_text_pos = 'eol', }) end - if m.status == 'done' then - local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' - local col_start = line:find('/%d+/') and select(2, line:find('/%d+/')) or 0 - vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, col_start, { - end_col = #line, - hl_group = 'PendingDone', - }) - end - local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' - local bracket_col = (line:find('%[') or 1) - 1 - local icon, icon_hl - if m.status == 'done' then - icon, icon_hl = icons.done, 'PendingDone' - elseif m.priority and m.priority > 0 then - icon, icon_hl = icons.priority, 'PendingPriority' - else - icon, icon_hl = icons.pending, 'Normal' - end - vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, bracket_col, { - virt_text = { { '[' .. icon .. ']', icon_hl } }, - virt_text_pos = 'overlay', - priority = 100, - }) - elseif m.type == 'header' then - local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' - vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, 0, { - end_col = #line, - hl_group = 'PendingHeader', - }) - vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, 0, { - virt_text = { { icons.category .. ' ', 'PendingHeader' } }, - virt_text_pos = 'overlay', - priority = 100, - }) end + apply_inline_row(bufnr, row, m, icons) end end diff --git a/lua/pending/init.lua b/lua/pending/init.lua index 3e8702d..4d05503 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -251,7 +251,7 @@ function M._setup_autocmds(bufnr) end end, }) - vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { + vim.api.nvim_create_autocmd('TextChangedI', { group = group, buffer = bufnr, callback = function() @@ -263,6 +263,25 @@ function M._setup_autocmds(bufnr) end end, }) + vim.api.nvim_create_autocmd('TextChanged', { + group = group, + buffer = bufnr, + callback = function() + if not vim.bo[bufnr].modified then + return + end + buffer.reapply_dirty_inline(bufnr) + end, + }) + vim.api.nvim_create_autocmd('InsertLeave', { + group = group, + buffer = bufnr, + callback = function() + if vim.bo[bufnr].modified then + buffer.reapply_dirty_inline(bufnr) + end + end, + }) vim.api.nvim_create_autocmd('WinClosed', { group = group, callback = function(ev)