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