feat(buffer): add configurable eol_format for EOL virtual text

Problem: EOL virtual text order (category → recurrence → due) and the
double-space separator are hardcoded in `apply_extmarks()`. Users cannot
reorder, omit, or restyle metadata fields.

Solution: Add `eol_format` config field (default `'%c  %r  %d'`) with
`%c`, `%r`, `%d` specifiers. `parse_eol_format()` tokenizes the format
string; `build_eol_virt()` resolves specifiers against `LineMeta` and
collapses literals around absent fields.
This commit is contained in:
Barrett Ruth 2026-03-08 14:25:22 -04:00
parent 27f46ae0dd
commit b6c28eb7b3
3 changed files with 116 additions and 15 deletions

View file

@ -352,30 +352,103 @@ 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 icons = config.get().icons
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)
for i, m in ipairs(line_meta) do
local row = i - 1
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
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
local virt_parts = build_eol_virt(eol_segments, m, icons)
if #virt_parts > 0 then
for p = 1, #virt_parts - 1 do
virt_parts[p][1] = virt_parts[p][1] .. ' '
end
vim.api.nvim_buf_set_extmark(bufnr, ns_eol, row, 0, {
virt_text = virt_parts,
virt_text_pos = 'eol',