Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/pivi/fold.lua

Raw
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', '<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,
}