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