import path from "node:path";
import type { LspDiagnostic, LspDiagnosticsResult } from "./types.js";
function displayPath(workspace: string, file: string): string {
const relative = path.relative(workspace, file);
return (relative && !relative.startsWith("..") && !path.isAbsolute(relative)
? relative
: file
).split(path.sep).join("/");
}
export type FormattedDiagnostics = {
lines: string[];
kept: LspDiagnostic[];
total: number;
errors: number;
warnings: number;
files: number;
omitted: number;
};
export function formatDiagnosticLines(
diagnostics: LspDiagnostic[],
workspace: string,
options: { maxTotal: number; maxPerFile: number; maxMessageChars?: number },
): FormattedDiagnostics {
const errors = diagnostics.filter((item) => item.severity === "error").length;
const warnings = diagnostics.length - errors;
const perFile = new Map<string, number>();
const kept: LspDiagnostic[] = [];
for (const item of diagnostics) {
if (kept.length >= options.maxTotal) break;
const count = perFile.get(item.file) ?? 0;
if (count >= options.maxPerFile) continue;
perFile.set(item.file, count + 1);
kept.push(item);
}
const maxChars = options.maxMessageChars ?? 240;
const lines = kept.map((item) => {
const code = item.code ? ` ${item.code}` : "";
const message = item.message.replace(/\s+/g, " ").slice(0, maxChars);
return `${displayPath(workspace, item.file)}(${item.line},${item.column}): ${item.severity}${code}: ${message}`;
});
return {
lines,
kept,
total: diagnostics.length,
errors,
warnings,
files: new Set(diagnostics.map((item) => item.file)).size,
omitted: diagnostics.length - kept.length,
};
}
function serverLabel(result: LspDiagnosticsResult): string {
return result.serverNames.length ? ` (${result.serverNames.join(", ")})` : "";
}
export function formatToolDiagnostics(
result: LspDiagnosticsResult,
workspace: string,
maxResults: number,
): string {
if (!result.diagnostics.length) {
if (result.unavailable) return `[diagnostics] Unavailable: ${result.unavailable}`;
return `[diagnostics] No errors or warnings${serverLabel(result)}.`;
}
const formatted = formatDiagnosticLines(result.diagnostics, workspace, {
maxTotal: maxResults,
maxPerFile: Math.max(5, Math.ceil(maxResults / 2)),
});
const header = `[diagnostics] ${formatted.errors} error${formatted.errors === 1 ? "" : "s"}, ${
formatted.warnings
} warning${formatted.warnings === 1 ? "" : "s"} in ${formatted.files} file${
formatted.files === 1 ? "" : "s"
}${serverLabel(result)}`;
const lines = [header, ...formatted.lines];
if (formatted.omitted > 0) {
lines.push(`... [${formatted.omitted} more omitted]`);
}
if (result.unavailable) {
lines.push(`[diagnostics] Partially unavailable: ${result.unavailable}`);
}
return lines.join("\n");
}
export function formatLspFeedback(
result: LspDiagnosticsResult,
workspace: string,
include?: (diagnostic: LspDiagnostic) => boolean,
): string | null {
const diagnostics = include ? result.diagnostics.filter(include) : result.diagnostics;
if (!diagnostics.length) return null;
const formatted = formatDiagnosticLines(diagnostics, workspace, {
maxTotal: 20,
maxPerFile: 5,
});
if (!formatted.lines.length) return null;
const lines = [
`--- lsp diagnostics: ${formatted.errors} error${
formatted.errors === 1 ? "" : "s"
}, ${formatted.warnings} warning${formatted.warnings === 1 ? "" : "s"} in ${
formatted.files
} file${formatted.files === 1 ? "" : "s"}${serverLabel(result)} ---`,
...formatted.lines,
];
if (formatted.omitted > 0) {
lines.push(`... [${formatted.omitted} more omitted]`);
}
return lines.join("\n");
}