1 use std::{
2     cmp,
3     fmt::{self, Display, Write},
4 };
5 
6 pub mod style;
7 
8 use self::style::{Style, StyleClass, Stylesheet};
9 
10 #[cfg(feature = "color")]
11 use crate::stylesheets::color::AnsiTermStylesheet;
12 use crate::{display_list::*, stylesheets::no_color::NoColorStylesheet};
13 
format_repeat_char(c: char, n: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result14 fn format_repeat_char(c: char, n: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15     for _ in 0..n {
16         f.write_char(c)?;
17     }
18     Ok(())
19 }
20 
21 #[inline]
is_annotation_empty(annotation: &Annotation<'_>) -> bool22 fn is_annotation_empty(annotation: &Annotation<'_>) -> bool {
23     annotation
24         .label
25         .iter()
26         .all(|fragment| fragment.content.is_empty())
27 }
28 
29 #[cfg(feature = "color")]
30 #[inline]
get_term_style(color: bool) -> Box<dyn Stylesheet>31 pub fn get_term_style(color: bool) -> Box<dyn Stylesheet> {
32     if color {
33         Box::new(AnsiTermStylesheet)
34     } else {
35         Box::new(NoColorStylesheet)
36     }
37 }
38 
39 #[cfg(not(feature = "color"))]
40 #[inline]
get_term_style(_color: bool) -> Box<dyn Stylesheet>41 pub fn get_term_style(_color: bool) -> Box<dyn Stylesheet> {
42     Box::new(NoColorStylesheet)
43 }
44 
45 impl<'a> fmt::Display for DisplayList<'a> {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result46     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47         let lineno_width = self.body.iter().fold(0, |max, line| match line {
48             DisplayLine::Source {
49                 lineno: Some(lineno),
50                 ..
51             } => {
52                 // The largest line is the largest width.
53                 cmp::max(*lineno, max)
54             }
55             _ => max,
56         });
57         let lineno_width = if lineno_width == 0 {
58             lineno_width
59         } else if self.anonymized_line_numbers {
60             Self::ANONYMIZED_LINE_NUM.len()
61         } else {
62             ((lineno_width as f64).log10().floor() as usize) + 1
63         };
64         let inline_marks_width = self.body.iter().fold(0, |max, line| match line {
65             DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max),
66             _ => max,
67         });
68 
69         for (i, line) in self.body.iter().enumerate() {
70             self.format_line(line, lineno_width, inline_marks_width, f)?;
71             if i + 1 < self.body.len() {
72                 f.write_char('\n')?;
73             }
74         }
75         Ok(())
76     }
77 }
78 
79 impl<'a> DisplayList<'a> {
80     const ANONYMIZED_LINE_NUM: &'static str = "LL";
81     const ERROR_TXT: &'static str = "error";
82     const HELP_TXT: &'static str = "help";
83     const INFO_TXT: &'static str = "info";
84     const NOTE_TXT: &'static str = "note";
85     const WARNING_TXT: &'static str = "warning";
86 
87     #[inline]
format_annotation_type( annotation_type: &DisplayAnnotationType, f: &mut fmt::Formatter<'_>, ) -> fmt::Result88     fn format_annotation_type(
89         annotation_type: &DisplayAnnotationType,
90         f: &mut fmt::Formatter<'_>,
91     ) -> fmt::Result {
92         match annotation_type {
93             DisplayAnnotationType::Error => f.write_str(Self::ERROR_TXT),
94             DisplayAnnotationType::Help => f.write_str(Self::HELP_TXT),
95             DisplayAnnotationType::Info => f.write_str(Self::INFO_TXT),
96             DisplayAnnotationType::Note => f.write_str(Self::NOTE_TXT),
97             DisplayAnnotationType::Warning => f.write_str(Self::WARNING_TXT),
98             DisplayAnnotationType::None => Ok(()),
99         }
100     }
101 
annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize102     fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize {
103         match annotation_type {
104             DisplayAnnotationType::Error => Self::ERROR_TXT.len(),
105             DisplayAnnotationType::Help => Self::HELP_TXT.len(),
106             DisplayAnnotationType::Info => Self::INFO_TXT.len(),
107             DisplayAnnotationType::Note => Self::NOTE_TXT.len(),
108             DisplayAnnotationType::Warning => Self::WARNING_TXT.len(),
109             DisplayAnnotationType::None => 0,
110         }
111     }
112 
get_annotation_style(&self, annotation_type: &DisplayAnnotationType) -> Box<dyn Style>113     fn get_annotation_style(&self, annotation_type: &DisplayAnnotationType) -> Box<dyn Style> {
114         self.stylesheet.get_style(match annotation_type {
115             DisplayAnnotationType::Error => StyleClass::Error,
116             DisplayAnnotationType::Warning => StyleClass::Warning,
117             DisplayAnnotationType::Info => StyleClass::Info,
118             DisplayAnnotationType::Note => StyleClass::Note,
119             DisplayAnnotationType::Help => StyleClass::Help,
120             DisplayAnnotationType::None => StyleClass::None,
121         })
122     }
123 
format_label( &self, label: &[DisplayTextFragment<'_>], f: &mut fmt::Formatter<'_>, ) -> fmt::Result124     fn format_label(
125         &self,
126         label: &[DisplayTextFragment<'_>],
127         f: &mut fmt::Formatter<'_>,
128     ) -> fmt::Result {
129         let emphasis_style = self.stylesheet.get_style(StyleClass::Emphasis);
130 
131         for fragment in label {
132             match fragment.style {
133                 DisplayTextStyle::Regular => fragment.content.fmt(f)?,
134                 DisplayTextStyle::Emphasis => emphasis_style.paint(&fragment.content, f)?,
135             }
136         }
137         Ok(())
138     }
139 
format_annotation( &self, annotation: &Annotation<'_>, continuation: bool, in_source: bool, f: &mut fmt::Formatter<'_>, ) -> fmt::Result140     fn format_annotation(
141         &self,
142         annotation: &Annotation<'_>,
143         continuation: bool,
144         in_source: bool,
145         f: &mut fmt::Formatter<'_>,
146     ) -> fmt::Result {
147         let color = self.get_annotation_style(&annotation.annotation_type);
148         let formatted_len = if let Some(id) = &annotation.id {
149             2 + id.len() + Self::annotation_type_len(&annotation.annotation_type)
150         } else {
151             Self::annotation_type_len(&annotation.annotation_type)
152         };
153 
154         if continuation {
155             format_repeat_char(' ', formatted_len + 2, f)?;
156             return self.format_label(&annotation.label, f);
157         }
158         if formatted_len == 0 {
159             self.format_label(&annotation.label, f)
160         } else {
161             color.paint_fn(
162                 Box::new(|f| {
163                     Self::format_annotation_type(&annotation.annotation_type, f)?;
164                     if let Some(id) = &annotation.id {
165                         f.write_char('[')?;
166                         f.write_str(id)?;
167                         f.write_char(']')?;
168                     }
169                     Ok(())
170                 }),
171                 f,
172             )?;
173             if !is_annotation_empty(annotation) {
174                 if in_source {
175                     color.paint_fn(
176                         Box::new(|f| {
177                             f.write_str(": ")?;
178                             self.format_label(&annotation.label, f)
179                         }),
180                         f,
181                     )?;
182                 } else {
183                     f.write_str(": ")?;
184                     self.format_label(&annotation.label, f)?;
185                 }
186             }
187             Ok(())
188         }
189     }
190 
191     #[inline]
format_source_line( &self, line: &DisplaySourceLine<'_>, f: &mut fmt::Formatter<'_>, ) -> fmt::Result192     fn format_source_line(
193         &self,
194         line: &DisplaySourceLine<'_>,
195         f: &mut fmt::Formatter<'_>,
196     ) -> fmt::Result {
197         match line {
198             DisplaySourceLine::Empty => Ok(()),
199             DisplaySourceLine::Content { text, .. } => {
200                 f.write_char(' ')?;
201                 if let Some(margin) = self.margin {
202                     let line_len = text.chars().count();
203                     let mut left = margin.left(line_len);
204                     let right = margin.right(line_len);
205 
206                     if margin.was_cut_left() {
207                         // We have stripped some code/whitespace from the beginning, make it clear.
208                         "...".fmt(f)?;
209                         left += 3;
210                     }
211 
212                     // On long lines, we strip the source line, accounting for unicode.
213                     let mut taken = 0;
214                     let cut_right = if margin.was_cut_right(line_len) {
215                         taken += 3;
216                         true
217                     } else {
218                         false
219                     };
220                     let range = text
221                         .char_indices()
222                         .skip(left)
223                         .take_while(|(_, ch)| {
224                             // Make sure that the trimming on the right will fall within the terminal width.
225                             // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is.
226                             // For now, just accept that sometimes the code line will be longer than desired.
227                             taken += unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1);
228                             if taken > right - left {
229                                 return false;
230                             }
231                             true
232                         })
233                         .fold((None, 0), |acc, (i, _)| {
234                             if acc.0.is_some() {
235                                 (acc.0, i)
236                             } else {
237                                 (Some(i), i)
238                             }
239                         });
240 
241                     text[range.0.expect("One character at line")..=range.1].fmt(f)?;
242 
243                     if cut_right {
244                         // We have stripped some code after the right-most span end, make it clear we did so.
245                         "...".fmt(f)?;
246                     }
247                     Ok(())
248                 } else {
249                     text.fmt(f)
250                 }
251             }
252             DisplaySourceLine::Annotation {
253                 range,
254                 annotation,
255                 annotation_type,
256                 annotation_part,
257             } => {
258                 let indent_char = match annotation_part {
259                     DisplayAnnotationPart::Standalone => ' ',
260                     DisplayAnnotationPart::LabelContinuation => ' ',
261                     DisplayAnnotationPart::Consequitive => ' ',
262                     DisplayAnnotationPart::MultilineStart => '_',
263                     DisplayAnnotationPart::MultilineEnd => '_',
264                 };
265                 let mark = match annotation_type {
266                     DisplayAnnotationType::Error => '^',
267                     DisplayAnnotationType::Warning => '-',
268                     DisplayAnnotationType::Info => '-',
269                     DisplayAnnotationType::Note => '-',
270                     DisplayAnnotationType::Help => '-',
271                     DisplayAnnotationType::None => ' ',
272                 };
273                 let color = self.get_annotation_style(annotation_type);
274                 let indent_length = match annotation_part {
275                     DisplayAnnotationPart::LabelContinuation => range.1,
276                     DisplayAnnotationPart::Consequitive => range.1,
277                     _ => range.0,
278                 };
279 
280                 color.paint_fn(
281                     Box::new(|f| {
282                         format_repeat_char(indent_char, indent_length + 1, f)?;
283                         format_repeat_char(mark, range.1 - indent_length, f)
284                     }),
285                     f,
286                 )?;
287 
288                 if !is_annotation_empty(&annotation) {
289                     f.write_char(' ')?;
290                     color.paint_fn(
291                         Box::new(|f| {
292                             self.format_annotation(
293                                 annotation,
294                                 annotation_part == &DisplayAnnotationPart::LabelContinuation,
295                                 true,
296                                 f,
297                             )
298                         }),
299                         f,
300                     )?;
301                 }
302 
303                 Ok(())
304             }
305         }
306     }
307 
308     #[inline]
format_raw_line( &self, line: &DisplayRawLine<'_>, lineno_width: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result309     fn format_raw_line(
310         &self,
311         line: &DisplayRawLine<'_>,
312         lineno_width: usize,
313         f: &mut fmt::Formatter<'_>,
314     ) -> fmt::Result {
315         match line {
316             DisplayRawLine::Origin {
317                 path,
318                 pos,
319                 header_type,
320             } => {
321                 let header_sigil = match header_type {
322                     DisplayHeaderType::Initial => "-->",
323                     DisplayHeaderType::Continuation => ":::",
324                 };
325                 let lineno_color = self.stylesheet.get_style(StyleClass::LineNo);
326 
327                 if let Some((col, row)) = pos {
328                     format_repeat_char(' ', lineno_width, f)?;
329                     lineno_color.paint(header_sigil, f)?;
330                     f.write_char(' ')?;
331                     path.fmt(f)?;
332                     f.write_char(':')?;
333                     col.fmt(f)?;
334                     f.write_char(':')?;
335                     row.fmt(f)
336                 } else {
337                     format_repeat_char(' ', lineno_width, f)?;
338                     lineno_color.paint(header_sigil, f)?;
339                     f.write_char(' ')?;
340                     path.fmt(f)
341                 }
342             }
343             DisplayRawLine::Annotation {
344                 annotation,
345                 source_aligned,
346                 continuation,
347             } => {
348                 if *source_aligned {
349                     if *continuation {
350                         format_repeat_char(' ', lineno_width + 3, f)?;
351                         self.format_annotation(annotation, *continuation, false, f)
352                     } else {
353                         let lineno_color = self.stylesheet.get_style(StyleClass::LineNo);
354                         format_repeat_char(' ', lineno_width, f)?;
355                         f.write_char(' ')?;
356                         lineno_color.paint("=", f)?;
357                         f.write_char(' ')?;
358                         self.format_annotation(annotation, *continuation, false, f)
359                     }
360                 } else {
361                     self.format_annotation(annotation, *continuation, false, f)
362                 }
363             }
364         }
365     }
366 
367     #[inline]
format_line( &self, dl: &DisplayLine<'_>, lineno_width: usize, inline_marks_width: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result368     fn format_line(
369         &self,
370         dl: &DisplayLine<'_>,
371         lineno_width: usize,
372         inline_marks_width: usize,
373         f: &mut fmt::Formatter<'_>,
374     ) -> fmt::Result {
375         match dl {
376             DisplayLine::Source {
377                 lineno,
378                 inline_marks,
379                 line,
380             } => {
381                 let lineno_color = self.stylesheet.get_style(StyleClass::LineNo);
382                 if self.anonymized_line_numbers && lineno.is_some() {
383                     lineno_color.paint_fn(
384                         Box::new(|f| {
385                             f.write_str(Self::ANONYMIZED_LINE_NUM)?;
386                             f.write_str(" |")
387                         }),
388                         f,
389                     )?;
390                 } else {
391                     lineno_color.paint_fn(
392                         Box::new(|f| {
393                             match lineno {
394                                 Some(n) => write!(f, "{:>width$}", n, width = lineno_width),
395                                 None => format_repeat_char(' ', lineno_width, f),
396                             }?;
397                             f.write_str(" |")
398                         }),
399                         f,
400                     )?;
401                 }
402                 if *line != DisplaySourceLine::Empty {
403                     if !inline_marks.is_empty() || 0 < inline_marks_width {
404                         f.write_char(' ')?;
405                         self.format_inline_marks(inline_marks, inline_marks_width, f)?;
406                     }
407                     self.format_source_line(line, f)?;
408                 } else if !inline_marks.is_empty() {
409                     f.write_char(' ')?;
410                     self.format_inline_marks(inline_marks, inline_marks_width, f)?;
411                 }
412                 Ok(())
413             }
414             DisplayLine::Fold { inline_marks } => {
415                 f.write_str("...")?;
416                 if !inline_marks.is_empty() || 0 < inline_marks_width {
417                     format_repeat_char(' ', lineno_width, f)?;
418                     self.format_inline_marks(inline_marks, inline_marks_width, f)?;
419                 }
420                 Ok(())
421             }
422             DisplayLine::Raw(line) => self.format_raw_line(line, lineno_width, f),
423         }
424     }
425 
format_inline_marks( &self, inline_marks: &[DisplayMark], inline_marks_width: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result426     fn format_inline_marks(
427         &self,
428         inline_marks: &[DisplayMark],
429         inline_marks_width: usize,
430         f: &mut fmt::Formatter<'_>,
431     ) -> fmt::Result {
432         format_repeat_char(' ', inline_marks_width - inline_marks.len(), f)?;
433         for mark in inline_marks {
434             self.get_annotation_style(&mark.annotation_type).paint_fn(
435                 Box::new(|f| {
436                     f.write_char(match mark.mark_type {
437                         DisplayMarkType::AnnotationThrough => '|',
438                         DisplayMarkType::AnnotationStart => '/',
439                     })
440                 }),
441                 f,
442             )?;
443         }
444         Ok(())
445     }
446 }
447