Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/pivi/tool.lua

Raw
local glyph = require 'std.icon'

local MAX_SUBJECT = 60

--- What each tool does, at a glance.
--- A reader scans a transcript for the shape of the work, so the icon says
--- what kind of work it was rather than which tool performed it.
local ICONS = {
  read = glyph.file,
  write = glyph.edit,
  edit = glyph.edit,
  bash = glyph.shell,
  powershell = glyph.shell,
  grep = glyph.search,
  find = glyph.search,
  ls = glyph.folder,
  nvim_help = glyph.help,
  nvim_state = glyph.vim,
  nvim_lua = glyph.vim,
  nvim_findings = glyph.list,
}

local DEFAULT_ICON = glyph.command

--- The icon for a tool entry.
local icon = function( meta )
  local name = meta and meta.tool
  return ICONS[name] or DEFAULT_ICON
end

--- What a tool reports about its own work.
--- Everything here is derived from that report; a tool that reports nothing
--- gets no outcome rather than an invented one.
local details_of = function( meta )
  local result = meta and meta.result
  if type( result ) ~= 'table' then return nil end
  if type( result.details ) ~= 'table' then return nil end
  return result.details
end

local relative = function( path, cwd )
  if type( path ) ~= 'string' or path == '' then return nil end
  local root = cwd and vim.fs.normalize( cwd ) or nil
  local full = vim.fs.normalize( path )
  if root and full:sub( 1, #root + 1 ) == root .. '/' then return full:sub( #root + 2 ) end
  return vim.fs.basename( full )
end

--- What the tool acted on, short enough to scan.
local subject = function( meta )
  local args = meta and type( meta.args ) == 'table' and meta.args or {}
  local found = args.path or args.file_path or args.filePath
    or args.command or args.pattern or args.tag or args.title

  if type( found ) ~= 'string' or found == '' then return nil end
  if found:find( '[/\\]' ) then found = relative( found, meta.cwd ) or found end

  found = found:gsub( '%s+', ' ' )
  if #found > MAX_SUBJECT then found = found:sub( 1, MAX_SUBJECT - 1 ) .. '\u{2026}' end
  return found
end

local counts = function( diff )
  local added, removed = 0, 0
  for _, line in ipairs( vim.split( diff or '', '\n', { plain = true, } ) ) do
    local mark = line:sub( 1, 1 )
    if mark == '+' then
      added = added + 1
    elseif mark == '-' then
      removed = removed + 1
    end
  end
  return added, removed
end

local LIMITS = {
  { field = 'matchLimitReached', label = 'match limit', },
  { field = 'resultLimitReached', label = 'result limit', },
  { field = 'entryLimitReached', label = 'entry limit', },
}

--- How the tool ended, or nothing when it reported nothing.
local outcome = function( meta )
  if not meta then return nil end
  local details = details_of( meta )
  local parts = {}

  if details and type( details.diff ) == 'string' then
    local added, removed = counts( details.diff )
    table.insert( parts, ( '+%d -%d' ):format( added, removed ) )
  end

  if details and type( details.truncation ) == 'table' and details.truncation.truncated then
    table.insert( parts, 'truncated' )
  end

  for _, limit in ipairs( LIMITS ) do
    if details and details[limit.field] then table.insert( parts, limit.label ) end
  end

  if meta.failed then table.insert( parts, 'failed' ) end

  if #parts == 0 then return nil end
  return table.concat( parts, '  ' )
end

--- The change a completed edit reported, as the tool formatted it.
local diff = function( meta )
  local details = details_of( meta )
  if not details or type( details.diff ) ~= 'string' then return nil end
  local text = details.diff:gsub( '\n+$', '' )
  if text == '' then return nil end
  return text
end

--- The line a reported change starts at, when the tool reported one.
local first_changed_line = function( meta )
  local details = details_of( meta )
  local line = details and details.firstChangedLine
  if type( line ) ~= 'number' or line < 1 then return nil end
  return math.floor( line )
end

--- How one line of a reported change should read.
local diff_highlight = function( text )
  local mark = ( text or '' ):sub( 1, 1 )
  if mark == '+' then return 'DiffAdd' end
  if mark == '-' then return 'DiffDelete' end
  return nil
end

--- The text a tool produced, as the tool produced it.
local produced = function( meta )
  local result = meta and meta.result
  if type( result ) ~= 'table' then return nil end
  local pieces = {}
  for _, block in ipairs( result.content or {} ) do
    if block.type == 'text' and type( block.text ) == 'string' then
      table.insert( pieces, block.text )
    end
  end
  local joined = vim.trim( table.concat( pieces, '\n' ) )
  if joined == '' then return nil end
  return joined
end

--- What a tool entry shows below its headline.
--- A reported change reads better than a sentence about it, so an edit shows
--- its change and every other tool shows what it produced.
local body = function( meta )
  local change = diff( meta )
  if change then return vim.split( change, '\n', { plain = true, } ) end

  local output = produced( meta )
  if output then return vim.split( output, '\n', { plain = true, } ) end

  if meta and meta.failed then return { 'failed', } end
  return {}
end

--- Tools whose output is the content of the file they name.
local CONTENT_TOOLS = { read = true, }

--- The language a tool's output came from, when that is known.
--- Only a tool that returns file content can claim one; anything else would
--- be a guess dressed up as syntax.
local language = function( meta )
  if not meta or not CONTENT_TOOLS[meta.tool] then return nil end
  if diff( meta ) then return nil end

  local args = type( meta.args ) == 'table' and meta.args or {}
  local path = args.path or args.file_path or args.filePath
  if type( path ) ~= 'string' or path == '' then return nil end

  local filetype = vim.filetype.match { filename = path, }
  if not filetype then return nil end

  local resolved = vim.treesitter.language.get_lang( filetype )
  if not resolved then return nil end
  if not pcall( vim.treesitter.language.add, resolved ) then return nil end
  return resolved
end

--- Whether a tool kept less than it produced.
local truncated = function( meta )
  local details = details_of( meta )
  local truncation = details and details.truncation
  return type( truncation ) == 'table' and truncation.truncated == true
end

--- Where everything a tool produced still lives, when it reported that.
local full_output = function( meta )
  local details = details_of( meta )
  local path = details and details.fullOutputPath
  if type( path ) ~= 'string' or path == '' then return nil end
  return path
end

--- The line a tool entry opens with: the tool and what it acted on.
local headline = function( meta )
  local name = tostring( meta and meta.tool or 'tool' )
  local acted_on = subject( meta )
  if not acted_on then return name end
  return name .. '  ' .. acted_on
end

--- Tool, subject and outcome in one line, for a collapsed entry.
local summary = function( meta )
  local ended = outcome( meta )
  if not ended then return headline( meta ) end
  return headline( meta ) .. '  ' .. ended
end

return {
  max_subject = MAX_SUBJECT,
  icons = ICONS,
  icon = icon,
  details = details_of,
  relative = relative,
  subject = subject,
  counts = counts,
  diff = diff,
  first_changed_line = first_changed_line,
  diff_highlight = diff_highlight,
  produced = produced,
  body = body,
  language = language,
  outcome = outcome,
  truncated = truncated,
  full_output = full_output,
  headline = headline,
  summary = summary,
}