local 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', '(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, }