diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 445946dab0c67..819c075473c82 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -776,37 +776,233 @@ export function formatLocation(file: SourceFile, start: number, host: FormatDiag return output; } +// Enhanced formatting constants (Issue #45717) +const ENHANCED_FORMAT_MIN_WIDTH = 60; +const ENHANCED_BULLET = "●"; +const ENHANCED_VERTICAL_BAR = "|"; +const ENHANCED_OVERLINE = "▔"; + +/** + * Formats code span with enhanced visual formatting (Issue #45717) + * Shows code with vertical bar and overline instead of tilde underline + */ +function formatCodeSpanEnhanced( + file: SourceFile, + start: number, + length: number, + indent: string, + squiggleColor: ForegroundColorEscapeSequences, + host: FormatDiagnosticsHost, +): string { + const { line: errorLine, character: errorStartChar } = getLineAndCharacterOfPosition(file, start); + + // Calculate end position safely, defaulting length to 1 if not provided + const errorLength = Math.max(1, length || 1); + const endPosition = Math.min(start + errorLength, file.text.length); + const { line: errorEndLine, character: errorEndChar } = getLineAndCharacterOfPosition(file, endPosition); + + // Only show enhanced formatting for single-line errors + if (errorLine !== errorEndLine) { + // For multi-line errors, fall back to showing just the first line + const lineStart = getPositionOfLineAndCharacter(file, errorLine, 0); + const lineEnd = file.text.indexOf("\n", lineStart); + const lineText = file.text.slice(lineStart, lineEnd === -1 ? file.text.length : lineEnd); + const cleanedLine = lineText.trimEnd(); + return formatCodeSpanEnhancedSingleLine(file, errorLine, errorStartChar, cleanedLine.length - errorStartChar, indent, squiggleColor, host); + } + + return formatCodeSpanEnhancedSingleLine(file, errorLine, errorStartChar, errorEndChar - errorStartChar, indent, squiggleColor, host); +} + +function formatCodeSpanEnhancedSingleLine( + file: SourceFile, + line: number, + startChar: number, + length: number, + indent: string, + squiggleColor: ForegroundColorEscapeSequences, + host: FormatDiagnosticsHost, +): string { + // Get line content + const lineStart = getPositionOfLineAndCharacter(file, line, 0); + const lineEnd = file.text.indexOf("\n", lineStart); + const lineText = file.text.slice(lineStart, lineEnd === -1 ? file.text.length : lineEnd); + const cleanedLine = lineText.trimEnd().replace(/\t/g, " "); + + let output = ""; + + // Line 1: Vertical bar + code + output += indent + formatColorAndReset(ENHANCED_VERTICAL_BAR, gutterStyleSequence) + " " + cleanedLine + host.getNewLine(); + + // Line 2: Vertical bar + overline at error position + output += indent + formatColorAndReset(ENHANCED_VERTICAL_BAR, gutterStyleSequence) + " "; + + // Add spacing - simpler approach without pre-allocated buffer + // Modern V8 optimizes repeat() well for reasonable sizes + if (startChar > 0 && startChar < cleanedLine.length) { + output += " ".repeat(startChar); + } + + // Calculate overline length, ensuring we stay within line bounds + if (startChar < cleanedLine.length) { + const overlineLength = Math.max(1, Math.min(length, cleanedLine.length - startChar)); + output += formatColorAndReset(ENHANCED_OVERLINE.repeat(overlineLength), squiggleColor); + } + + return output; +} + +/** + * Determines if enhanced formatting should be used based on terminal width + */ +function shouldUseEnhancedFormatting(): boolean { + if (!sys) return false; + + // Respect NO_COLOR environment variable + if (sys.getEnvironmentVariable?.("NO_COLOR")) { + return false; + } + + // Check terminal width if available + if (sys.getWidthOfTerminal) { + const width = sys.getWidthOfTerminal(); + if (width !== undefined && width < ENHANCED_FORMAT_MIN_WIDTH) { + return false; + } + } + + // For CI environments, require explicit opt-in + if (sys.getEnvironmentVariable?.("CI") === "true") { + return sys.getEnvironmentVariable("TS_ENHANCED_ERRORS") === "true"; + } + + // Check if we're in a TTY with color support + if (sys.writeOutputIsTTY?.()) { + const term = sys.getEnvironmentVariable?.("TERM"); + // Exclude known limited terminals + return term !== "dumb" && term !== "unknown"; + } + + // Not a TTY, so no enhanced formatting + return false; +} + export function formatDiagnosticsWithColorAndContext(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string { + const useEnhancedFormatting = shouldUseEnhancedFormatting(); + let output = ""; for (const diagnostic of diagnostics) { - if (diagnostic.file) { - const { file, start } = diagnostic; - output += formatLocation(file, start!, host); // TODO: GH#18217 - output += " - "; + if (useEnhancedFormatting) { + // Enhanced formatting (Issue #45717) + output += formatDiagnosticEnhanced(diagnostic, host); + } + else { + // Original formatting for backwards compatibility + output += formatDiagnosticOriginal(diagnostic, host); } + output += host.getNewLine(); + } + return output; +} - output += formatColorAndReset(diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category)); - output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey); - output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); +/** + * Enhanced diagnostic formatting as specified in Issue #45717 + * Format: ● file:line:col TS#### + * | code line + * ▔ + * Error message + */ +function formatDiagnosticEnhanced(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string { + let output = ""; + + // Header line: ● file:line:col TS#### + output += formatColorAndReset(ENHANCED_BULLET + " ", getCategoryFormat(diagnostic.category)); + + if (diagnostic.file && diagnostic.start !== undefined) { + output += formatLocation(diagnostic.file, diagnostic.start, host); + output += " "; + } + + output += formatColorAndReset(`TS${diagnostic.code}`, ForegroundColorEscapeSequences.Grey); + output += host.getNewLine(); + + // Code span (if applicable) + if (diagnostic.file && diagnostic.start !== undefined && diagnostic.code !== Diagnostics.File_appears_to_be_binary.code) { + output += formatCodeSpanEnhanced( + diagnostic.file, + diagnostic.start, + diagnostic.length || 1, + "", + getCategoryFormat(diagnostic.category), + host, + ); + output += host.getNewLine(); + } + + // Error message + output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); - if (diagnostic.file && diagnostic.code !== Diagnostics.File_appears_to_be_binary.code) { + // Related information + if (diagnostic.relatedInformation) { + for (const related of diagnostic.relatedInformation) { output += host.getNewLine(); - output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", getCategoryFormat(diagnostic.category), host); // TODO: GH#18217 - } - if (diagnostic.relatedInformation) { output += host.getNewLine(); - for (const { file, start, length, messageText } of diagnostic.relatedInformation) { - if (file) { - output += host.getNewLine(); - output += halfIndent + formatLocation(file, start!, host); // TODO: GH#18217 - output += formatCodeSpan(file, start!, length!, indent, ForegroundColorEscapeSequences.Cyan, host); // TODO: GH#18217 - } + + if (related.file && related.start !== undefined) { + output += halfIndent + formatLocation(related.file, related.start, host); + output += host.getNewLine(); + output += formatCodeSpanEnhanced( + related.file, + related.start, + related.length || 1, + halfIndent, + ForegroundColorEscapeSequences.Cyan, + host, + ); output += host.getNewLine(); - output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine()); } + + output += halfIndent + flattenDiagnosticMessageText(related.messageText, host.getNewLine()); } + } + + return output; +} + +/** + * Original diagnostic formatting for backwards compatibility + */ +function formatDiagnosticOriginal(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string { + let output = ""; + + if (diagnostic.file) { + const { file, start } = diagnostic; + output += formatLocation(file, start!, host); // TODO: GH#18217 + output += " - "; + } + + output += formatColorAndReset(diagnosticCategoryName(diagnostic), getCategoryFormat(diagnostic.category)); + output += formatColorAndReset(` TS${diagnostic.code}: `, ForegroundColorEscapeSequences.Grey); + output += flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine()); + + if (diagnostic.file && diagnostic.code !== Diagnostics.File_appears_to_be_binary.code) { + output += host.getNewLine(); + output += formatCodeSpan(diagnostic.file, diagnostic.start!, diagnostic.length!, "", getCategoryFormat(diagnostic.category), host); // TODO: GH#18217 + } + + if (diagnostic.relatedInformation) { output += host.getNewLine(); + for (const { file, start, length, messageText } of diagnostic.relatedInformation) { + if (file) { + output += host.getNewLine(); + output += halfIndent + formatLocation(file, start!, host); // TODO: GH#18217 + output += formatCodeSpan(file, start!, length!, indent, ForegroundColorEscapeSequences.Cyan, host); // TODO: GH#18217 + } + output += host.getNewLine(); + output += indent + flattenDiagnosticMessageText(messageText, host.getNewLine()); + } } + return output; }