1use std::{
2 cmp,
3 fmt::{self, Display, Write},
4 iter::once,
5};
6
7pub mod style;
8
9use self::style::{Style, StyleClass, Stylesheet};
10
11#[cfg(feature = "color")]
12use crate::stylesheets::color::AnsiTermStylesheet;
13use crate::{display_list::*, stylesheets::no_color::NoColorStylesheet};
14
15fn 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]
23fn is_annotation_empty(annotation: &Annotation<'_>) -> bool {
24 annotationIter<'_, DisplayTextFragment<'_>>
25 .label
26 .iter()
27 .all(|fragment: &DisplayTextFragment<'_>| fragment.content.is_empty())
28}
29
30#[cfg(feature = "color")]
31#[inline]
32pub 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]
42pub fn get_term_style(_color: bool) -> Box<dyn Stylesheet> {
43 Box::new(NoColorStylesheet)
44}
45
46impl<'a> fmt::Display for DisplayList<'a> {
47 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
80impl<'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]
89 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
103 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
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
125 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
141 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]
193 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]
322 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]
378 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
436 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