feat(buffer): render forge links as inline conceal overlays

Problem: forge tokens were stripped from the buffer and shown as EOL
virtual text via `%l`. The token disappeared from the editable line,
and multi-ref tasks broke.

Solution: compute `forge_spans` in `views.lua` with byte offsets for
each forge token in the rendered line. In `apply_inline_row()`, place
extmarks with `conceal=''` and `virt_text_pos='inline'` to visually
replace each raw token with its formatted label. Clear stale
`forge_spans` on dirty rows to prevent `end_col` out-of-range errors
after edits like `dd`.
This commit is contained in:
Barrett Ruth 2026-03-10 18:58:36 -04:00
parent 0a64691edd
commit 819d27d751
2 changed files with 47 additions and 4 deletions

View file

@ -168,6 +168,19 @@ local function apply_inline_row(bufnr, row, m, icons)
virt_text_pos = 'overlay',
priority = 100,
})
if m.forge_spans then
local forge = require('pending.forge')
for _, span in ipairs(m.forge_spans) do
local label_text, hl_group = forge.format_label(span.ref, span.cache)
vim.api.nvim_buf_set_extmark(bufnr, ns_inline, row, span.col_start, {
end_col = span.col_end,
conceal = '',
virt_text = { { label_text, hl_group } },
virt_text_pos = 'inline',
priority = 90,
})
end
end
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, {
@ -213,6 +226,7 @@ function M.reapply_dirty_inline(bufnr)
local line = vim.api.nvim_buf_get_lines(bufnr, row - 1, row, false)[1] or ''
local old_status = m.status
m.status = infer_status(line) or m.status
m.forge_spans = nil
log.debug(
('reapply_dirty: row=%d line=%q old_status=%s new_status=%s'):format(
row,
@ -379,10 +393,6 @@ local function setup_syntax(bufnr)
syntax match taskCheckbox /\[!\]/ contained containedin=taskLine
syntax match taskLine /^\/\d\+\/- \[.\] .*$/ contains=taskId,taskCheckbox
]])
local forge = require('pending.forge')
for _, pat in ipairs(forge.conceal_patterns()) do
vim.cmd('syntax match forgeRef /' .. pat .. '/ conceal contained containedin=taskLine')
end
end)
end

View file

@ -1,6 +1,13 @@
local config = require('pending.config')
local forge = require('pending.forge')
local parse = require('pending.parse')
---@class pending.ForgeLineMeta
---@field ref pending.ForgeRef
---@field cache? pending.ForgeCache
---@field col_start integer
---@field col_end integer
---@class pending.LineMeta
---@field type 'task'|'header'|'blank'|'filter'
---@field id? integer
@ -14,6 +21,7 @@ local parse = require('pending.parse')
---@field recur? string
---@field forge_ref? pending.ForgeRef
---@field forge_cache? pending.ForgeCache
---@field forge_spans? pending.ForgeLineMeta[]
---@class pending.views
local M = {}
@ -43,6 +51,27 @@ local function format_due(due)
return formatted
end
---@param task pending.Task
---@param prefix_len integer
---@return pending.ForgeLineMeta[]?
local function compute_forge_spans(task, prefix_len)
local refs = forge.find_refs(task.description)
if #refs == 0 then
return nil
end
local cache = task._extra and task._extra._forge_cache or nil
local spans = {}
for _, r in ipairs(refs) do
table.insert(spans, {
ref = r.ref,
cache = cache,
col_start = prefix_len + r.start_byte,
col_end = prefix_len + r.end_byte,
})
end
return spans
end
---@type table<string, integer>
local status_rank = { wip = 0, pending = 1, blocked = 2, done = 3 }
@ -178,6 +207,7 @@ function M.category_view(tasks)
local prefix = '/' .. task.id .. '/'
local state = state_char(task)
local line = prefix .. '- [' .. state .. '] ' .. task.description
local prefix_len = #prefix + #('- [' .. state .. '] ')
table.insert(lines, line)
table.insert(meta, {
type = 'task',
@ -191,6 +221,7 @@ function M.category_view(tasks)
recur = task.recur,
forge_ref = task._extra and task._extra._forge_ref or nil,
forge_cache = task._extra and task._extra._forge_cache or nil,
forge_spans = compute_forge_spans(task, prefix_len),
})
end
end
@ -231,6 +262,7 @@ function M.priority_view(tasks)
local prefix = '/' .. task.id .. '/'
local state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ')
local line = prefix .. '- [' .. state .. '] ' .. task.description
local prefix_len = #prefix + #('- [' .. state .. '] ')
table.insert(lines, line)
table.insert(meta, {
type = 'task',
@ -245,6 +277,7 @@ function M.priority_view(tasks)
recur = task.recur,
forge_ref = task._extra and task._extra._forge_ref or nil,
forge_cache = task._extra and task._extra._forge_cache or nil,
forge_spans = compute_forge_spans(task, prefix_len),
})
end