1use colored::*;
2use unicode_width::UnicodeWidthStr;
3
4use crate::diagnostic::{Diagnostic, DiagnosticCode, Label, LabelStyle, Suggestion};
5use crate::style::{arrow, bar, code_word, eq_sep, meta_label, paint, paint_label, severity_word};
6
7#[derive(Debug, Clone)]
9pub struct SourceCache<'a> {
10 lines: Vec<&'a str>,
11}
12
13impl<'a> SourceCache<'a> {
14 pub fn new(source: &'a str) -> Self {
16 Self { lines: source.lines().collect() }
17 }
18
19 pub fn line(&self, line_num_1based: usize) -> Option<&str> {
21 if line_num_1based == 0 {
22 return None;
23 }
24 self.lines.get(line_num_1based - 1).copied()
25 }
26
27 pub fn len(&self) -> usize {
29 self.lines.len()
30 }
31
32 pub fn is_empty(&self) -> bool {
34 self.lines.is_empty()
35 }
36}
37
38#[derive(Debug, Clone)]
41struct OwnedSource(Vec<String>);
42
43impl OwnedSource {
44 fn new(source: &str) -> Self {
45 Self(source.lines().map(String::from).collect())
46 }
47 fn line(&self, n: usize) -> Option<&str> {
48 if n == 0 {
49 None
50 } else {
51 self.0.get(n - 1).map(String::as_str)
52 }
53 }
54 fn len(&self) -> usize {
55 self.0.len()
56 }
57}
58
59enum CacheRef<'a, 'src> {
60 Borrowed(&'a SourceCache<'src>),
61 Owned(OwnedSource),
62}
63
64impl<'a, 'src> CacheRef<'a, 'src> {
65 fn line(&self, n: usize) -> Option<&str> {
66 match self {
67 Self::Borrowed(c) => c.line(n),
68 Self::Owned(o) => o.line(n),
69 }
70 }
71 fn len(&self) -> usize {
72 match self {
73 Self::Borrowed(c) => c.len(),
74 Self::Owned(o) => o.len(),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy)]
81pub struct RenderOptions {
82 pub tab_width: usize,
84 pub context_lines: usize,
86 pub max_line_width: usize,
88 pub color: bool,
90}
91
92impl Default for RenderOptions {
93 fn default() -> Self {
94 Self { tab_width: 4, context_lines: 0, max_line_width: 0, color: true }
95 }
96}
97
98pub struct DiagnosticFormatter<'a, 'src, C: DiagnosticCode> {
101 diagnostic: &'a Diagnostic<C>,
102 cache: CacheRef<'a, 'src>,
103 options: RenderOptions,
104}
105
106impl<'a, 'src, C: DiagnosticCode> DiagnosticFormatter<'a, 'src, C> {
107 pub fn new(diagnostic: &'a Diagnostic<C>, source: &str) -> Self {
110 Self {
111 diagnostic,
112 cache: CacheRef::Owned(OwnedSource::new(source)),
113 options: RenderOptions::default(),
114 }
115 }
116
117 pub fn with_cache(diagnostic: &'a Diagnostic<C>, cache: &'a SourceCache<'src>) -> Self {
120 Self { diagnostic, cache: CacheRef::Borrowed(cache), options: RenderOptions::default() }
121 }
122
123 pub fn with_options(mut self, options: RenderOptions) -> Self {
125 self.options = options;
126 self
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 d = &self.diagnostic;
163 out.push_str(&format!(
164 "{}: [{}]: {}",
165 severity_word(d.severity, color),
166 code_word(d.severity, d.code.code(), color),
167 d.message,
168 ));
169 if let Some(u) = d.code.url() {
170 out.push_str(&format!(" {}", paint(&format!("(see {u})"), color, |s| s.blue().italic())));
171 }
172 out.push('\n');
173 }
174
175 fn write_labels_grouped(&self, out: &mut String, color: bool) {
176 let labels = &self.diagnostic.labels;
177 if labels.is_empty() {
178 return;
179 }
180
181 let mut files: Vec<&str> = Vec::new();
183 for l in labels {
184 if !files.iter().any(|f| **f == *l.span.file) {
185 files.push(&l.span.file);
186 }
187 }
188
189 for (idx, file) in files.iter().enumerate() {
190 let in_file: Vec<&Label> = labels.iter().filter(|l| *l.span.file == **file).collect();
191 let primary_in_file = in_file
192 .iter()
193 .find(|l| l.style == LabelStyle::Primary)
194 .copied()
195 .or(in_file.first().copied());
196 let primary = match primary_in_file {
197 Some(l) => l,
198 None => continue,
199 };
200
201 let loc = if color {
202 format!(
203 "{}:{}:{}",
204 primary.span.file.clone().white().bold(),
205 primary.span.line.to_string().white().bold(),
206 primary.span.column.to_string().white().bold(),
207 )
208 } else {
209 format!("{}:{}:{}", primary.span.file, primary.span.line, primary.span.column)
210 };
211 out.push_str(&format!(" {} {}\n", arrow(color), loc));
212
213 self.write_file_section(out, &in_file, color);
214
215 if idx + 1 < files.len() {
216 out.push('\n');
217 }
218 }
219 }
220
221 fn write_file_section(&self, out: &mut String, labels: &[&Label], color: bool) {
222 let min_line = labels.iter().map(|l| l.span.line).min().unwrap_or(0);
225 let max_line = labels.iter().map(|l| l.span.line).max().unwrap_or(0);
226 if min_line == 0 {
227 return;
229 }
230
231 let start = min_line.saturating_sub(self.options.context_lines).max(1);
232 let end = (max_line + self.options.context_lines).min(self.cache.len());
233
234 let gutter_w = end.to_string().len().max(2);
235 let bar_s = bar(color);
236 let blank_gutter = " ".repeat(gutter_w);
237 out.push_str(&format!(" {} {}\n", blank_gutter, bar_s));
238
239 for line_num in start..=end {
240 let raw = self.cache.line(line_num).unwrap_or("");
241 let expanded = expand_tabs(raw, self.options.tab_width);
242 let truncated = truncate_line(&expanded, self.options.max_line_width);
243 let line_label = format!("{:>w$}", line_num, w = gutter_w);
244 let line_label_c = paint(&line_label, color, |s| s.blue().bold());
245 out.push_str(&format!(" {} {} {}\n", line_label_c, bar_s, truncated));
246
247 let mut on_line: Vec<&Label> =
249 labels.iter().copied().filter(|l| label_touches(l, line_num)).collect();
250 if on_line.is_empty() {
251 continue;
252 }
253 on_line.sort_by_key(|l| l.span.column);
254 self.write_caret_block(out, &on_line, line_num, raw, gutter_w, color);
255 }
256
257 out.push_str(&format!(" {} {}\n", blank_gutter, bar_s));
258 }
259
260 fn write_caret_block(
271 &self,
272 out: &mut String,
273 sorted: &[&Label],
274 line_num: usize,
275 raw_line: &str,
276 gutter_w: usize,
277 color: bool,
278 ) {
279 let infos: Vec<(usize, usize, &Label)> = sorted
280 .iter()
281 .map(|label| {
282 let (col_start, col_end) = label_columns_on_line(label, line_num, raw_line);
283 let pad =
284 display_width_prefix(raw_line, col_start.saturating_sub(1), self.options.tab_width);
285 let len = display_width_range(
286 raw_line,
287 col_start.saturating_sub(1),
288 col_end.saturating_sub(1),
289 self.options.tab_width,
290 )
291 .max(1);
292 (pad, len, *label)
293 })
294 .collect();
295
296 let n = infos.len();
297 let bar_s = bar(color);
298 let blank_gutter = " ".repeat(gutter_w);
299
300 for k in 0..n {
301 let m = n - 1 - k;
302 let visible = &infos[..=m];
303
304 let mut buf = String::new();
306 let mut cursor = 0usize;
307 for (pad, len, lbl) in visible {
308 while cursor < *pad {
309 buf.push(' ');
310 cursor += 1;
311 }
312 let ch = Self::underline_char(lbl.style);
313 let underline: String = std::iter::repeat_n(ch, *len).collect();
314 buf.push_str(&paint_label(self.diagnostic.severity, lbl.style, &underline, color));
315 cursor += *len;
316 }
317
318 let m_label = visible.last().unwrap().2;
320 let line = match &m_label.message {
321 Some(msg) => format!(
322 " {} {} {} {}\n",
323 blank_gutter,
324 bar_s,
325 buf,
326 paint_label(self.diagnostic.severity, m_label.style, msg, color),
327 ),
328 None => format!(" {} {} {}\n", blank_gutter, bar_s, buf),
329 };
330 out.push_str(&line);
331
332 if let Some(note) = &m_label.note {
333 let note_c = if color { note.cyan().italic().to_string() } else { format!("note: {note}") };
334 out.push_str(&format!(
335 " {} {} {}↳ {}\n",
336 blank_gutter,
337 bar_s,
338 " ".repeat(infos[m].0),
339 note_c,
340 ));
341 }
342 }
343 }
344
345 fn write_notes_help(&self, out: &mut String, color: bool) {
346 let eq = eq_sep(color);
347 for note in &self.diagnostic.notes {
348 out.push_str(&format!(" {} {}: {}\n", eq, meta_label("note", color), note));
349 }
350 if let Some(help) = &self.diagnostic.help {
351 out.push_str(&format!(" {} {}: {}\n", eq, meta_label("help", color), help));
352 }
353 }
354
355 fn write_suggestions(&self, out: &mut String, color: bool) {
356 if self.diagnostic.suggestions.is_empty() {
357 return;
358 }
359 let eq = eq_sep(color);
360 let help = meta_label("help", color);
361 for s in &self.diagnostic.suggestions {
362 let header = s.message.clone().unwrap_or_else(|| "try this:".to_string());
363 out.push_str(&format!(" {} {}: {}\n", eq, help, header));
364 self.write_suggestion_diff(out, s, color);
365 Self::write_applicability(out, s, color);
366 }
367 }
368
369 fn write_suggestion_diff(&self, out: &mut String, s: &Suggestion, color: bool) {
373 let line_num = s.span.line;
374 let orig_line = match self.cache.line(line_num) {
375 Some(l) => l,
376 None => {
377 for line in s.replacement.lines() {
378 out.push_str(&format!(" {}\n", paint(line, color, |s| s.green())));
379 }
380 return;
381 },
382 };
383
384 let col0 = s.span.column.saturating_sub(1);
387 let line_bytes = orig_line.len();
388 let start = col0.min(line_bytes);
389 let end = (start + s.span.length).min(line_bytes);
390 let prefix = &orig_line[..start];
391 let suffix = &orig_line[end..];
392
393 let repl_lines: Vec<&str> = s.replacement.split('\n').collect();
397 let mut new_lines: Vec<String> = Vec::with_capacity(repl_lines.len());
398 for (i, r) in repl_lines.iter().enumerate() {
399 let head = if i == 0 { prefix } else { "" };
400 let tail = if i == repl_lines.len() - 1 { suffix } else { "" };
401 new_lines.push(format!("{}{}{}", head, r, tail));
402 }
403
404 let last_line = line_num + new_lines.len().saturating_sub(1);
405 let gutter_w = last_line.to_string().len().max(2);
406 let bar_s = bar(color);
407 let blank_gutter = " ".repeat(gutter_w);
408 let minus = paint("-", color, |s| s.red().bold());
409 let plus = paint("+", color, |s| s.green().bold());
410
411 out.push_str(&format!(" {} {}\n", blank_gutter, bar_s));
412
413 let lbl = format!("{:>w$}", line_num, w = gutter_w);
414 out.push_str(&format!(
415 " {} {} {}\n",
416 paint(&lbl, color, |s| s.blue().bold()),
417 minus,
418 paint(orig_line, color, |s| s.red()),
419 ));
420
421 for (i, body) in new_lines.iter().enumerate() {
422 let lbl = format!("{:>w$}", line_num + i, w = gutter_w);
423 out.push_str(&format!(
424 " {} {} {}\n",
425 paint(&lbl, color, |s| s.blue().bold()),
426 plus,
427 paint(body, color, |s| s.green()),
428 ));
429 }
430
431 out.push_str(&format!(" {} {}\n", blank_gutter, bar_s));
432 }
433
434 fn write_applicability(out: &mut String, s: &Suggestion, color: bool) {
435 let kind = match s.applicability {
436 crate::diagnostic::Applicability::MachineApplicable => "auto-applicable",
437 crate::diagnostic::Applicability::MaybeIncorrect => "review needed",
438 crate::diagnostic::Applicability::HasPlaceholders => "has placeholders",
439 crate::diagnostic::Applicability::Unspecified => return,
440 };
441 out.push_str(&format!(" ({})\n", paint(kind, color, |s| s.dimmed())));
442 }
443}
444
445fn label_touches(label: &Label, line: usize) -> bool {
448 let start = label.span.line;
449 let end_line = end_line_of(label);
450 line >= start && line <= end_line
451}
452
453fn label_columns_on_line(label: &Label, line: usize, raw_line: &str) -> (usize, usize) {
455 let start_line = label.span.line;
456 let line_byte_len = raw_line.len();
457 let line_end_col = line_byte_len + 1; let start_col = if line == start_line { label.span.column.max(1) } else { 1 };
460 let end_col_inclusive = if end_line_of(label) == line {
461 if line == start_line {
462 (label.span.column + label.span.length).max(label.span.column + 1)
464 } else {
465 line_end_col
468 }
469 } else {
470 line_end_col
471 };
472
473 (start_col, end_col_inclusive.min(line_end_col).max(start_col + 1))
474}
475
476fn end_line_of(label: &Label) -> usize {
477 label.span.line
482}
483
484fn expand_tabs(line: &str, tab_width: usize) -> String {
485 if !line.contains('\t') {
486 return line.to_string();
487 }
488 let mut out = String::with_capacity(line.len() + tab_width);
489 let mut col = 0usize;
490 for ch in line.chars() {
491 if ch == '\t' {
492 let advance = tab_width - (col % tab_width.max(1));
493 for _ in 0..advance {
494 out.push(' ');
495 }
496 col += advance;
497 } else {
498 out.push(ch);
499 col += UnicodeWidthStr::width(ch.to_string().as_str());
500 }
501 }
502 out
503}
504
505fn truncate_line(line: &str, max: usize) -> String {
506 if max == 0 {
507 return line.to_string();
508 }
509 let w = UnicodeWidthStr::width(line);
510 if w <= max {
511 return line.to_string();
512 }
513 let mut out = String::new();
515 let mut acc = 0usize;
516 for ch in line.chars() {
517 let cw = UnicodeWidthStr::width(ch.to_string().as_str());
518 if acc + cw + 1 > max {
519 break;
520 }
521 out.push(ch);
522 acc += cw;
523 }
524 out.push('…');
525 out
526}
527
528fn display_width_prefix(line: &str, byte_offset_0based: usize, tab_width: usize) -> usize {
530 let mut width = 0usize;
531 let mut byte_seen = 0usize;
532 for ch in line.chars() {
533 if byte_seen >= byte_offset_0based {
534 break;
535 }
536 if ch == '\t' {
537 width += tab_width - (width % tab_width.max(1));
538 } else {
539 width += UnicodeWidthStr::width(ch.to_string().as_str());
540 }
541 byte_seen += ch.len_utf8();
542 }
543 width
544}
545
546fn display_width_range(
547 line: &str,
548 start_byte_0based: usize,
549 end_byte_0based: usize,
550 tab_width: usize,
551) -> usize {
552 if end_byte_0based <= start_byte_0based {
553 return 0;
554 }
555 let mut width = 0usize;
556 let mut byte_seen = 0usize;
557 for ch in line.chars() {
558 if byte_seen >= end_byte_0based {
559 break;
560 }
561 if byte_seen >= start_byte_0based {
562 if ch == '\t' {
563 width += tab_width - (width % tab_width.max(1));
564 } else {
565 width += UnicodeWidthStr::width(ch.to_string().as_str());
566 }
567 }
568 byte_seen += ch.len_utf8();
569 }
570 width
571}