use std::cmp::{max, min}; use std::fmt; use crate::formatter::{get_term_style, style::Stylesheet}; /// List of lines to be displayed. pub struct DisplayList<'a> { pub body: Vec>, pub stylesheet: Box, pub anonymized_line_numbers: bool, pub margin: Option, } impl<'a> From>> for DisplayList<'a> { fn from(body: Vec>) -> DisplayList<'a> { Self { body, anonymized_line_numbers: false, stylesheet: get_term_style(false), margin: None, } } } impl<'a> PartialEq for DisplayList<'a> { fn eq(&self, other: &Self) -> bool { self.body == other.body && self.anonymized_line_numbers == other.anonymized_line_numbers } } impl<'a> fmt::Debug for DisplayList<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DisplayList") .field("body", &self.body) .field("anonymized_line_numbers", &self.anonymized_line_numbers) .finish() } } #[derive(Debug, Default, Copy, Clone)] pub struct FormatOptions { pub color: bool, pub anonymized_line_numbers: bool, pub margin: Option, } #[derive(Clone, Copy, Debug)] pub struct Margin { /// The available whitespace in the left that can be consumed when centering. whitespace_left: usize, /// The column of the beginning of left-most span. span_left: usize, /// The column of the end of right-most span. span_right: usize, /// The beginning of the line to be displayed. computed_left: usize, /// The end of the line to be displayed. computed_right: usize, /// The current width of the terminal. 140 by default and in tests. column_width: usize, /// The end column of a span label, including the span. Doesn't account for labels not in the /// same line as the span. label_right: usize, } impl Margin { pub fn new( whitespace_left: usize, span_left: usize, span_right: usize, label_right: usize, column_width: usize, max_line_len: usize, ) -> Self { // The 6 is padding to give a bit of room for `...` when displaying: // ``` // error: message // --> file.rs:16:58 // | // 16 | ... fn foo(self) -> Self::Bar { // | ^^^^^^^^^ // ``` let mut m = Margin { whitespace_left: whitespace_left.saturating_sub(6), span_left: span_left.saturating_sub(6), span_right: span_right + 6, computed_left: 0, computed_right: 0, column_width, label_right: label_right + 6, }; m.compute(max_line_len); m } pub(crate) fn was_cut_left(&self) -> bool { self.computed_left > 0 } pub(crate) fn was_cut_right(&self, line_len: usize) -> bool { let right = if self.computed_right == self.span_right || self.computed_right == self.label_right { // Account for the "..." padding given above. Otherwise we end up with code lines that // do fit but end in "..." as if they were trimmed. self.computed_right - 6 } else { self.computed_right }; right < line_len && self.computed_left + self.column_width < line_len } fn compute(&mut self, max_line_len: usize) { // When there's a lot of whitespace (>20), we want to trim it as it is useless. self.computed_left = if self.whitespace_left > 20 { self.whitespace_left - 16 // We want some padding. } else { 0 }; // We want to show as much as possible, max_line_len is the right-most boundary for the // relevant code. self.computed_right = max(max_line_len, self.computed_left); if self.computed_right - self.computed_left > self.column_width { // Trimming only whitespace isn't enough, let's get craftier. if self.label_right - self.whitespace_left <= self.column_width { // Attempt to fit the code window only trimming whitespace. self.computed_left = self.whitespace_left; self.computed_right = self.computed_left + self.column_width; } else if self.label_right - self.span_left <= self.column_width { // Attempt to fit the code window considering only the spans and labels. let padding_left = (self.column_width - (self.label_right - self.span_left)) / 2; self.computed_left = self.span_left.saturating_sub(padding_left); self.computed_right = self.computed_left + self.column_width; } else if self.span_right - self.span_left <= self.column_width { // Attempt to fit the code window considering the spans and labels plus padding. let padding_left = (self.column_width - (self.span_right - self.span_left)) / 5 * 2; self.computed_left = self.span_left.saturating_sub(padding_left); self.computed_right = self.computed_left + self.column_width; } else { // Mostly give up but still don't show the full line. self.computed_left = self.span_left; self.computed_right = self.span_right; } } } pub(crate) fn left(&self, line_len: usize) -> usize { min(self.computed_left, line_len) } pub(crate) fn right(&self, line_len: usize) -> usize { if line_len.saturating_sub(self.computed_left) <= self.column_width { line_len } else { min(line_len, self.computed_right) } } } /// Inline annotation which can be used in either Raw or Source line. #[derive(Debug, PartialEq)] pub struct Annotation<'a> { pub annotation_type: DisplayAnnotationType, pub id: Option<&'a str>, pub label: Vec>, } /// A single line used in `DisplayList`. #[derive(Debug, PartialEq)] pub enum DisplayLine<'a> { /// A line with `lineno` portion of the slice. Source { lineno: Option, inline_marks: Vec, line: DisplaySourceLine<'a>, }, /// A line indicating a folded part of the slice. Fold { inline_marks: Vec }, /// A line which is displayed outside of slices. Raw(DisplayRawLine<'a>), } /// A source line. #[derive(Debug, PartialEq)] pub enum DisplaySourceLine<'a> { /// A line with the content of the Slice. Content { text: &'a str, range: (usize, usize), // meta information for annotation placement. }, /// An annotation line which is displayed in context of the slice. Annotation { annotation: Annotation<'a>, range: (usize, usize), annotation_type: DisplayAnnotationType, annotation_part: DisplayAnnotationPart, }, /// An empty source line. Empty, } /// Raw line - a line which does not have the `lineno` part and is not considered /// a part of the snippet. #[derive(Debug, PartialEq)] pub enum DisplayRawLine<'a> { /// A line which provides information about the location of the given /// slice in the project structure. Origin { path: &'a str, pos: Option<(usize, usize)>, header_type: DisplayHeaderType, }, /// An annotation line which is not part of any snippet. Annotation { annotation: Annotation<'a>, /// If set to `true`, the annotation will be aligned to the /// lineno delimiter of the snippet. source_aligned: bool, /// If set to `true`, only the label of the `Annotation` will be /// displayed. It allows for a multiline annotation to be aligned /// without displaing the meta information (`type` and `id`) to be /// displayed on each line. continuation: bool, }, } /// An inline text fragment which any label is composed of. #[derive(Debug, PartialEq)] pub struct DisplayTextFragment<'a> { pub content: &'a str, pub style: DisplayTextStyle, } /// A style for the `DisplayTextFragment` which can be visually formatted. /// /// This information may be used to emphasis parts of the label. #[derive(Debug, Clone, Copy, PartialEq)] pub enum DisplayTextStyle { Regular, Emphasis, } /// An indicator of what part of the annotation a given `Annotation` is. #[derive(Debug, Clone, PartialEq)] pub enum DisplayAnnotationPart { /// A standalone, single-line annotation. Standalone, /// A continuation of a multi-line label of an annotation. LabelContinuation, /// A consequitive annotation in case multiple annotations annotate a single line. Consequitive, /// A line starting a multiline annotation. MultilineStart, /// A line ending a multiline annotation. MultilineEnd, } /// A visual mark used in `inline_marks` field of the `DisplaySourceLine`. #[derive(Debug, Clone, PartialEq)] pub struct DisplayMark { pub mark_type: DisplayMarkType, pub annotation_type: DisplayAnnotationType, } /// A type of the `DisplayMark`. #[derive(Debug, Clone, PartialEq)] pub enum DisplayMarkType { /// A mark indicating a multiline annotation going through the current line. AnnotationThrough, /// A mark indicating a multiline annotation starting on the given line. AnnotationStart, } /// A type of the `Annotation` which may impact the sigils, style or text displayed. /// /// There are several ways to uses this information when formatting the `DisplayList`: /// /// * An annotation may display the name of the type like `error` or `info`. /// * An underline for `Error` may be `^^^` while for `Warning` it coule be `---`. /// * `ColorStylesheet` may use different colors for different annotations. #[derive(Debug, Clone, PartialEq)] pub enum DisplayAnnotationType { None, Error, Warning, Info, Note, Help, } /// Information whether the header is the initial one or a consequitive one /// for multi-slice cases. // TODO: private #[derive(Debug, Clone, PartialEq)] pub enum DisplayHeaderType { /// Initial header is the first header in the snippet. Initial, /// Continuation marks all headers of following slices in the snippet. Continuation, }