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) = if line_num == span_info.start_line {
313 (span_info.start_col - 1, line_content.len(), true, false)
314 } else if line_num == span_info.end_line {
315 (0, span_info.end_col - 1, false, true)
316 } else {
317 (0, line_content.len(), false, false)
318 };
319
320 if show_start {
322 writeln!(
324 w,
325 " {blue}{:>width$} |{reset} {span_color}/{reset} {}",
326 line_num,
327 line_content,
328 width = line_num_width
329 )?;
330 } else if show_end {
331 writeln!(
333 w,
334 " {blue}{:>width$} |{reset} {span_color}|{reset} {}",
335 line_num,
336 line_content,
337 width = line_num_width
338 )?;
339
340 let padding = " ".repeat(end_col);
342 writeln!(
343 w,
344 " {blue}{:>width$} |{reset} {span_color}|_{padding}^{reset}",
345 "",
346 width = line_num_width
347 )?;
348 } else {
349 writeln!(
351 w,
352 " {blue}{:>width$} |{reset} {span_color}|{reset} {}",
353 line_num,
354 line_content,
355 width = line_num_width
356 )?;
357 }
358 }
359
360 if !label.message.is_empty() {
362 writeln!(
363 w,
364 " {blue}{:>width$} |{reset} {span_color}{}{reset}",
365 "",
366 label.message,
367 width = line_num_width
368 )?;
369 }
370
371 Ok(())
372 }
373
374 fn render_note(&self, note: &str, w: &mut impl Write) -> std::io::Result<()> {
376 let blue = self.blue();
377 let reset = self.reset();
378
379 for (i, line) in note.lines().enumerate() {
381 if i == 0 {
382 writeln!(w, " {blue}={reset} {blue}note{reset}: {line}")?;
383 } else {
384 writeln!(w, " {line}")?;
385 }
386 }
387
388 Ok(())
389 }
390
391 fn render_suggestion(
393 &self,
394 suggestion: &Suggestion,
395 w: &mut impl Write,
396 ) -> std::io::Result<()> {
397 let green = if self.config.colors {
398 colors::BOLD_GREEN
399 } else {
400 ""
401 };
402 let reset = self.reset();
403 let blue = self.blue();
404
405 writeln!(
406 w,
407 " {blue}={reset} {green}help{reset}: {}",
408 suggestion.message
409 )?;
410
411 if !suggestion.replacement.is_empty() {
412 writeln!(w, " {blue}|{reset}")?;
413 for line in suggestion.replacement.lines() {
414 writeln!(w, " {blue}|{reset} {green}{line}{reset}")?;
415 }
416 }
417
418 Ok(())
419 }
420
421 pub fn render_all(&self, diagnostics: &[Diagnostic]) {
423 let mut stderr = std::io::stderr().lock();
424 for diag in diagnostics {
425 let _ = self.render(diag, &mut stderr);
426 }
427 }
428
429 #[must_use]
431 pub fn render_to_string(&self, diagnostic: &Diagnostic) -> String {
432 let mut buf = Vec::new();
433 let _ = self.render(diagnostic, &mut buf);
434 String::from_utf8_lossy(&buf).into_owned()
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use bhc_span::{FileId, FullSpan, Span};
442
443 fn create_test_source_map() -> SourceMap {
444 let mut sm = SourceMap::new();
445 sm.add_file(
446 "test.hs".to_string(),
447 "module Test where\n\nfoo :: Int -> Int\nfoo x = x + y\n\nmain = foo 42\n".to_string(),
448 );
449 sm
450 }
451
452 #[test]
453 fn test_simple_error() {
454 let sm = create_test_source_map();
455 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
456
457 let diag = Diagnostic::error("undefined variable `y`")
458 .with_code("E0001")
459 .with_label(
460 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
461 "not found in this scope",
462 );
463
464 let output = renderer.render_to_string(&diag);
465
466 assert!(output.contains("error[E0001]"));
467 assert!(output.contains("undefined variable `y`"));
468 assert!(output.contains("test.hs:4:"));
469 assert!(output.contains("not found in this scope"));
470 }
471
472 #[test]
473 fn test_error_with_note() {
474 let sm = create_test_source_map();
475 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
476
477 let diag = Diagnostic::error("type mismatch")
478 .with_code("E0002")
479 .with_label(
480 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
481 "expected `Int`, found `String`",
482 )
483 .with_note("consider using `show` to convert to String");
484
485 let output = renderer.render_to_string(&diag);
486
487 assert!(output.contains("= note:"));
488 assert!(output.contains("consider using `show`"));
489 }
490
491 #[test]
492 fn test_warning() {
493 let sm = create_test_source_map();
494 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
495
496 let diag = Diagnostic::warning("unused variable `x`")
497 .with_code("W0001")
498 .with_label(
499 FullSpan::new(FileId::new(0), Span::from_raw(40, 41)),
500 "this variable is never used",
501 );
502
503 let output = renderer.render_to_string(&diag);
504
505 assert!(output.contains("warning[W0001]"));
506 assert!(output.contains("unused variable"));
507 }
508
509 #[test]
510 fn test_suggestion() {
511 let sm = create_test_source_map();
512 let renderer = CargoRenderer::with_config(&sm, RenderConfig::plain());
513
514 let diag = Diagnostic::error("undefined variable `y`")
515 .with_label(
516 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
517 "not found",
518 )
519 .with_suggestion(crate::Suggestion::new(
520 "did you mean `x`?",
521 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
522 "x",
523 crate::Applicability::MaybeIncorrect,
524 ));
525
526 let output = renderer.render_to_string(&diag);
527
528 assert!(output.contains("= help:"));
529 assert!(output.contains("did you mean `x`?"));
530 }
531}