repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi/entry.lua
Rawlocal transcript = require 'bugabinga.pivi.transcript'
local NAMESPACE = 'pivi-entry'
local KINDS = {
prompt = true,
answer = true,
tool = true,
error = true,
}
local records = {}
local versions = {}
--- Bumped whenever the entries of a buffer change shape.
--- Buffer text alone is not enough to know that: re-anchoring an entry moves
--- its bounds without touching a single character.
local version = function( bufnr )
return versions[bufnr] or 0
end
local touch = function( bufnr )
versions[bufnr] = version( bufnr ) + 1
end
local namespace = function()
return vim.api.nvim_create_namespace( NAMESPACE )
end
local of_buffer = function( bufnr )
records[bufnr] = records[bufnr] or {}
return records[bufnr]
end
local last_row = function( bufnr )
return vim.api.nvim_buf_line_count( bufnr ) - 1
end
local text_of = function( bufnr, row )
return vim.api.nvim_buf_get_lines( bufnr, row, row + 1, false )[1] or ''
end
--- Anchors an entry's range to the buffer itself.
--- Fixed line numbers would drift as later entries are appended, so bounds
--- are extmark-backed and re-anchored whenever the entry grows.
local anchor = function( bufnr, id, first, last )
touch( bufnr )
vim.api.nvim_buf_set_extmark( bufnr, namespace(), first, 0, {
id = id,
end_row = last,
end_col = #text_of( bufnr, last ),
right_gravity = false,
end_right_gravity = true,
} )
end
--- Starts an entry at the end of the transcript.
--- An entry always begins on its own line, so its bounds never share a line
--- with the entry before it.
--- @param bufnr number
--- @param kind string prompt, answer, tool, or error
--- @param meta table? entry metadata such as tool, path, line, args
--- @return number? id
local open = function( bufnr, kind, meta )
vim.validate( 'bufnr', bufnr, 'number' )
vim.validate( 'kind', kind, 'string' )
if not KINDS[kind] then error( 'unknown entry kind: ' .. kind ) end
if not vim.api.nvim_buf_is_valid( bufnr ) then return nil end
-- Virtual lines above the very first buffer line are not shown once that
-- line is at the top of a window, so an entry never starts on line one.
if last_row( bufnr ) == 0 or text_of( bufnr, last_row( bufnr ) ) ~= '' then
transcript.line( bufnr, '' )
end
local row = last_row( bufnr )
local id = vim.api.nvim_buf_set_extmark( bufnr, namespace(), row, 0, {
end_row = row,
end_col = 0,
right_gravity = false,
end_right_gravity = true,
} )
of_buffer( bufnr )[id] = { id = id, kind = kind, meta = meta or {}, }
touch( bufnr )
return id
end
local start_row = function( bufnr, id )
local mark = vim.api.nvim_buf_get_extmark_by_id( bufnr, namespace(), id, {} )
if not mark or #mark == 0 then return nil end
return mark[1]
end
--- The last row an entry actually shows.
--- Appending a complete line leaves an empty row behind it, and that row is
--- where the next entry starts rather than part of this one.
local content_row = function( bufnr, first )
local row = last_row( bufnr )
while row > first and text_of( bufnr, row ) == '' do row = row - 1 end
return row
end
--- Re-anchors an entry to everything written since it opened.
local extend = function( bufnr, id )
if not id or not vim.api.nvim_buf_is_valid( bufnr ) then return end
if not of_buffer( bufnr )[id] then return end
local first = start_row( bufnr, id )
if not first then return end
anchor( bufnr, id, first, content_row( bufnr, first ) )
end
--- Appends text to an open entry, continuing its final line.
local write = function( bufnr, id, text )
if not id then return end
transcript.append( bufnr, text )
extend( bufnr, id )
end
--- Appends one complete line to an open entry.
local line = function( bufnr, id, text )
if not id then return end
transcript.line( bufnr, text )
extend( bufnr, id )
end
--- Replaces everything an entry shows below its opening line.
--- A tool that reports progress rewrites its own tail, so the entry keeps its
--- identity and its bounds while its content changes.
local rewrite = function( bufnr, id, text )
if not id or not vim.api.nvim_buf_is_valid( bufnr ) then return end
if not of_buffer( bufnr )[id] then return end
local first = start_row( bufnr, id )
if not first then return end
transcript.replace( bufnr, first + 1, content_row( bufnr, first ) + 1, text )
extend( bufnr, id )
end
local close = function( bufnr, id )
extend( bufnr, id )
return nil
end
--- @return number? first 1-based first line
--- @return number? last 1-based last line
local range = function( bufnr, id )
if not vim.api.nvim_buf_is_valid( bufnr ) then return nil end
local mark = vim.api.nvim_buf_get_extmark_by_id( bufnr, namespace(), id, { details = true, } )
if not mark or #mark == 0 then return nil end
local details = mark[3] or {}
return mark[1] + 1, ( details.end_row or mark[1] ) + 1
end
local describe = function( bufnr, record )
local first, last = range( bufnr, record.id )
if not first then return nil end
return {
id = record.id,
kind = record.kind,
meta = record.meta,
first = first,
last = last,
}
end
--- Every entry of a transcript, in buffer order.
local list = function( bufnr )
if not vim.api.nvim_buf_is_valid( bufnr ) then return {} end
local found = {}
for _, record in pairs( of_buffer( bufnr ) ) do
local described = describe( bufnr, record )
if described then table.insert( found, described ) end
end
table.sort( found, function( left, right ) return left.first < right.first end )
return found
end
--- The entry covering a 1-based line, if any.
local at = function( bufnr, line_number )
local found
for _, entry in ipairs( list( bufnr ) ) do
if entry.first <= line_number and line_number <= entry.last then found = entry end
end
return found
end
local meta = function( bufnr, id )
local record = of_buffer( bufnr )[id]
return record and record.meta or nil
end
--- Replaces an entry's metadata fields, leaving its text untouched.
local annotate = function( bufnr, id, fields )
local record = of_buffer( bufnr )[id]
if not record then return end
record.meta = vim.tbl_extend( 'force', record.meta, fields or {} )
end
local clear = function( bufnr )
records[bufnr] = nil
versions[bufnr] = nil
if vim.api.nvim_buf_is_valid( bufnr ) then
vim.api.nvim_buf_clear_namespace( bufnr, namespace(), 0, -1 )
end
end
transcript.on_reset( clear )
return {
namespace = namespace,
version = version,
kinds = KINDS,
open = open,
write = write,
line = line,
rewrite = rewrite,
extend = extend,
close = close,
range = range,
list = list,
at = at,
meta = meta,
annotate = annotate,
clear = clear,
}