diff --git a/Cargo.toml b/Cargo.toml index 28a773b..55a45f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ coveralls = { repository = "rust-lang/annotate-snippets-rs", branch = "master", maintenance = { status = "actively-developed" } [dependencies] +unicode-width = "0.1" yansi-term = { version = "0.1", optional = true } [dev-dependencies] diff --git a/README.md b/README.md index 592a51e..f80d659 100644 --- a/README.md +++ b/README.md @@ -35,49 +35,47 @@ Usage ```rust use annotate_snippets::{ - display_list::DisplayList, - formatter::DisplayListFormatter, + display_list::{DisplayList, FormatOptions}, snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, }; fn main() { let snippet = Snippet { title: Some(Annotation { - label: Some("expected type, found `22`".to_string()), + label: Some("expected type, found `22`"), id: None, annotation_type: AnnotationType::Error, }), footer: vec![], - slices: vec![ - Slice { - source: r#" -This is an example -content of the slice -which will be annotated -with the list of annotations below. - "#.to_string(), - line_start: 26, - origin: Some("examples/example.txt".to_string()), - fold: false, - annotations: vec![ - SourceAnnotation { - label: "Example error annotation".to_string(), - annotation_type: AnnotationType::Error, - range: (13, 18), - }, - SourceAnnotation { - label: "and here's a warning".to_string(), - annotation_type: AnnotationType::Warning, - range: (34, 50), - }, - ], - }, - ], + slices: vec![Slice { + source: r#" annotations: vec![SourceAnnotation { + label: "expected struct `annotate_snippets::snippet::Slice`, found reference" + , + range: <22, 25>,"#, + line_start: 26, + origin: Some("examples/footer.rs"), + fold: true, + annotations: vec![ + SourceAnnotation { + label: "", + annotation_type: AnnotationType::Error, + range: (205, 207), + }, + SourceAnnotation { + label: "while parsing this struct", + annotation_type: AnnotationType::Info, + range: (34, 50), + }, + ], + }], + opt: FormatOptions { + color: true, + ..Default::default() + }, }; let dl = DisplayList::from(snippet); - let dlf = DisplayListFormatter::new(true, false); - println!("{}", dlf.format(&dl)); + println!("{}", dl); } ``` diff --git a/src/display_list/from_snippet.rs b/src/display_list/from_snippet.rs index da0deea..d32d1e0 100644 --- a/src/display_list/from_snippet.rs +++ b/src/display_list/from_snippet.rs @@ -107,12 +107,13 @@ fn format_slice( slice: snippet::Slice<'_>, is_first: bool, has_footer: bool, + margin: Option, ) -> Vec> { let main_range = slice.annotations.get(0).map(|x| x.range.0); let origin = slice.origin; let line_start = slice.line_start; let need_empty_header = origin.is_some() || is_first; - let mut body = format_body(slice, need_empty_header, has_footer); + let mut body = format_body(slice, need_empty_header, has_footer, margin); let header = format_header(origin, main_range, line_start, &body, is_first); let mut result = vec![]; @@ -273,6 +274,7 @@ fn format_body( slice: snippet::Slice<'_>, need_empty_header: bool, has_footer: bool, + margin: Option, ) -> Vec> { let source_len = slice.source.chars().count(); if let Some(bigger) = slice.annotations.iter().find_map(|x| { @@ -312,6 +314,9 @@ fn format_body( let mut annotation_line_count = 0; let mut annotations = slice.annotations; for (idx, (line_start, line_end)) in line_index_ranges.into_iter().enumerate() { + let margin_left = margin + .map(|m| m.left(line_end - line_start)) + .unwrap_or_default(); // It would be nice to use filter_drain here once it's stable. annotations = annotations .into_iter() @@ -328,7 +333,10 @@ fn format_body( if start >= line_start && end <= line_end || start == line_end && end - start <= 1 => { - let range = (start - line_start, end - line_start); + let range = ( + (start - line_start) - margin_left, + (end - line_start) - margin_left, + ); body.insert( body_idx + 1, DisplayLine::Source { @@ -419,7 +427,10 @@ fn format_body( }); } - let range = (end - line_start, end - line_start + 1); + let range = ( + (end - line_start) - margin_left, + (end - line_start + 1) - margin_left, + ); body.insert( body_idx + 1, DisplayLine::Source { @@ -499,7 +510,12 @@ impl<'a> From> for DisplayList<'a> { } for (idx, slice) in slices.into_iter().enumerate() { - body.append(&mut format_slice(slice, idx == 0, !footer.is_empty())); + body.append(&mut format_slice( + slice, + idx == 0, + !footer.is_empty(), + opt.margin, + )); } for annotation in footer { @@ -509,12 +525,14 @@ impl<'a> From> for DisplayList<'a> { let FormatOptions { color, anonymized_line_numbers, + margin, } = opt; Self { body, stylesheet: get_term_style(color), anonymized_line_numbers, + margin, } } } diff --git a/src/display_list/structs.rs b/src/display_list/structs.rs index 8f6d8fc..7941d5f 100644 --- a/src/display_list/structs.rs +++ b/src/display_list/structs.rs @@ -1,3 +1,4 @@ +use std::cmp::{max, min}; use std::fmt; use crate::formatter::{get_term_style, style::Stylesheet}; @@ -7,6 +8,7 @@ pub struct DisplayList<'a> { pub body: Vec>, pub stylesheet: Box, pub anonymized_line_numbers: bool, + pub margin: Option, } impl<'a> From>> for DisplayList<'a> { @@ -15,6 +17,7 @@ impl<'a> From>> for DisplayList<'a> { body, anonymized_line_numbers: false, stylesheet: get_term_style(false), + margin: None, } } } @@ -38,6 +41,121 @@ impl<'a> fmt::Debug for DisplayList<'a> { 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. @@ -162,8 +280,7 @@ pub enum DisplayMarkType { /// A type of the `Annotation` which may impact the sigils, style or text displayed. /// -/// There are several ways in which the `DisplayListFormatter` uses this information -/// when formatting the `DisplayList`: +/// 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 `---`. diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs index e93aeb3..ed31b86 100644 --- a/src/formatter/mod.rs +++ b/src/formatter/mod.rs @@ -198,7 +198,56 @@ impl<'a> DisplayList<'a> { DisplaySourceLine::Empty => Ok(()), DisplaySourceLine::Content { text, .. } => { f.write_char(' ')?; - text.fmt(f) + 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 + }; + let range = text + .char_indices() + .skip(left) + .take_while(|(_, ch)| { + // 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 { + return false; + } + true + }) + .fold((None, 0), |acc, (i, _)| { + if acc.0.is_some() { + (acc.0, i) + } else { + (Some(i), i) + } + }); + + 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, diff --git a/src/formatter/style.rs b/src/formatter/style.rs index f76e6b0..3fc01c1 100644 --- a/src/formatter/style.rs +++ b/src/formatter/style.rs @@ -39,8 +39,7 @@ pub trait Style { c: Box) -> fmt::Result + 'a>, f: &mut fmt::Formatter<'_>, ) -> fmt::Result; - /// The method used by the DisplayListFormatter to display the message - /// in bold font. + /// The method used by the `Formatter` to display the message in bold font. fn bold(&self) -> Box; } diff --git a/src/lib.rs b/src/lib.rs index 46b25e1..d581367 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,8 +38,7 @@ //! of lines containing semantic information about each line. //! This structure is the easiest to manipulate and organize. //! -//! Finally, [DisplayListFormatter](self::formatter::DisplayListFormatter) is -//! used to format the `DisplayList` using a `Stylesheet` into a final `String` output. +//! Finally, `impl Display` into a final `String` output. //! //! A user of the crate may choose to provide their own equivalent of the input //! structure with an `Into` trait. @@ -47,6 +46,7 @@ //! A user of the crate may also choose to provide their own formatter logic, //! to convert a `DisplayList` into a `String`, or just a `Stylesheet` to //! use the crate's formatting logic, but with a custom stylesheet. +// TODO: check documentation pub mod display_list; pub mod formatter; diff --git a/src/stylesheets/mod.rs b/src/stylesheets/mod.rs index 49f6ea0..4648852 100644 --- a/src/stylesheets/mod.rs +++ b/src/stylesheets/mod.rs @@ -1,4 +1,4 @@ -//! List of stylesheets that can be used by the `DisplayListFormatter`. +//! List of stylesheets //! //! The list depends on what optional dependencies the crate has been //! compiled with. diff --git a/tests/dl_from_snippet.rs b/tests/dl_from_snippet.rs index 2ed9902..0dcfcfa 100644 --- a/tests/dl_from_snippet.rs +++ b/tests/dl_from_snippet.rs @@ -28,6 +28,7 @@ fn test_format_title() { })], stylesheet: get_term_style(input.opt.color), anonymized_line_numbers: input.opt.anonymized_line_numbers, + margin: None, }; assert_eq!(dl::DisplayList::from(input), output); } @@ -80,6 +81,7 @@ fn test_format_slice() { ], stylesheet: get_term_style(input.opt.color), anonymized_line_numbers: input.opt.anonymized_line_numbers, + margin: None, }; assert_eq!(dl::DisplayList::from(input), output); } @@ -162,6 +164,7 @@ fn test_format_slices_continuation() { ], stylesheet: get_term_style(input.opt.color), anonymized_line_numbers: input.opt.anonymized_line_numbers, + margin: None, }; assert_eq!(dl::DisplayList::from(input), output); } @@ -237,6 +240,7 @@ fn test_format_slice_annotation_standalone() { ], stylesheet: get_term_style(input.opt.color), anonymized_line_numbers: input.opt.anonymized_line_numbers, + margin: None, }; assert_eq!(dl::DisplayList::from(input), output); } @@ -278,6 +282,7 @@ fn test_format_label() { })], stylesheet: get_term_style(input.opt.color), anonymized_line_numbers: input.opt.anonymized_line_numbers, + margin: None, }; assert_eq!(dl::DisplayList::from(input), output); } @@ -395,6 +400,7 @@ fn test_i_29() { ], stylesheet: get_term_style(false), anonymized_line_numbers: false, + margin: None, }; assert_eq!(DisplayList::from(snippets), expected); diff --git a/tests/fixtures/no-color/strip_line.toml b/tests/fixtures/no-color/strip_line.toml new file mode 100644 index 0000000..76d9519 --- /dev/null +++ b/tests/fixtures/no-color/strip_line.toml @@ -0,0 +1,25 @@ +[title] +id = "E0308" +label = "mismatched types" +annotation_type = "Error" + +[[slices]] +source = " let _: () = 42;" +line_start = 4 +origin = "$DIR/whitespace-trimming.rs" + +[[slices.annotations]] +label = "expected (), found integer" +annotation_type = "Error" +range = [192, 194] + +[opt] +color = false +anonymized_line_numbers = true +[opt.margin] +whitespace_left = 180 +span_left = 192 +span_right = 194 +label_right = 221 +column_width = 140 +max_line_len = 195 diff --git a/tests/fixtures/no-color/strip_line.txt b/tests/fixtures/no-color/strip_line.txt new file mode 100644 index 0000000..65b0538 --- /dev/null +++ b/tests/fixtures/no-color/strip_line.txt @@ -0,0 +1,6 @@ +error[E0308]: mismatched types + --> $DIR/whitespace-trimming.rs:4:193 + | +LL | ... let _: () = 42; + | ^^ expected (), found integer + | diff --git a/tests/fixtures/no-color/strip_line_non_ws.toml b/tests/fixtures/no-color/strip_line_non_ws.toml new file mode 100644 index 0000000..5129f5c --- /dev/null +++ b/tests/fixtures/no-color/strip_line_non_ws.toml @@ -0,0 +1,25 @@ +[title] +id = "E0308" +label = "mismatched types" +annotation_type = "Error" + +[[slices]] +source = " let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = 42; let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = ();" +line_start = 4 +origin = "$DIR/non-whitespace-trimming.rs" + +[[slices.annotations]] +label = "expected (), found integer" +annotation_type = "Error" +range = [240, 242] + +[opt] +color = false +anonymized_line_numbers = true +[opt.margin] +whitespace_left = 4 +span_left = 240 +span_right = 242 +label_right = 271 +column_width = 140 +max_line_len = 371 diff --git a/tests/fixtures/no-color/strip_line_non_ws.txt b/tests/fixtures/no-color/strip_line_non_ws.txt new file mode 100644 index 0000000..850619a --- /dev/null +++ b/tests/fixtures/no-color/strip_line_non_ws.txt @@ -0,0 +1,6 @@ +error[E0308]: mismatched types + --> $DIR/non-whitespace-trimming.rs:4:241 + | +LL | ... = (); let _: () = (); let _: () = (); let _: () = 42; let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () = ();... + | ^^ expected (), found integer + | diff --git a/tests/snippet/mod.rs b/tests/snippet/mod.rs index cc747a4..247c26d 100644 --- a/tests/snippet/mod.rs +++ b/tests/snippet/mod.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Deserializer, Serialize}; use annotate_snippets::{ - display_list::FormatOptions, + display_list::{FormatOptions, Margin}, snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, }; @@ -57,6 +57,45 @@ pub struct FormatOptionsDef { pub color: bool, #[serde(default)] pub anonymized_line_numbers: bool, + #[serde(deserialize_with = "deserialize_margin")] + #[serde(default)] + pub margin: Option, +} + +fn deserialize_margin<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + struct Wrapper { + whitespace_left: usize, + span_left: usize, + span_right: usize, + label_right: usize, + column_width: usize, + max_line_len: usize, + }; + + Option::::deserialize(deserializer).map(|opt_wrapped: Option| { + opt_wrapped.map(|wrapped: Wrapper| { + let Wrapper { + whitespace_left, + span_left, + span_right, + label_right, + column_width, + max_line_len, + } = wrapped; + Margin::new( + whitespace_left, + span_left, + span_right, + label_right, + column_width, + max_line_len, + ) + }) + }) } fn deserialize_slices<'de, D>(deserializer: D) -> Result>, D::Error>