1use std::io::Write;
12
13use crate::{Diagnostic, Label, Severity, SourceMap, Suggestion};
14
15pub mod colors {
17 pub const RESET: &str = "\x1b[0m";
19 pub const BOLD: &str = "\x1b[1m";
21 pub const RED: &str = "\x1b[31m";
23 pub const BOLD_RED: &str = "\x1b[1;31m";
25 pub const YELLOW: &str = "\x1b[33m";
27 pub const BOLD_YELLOW: &str = "\x1b[1;33m";
29 pub const BLUE: &str = "\x1b[34m";
31 pub const BOLD_BLUE: &str = "\x1b[1;34m";
33 pub const CYAN: &str = "\x1b[36m";
35 pub const BOLD_CYAN: &str = "\x1b[1;36m";
37 pub const GREEN: &str = "\x1b[32m";
39 pub const BOLD_GREEN: &str = "\x1b[1;32m";
41 pub const MAGENTA: &str = "\x1b[35m";
43 pub const BOLD_MAGENTA: &str = "\x1b[1;35m";
45 pub const WHITE: &str = "\x1b[37m";
47 pub const BOLD_WHITE: &str = "\x1b[1;37m";
49}
50
51#[derive(Clone, Debug)]
53pub struct RenderConfig {
54 pub colors: bool,
56 pub show_codes: bool,
58 pub context_lines: usize,
60 pub max_width: usize,
62}
63
64impl Default for RenderConfig {
65 fn default() -> Self {
66 Self {
67 colors: true,
68 show_codes: true,
69 context_lines: 0,
70 max_width: 140,
71 }
72 }
73}
74
75impl RenderConfig {
76 #[must_use]
78 pub fn colored() -> Self {
79 Self::default()
80 }
81
82 #[must_use]
84 pub fn plain() -> Self {
85 Self {
86 colors: false,
87 ..Self::default()
88 }
89 }
90}
91
92pub struct CargoRenderer<'a> {
94 source_map: &'a SourceMap,
95 config: RenderConfig,
96}
97
98impl<'a> CargoRenderer<'a> {
99 #[must_use]
101 pub fn new(source_map: &'a SourceMap) -> Self {
102 Self {
103 source_map,
104 config: RenderConfig::default(),
105 }
106 }
107
108 #[must_use]
110 pub fn with_config(source_map: &'a SourceMap, config: RenderConfig) -> Self {
111 Self { source_map, config }
112 }
113
114 fn severity_color(&self, severity: Severity) -> &'static str {
116 if !self.config.colors {
117 return "";
118 }
119 match severity {
120 Severity::Bug => colors::BOLD_MAGENTA,
121 Severity::Error => colors::BOLD_RED,
122 Severity::Warning => colors::BOLD_YELLOW,
123 Severity::Note => colors::BOLD_CYAN,
124 Severity::Help => colors::BOLD_GREEN,
125 }
126 }
127
128 fn reset(&self) -> &'static str {
130 if self.config.colors {
131 colors::RESET
132 } else {
133 ""
134 }
135 }
136
137 fn blue(&self) -> &'static str {
139 if self.config.colors {
140 colors::BOLD_BLUE
141 } else {
142 ""
143 }
144 }
145
146 pub fn render(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
148 self.render_header(diagnostic, w)?;
150
151 self.render_labels(diagnostic, w)?;
153
154 for note in &diagnostic.notes {
156 self.render_note(note, w)?;
157 }
158
159 for suggestion in &diagnostic.suggestions {
161 self.render_suggestion(suggestion, w)?;
162 }
163
164 writeln!(w)?;
165 Ok(())
166 }
167
168 fn render_header(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
170 let color = self.severity_color(diagnostic.severity);
171 let reset = self.reset();
172
173 write!(w, "{color}{}{reset}", diagnostic.severity.label())?;
174
175 if self.config.show_codes {
176 if let Some(code) = &diagnostic.code {
177 write!(w, "{color}[{code}]{reset}")?;
178 }
179 }
180
181 writeln!(w, ": {color}{}{reset}", diagnostic.message)?;
182
183 Ok(())
184 }
185
186 fn render_labels(&self, diagnostic: &Diagnostic, w: &mut impl Write) -> std::io::Result<()> {
188 let blue = self.blue();
189 let reset = self.reset();
190
191 for (i, label) in diagnostic.labels.iter().enumerate() {
192 let Some(file) = self.source_map.get_file(label.span.file) else {
193 continue;
194 };
195
196 if label.span.span.is_dummy() {
197 continue;
198 }
199
200 let span_info = file.span_lines(label.span.span);
201
202 let arrow = if label.primary && i == 0 {
204 "-->"
205 } else {
206 " "
207 };
208 writeln!(
209 w,
210 " {blue}{arrow}{reset} {}:{}:{}",
211 file.name, span_info.start_line, span_info.start_col
212 )?;
213
214 let line_num_width = span_info.end_line.to_string().len().max(3);
216
217 writeln!(w, " {blue}{:>width$} |{reset}", "", width = line_num_width)?;
219
220 if span_info.is_multiline() {
222 self.render_multiline_span(file, &span_info, label, line_num_width, w)?;
223 } else {
224 self.render_single_line_span(file, &span_info, label, line_num_width, w)?;
225 }
226 }
227
228 Ok(())
229 }
230
231 fn render_single_line_span(
233 &self,
234 file: &bhc_span::SourceFile,
235 span_info: &bhc_span::SpanLines,
236 label: &Label,
237 line_num_width: usize,
238 w: &mut impl Write,
239 ) -> std::io::Result<()> {
240 let blue = self.blue();
241 let reset = self.reset();
242 let underline_color = if label.primary {
243 self.severity_color(Severity::Error)
244 } else {
245 self.blue()
246 };
247
248 let line_idx = span_info.start_line - 1;
250 let Some(line_content) = file.line_content(line_idx) else {
251 return Ok(());
252 };
253
254 writeln!(
256 w,
257 " {blue}{:>width$} |{reset} {}",
258 span_info.start_line,
259 line_content,
260 width = line_num_width
261 )?;
262
263 let start_col = span_info.start_col.saturating_sub(1);
265 let end_col = span_info.end_col.saturating_sub(1);
266 let underline_len = (end_col - start_col).max(1);
267
268 let padding = " ".repeat(start_col);
269 let underline = "^".repeat(underline_len);
270
271 write!(
272 w,
273 " {blue}{:>width$} |{reset} {padding}{underline_color}{underline}{reset}",
274 "",
275 width = line_num_width
276 )?;
277
278 if !label.message.is_empty() {
280 writeln!(w, " {underline_color}{}{reset}", label.message)?;
281 } else {
282 writeln!(w)?;
283 }
284
285 Ok(())
286 }
287
288 fn render_multiline_span(
290 &self,
291 file: &bhc_span::SourceFile,
292 span_info: &bhc_span::SpanLines,
293 label: &Label,
294 line_num_width: usize,
295 w: &mut impl Write,
296 ) -> std::io::Result<()> {
297 let blue = self.blue();
298 let reset = self.reset();
299 let span_color = if label.primary {
300 self.severity_color(Severity::Error)
301 } else {
302 self.blue()
303 };
304
305 for line_num in span_info.start_line..=span_info.end_line {
306 let line_idx = line_num - 1;
307 let Some(line_content) = file.line_content(line_idx) else {
308 continue;
309 };
310
311 let (_start_col, end_col, show_start, show_end) =
313 if line_num == span_info.start_line {
314 (span_info.start_col - 1, line_content.len(), true, false)
315 } else if line_num == span_info.end_line {
316 (0, span_info.end_col - 1, false, true)
317 } else {
318 (0, line_content.len(), false, false)
319 };
320
321 if show_start {
323 writeln!(
325 w,
326 " {blue}{:>width$} |{reset} {span_color}/{reset} {}",
327 line_num,
328 line_content,
329 width = line_num_width
330 )?;
331 } else if show_end {
332 writeln!(
334 w,
335 " {blue}{:>width$} |{reset} {span_color}|{reset} {}",
336 line_num,
337 line_content,
338 width = line_num_width
339 )?;
340
341 let padding = " ".repeat(end_col);
343 writeln!(
344 w,
345 " {blue}{:>width$} |{reset} {span_color}|_{padding}^{reset}",
346 "",
347 width = line_num_width
348 )?;
349 } else {
350 writeln!(
352 w,
353 " {blue}{:>width$} |{reset} {span_color}|{reset} {}",
354 line_num,
355 line_content,
356 width = line_num_width
357 )?;
358 }
359 }
360
361 if !label.message.is_empty() {
363 writeln!(
364 w,
365 " {blue}{:>width$} |{reset} {span_color}{}{reset}",
366 "",
367 label.message,
368 width = line_num_width
369 )?;
370 }
371
372 Ok(())
373 }
374
375 fn render_note(&self, note: &str, w: &mut impl Write) -> std::io::Result<()> {
377 let blue = self.blue();
378 let reset = self.reset();
379
380 for (i, line) in note.lines().enumerate() {
382 if i == 0 {
383 writeln!(w, " {blue}={reset} {blue}note{reset}: {line}")?;
384 } else {
385 writeln!(w, " {line}")?;
386 }
387 }
388
389 Ok(())
390 }
391
392 fn render_suggestion(&self, suggestion: &Suggestion, w: &mut impl Write) -> std::io::Result<()> {
394 let green = if self.config.colors {
395 colors::BOLD_GREEN
396 } else {
397 ""
398 };
399 let reset = self.reset();
400 let blue = self.blue();
401
402 writeln!(w, " {blue}={reset} {green}help{reset}: {}", suggestion.message)?;
403
404 if !suggestion.replacement.is_empty() {
405 writeln!(w, " {blue}|{reset}")?;
406 for line in suggestion.replacement.lines() {
407 writeln!(w, " {blue}|{reset} {green}{line}{reset}")?;
408 }
409 }
410
411 Ok(())
412 }
413
414 pub fn render_all(&self, diagnostics: &[Diagnostic]) {
416 let mut stderr = std::io::stderr().lock();
417 for diag in diagnostics {
418 let _ = self.render(diag, &mut stderr);
419 }
420 }
421
422 #[must_use]
424 pub fn render_to_string(&self, diagnostic: &Diagnostic) -> String {
425 let mut buf = Vec::new();
426 let _ = self.render(diagnostic, &mut buf);
427 String::from_utf8_lossy(&buf).into_owned()
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use bhc_span::{FileId, FullSpan, Span};
435
436 fn create_test_source_map() -> SourceMap {
437 let mut sm = SourceMap::new();
438 sm.add_file(
439 "test.hs".to_string(),
440 "module Test where\n\nfoo :: Int -> Int\nfoo x = x + y\n\nmain = foo 42\n".to_string(),
441 );
442 sm
443 }
444
445 #[test]
446 fn test_simple_error() {
447 let sm = create_test_source_map();
448 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
449
450 let diag = Diagnostic::error("undefined variable `y`")
451 .with_code("E0001")
452 .with_label(
453 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
454 "not found in this scope",
455 );
456
457 let output = renderer.render_to_string(&diag);
458
459 assert!(output.contains("error[E0001]"));
460 assert!(output.contains("undefined variable `y`"));
461 assert!(output.contains("test.hs:4:"));
462 assert!(output.contains("not found in this scope"));
463 }
464
465 #[test]
466 fn test_error_with_note() {
467 let sm = create_test_source_map();
468 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
469
470 let diag = Diagnostic::error("type mismatch")
471 .with_code("E0002")
472 .with_label(
473 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
474 "expected `Int`, found `String`",
475 )
476 .with_note("consider using `show` to convert to String");
477
478 let output = renderer.render_to_string(&diag);
479
480 assert!(output.contains("= note:"));
481 assert!(output.contains("consider using `show`"));
482 }
483
484 #[test]
485 fn test_warning() {
486 let sm = create_test_source_map();
487 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
488
489 let diag = Diagnostic::warning("unused variable `x`")
490 .with_code("W0001")
491 .with_label(
492 FullSpan::new(FileId::new(0), Span::from_raw(40, 41)),
493 "this variable is never used",
494 );
495
496 let output = renderer.render_to_string(&diag);
497
498 assert!(output.contains("warning[W0001]"));
499 assert!(output.contains("unused variable"));
500 }
501
502 #[test]
503 fn test_suggestion() {
504 let sm = create_test_source_map();
505 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
506
507 let diag = Diagnostic::error("undefined variable `y`")
508 .with_label(
509 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
510 "not found",
511 )
512 .with_suggestion(crate::Suggestion::new(
513 "did you mean `x`?",
514 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
515 "x",
516 crate::Applicability::MaybeIncorrect,
517 ));
518
519 let output = renderer.render_to_string(&diag);
520
521 assert!(output.contains("= help:"));
522 assert!(output.contains("did you mean `x`?"));
523 }
524}