diff --git a/doc/pending.txt b/doc/pending.txt index 11c3973..2883664 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -648,32 +648,6 @@ Fields: ~ virtual text in the buffer. Examples: `'%Y-%m-%d'` for ISO dates, `'%d %b'` for day-first. - {eol_format} (string, default: '%c %r %d') - Format string controlling the order, content, and - separators of end-of-line virtual text on task lines. - Three specifiers are available: - - `%c` category icon + name (`PendingHeader`) - `%r` recurrence icon + pattern (`PendingRecur`) - `%d` due icon + date (`PendingDue` / `PendingOverdue`) - - Literal text between specifiers is rendered with the - `Normal` highlight group and acts as a separator. - When a specifier's data is absent (e.g. `%d` on a - task with no due date), the specifier and any - surrounding literal text up to the next specifier - are omitted — missing fields never leave gaps. - - `%c` only renders in priority view (where - `show_category` is true). In category view it is - always omitted regardless of the format string. - - Examples: >lua - vim.g.pending = { eol_format = '%d %r' } - vim.g.pending = { eol_format = '%d | %r' } - vim.g.pending = { eol_format = '%c %d %r' } -< - {input_date_formats} (string[], default: {}) *pending-input-formats* List of strftime-like format strings tried in order when parsing a `due:` token that does not match the diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index 34ce3b3..ecdae4c 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -12,8 +12,7 @@ local _store = nil local task_bufnr = nil ---@type integer? local task_winid = nil -local ns_eol = vim.api.nvim_create_namespace('pending_eol') -local ns_inline = vim.api.nvim_create_namespace('pending_inline') +local task_ns = vim.api.nvim_create_namespace('pending') ---@type 'category'|'priority'|nil local current_view = nil ---@type pending.LineMeta[] @@ -26,12 +25,6 @@ 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() @@ -96,127 +89,7 @@ end ---@param b? integer ---@return nil function M.clear_marks(b) - 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, - }) + vim.api.nvim_buf_clear_namespace(b or task_bufnr, task_ns, 0, -1) end ---@return nil @@ -332,6 +205,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 @@ -352,110 +226,75 @@ function M.get_fold() end end ----@class pending.EolSegment ----@field type 'specifier'|'literal' ----@field key? 'c'|'r'|'d' ----@field text? string - ----@param fmt string ----@return pending.EolSegment[] -local function parse_eol_format(fmt) - local segments = {} - local pos = 1 - local len = #fmt - while pos <= len do - if fmt:sub(pos, pos) == '%' and pos + 1 <= len then - local key = fmt:sub(pos + 1, pos + 1) - if key == 'c' or key == 'r' or key == 'd' then - table.insert(segments, { type = 'specifier', key = key }) - pos = pos + 2 - else - table.insert(segments, { type = 'literal', text = '%' .. key }) - pos = pos + 2 - end - else - local next_pct = fmt:find('%%', pos + 1) - local chunk = next_pct and fmt:sub(pos, next_pct - 1) or fmt:sub(pos) - table.insert(segments, { type = 'literal', text = chunk }) - pos = pos + #chunk - end - end - return segments -end - ----@param segments pending.EolSegment[] ----@param m pending.LineMeta ----@param icons pending.Icons ----@return string[][] -local function build_eol_virt(segments, m, icons) - local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue' - local resolved = {} - for i, seg in ipairs(segments) do - if seg.type == 'specifier' then - local text, hl - if seg.key == 'c' and m.show_category and m.category then - text = icons.category .. ' ' .. m.category - hl = 'PendingHeader' - elseif seg.key == 'r' and m.recur then - text = icons.recur .. ' ' .. m.recur - hl = 'PendingRecur' - elseif seg.key == 'd' and m.due then - text = icons.due .. ' ' .. m.due - hl = due_hl - end - resolved[i] = text and { text = text, hl = hl, present = true } - or { present = false } - else - resolved[i] = { text = seg.text, hl = 'Normal', literal = true } - end - end - - local virt_parts = {} - for i, r in ipairs(resolved) do - if r.literal then - local prev_present, next_present = false, false - for j = i - 1, 1, -1 do - if not resolved[j].literal then - prev_present = resolved[j].present - break - end - end - for j = i + 1, #resolved do - if not resolved[j].literal then - next_present = resolved[j].present - break - end - end - if prev_present and next_present then - table.insert(virt_parts, { r.text, r.hl }) - end - elseif r.present then - table.insert(virt_parts, { r.text, r.hl }) - end - end - return virt_parts -end - ---@param bufnr integer ---@param line_meta pending.LineMeta[] local function apply_extmarks(bufnr, line_meta) - local cfg = config.get() - local icons = cfg.icons - local eol_segments = parse_eol_format(cfg.eol_format or '%c %r %d') - vim.api.nvim_buf_clear_namespace(bufnr, ns_eol, 0, -1) - vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) + local icons = config.get().icons + vim.api.nvim_buf_clear_namespace(bufnr, task_ns, 0, -1) for i, m in ipairs(line_meta) do local row = i - 1 - if m.type == 'task' then - local virt_parts = build_eol_virt(eol_segments, 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, task_ns, row, 0, { + end_col = #line, + hl_group = 'PendingFilter', + }) + elseif m.type == 'task' then + local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue' + local virt_parts = {} + if m.show_category and m.category then + table.insert(virt_parts, { icons.category .. ' ' .. m.category, 'PendingHeader' }) + end + if m.recur then + table.insert(virt_parts, { icons.recur .. ' ' .. m.recur, 'PendingRecur' }) + end + if m.due then + table.insert(virt_parts, { icons.due .. ' ' .. m.due, due_hl }) + end if #virt_parts > 0 then - vim.api.nvim_buf_set_extmark(bufnr, ns_eol, row, 0, { + 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, { 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 @@ -609,15 +448,12 @@ 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 @@ -671,7 +507,6 @@ 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/config.lua b/lua/pending/config.lua index b7a42ad..d926037 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -64,7 +64,6 @@ ---@field keymaps pending.Keymaps ---@field folding? boolean|pending.FoldingConfig ---@field sync? pending.SyncConfig ----@field eol_format? string ---@field icons pending.Icons ---@class pending.config @@ -79,7 +78,6 @@ local defaults = { date_syntax = 'due', recur_syntax = 'rec', someday_date = '9999-12-30', - eol_format = '%c %r %d', folding = true, category_order = {}, keymaps = { diff --git a/lua/pending/init.lua b/lua/pending/init.lua index 4d05503..af018a7 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -251,34 +251,12 @@ function M._setup_autocmds(bufnr) end end, }) - 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', { + vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, { group = group, buffer = bufnr, callback = function() if vim.bo[bufnr].modified then - buffer.reapply_dirty_inline(bufnr) + buffer.clear_marks(bufnr) end end, })