From 819d27d751149f93aa6c51706120601d0aabe9d2 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 10 Mar 2026 18:58:36 -0400 Subject: [PATCH] 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`. --- lua/pending/buffer.lua | 18 ++++++++++++++---- lua/pending/views.lua | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index ff3c13f..62c9539 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -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 diff --git a/lua/pending/views.lua b/lua/pending/views.lua index 149547e..fd76a49 100644 --- a/lua/pending/views.lua +++ b/lua/pending/views.lua @@ -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 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