use std::{ cmp, fmt::{self, Display, Write}, iter::once, }; pub mod style; use self::style::{Style, StyleClass, Stylesheet}; #[cfg(feature = "color")] use crate::stylesheets::color::AnsiTermStylesheet; use crate::{display_list::*, stylesheets::no_color::NoColorStylesheet}; fn format_repeat_char(c: char, n: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result { for _ in 0..n { f.write_char(c)?; } Ok(()) } #[inline] fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { annotation .label .iter() .all(|fragment| fragment.content.is_empty()) } #[cfg(feature = "color")] #[inline] pub fn get_term_style(color: bool) -> Box { if color { Box::new(AnsiTermStylesheet) } else { Box::new(NoColorStylesheet) } } #[cfg(not(feature = "color"))] #[inline] pub fn get_term_style(_color: bool) -> Box { Box::new(NoColorStylesheet) } impl<'a> fmt::Display for DisplayList<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let lineno_width = self.body.iter().fold(0, |max, line| match line { DisplayLine::Source { lineno: Some(lineno), .. } => { // The largest line is the largest width. cmp::max(*lineno, max) } _ => max, }); let lineno_width = if lineno_width == 0 { lineno_width } else if self.anonymized_line_numbers { Self::ANONYMIZED_LINE_NUM.len() } else { ((lineno_width as f64).log10().floor() as usize) + 1 }; let inline_marks_width = self.body.iter().fold(0, |max, line| match line { DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max), _ => max, }); for (i, line) in self.body.iter().enumerate() { self.format_line(line, lineno_width, inline_marks_width, f)?; if i + 1 < self.body.len() { f.write_char('\n')?; } } Ok(()) } } impl<'a> DisplayList<'a> { const ANONYMIZED_LINE_NUM: &'static str = "LL"; const ERROR_TXT: &'static str = "error"; const HELP_TXT: &'static str = "help"; const INFO_TXT: &'static str = "info"; const NOTE_TXT: &'static str = "note"; const WARNING_TXT: &'static str = "warning"; #[inline] fn format_annotation_type( annotation_type: &DisplayAnnotationType, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { match annotation_type { DisplayAnnotationType::Error => f.write_str(Self::ERROR_TXT), DisplayAnnotationType::Help => f.write_str(Self::HELP_TXT), DisplayAnnotationType::Info => f.write_str(Self::INFO_TXT), DisplayAnnotationType::Note => f.write_str(Self::NOTE_TXT), DisplayAnnotationType::Warning => f.write_str(Self::WARNING_TXT), DisplayAnnotationType::None => Ok(()), } } fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { match annotation_type { DisplayAnnotationType::Error => Self::ERROR_TXT.len(), DisplayAnnotationType::Help => Self::HELP_TXT.len(), DisplayAnnotationType::Info => Self::INFO_TXT.len(), DisplayAnnotationType::Note => Self::NOTE_TXT.len(), DisplayAnnotationType::Warning => Self::WARNING_TXT.len(), DisplayAnnotationType::None => 0, } } fn get_annotation_style(&self, annotation_type: &DisplayAnnotationType) -> Box { self.stylesheet.get_style(match annotation_type { DisplayAnnotationType::Error => StyleClass::Error, DisplayAnnotationType::Warning => StyleClass::Warning, DisplayAnnotationType::Info => StyleClass::Info, DisplayAnnotationType::Note => StyleClass::Note, DisplayAnnotationType::Help => StyleClass::Help, DisplayAnnotationType::None => StyleClass::None, }) } fn format_label( &self, label: &[DisplayTextFragment<'_>], f: &mut fmt::Formatter<'_>, ) -> fmt::Result { let emphasis_style = self.stylesheet.get_style(StyleClass::Emphasis); for fragment in label { match fragment.style { DisplayTextStyle::Regular => fragment.content.fmt(f)?, DisplayTextStyle::Emphasis => emphasis_style.paint(fragment.content, f)?, } } Ok(()) } fn format_annotation( &self, annotation: &Annotation<'_>, continuation: bool, in_source: bool, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { let color = self.get_annotation_style(&annotation.annotation_type); let formatted_len = if let Some(id) = &annotation.id { 2 + id.len() + Self::annotation_type_len(&annotation.annotation_type) } else { Self::annotation_type_len(&annotation.annotation_type) }; if continuation { format_repeat_char(' ', formatted_len + 2, f)?; return self.format_label(&annotation.label, f); } if formatted_len == 0 { self.format_label(&annotation.label, f) } else { color.paint_fn( Box::new(|f| { Self::format_annotation_type(&annotation.annotation_type, f)?; if let Some(id) = &annotation.id { f.write_char('[')?; f.write_str(id)?; f.write_char(']')?; } Ok(()) }), f, )?; if !is_annotation_empty(annotation) { if in_source { color.paint_fn( Box::new(|f| { f.write_str(": ")?; self.format_label(&annotation.label, f) }), f, )?; } else { f.write_str(": ")?; self.format_label(&annotation.label, f)?; } } Ok(()) } } #[inline] fn format_source_line( &self, line: &DisplaySourceLine<'_>, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { match line { DisplaySourceLine::Empty => Ok(()), DisplaySourceLine::Content { text, .. } => { f.write_char(' ')?; if let Some(margin) = self.margin { let line_len = text.chars().count(); let mut left = margin.left(line_len); let right = margin.right(line_len); if margin.was_cut_left() { // We have stripped some code/whitespace from the beginning, make it clear. "...".fmt(f)?; left += 3; } // On long lines, we strip the source line, accounting for unicode. let mut taken = 0; let cut_right = if margin.was_cut_right(line_len) { taken += 3; true } else { false }; // Specifies that it will end on the next character, so it will return // until the next one to the final condition. let mut ended = false; let range = text .char_indices() .skip(left) // Complete char iterator with final character .chain(once((text.len(), '\0'))) // Take until the next one to the final condition .take_while(|(_, ch)| { // Fast return to iterate over final byte position if ended { return false; } // Make sure that the trimming on the right will fall within the terminal width. // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. // For now, just accept that sometimes the code line will be longer than desired. taken += unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1); if taken > right - left { ended = true; } true }) // Reduce to start and end byte position .fold((None, 0), |acc, (i, _)| { if acc.0.is_some() { (acc.0, i) } else { (Some(i), i) } }); // Format text with margins text[range.0.expect("One character at line")..range.1].fmt(f)?; if cut_right { // We have stripped some code after the right-most span end, make it clear we did so. "...".fmt(f)?; } Ok(()) } else { text.fmt(f) } } DisplaySourceLine::Annotation { range, annotation, annotation_type, annotation_part, } => { let indent_char = match annotation_part { DisplayAnnotationPart::Standalone => ' ', DisplayAnnotationPart::LabelContinuation => ' ', DisplayAnnotationPart::Consequitive => ' ', DisplayAnnotationPart::MultilineStart => '_', DisplayAnnotationPart::MultilineEnd => '_', }; let mark = match annotation_type { DisplayAnnotationType::Error => '^', DisplayAnnotationType::Warning => '-', DisplayAnnotationType::Info => '-', DisplayAnnotationType::Note => '-', DisplayAnnotationType::Help => '-', DisplayAnnotationType::None => ' ', }; let color = self.get_annotation_style(annotation_type); let indent_length = match annotation_part { DisplayAnnotationPart::LabelContinuation => range.1, DisplayAnnotationPart::Consequitive => range.1, _ => range.0, }; color.paint_fn( Box::new(|f| { format_repeat_char(indent_char, indent_length + 1, f)?; format_repeat_char(mark, range.1 - indent_length, f) }), f, )?; if !is_annotation_empty(annotation) { f.write_char(' ')?; color.paint_fn( Box::new(|f| { self.format_annotation( annotation, annotation_part == &DisplayAnnotationPart::LabelContinuation, true, f, ) }), f, )?; } Ok(()) } } } #[inline] fn format_raw_line( &self, line: &DisplayRawLine<'_>, lineno_width: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { match line { DisplayRawLine::Origin { path, pos, header_type, } => { let header_sigil = match header_type { DisplayHeaderType::Initial => "-->", DisplayHeaderType::Continuation => ":::", }; let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); if let Some((col, row)) = pos { format_repeat_char(' ', lineno_width, f)?; lineno_color.paint(header_sigil, f)?; f.write_char(' ')?; path.fmt(f)?; f.write_char(':')?; col.fmt(f)?; f.write_char(':')?; row.fmt(f) } else { format_repeat_char(' ', lineno_width, f)?; lineno_color.paint(header_sigil, f)?; f.write_char(' ')?; path.fmt(f) } } DisplayRawLine::Annotation { annotation, source_aligned, continuation, } => { if *source_aligned { if *continuation { format_repeat_char(' ', lineno_width + 3, f)?; } else { let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); format_repeat_char(' ', lineno_width, f)?; f.write_char(' ')?; lineno_color.paint("=", f)?; f.write_char(' ')?; } } self.format_annotation(annotation, *continuation, false, f) } } } #[inline] fn format_line( &self, dl: &DisplayLine<'_>, lineno_width: usize, inline_marks_width: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { match dl { DisplayLine::Source { lineno, inline_marks, line, } => { let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); if self.anonymized_line_numbers && lineno.is_some() { lineno_color.paint_fn( Box::new(|f| { f.write_str(Self::ANONYMIZED_LINE_NUM)?; f.write_str(" |") }), f, )?; } else { lineno_color.paint_fn( Box::new(|f| { match lineno { Some(n) => write!(f, "{:>width$}", n, width = lineno_width), None => format_repeat_char(' ', lineno_width, f), }?; f.write_str(" |") }), f, )?; } if *line != DisplaySourceLine::Empty { if !inline_marks.is_empty() || 0 < inline_marks_width { f.write_char(' ')?; self.format_inline_marks(inline_marks, inline_marks_width, f)?; } self.format_source_line(line, f)?; } else if !inline_marks.is_empty() { f.write_char(' ')?; self.format_inline_marks(inline_marks, inline_marks_width, f)?; } Ok(()) } DisplayLine::Fold { inline_marks } => { f.write_str("...")?; if !inline_marks.is_empty() || 0 < inline_marks_width { format_repeat_char(' ', lineno_width, f)?; self.format_inline_marks(inline_marks, inline_marks_width, f)?; } Ok(()) } DisplayLine::Raw(line) => self.format_raw_line(line, lineno_width, f), } } fn format_inline_marks( &self, inline_marks: &[DisplayMark], inline_marks_width: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { format_repeat_char(' ', inline_marks_width - inline_marks.len(), f)?; for mark in inline_marks { self.get_annotation_style(&mark.annotation_type).paint_fn( Box::new(|f| { f.write_char(match mark.mark_type { DisplayMarkType::AnnotationThrough => '|', DisplayMarkType::AnnotationStart => '/', }) }), f, )?; } Ok(()) } }