use std::io::Write;
use crate::{Diagnostic, Label, Severity, SourceMap, Suggestion};
pub mod colors {
pub const RESET: &str = "\x1b[0m";
pub const BOLD: &str = "\x1b[1m";
pub const RED: &str = "\x1b[31m";
pub const BOLD_RED: &str = "\x1b[1;31m";
pub const YELLOW: &str = "\x1b[33m";
pub const BOLD_YELLOW: &str = "\x1b[1;33m";
pub const BLUE: &str = "\x1b[34m";
pub const BOLD_BLUE: &str = "\x1b[1;34m";
pub const CYAN: &str = "\x1b[36m";
pub const BOLD_CYAN: &str = "\x1b[1;36m";
pub const GREEN: &str = "\x1b[32m";
pub const BOLD_GREEN: &str = "\x1b[1;32m";
pub const MAGENTA: &str = "\x1b[35m";
pub const BOLD_MAGENTA: &str = "\x1b[1;35m";
pub const WHITE: &str = "\x1b[37m";
pub const BOLD_WHITE: &str = "\x1b[1;37m";
}
#[derive(Clone, Debug)]
pub struct RenderConfig {
pub colors: bool,
pub show_codes: bool,
pub context_lines: usize,
pub max_width: usize,
}
impl Default for RenderConfig {
fn default() -> Self {
Self {
colors: true,
show_codes: true,
context_lines: 0,
max_width: 140,
}
}
}
impl RenderConfig {
#[must_use]
pub fn colored() -> Self {
Self::default()
}
#[must_use]
pub fn plain() -> Self {
Self {
colors: false,
..Self::default()
}
}
}
pub struct CargoRenderer<'a> {
source_map: &'a SourceMap,
config: RenderConfig,
}
impl<'a> CargoRenderer<'a> {
#[must_use]
pub fn new(source_map: &'a SourceMap) -> Self {
Self {
source_map,
config: RenderConfig::default(),
}
}
#[must_use]
pub fn with_config(source_map: &'a SourceMap, config: RenderConfig) -> Self {
Self { source_map, config }
}
fn severity_color(&self, severity: Severity) -> &'static str {
if !self.config.colors {
return "";
}
match severity {
Severity::Bug => colors::BOLD_MAGENTA,
Severity::Error => colors::BOLD_RED,
Severity::Warning => colors::BOLD_YELLOW,
Severity::Note => colors::BOLD_CYAN,
Severity::Help => colors::BOLD_GREEN,
}
}
fn reset(&self) -> &'static str {
if self.config.colors {
colors::RESET
} else {
""
}
}
fn blue(&self) -> &'static str {
if self.config.colors {
colors::BOLD_BLUE
} else {
""
}
}
pub fn render(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
self.render_header(diagnostic, w)?;
self.render_labels(diagnostic, w)?;
for note in &diagnostic.notes {
self.render_note(note, w)?;
}
for suggestion in &diagnostic.suggestions {
self.render_suggestion(suggestion, w)?;
}
writeln!(w)?;
Ok(())
}
fn render_header(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
let color = self.severity_color(diagnostic.severity);
let reset = self.reset();
write!(w, "{color}{}{reset}", diagnostic.severity.label())?;
if self.config.show_codes {
if let Some(code) = &diagnostic.code {
write!(w, "{color}[{code}]{reset}")?;
}
}
writeln!(w, ": {color}{}{reset}", diagnostic.message)?;
Ok(())
}
fn render_labels(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
let blue = self.blue();
let reset = self.reset();
for (i, label) in diagnostic.labels.iter().enumerate() {
let Some(file) = self.source_map.get_file(label.span.file) else {
continue;
};
if label.span.span.is_dummy() {
continue;
}
let span_info = file.span_lines(label.span.span);
let arrow = if label.primary && i == 0 {
"-->"
} else {
" "
};
writeln!(
w,
" {blue}{arrow}{reset} {}:{}:{}",
file.name, span_info.start_line, span_info.start_col
)?;
let line_num_width = span_info.end_line.to_string().len().max(3);
writeln!(w, " {blue}{:>width$} |{reset}", "", width = line_num_width)?;
if span_info.is_multiline() {
self.render_multiline_span(file, &span_info, label, line_num_width, w)?;
} else {
self.render_single_line_span(file, &span_info, label, line_num_width, w)?;
}
}
Ok(())
}
fn render_single_line_span(
&self,
file: &bhc_span::SourceFile,
span_info: &bhc_span::SpanLines,
label: &Label,
line_num_width: usize,
w: &mut impl Write,
) -> std::io::Result<()> {
let blue = self.blue();
let reset = self.reset();
let underline_color = if label.primary {
self.severity_color(Severity::Error)
} else {
self.blue()
};
let line_idx = span_info.start_line - 1;
let Some(line_content) = file.line_content(line_idx) else {
return Ok(());
};
writeln!(
w,
" {blue}{:>width$} |{reset} {}",
span_info.start_line,
line_content,
width = line_num_width
)?;
let start_col = span_info.start_col.saturating_sub(1);
let end_col = span_info.end_col.saturating_sub(1);
let underline_len = (end_col - start_col).max(1);
let padding = " ".repeat(start_col);
let underline = "^".repeat(underline_len);
write!(
w,
" {blue}{:>width$} |{reset} {padding}{underline_color}{underline}{reset}",
"",
width = line_num_width
)?;
if !label.message.is_empty() {
writeln!(w, " {underline_color}{}{reset}", label.message)?;
} else {
writeln!(w)?;
}
Ok(())
}
fn render_multiline_span(
&self,
file: &bhc_span::SourceFile,
span_info: &bhc_span::SpanLines,
label: &Label,
line_num_width: usize,
w: &mut impl Write,
) -> std::io::Result<()> {
let blue = self.blue();
let reset = self.reset();
let span_color = if label.primary {
self.severity_color(Severity::Error)
} else {
self.blue()
};
for line_num in span_info.start_line..=span_info.end_line {
let line_idx = line_num - 1;
let Some(line_content) = file.line_content(line_idx) else {
continue;
};
let (_start_col, end_col, show_start, show_end) = if line_num == span_info.start_line {
(span_info.start_col - 1, line_content.len(), true, false)
} else if line_num == span_info.end_line {
(0, span_info.end_col - 1, false, true)
} else {
(0, line_content.len(), false, false)
};
if show_start {
writeln!(
w,
" {blue}{:>width$} |{reset} {span_color}/{reset} {}",
line_num,
line_content,
width = line_num_width
)?;
} else if show_end {
writeln!(
w,
" {blue}{:>width$} |{reset} {span_color}|{reset} {}",
line_num,
line_content,
width = line_num_width
)?;
let padding = " ".repeat(end_col);
writeln!(
w,
" {blue}{:>width$} |{reset} {span_color}|_{padding}^{reset}",
"",
width = line_num_width
)?;
} else {
writeln!(
w,
" {blue}{:>width$} |{reset} {span_color}|{reset} {}",
line_num,
line_content,
width = line_num_width
)?;
}
}
if !label.message.is_empty() {
writeln!(
w,
" {blue}{:>width$} |{reset} {span_color}{}{reset}",
"",
label.message,
width = line_num_width
)?;
}
Ok(())
}
fn render_note(&self, note: &str, w: &mut impl Write) -> std::io::Result<()> {
let blue = self.blue();
let reset = self.reset();
for (i, line) in note.lines().enumerate() {
if i == 0 {
writeln!(w, " {blue}={reset} {blue}note{reset}: {line}")?;
} else {
writeln!(w, " {line}")?;
}
}
Ok(())
}
fn render_suggestion(
&self,
suggestion: &Suggestion,
w: &mut impl Write,
) -> std::io::Result<()> {
let green = if self.config.colors {
colors::BOLD_GREEN
} else {
""
};
let reset = self.reset();
let blue = self.blue();
writeln!(
w,
" {blue}={reset} {green}help{reset}: {}",
suggestion.message
)?;
if !suggestion.replacement.is_empty() {
writeln!(w, " {blue}|{reset}")?;
for line in suggestion.replacement.lines() {
writeln!(w, " {blue}|{reset} {green}{line}{reset}")?;
}
}
Ok(())
}
pub fn render_all(&self, diagnostics: &[Diagnostic]) {
let mut stderr = std::io::stderr().lock();
for diag in diagnostics {
let _ = self.render(diag, &mut stderr);
}
}
#[must_use]
pub fn render_to_string(&self, diagnostic: &Diagnostic) -> String {
let mut buf = Vec::new();
let _ = self.render(diagnostic, &mut buf);
String::from_utf8_lossy(&buf).into_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use bhc_span::{FileId, FullSpan, Span};
fn create_test_source_map() -> SourceMap {
let mut sm = SourceMap::new();
sm.add_file(
"test.hs".to_string(),
"module Test where\n\nfoo :: Int -> Int\nfoo x = x + y\n\nmain = foo 42\n".to_string(),
);
sm
}
#[test]
fn test_simple_error() {
let sm = create_test_source_map();
let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
let diag = Diagnostic::error("undefined variable `y`")
.with_code("E0001")
.with_label(
FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
"not found in this scope",
);
let output = renderer.render_to_string(&diag);
assert!(output.contains("error[E0001]"));
assert!(output.contains("undefined variable `y`"));
assert!(output.contains("test.hs:4:"));
assert!(output.contains("not found in this scope"));
}
#[test]
fn test_error_with_note() {
let sm = create_test_source_map();
let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
let diag = Diagnostic::error("type mismatch")
.with_code("E0002")
.with_label(
FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
"expected `Int`, found `String`",
)
.with_note("consider using `show` to convert to String");
let output = renderer.render_to_string(&diag);
assert!(output.contains("= note:"));
assert!(output.contains("consider using `show`"));
}
#[test]
fn test_warning() {
let sm = create_test_source_map();
let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
let diag = Diagnostic::warning("unused variable `x`")
.with_code("W0001")
.with_label(
FullSpan::new(FileId::new(0), Span::from_raw(40, 41)),
"this variable is never used",
);
let output = renderer.render_to_string(&diag);
assert!(output.contains("warning[W0001]"));
assert!(output.contains("unused variable"));
}
#[test]
fn test_suggestion() {
let sm = create_test_source_map();
let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
let diag = Diagnostic::error("undefined variable `y`")
.with_label(
FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
"not found",
)
.with_suggestion(crate::Suggestion::new(
"did you mean `x`?",
FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
"x",
crate::Applicability::MaybeIncorrect,
));
let output = renderer.render_to_string(&diag);
assert!(output.contains("= help:"));
assert!(output.contains("did you mean `x`?"));
}
}