/** `lsp` — language-server queries: diagnostics, definitions, references, hover, rename, … */ import type { ReactNode } from "react"; import type { Tone } from "../parts"; import { Badge, InvalidArg, Kv, KvGrid, Output, PathText, ResultText, Row } from "../parts"; import type { ToolRenderer, ToolRenderProps } from "../types"; import { detailsRecord, normalizeWs, num, resultTextOf, str, truncate } from "../util"; /** `file:line:col [severity] message` — the diagnostics line format the tool emits. */ const DIAG_RE = /^(.*):(\d+):(\d+)\s+\[(\w+)\]\s*(.*)$/; /** Bare `file:line:col` location line (references, definitions, implementations). */ const LOC_RE = /^(.+):(\d+):(\d+)$/; /** Actions whose result text is a list of locations. */ const LOCATION_ACTIONS: Record = { definition: true, references: true, type_definition: true, implementation: true, }; const MAX_ROWS = 24; interface DiagRow { file: string; line: string; col: string; severity: string; message: string; } interface LocRow { file: string; line: string; col: string; } function parseDiagnostics(text: string): DiagRow[] { const rows: DiagRow[] = []; for (const raw of text.split("\n")) { const m = raw.trim().match(DIAG_RE); if (m) rows.push({ file: m[1], line: m[2], col: m[3], severity: m[4].toLowerCase(), message: normalizeWs(m[5]) }); } return rows; } function parseLocations(text: string): LocRow[] { const rows: LocRow[] = []; for (const raw of text.split("\n")) { const m = raw.trim().match(LOC_RE); if (m) rows.push({ file: m[1], line: m[2], col: m[3] }); } return rows; } function severityTone(severity: string): Tone | undefined { switch (severity) { case "error": return "err"; case "warning": return "warn"; case "info": return "accent"; default: return undefined; } } /** Kv row for an optional arg: hidden when absent, InvalidArg when mistyped. */ function ArgKv({ k, raw, val }: { k: string; raw: unknown; val: ReactNode }): ReactNode { if (raw === undefined) return null; return {val == null || val === false ? : val}; } function DiagnosticRows({ text, rows }: { text: string; rows: DiagRow[] }): ReactNode { const errMatch = text.match(/(\d+)\s+error\(s\)/); const warnMatch = text.match(/(\d+)\s+warning\(s\)/); const shown = rows.slice(0, MAX_ROWS); return ( <> {(errMatch || warnMatch) && ( {errMatch && ( {errMatch[1]} error{errMatch[1] === "1" ? "" : "s"} )} {warnMatch && ( {warnMatch[1]} warning{warnMatch[1] === "1" ? "" : "s"} )} )}
{shown.map((d, i) => ( {d.severity}}> {d.message && {truncate(d.message, 160)}} ))} {rows.length > shown.length && ( … {rows.length - shown.length} more )}
); } function LocationRows({ text, rows }: { text: string; rows: LocRow[] }): ReactNode { const refMatch = text.match(/(\d+)\s+reference\(s\)/); const shown = rows.slice(0, MAX_ROWS); return ( <> {refMatch && ( {refMatch[1]} reference{refMatch[1] === "1" ? "" : "s"} )}
{shown.map((l, i) => ( ))} {rows.length > shown.length && ( … {rows.length - shown.length} more )}
); } function Summary({ args }: ToolRenderProps): ReactNode { const action = str(args.action); const file = str(args.file); const line = num(args.line); const symbol = str(args.symbol); const query = str(args.query); const newName = str(args.new_name); return ( <> {action ? action.replace(/_/g, " ") : "request"} {file === "*" && workspace} {file && file !== "*" && } {!file && line != null && line {line}} {symbol && {truncate(normalizeWs(symbol), 48)}} {query && {truncate(normalizeWs(query), 48)}} {newName && → {truncate(normalizeWs(newName), 48)}} ); } function Body({ args, result }: ToolRenderProps): ReactNode { const details = detailsRecord(result); const file = str(args.file); const line = num(args.line); const symbol = str(args.symbol); const query = str(args.query); const newName = str(args.new_name); const apply = typeof args.apply === "boolean" ? args.apply : null; const timeout = num(args.timeout); const payload = str(args.payload); const serverName = details ? str(details.serverName) : null; const action = str(args.action) ?? (details ? str(details.action) : null); const text = result && !result.isError ? resultTextOf(result) : ""; const diags = text ? parseDiagnostics(text) : []; const locs = diags.length === 0 && text && ((action != null && LOCATION_ACTIONS[action]) || /\d+\s+reference\(s\)/.test(text)) ? parseLocations(text) : []; return ( <> workspace : file && } /> {!file && } {args.payload !== undefined && payload == null && ( )} {serverName && {serverName}} {payload && } {diags.length > 0 ? ( ) : locs.length > 0 ? ( ) : ( )} ); } export const lspRenderer: ToolRenderer = { Summary, Body };