repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/bugabinga/pivi/fold.lua
Rawlocal entry = require 'bugabinga.pivi.entry'
local transcript = require 'bugabinga.pivi.transcript'
local tool = require 'bugabinga.pivi.tool'
local glyph = require 'std.icon'
local EXPRESSION = "v:lua.require'bugabinga.pivi.fold'.expr()"
local TEXT = "v:lua.require'bugabinga.pivi.fold'.text()"
local ICONS = {
prompt = glyph.user,
answer = glyph.star,
tool = glyph.command,
error = glyph.error,
}
local levels = {}
local wanted = {}
--- An entry a reader rarely wants in full on arrival.
--- Tool output is voluminous and secondary; a question, an answer, and an
--- error are the conversation itself and stay open.
local voluminous = function( item )
return item.kind == 'tool' and item.last > item.first
end
--- Fold levels, recomputed whenever entry bounds could have moved.
--- Buffer text alone is not a sufficient key, because an entry is re-anchored
--- after its text is written and that never changes a character.
local folds_of = function( bufnr )
local tick = vim.api.nvim_buf_get_changedtick( bufnr )
local shape = entry.version( bufnr )
local cached = levels[bufnr]
if cached and cached.tick == tick and cached.shape == shape then return cached.rows end
local rows = {}
for _, item in ipairs( entry.list( bufnr ) ) do
if item.last > item.first then
rows[item.first] = '>1'
for row = item.first + 1, item.last do rows[row] = '1' end
end
end
levels[bufnr] = { tick = tick, shape = shape, rows = rows, }
return rows
end
--- Fold level of the line being evaluated.
--- Levels come from entry bounds, so a fold is always exactly one entry.
local expr = function()
local bufnr = vim.api.nvim_get_current_buf()
return folds_of( bufnr )[vim.v.lnum] or '0'
end
--- One readable line standing in for a collapsed entry.
--- A collapsed tool entry still states what it acted on and how it ended,
--- because that is the whole reason a reader leaves it collapsed.
local text = function()
local bufnr = vim.api.nvim_get_current_buf()
local first = vim.v.foldstart or 1
local item = entry.at( bufnr, first )
local headline = vim.api.nvim_buf_get_lines( bufnr, first - 1, first, false )[1] or ''
local icon = item and ICONS[item.kind] or glyph.command
if item and item.kind == 'tool' then
icon = tool.icon( item.meta )
local ended = tool.outcome( item.meta )
if ended then headline = vim.trim( headline ) .. ' ' .. ended end
end
return ( '%s %s %s %d more lines' ):format(
icon, vim.trim( headline ), glyph.middle_ellipsis, vim.v.foldend - first
)
end
local windows_of = function( bufnr )
local found = {}
for _, win in ipairs( vim.api.nvim_list_wins() ) do
if vim.api.nvim_win_get_buf( win ) == bufnr then table.insert( found, win ) end
end
return found
end
--- Turns a window into an entry-folding view of a transcript.
local attach = function( win )
vim.wo[win].foldmethod = 'expr'
vim.wo[win].foldexpr = EXPRESSION
vim.wo[win].foldtext = TEXT
vim.wo[win].foldenable = true
vim.wo[win].foldlevel = 99
vim.wo[win].foldcolumn = '0'
vim.wo[win].fillchars = 'eob: ,fold: '
end
--- Whether an entry should currently be collapsed.
--- A reader's choice wins; otherwise only voluminous entries start collapsed.
local collapsed = function( bufnr, item )
local choice = ( wanted[bufnr] or {} )[item.id]
if choice ~= nil then return choice end
return voluminous( item )
end
--- Brings every window on a transcript to the intended fold state.
--- Text appended to a buffer does not recompute folds until a redraw, so the
--- folds are recomputed first and then driven from intent rather than from
--- whatever state a window happened to be left in.
local apply = function( bufnr )
if not vim.api.nvim_buf_is_valid( bufnr ) then return end
local wins = windows_of( bufnr )
if #wins == 0 then return end
local items = entry.list( bufnr )
for _, win in ipairs( wins ) do
pcall( vim.api.nvim_win_call, win, function()
vim.cmd 'normal! zX'
for _, item in ipairs( items ) do
if item.last > item.first and collapsed( bufnr, item ) then
pcall( function() vim.cmd( tostring( item.first ) .. 'foldclose' ) end )
end
end
end )
end
end
--- Collapses an expanded entry, or expands a collapsed one.
local toggle = function( bufnr, line_number )
local item = entry.at( bufnr, line_number )
if not item or item.last == item.first then return false end
wanted[bufnr] = wanted[bufnr] or {}
local target = not collapsed( bufnr, item )
wanted[bufnr][item.id] = target
local command = target and 'foldclose' or 'foldopen'
return ( pcall( function() vim.cmd( tostring( item.first ) .. command ) end ) )
end
local closed = function( line_number )
return vim.fn.foldclosed( line_number ) ~= -1
end
local reset = function( bufnr )
levels[bufnr] = nil
wanted[bufnr] = nil
end
--- pivi publishes intent and never chooses a key.
--- Ordinary fold keys already work here, because entries are real folds.
local bind = function( bufnr )
vim.keymap.set( 'n', '<Plug>(pivi-toggle-entry)', function()
toggle( bufnr, vim.api.nvim_win_get_cursor( 0 )[1] )
end, { buffer = bufnr, desc = 'pivi collapse or expand the entry', } )
end
transcript.on_reset( reset )
transcript.on_window( attach )
transcript.on_buffer( bind )
return {
expression = EXPRESSION,
fold_text = TEXT,
voluminous = voluminous,
collapsed = collapsed,
expr = expr,
text = text,
attach = attach,
apply = apply,
toggle = toggle,
closed = closed,
reset = reset,
}