1use colored::*;
2use unicode_width::UnicodeWidthStr;
3
4use crate::diagnostic::{Diagnostic, DiagnosticCode, Label, LabelStyle, Severity, Suggestion};
5
6#[derive(Debug, Clone)]
8pub struct SourceCache<'a> {
9 lines: Vec<&'a str>,
10}
11
12impl<'a> SourceCache<'a> {
13 pub fn new(source: &'a str) -> Self {
14 Self { lines: source.lines().collect() }
15 }
16
17 pub fn line(&self, line_num_1based: usize) -> Option<&str> {
18 if line_num_1based == 0 {
19 return None;
20 }
21 self.lines.get(line_num_1based - 1).copied()
22 }
23
24 pub fn len(&self) -> usize {
25 self.lines.len()
26 }
27
28 pub fn is_empty(&self) -> bool {
29 self.lines.is_empty()
30 }
31}
32
33#[derive(Debug, Clone)]
36struct OwnedSource(Vec<String>);
37
38impl OwnedSource {
39 fn new(source: &str) -> Self {
40 Self(source.lines().map(String::from).collect())
41 }
42 fn line(&self, n: usize) -> Option<&str> {
43 if n == 0 {
44 None
45 } else {
46 self.0.get(n - 1).map(String::as_str)
47 }
48 }
49 fn len(&self) -> usize {
50 self.0.len()
51 }
52}
53
54enum CacheRef<'a, 'src> {
55 Borrowed(&'a SourceCache<'src>),
56 Owned(OwnedSource),
57}
58
59impl<'a, 'src> CacheRef<'a, 'src> {
60 fn line(&self, n: usize) -> Option<&str> {
61 match self {
62 Self::Borrowed(c) => c.line(n),
63 Self::Owned(o) => o.line(n),
64 }
65 }
66 fn len(&self) -> usize {
67 match self {
68 Self::Borrowed(c) => c.len(),
69 Self::Owned(o) => o.len(),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy)]
76pub struct RenderOptions {
77 pub tab_width: usize,
79 pub context_lines: usize,
81 pub max_line_width: usize,
83 pub color: bool,
85}
86
87impl Default for RenderOptions {
88 fn default() -> Self {
89 Self { tab_width: 4, context_lines: 0, max_line_width: 0, color: true }
90 }
91}
92
93pub struct DiagnosticFormatter<'a, 'src, C: DiagnosticCode> {
94 diagnostic: &'a Diagnostic<C>,
95 cache: CacheRef<'a, 'src>,
96 options: RenderOptions,
97}
98
99impl<'a, 'src, C: DiagnosticCode> DiagnosticFormatter<'a, 'src, C> {
100 pub fn new(diagnostic: &'a Diagnostic<C>, source: &str) -> Self {
103 Self {
104 diagnostic,
105 cache: CacheRef::Owned(OwnedSource::new(source)),
106 options: RenderOptions::default(),
107 }
108 }
109
110 pub fn with_cache(diagnostic: &'a Diagnostic<C>, cache: &'a SourceCache<'src>) -> Self {
111 Self { diagnostic, cache: CacheRef::Borrowed(cache), options: RenderOptions::default() }
112 }
113
114 pub fn with_options(mut self, options: RenderOptions) -> Self {
115 self.options = options;
116 self
117 }
118
119 fn severity_text(&self) -> &'static str {
120 match self.diagnostic.severity {
121 Severity::Bug => "internal error",
122 Severity::Error => "error",
123 Severity::Warning => "warning",
124 Severity::Note => "note",
125 Severity::Help => "help",
126 }
127 }
128
129 fn underline_char(style: LabelStyle) -> char {
130 match style {
131 LabelStyle::Primary => '^',
132 LabelStyle::Secondary => '-',
133 }
134 }
135
136 pub fn format(&self) -> String {
138 if self.options.color {
139 self.format_inner(true)
140 } else {
141 self.format_inner(false)
142 }
143 }
144
145 pub fn format_plain(&self) -> String {
147 self.format_inner(false)
148 }
149
150 fn format_inner(&self, color: bool) -> String {
151 let mut out = String::new();
152 self.write_header(&mut out, color);
153 self.write_labels_grouped(&mut out, color);
154 self.write_notes_help(&mut out, color);
155 self.write_suggestions(&mut out, color);
156 out.push('\n');
158 out
159 }
160
161 fn write_header(&self, out: &mut String, color: bool) {
162 let sev = self.severity_text();
163 let code_str = self.diagnostic.code.code();
164 let url = self.diagnostic.code.url();
165
166 if color {
167 let (sev_c, code_c) = match self.diagnostic.severity {
168 Severity::Bug | Severity::Error => (sev.red().bold(), code_str.red().bold()),
169 Severity::Warning => (sev.yellow().bold(), code_str.yellow().bold()),
170 _ => (sev.cyan().bold(), code_str.cyan().bold()),
171 };
172 out.push_str(&format!("{}: [{}]: {}", sev_c, code_c, self.diagnostic.message));
173 } else {
174 out.push_str(&format!("{}: [{}]: {}", sev, code_str, self.diagnostic.message));
175 }
176
177 if let Some(u) = url {
178 if color {
179 out.push_str(&format!(" {}", format!("(see {u})").blue().italic()));
180 } else {
181 out.push_str(&format!(" (see {u})"));
182 }
183 }
184 out.push('\n');
185 }
186
187 fn write_labels_grouped(&self, out: &mut String, color: bool) {
188 let labels = &self.diagnostic.labels;
189 if labels.is_empty() {
190 return;
191 }
192
193 let mut files: Vec<&str> = Vec::new();
195 for l in labels {
196 if !files.iter().any(|f| *f == l.span.file.as_str()) {
197 files.push(l.span.file.as_str());
198 }
199 }
200
201 for (idx, file) in files.iter().enumerate() {
202 let in_file: Vec<&Label> = labels.iter().filter(|l| l.span.file.as_str() == *file).collect();
203 let primary_in_file = in_file
204 .iter()
205 .find(|l| l.style == LabelStyle::Primary)
206 .copied()
207 .or(in_file.first().copied());
208 let primary = match primary_in_file {
209 Some(l) => l,
210 None => continue,
211 };
212
213 let header_line = format!(
215 " {} {}:{}:{}",
216 if color { "-->".blue().bold().to_string() } else { "-->".to_string() },
217 if color {
218 primary.span.file.white().bold().to_string()
219 } else {
220 primary.span.file.clone()
221 },
222 if color {
223 primary.span.line.to_string().white().bold().to_string()
224 } else {
225 primary.span.line.to_string()
226 },
227 if color {
228 primary.span.column.to_string().white().bold().to_string()
229 } else {
230 primary.span.column.to_string()
231 },
232 );
233 out.push_str(&header_line);
234 out.push('\n');
235
236 self.write_file_section(out, &in_file, color);
237
238 if idx + 1 < files.len() {
239 out.push('\n');
240 }
241 }
242 }
243
244 fn write_file_section(&self, out: &mut String, labels: &[&Label], color: bool) {
245 let min_line = labels.iter().map(|l| l.span.line).min().unwrap_or(0);
248 let max_line = labels.iter().map(|l| l.span.line).max().unwrap_or(0);
249 if min_line == 0 {
250 return;
252 }
253
254 let start = min_line.saturating_sub(self.options.context_lines).max(1);
255 let end = (max_line + self.options.context_lines).min(self.cache.len());
256
257 let gutter_w = end.to_string().len().max(2);
258 let bar = if color { "|".blue().bold().to_string() } else { "|".to_string() };
259
260 let blank_gutter = " ".repeat(gutter_w);
261 out.push_str(&format!(" {} {}\n", blank_gutter, bar));
262
263 for line_num in start..=end {
264 let raw = self.cache.line(line_num).unwrap_or("");
265 let expanded = expand_tabs(raw, self.options.tab_width);
266 let truncated = truncate_line(&expanded, self.options.max_line_width);
267 let line_label = format!("{:>w$}", line_num, w = gutter_w);
268 let line_label_c =
269 if color { line_label.blue().bold().to_string() } else { line_label.clone() };
270 out.push_str(&format!(" {} {} {}\n", line_label_c, bar, truncated));
271
272 let mut on_line: Vec<&Label> =
274 labels.iter().copied().filter(|l| label_touches(l, line_num)).collect();
275 if on_line.is_empty() {
276 continue;
277 }
278 on_line.sort_by_key(|l| l.span.column);
279 self.write_caret_block(out, &on_line, line_num, raw, gutter_w, color);
280 }
281
282 out.push_str(&format!(" {} {}\n", blank_gutter, bar));
283 }
284
285 fn write_caret_block(
296 &self,
297 out: &mut String,
298 sorted: &[&Label],
299 line_num: usize,
300 raw_line: &str,
301 gutter_w: usize,
302 color: bool,
303 ) {
304 let infos: Vec<(usize, usize, &Label)> = sorted
305 .iter()
306 .map(|label| {
307 let (col_start, col_end) = label_columns_on_line(label, line_num, raw_line);
308 let pad =
309 display_width_prefix(raw_line, col_start.saturating_sub(1), self.options.tab_width);
310 let len = display_width_range(
311 raw_line,
312 col_start.saturating_sub(1),
313 col_end.saturating_sub(1),
314 self.options.tab_width,
315 )
316 .max(1);
317 (pad, len, *label)
318 })
319 .collect();
320
321 let n = infos.len();
322 let bar = if color { "|".blue().bold().to_string() } else { "|".to_string() };
323 let blank_gutter = " ".repeat(gutter_w);
324
325 for k in 0..n {
326 let m = n - 1 - k;
327 let visible = &infos[..=m];
328
329 let mut buf = String::new();
331 let mut cursor = 0usize;
332 for (pad, len, lbl) in visible {
333 while cursor < *pad {
334 buf.push(' ');
335 cursor += 1;
336 }
337 let ch = Self::underline_char(lbl.style);
338 let underline: String = std::iter::repeat(ch).take(*len).collect();
339 let painted = if color {
340 color_for(self.diagnostic.severity, lbl.style, &underline)
341 } else {
342 underline
343 };
344 buf.push_str(&painted);
345 cursor += *len;
346 }
347
348 let m_label = visible.last().unwrap().2;
350 let line = match &m_label.message {
351 Some(msg) => {
352 let painted_msg = if color {
353 color_for(self.diagnostic.severity, m_label.style, msg)
354 } else {
355 msg.clone()
356 };
357 format!(" {} {} {} {}\n", blank_gutter, bar, buf, painted_msg)
358 },
359 None => format!(" {} {} {}\n", blank_gutter, bar, buf),
360 };
361 out.push_str(&line);
362
363 if let Some(note) = &m_label.note {
365 let note_c = if color { note.cyan().italic().to_string() } else { format!("note: {note}") };
366 out.push_str(&format!(
367 " {} {} {}↳ {}\n",
368 blank_gutter,
369 bar,
370 " ".repeat(infos[m].0),
371 note_c
372 ));
373 }
374 }
375 }
376
377 fn write_notes_help(&self, out: &mut String, color: bool) {
378 let eq = if color { "=".blue().bold().to_string() } else { "=".to_string() };
379 for note in &self.diagnostic.notes {
380 let label = if color { "note".cyan().bold().to_string() } else { "note".to_string() };
381 out.push_str(&format!(" {} {}: {}\n", eq, label, note));
382 }
383 if let Some(help) = &self.diagnostic.help {
384 let label = if color { "help".cyan().bold().to_string() } else { "help".to_string() };
385 out.push_str(&format!(" {} {}: {}\n", eq, label, help));
386 }
387 }
388
389 fn write_suggestions(&self, out: &mut String, color: bool) {
390 if self.diagnostic.suggestions.is_empty() {
391 return;
392 }
393 let eq = if color { "=".blue().bold().to_string() } else { "=".to_string() };
394 let label = if color { "help".cyan().bold().to_string() } else { "help".to_string() };
395 for s in &self.diagnostic.suggestions {
396 let header = match &s.message {
397 Some(m) => m.to_string(),
398 None => "try this:".to_string(),
399 };
400 out.push_str(&format!(" {} {}: {}\n", eq, label, header));
401 self.write_suggestion_diff(out, s, color);
402 Self::write_applicability(out, s, color);
403 }
404 }
405
406 fn write_suggestion_diff(&self, out: &mut String, s: &Suggestion, color: bool) {
410 let line_num = s.span.line;
411 let orig_line = match self.cache.line(line_num) {
412 Some(l) => l,
413 None => {
414 for line in s.replacement.lines() {
416 let body = if color { line.green().to_string() } else { line.to_string() };
417 out.push_str(&format!(" {}\n", body));
418 }
419 return;
420 },
421 };
422
423 let col0 = s.span.column.saturating_sub(1);
426 let line_bytes = orig_line.len();
427 let start = col0.min(line_bytes);
428 let end = (start + s.span.length).min(line_bytes);
429 let prefix = &orig_line[..start];
430 let suffix = &orig_line[end..];
431
432 let repl_lines: Vec<&str> = s.replacement.split('\n').collect();
436 let mut new_lines: Vec<String> = Vec::with_capacity(repl_lines.len());
437 for (i, r) in repl_lines.iter().enumerate() {
438 let head = if i == 0 { prefix } else { "" };
439 let tail = if i == repl_lines.len() - 1 { suffix } else { "" };
440 new_lines.push(format!("{}{}{}", head, r, tail));
441 }
442
443 let last_line = line_num + new_lines.len().saturating_sub(1);
445 let gutter_w = last_line.to_string().len().max(2);
446 let bar = if color { "|".blue().bold().to_string() } else { "|".to_string() };
447 let blank_gutter = " ".repeat(gutter_w);
448
449 out.push_str(&format!(" {} {}\n", blank_gutter, bar));
450
451 let line_label = format!("{:>w$}", line_num, w = gutter_w);
453 let line_label_c =
454 if color { line_label.blue().bold().to_string() } else { line_label.clone() };
455 let minus = if color { "-".red().bold().to_string() } else { "-".to_string() };
456 let orig_body = if color { orig_line.red().to_string() } else { orig_line.to_string() };
457 out.push_str(&format!(" {} {} {}\n", line_label_c, minus, orig_body));
458
459 let plus = if color { "+".green().bold().to_string() } else { "+".to_string() };
461 for (i, body) in new_lines.iter().enumerate() {
462 let n = line_num + i;
463 let lbl = format!("{:>w$}", n, w = gutter_w);
464 let lbl_c = if color { lbl.blue().bold().to_string() } else { lbl.clone() };
465 let body_c = if color { body.green().to_string() } else { body.to_string() };
466 out.push_str(&format!(" {} {} {}\n", lbl_c, plus, body_c));
467 }
468
469 out.push_str(&format!(" {} {}\n", blank_gutter, bar));
470 }
471
472 fn write_applicability(out: &mut String, s: &Suggestion, color: bool) {
473 let kind = match s.applicability {
474 crate::diagnostic::Applicability::MachineApplicable => "auto-applicable",
475 crate::diagnostic::Applicability::MaybeIncorrect => "review needed",
476 crate::diagnostic::Applicability::HasPlaceholders => "has placeholders",
477 crate::diagnostic::Applicability::Unspecified => return,
478 };
479 let body = if color { kind.dimmed().to_string() } else { kind.to_string() };
480 out.push_str(&format!(" ({})\n", body));
481 }
482}
483
484fn color_for(sev: Severity, style: LabelStyle, s: &str) -> String {
487 match (sev, style) {
488 (Severity::Bug | Severity::Error, LabelStyle::Primary) => s.red().bold().to_string(),
489 (Severity::Warning, LabelStyle::Primary) => s.yellow().bold().to_string(),
490 _ => s.cyan().bold().to_string(),
491 }
492}
493
494fn label_touches(label: &Label, line: usize) -> bool {
495 let start = label.span.line;
496 let end_line = end_line_of(label);
497 line >= start && line <= end_line
498}
499
500fn label_columns_on_line(label: &Label, line: usize, raw_line: &str) -> (usize, usize) {
502 let start_line = label.span.line;
503 let line_byte_len = raw_line.len();
504 let line_end_col = line_byte_len + 1; let start_col = if line == start_line { label.span.column.max(1) } else { 1 };
507 let end_col_inclusive = if end_line_of(label) == line {
508 if line == start_line {
509 (label.span.column + label.span.length).max(label.span.column + 1)
511 } else {
512 line_end_col
515 }
516 } else {
517 line_end_col
518 };
519
520 (start_col, end_col_inclusive.min(line_end_col).max(start_col + 1))
521}
522
523fn end_line_of(label: &Label) -> usize {
524 label.span.line
529}
530
531fn expand_tabs(line: &str, tab_width: usize) -> String {
532 if !line.contains('\t') {
533 return line.to_string();
534 }
535 let mut out = String::with_capacity(line.len() + tab_width);
536 let mut col = 0usize;
537 for ch in line.chars() {
538 if ch == '\t' {
539 let advance = tab_width - (col % tab_width.max(1));
540 for _ in 0..advance {
541 out.push(' ');
542 }
543 col += advance;
544 } else {
545 out.push(ch);
546 col += UnicodeWidthStr::width(ch.to_string().as_str());
547 }
548 }
549 out
550}
551
552fn truncate_line(line: &str, max: usize) -> String {
553 if max == 0 {
554 return line.to_string();
555 }
556 let w = UnicodeWidthStr::width(line);
557 if w <= max {
558 return line.to_string();
559 }
560 let mut out = String::new();
562 let mut acc = 0usize;
563 for ch in line.chars() {
564 let cw = UnicodeWidthStr::width(ch.to_string().as_str());
565 if acc + cw + 1 > max {
566 break;
567 }
568 out.push(ch);
569 acc += cw;
570 }
571 out.push('…');
572 out
573}
574
575fn display_width_prefix(line: &str, byte_offset_0based: usize, tab_width: usize) -> usize {
577 let mut width = 0usize;
578 let mut byte_seen = 0usize;
579 for ch in line.chars() {
580 if byte_seen >= byte_offset_0based {
581 break;
582 }
583 if ch == '\t' {
584 width += tab_width - (width % tab_width.max(1));
585 } else {
586 width += UnicodeWidthStr::width(ch.to_string().as_str());
587 }
588 byte_seen += ch.len_utf8();
589 }
590 width
591}
592
593fn display_width_range(
594 line: &str,
595 start_byte_0based: usize,
596 end_byte_0based: usize,
597 tab_width: usize,
598) -> usize {
599 if end_byte_0based <= start_byte_0based {
600 return 0;
601 }
602 let mut width = 0usize;
603 let mut byte_seen = 0usize;
604 for ch in line.chars() {
605 if byte_seen >= end_byte_0based {
606 break;
607 }
608 if byte_seen >= start_byte_0based {
609 if ch == '\t' {
610 width += tab_width - (width % tab_width.max(1));
611 } else {
612 width += UnicodeWidthStr::width(ch.to_string().as_str());
613 }
614 }
615 byte_seen += ch.len_utf8();
616 }
617 width
618}