diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index ecdae4c..c3e666f 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -12,7 +12,8 @@ local _store = nil local task_bufnr = nil ---@type integer? local task_winid = nil -local task_ns = vim.api.nvim_create_namespace('pending') +local ns_eol = vim.api.nvim_create_namespace('pending_eol') +local ns_inline = vim.api.nvim_create_namespace('pending_inline') ---@type 'category'|'priority'|nil local current_view = nil ---@type pending.LineMeta[] @@ -25,6 +26,12 @@ local _initial_fold_loaded = false local _filter_predicates = {} ---@type table local _hidden_ids = {} +---@type table +local _dirty_rows = {} +---@type boolean +local _on_bytes_active = false +---@type boolean +local _rendering = false ---@return pending.LineMeta[] function M.meta() @@ -89,7 +96,127 @@ end ---@param b? integer ---@return nil function M.clear_marks(b) - vim.api.nvim_buf_clear_namespace(b or task_bufnr, task_ns, 0, -1) + local bufnr = b or task_bufnr + vim.api.nvim_buf_clear_namespace(bufnr, ns_eol, 0, -1) + vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) +end + +---@param b integer +---@param row integer +---@return nil +function M.clear_inline_row(b, row) + vim.api.nvim_buf_clear_namespace(b, ns_inline, row - 1, row) +end + +---@return table +function M.dirty_rows() + return _dirty_rows +end + +---@return nil +function M.clear_dirty_rows() + _dirty_rows = {} +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 +---@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) + if _on_bytes_active then + return + end + _on_bytes_active = true + vim.api.nvim_buf_attach(bufnr, false, { + on_bytes = function(_, buf, _, start_row, _, _, old_end_row, _, _, new_end_row, _, _) + if buf ~= task_bufnr then + _on_bytes_active = false + return true + end + if _rendering then + return + end + local delta = new_end_row - old_end_row + if delta > 0 then + for _ = 1, delta do + table.insert(_meta, start_row + 2, { type = 'task' }) + end + elseif delta < 0 then + for _ = 1, -delta do + if _meta[start_row + 2] then + table.remove(_meta, start_row + 2) + end + end + end + for r = start_row + 1, start_row + 1 + math.max(0, new_end_row) do + _dirty_rows[r] = true + end + end, + }) end ---@return nil @@ -205,7 +332,6 @@ 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 @@ -230,16 +356,11 @@ end ---@param line_meta pending.LineMeta[] local function apply_extmarks(bufnr, line_meta) local icons = config.get().icons - vim.api.nvim_buf_clear_namespace(bufnr, task_ns, 0, -1) + vim.api.nvim_buf_clear_namespace(bufnr, ns_eol, 0, -1) + 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, task_ns, 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 @@ -255,46 +376,13 @@ local function apply_extmarks(bufnr, line_meta) for p = 1, #virt_parts - 1 do virt_parts[p][1] = virt_parts[p][1] .. ' ' end - vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { + vim.api.nvim_buf_set_extmark(bufnr, ns_eol, row, 0, { virt_text = virt_parts, 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, task_ns, 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, task_ns, 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, task_ns, row, 0, { - end_col = #line, - hl_group = 'PendingHeader', - }) - vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { - virt_text = { { icons.category .. ' ', 'PendingHeader' } }, - virt_text_pos = 'overlay', - priority = 100, - }) end + apply_inline_row(bufnr, row, m, icons) end end @@ -448,12 +536,15 @@ function M.render(bufnr) end _meta = line_meta + _dirty_rows = {} snapshot_folds(bufnr) vim.bo[bufnr].modifiable = true local saved = vim.bo[bufnr].undolevels vim.bo[bufnr].undolevels = -1 + _rendering = true vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + _rendering = false vim.bo[bufnr].modified = false vim.bo[bufnr].undolevels = saved @@ -507,6 +598,7 @@ function M.open() if not (task_bufnr and vim.api.nvim_buf_is_valid(task_bufnr)) then task_bufnr = vim.api.nvim_create_buf(true, false) set_buf_options(task_bufnr) + M.attach_bytes(task_bufnr) end vim.cmd('botright new') diff --git a/lua/pending/init.lua b/lua/pending/init.lua index af018a7..4d05503 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -251,12 +251,34 @@ 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() + if not vim.bo[bufnr].modified then + return + end + for row in pairs(buffer.dirty_rows()) do + buffer.clear_inline_row(bufnr, row) + 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.clear_marks(bufnr) + buffer.reapply_dirty_inline(bufnr) end end, })