| 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<DisplayLine<'a>>, |
| pub stylesheet: Box<dyn Stylesheet>, |
| pub anonymized_line_numbers: bool, |
| pub margin: Option<Margin>, |
| } |
| |
| impl<'a> From<Vec<DisplayLine<'a>>> for DisplayList<'a> { |
| fn from(body: Vec<DisplayLine<'a>>) -> 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<Margin>, |
| } |
| |
| #[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<DisplayTextFragment<'a>>, |
| } |
| |
| /// A single line used in `DisplayList`. |
| #[derive(Debug, PartialEq)] |
| pub enum DisplayLine<'a> { |
| /// A line with `lineno` portion of the slice. |
| Source { |
| lineno: Option<usize>, |
| inline_marks: Vec<DisplayMark>, |
| line: DisplaySourceLine<'a>, |
| }, |
| |
| /// A line indicating a folded part of the slice. |
| Fold { inline_marks: Vec<DisplayMark> }, |
| |
| /// 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, |
| } |