From 5d72f82b24c990d92aab6a2d887308fd22eab757 Mon Sep 17 00:00:00 2001 From: l3ops Date: Wed, 30 Mar 2022 14:37:44 +0200 Subject: [PATCH 1/2] feat(rome_console): add a Codespan utility struct to display annotated code fragments --- Cargo.lock | 1 + crates/rome_console/Cargo.toml | 1 + crates/rome_console/src/codespan/mod.rs | 592 +++++++++++ crates/rome_console/src/codespan/render.rs | 1093 ++++++++++++++++++++ crates/rome_console/src/fmt.rs | 8 + crates/rome_console/src/lib.rs | 1 + crates/rome_console/src/markup.rs | 7 + 7 files changed, 1703 insertions(+) create mode 100644 crates/rome_console/src/codespan/mod.rs create mode 100644 crates/rome_console/src/codespan/render.rs diff --git a/Cargo.lock b/Cargo.lock index 2000038cef1..6df3dd50a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1379,6 +1379,7 @@ dependencies = [ "similar", "termcolor", "trybuild", + "unicode-width", ] [[package]] diff --git a/crates/rome_console/Cargo.toml b/crates/rome_console/Cargo.toml index 96cc8ccc8b8..251ba600190 100644 --- a/crates/rome_console/Cargo.toml +++ b/crates/rome_console/Cargo.toml @@ -11,6 +11,7 @@ rome_markup = { path = "../rome_markup" } lazy_static = "1.4.0" termcolor = "1.1.2" similar = "2.1.0" +unicode-width = "0.1.9" [dev-dependencies] trybuild = "1.0" diff --git a/crates/rome_console/src/codespan/mod.rs b/crates/rome_console/src/codespan/mod.rs new file mode 100644 index 00000000000..febe9fc7b4c --- /dev/null +++ b/crates/rome_console/src/codespan/mod.rs @@ -0,0 +1,592 @@ +///! This module if a fork of https://github.com/brendanzab/codespan, +/// adapted to use the `rome_console` markup for formatting +use std::collections::BTreeMap; +use std::io; +use std::ops::Range; + +use crate::fmt::{Display, Formatter}; +use crate::Markup; + +use self::render::{MultiLabel, Renderer, SingleLabel}; + +mod render; + +const START_CONTEXT_LINES: usize = 3; +const END_CONTEXT_LINES: usize = 1; + +/// A label describing an underlined region of code associated with a diagnostic. +#[derive(Clone)] +pub struct Label<'diagnostic> { + /// The style of the label. + pub style: LabelStyle, + /// The range in bytes we are going to include in the final snippet. + pub range: Range, + /// An optional message to provide some additional information for the + /// underlined code. These should not include line breaks. + pub message: Markup<'diagnostic>, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd)] +pub enum LabelStyle { + /// Labels that describe the primary cause of a diagnostic. + Primary, + /// Labels that provide additional context for a diagnostic. + Secondary, +} + +/// A severity level for diagnostic messages. +/// +/// These are ordered in the following way: +#[derive(Copy, Clone, PartialEq, Hash, Debug)] +pub enum Severity { + /// An unexpected bug. + Bug, + /// An error. + Error, + /// A warning. + Warning, + /// A note. + Note, + /// A help message. + Help, +} + +/// The 'location focus' of a source code snippet. +pub enum Locus { + File { + /// The user-facing name of the file. + name: String, + }, + FileLocation { + /// The user-facing name of the file. + name: String, + /// The location. + location: Location, + }, +} + +/// A user-facing location in a source file. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Location { + /// The user-facing line number. + pub line_number: usize, + /// The user-facing column number. + pub column_number: usize, +} + +pub struct Codespan<'diagnostic> { + severity: Severity, + locus: Option, + source_file: &'diagnostic SourceFile<'diagnostic>, + labeled_file: Option>, + outer_padding: usize, +} + +impl<'diagnostic> Codespan<'diagnostic> { + /// Create a new codespan from a slice of source text, an overall severity + /// level and an optional "locus" to be displayed at the top + pub fn new( + source_file: &'diagnostic SourceFile<'diagnostic>, + severity: Severity, + locus: Option, + ) -> Self { + Self { + severity, + locus, + source_file, + labeled_file: None, + outer_padding: 0, + } + } + + /// Insert a new label into this codespan + pub fn add_label(&mut self, label: Label<'diagnostic>) -> Result<(), OverflowError> { + let start_line_index = self.source_file.line_index(label.range.start); + let start_line_number = self.source_file.line_number(start_line_index); + + let start_line_range = self.source_file.line_range(start_line_index)?; + let end_line_index = self.source_file.line_index(label.range.end); + let end_line_number = self.source_file.line_number(end_line_index); + + let end_line_range = self.source_file.line_range(end_line_index)?; + + self.outer_padding = std::cmp::max(self.outer_padding, count_digits(start_line_number)); + self.outer_padding = std::cmp::max(self.outer_padding, count_digits(end_line_number)); + + let labeled_file = match &mut self.labeled_file { + Some(labeled_file) => { + // other labezls already exist in this codespan + if labeled_file.max_label_style > label.style + || (labeled_file.max_label_style == label.style + && labeled_file.start > label.range.start) + { + // this label has a higher style or has the same style but starts earlier + labeled_file.start = label.range.start; + labeled_file.location = self.source_file.location(label.range.start)?; + labeled_file.max_label_style = label.style; + } + labeled_file + } + None => { + // this is the first label inserted into this codespan + self.labeled_file.get_or_insert(LabeledFile { + start: label.range.start, + location: self.source_file.location(label.range.start)?, + num_multi_labels: 0, + lines: BTreeMap::new(), + max_label_style: label.style, + }) + } + }; + + if start_line_index == end_line_index { + // Single line + // + // ```text + // 2 │ (+ test "") + // │ ^^ expected `Int` but found `String` + // ``` + let label_start = label.range.start - start_line_range.start; + // Ensure that we print at least one caret, even when we + // have a zero-length source range. + let label_end = usize::max(label.range.end - start_line_range.start, label_start + 1); + + let line = labeled_file.get_or_insert_line( + start_line_index, + start_line_range, + start_line_number, + ); + + // Ensure that the single line labels are lexicographically + // sorted by the range of source code that they cover. + let index = match line.single_labels.binary_search_by(|(_, range, _)| { + // `Range` doesn't implement `Ord`, so convert to `(usize, usize)` + // to piggyback off its lexicographic comparison implementation. + (range.start, range.end).cmp(&(label_start, label_end)) + }) { + // If the ranges are the same, order the labels in reverse + // to how they were originally specified in the diagnostic. + // This helps with printing in the renderer. + Ok(index) | Err(index) => index, + }; + + line.single_labels + .insert(index, (label.style, label_start..label_end, label.message)); + + // If this line is not rendered, the SingleLabel is not visible. + line.must_render = true; + } else { + // Multiple lines + // + // ```text + // 4 │ fizz₁ num = case (mod num 5) (mod num 3) of + // │ ╭─────────────^ + // 5 │ │ 0 0 => "FizzBuzz" + // 6 │ │ 0 _ => "Fizz" + // 7 │ │ _ 0 => "Buzz" + // 8 │ │ _ _ => num + // │ ╰──────────────^ `case` clauses have incompatible types + // ``` + + let label_index = labeled_file.num_multi_labels; + labeled_file.num_multi_labels += 1; + + // First labeled line + let label_start = label.range.start - start_line_range.start; + + let start_line = labeled_file.get_or_insert_line( + start_line_index, + start_line_range, + start_line_number, + ); + + start_line + .multi_labels + .push((label_index, label.style, MultiLabel::Top(label_start))); + + // The first line has to be rendered so the start of the label is visible. + start_line.must_render = true; + + // Marked lines + // + // ```text + // 5 │ │ 0 0 => "FizzBuzz" + // 6 │ │ 0 _ => "Fizz" + // 7 │ │ _ 0 => "Buzz" + // ``` + for line_index in (start_line_index + 1)..end_line_index { + let line_range = self.source_file.line_range(line_index)?; + let line_number = self.source_file.line_number(line_index); + + self.outer_padding = std::cmp::max(self.outer_padding, count_digits(line_number)); + + let line = labeled_file.get_or_insert_line(line_index, line_range, line_number); + + line.multi_labels + .push((label_index, label.style, MultiLabel::Left)); + + // The line should be rendered to match the configuration of how much context to show. + line.must_render |= + // Is this line part of the context after the start of the label? + line_index - start_line_index <= START_CONTEXT_LINES + || + // Is this line part of the context before the end of the label? + end_line_index - line_index <= END_CONTEXT_LINES; + } + + // Last labeled line + // + // ```text + // 8 │ │ _ _ => num + // │ ╰──────────────^ `case` clauses have incompatible types + // ``` + let label_end = label.range.end - end_line_range.start; + + let end_line = + labeled_file.get_or_insert_line(end_line_index, end_line_range, end_line_number); + + end_line.multi_labels.push(( + label_index, + label.style, + MultiLabel::Bottom(label_end, label.message), + )); + + // The last line has to be rendered so the end of the label is visible. + end_line.must_render = true; + } + + Ok(()) + } +} + +impl<'diagnostic> Display for Codespan<'diagnostic> { + fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { + let mut renderer = Renderer::new(&mut *fmt); + let file = match &self.labeled_file { + Some(file) => file, + None => return Ok(()), + }; + + // Top left border and locus. + // + // ```text + // ┌─ test:2:9 + // ``` + if !file.lines.is_empty() { + if let Some(locus) = &self.locus { + renderer.render_snippet_start(self.outer_padding, locus)?; + } + + renderer.render_snippet_empty( + self.outer_padding, + self.severity, + file.num_multi_labels, + &[], + )?; + } + + let mut lines = file + .lines + .iter() + .filter(|(_, line)| line.must_render) + .peekable(); + + while let Some((line_index, line)) = lines.next() { + renderer.render_snippet_source( + self.outer_padding, + line.number, + line.range.clone(), + self.source_file.source, + self.severity, + &line.single_labels, + file.num_multi_labels, + &line.multi_labels, + )?; + + // Check to see if we need to render any intermediate stuff + // before rendering the next line. + if let Some((next_line_index, _)) = lines.peek() { + match next_line_index.checked_sub(*line_index) { + // Consecutive lines + Some(1) => {} + // One line between the current line and the next line + Some(2) => { + // This line was not intended to be rendered initially. + // To render the line right, we have to get back the original labels. + let labels = file + .lines + .get(&(line_index + 1)) + .map_or(&[][..], |line| &line.multi_labels[..]); + + let line_number = self.source_file.line_number(line_index + 1); + let line_range = self + .source_file + .line_range(line_index + 1) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "overflow error"))?; + + renderer.render_snippet_source( + self.outer_padding, + line_number, + line_range.clone(), + self.source_file.source, + self.severity, + &[], + file.num_multi_labels, + labels, + )?; + } + // More than one line between the current line and the next line. + Some(_) | None => { + // Source break + // + // ```text + // · + // ``` + renderer.render_snippet_break( + self.outer_padding, + self.severity, + file.num_multi_labels, + &line.multi_labels, + )?; + } + } + } + } + + Ok(()) + } +} + +/// Error type returned when a label is inserted with a range that falls +/// outside of the source file +#[derive(Debug)] +pub struct OverflowError; + +/// Representation of a single source file holding additional information for +/// efficiently rendering [Codespan] +pub struct SourceFile<'diagnostic> { + /// The source code of the file. + source: &'diagnostic str, + /// The starting byte indices in the source code. + line_starts: Vec, +} + +impl<'diagnostic> SourceFile<'diagnostic> { + /// Create a new [SourceFile] from a slice of text + pub fn new(source: &'diagnostic str) -> Self { + Self { + source, + line_starts: line_starts(source).collect(), + } + } + + /// Return the starting byte index of the line with the specified line index. + /// Convenience method that already generates errors if necessary. + fn line_start(&self, line_index: usize) -> Result { + use std::cmp::Ordering; + + match line_index.cmp(&self.line_starts.len()) { + Ordering::Less => Ok(self + .line_starts + .get(line_index) + .cloned() + .expect("failed despite previous check")), + Ordering::Equal => Ok(self.source.len()), + Ordering::Greater => Err(OverflowError), + } + } + + fn line_index(&self, byte_index: usize) -> usize { + self.line_starts + .binary_search(&byte_index) + .unwrap_or_else(|next_line| next_line - 1) + } + + fn line_range(&self, line_index: usize) -> Result, OverflowError> { + let line_start = self.line_start(line_index)?; + let next_line_start = self.line_start(line_index + 1)?; + + Ok(line_start..next_line_start) + } + + fn line_number(&self, line_index: usize) -> usize { + line_index + 1 + } + + fn column_number(&self, line_index: usize, byte_index: usize) -> Result { + let source = self.source; + let line_range = self.line_range(line_index)?; + let column_index = column_index(source, line_range, byte_index); + + Ok(column_index + 1) + } + + fn location(&self, byte_index: usize) -> Result { + let line_index = self.line_index(byte_index); + + Ok(Location { + line_number: self.line_number(line_index), + column_number: self.column_number(line_index, byte_index)?, + }) + } +} + +/// Return the starting byte index of each line in the source string. +/// +/// This can make it easier to implement [`Files::line_index`] by allowing +/// implementors of [`Files`] to pre-compute the line starts, then search for +/// the corresponding line range, as shown in the example below. +/// +/// [`Files`]: Files +/// [`Files::line_index`]: Files::line_index +fn line_starts(source: &'_ str) -> impl '_ + Iterator { + std::iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1)) +} + +/// The column index at the given byte index in the source file. +/// This is the number of characters to the given byte index. +/// +/// If the byte index is smaller than the start of the line, then `0` is returned. +/// If the byte index is past the end of the line, the column index of the last +/// character `+ 1` is returned. +fn column_index(source: &str, line_range: Range, byte_index: usize) -> usize { + let end_index = std::cmp::min(byte_index, std::cmp::min(line_range.end, source.len())); + + (line_range.start..end_index) + .filter(|byte_index| source.is_char_boundary(byte_index + 1)) + .count() +} + +/// Count the number of decimal digits in `n`. +fn count_digits(mut n: usize) -> usize { + let mut count = 0; + while n != 0 { + count += 1; + n /= 10; // remove last digit + } + count +} + +struct LabeledFile<'diagnostic> { + start: usize, + location: Location, + num_multi_labels: usize, + lines: BTreeMap>, + max_label_style: LabelStyle, +} + +impl<'diagnostic> LabeledFile<'diagnostic> { + fn get_or_insert_line( + &mut self, + line_index: usize, + line_range: Range, + line_number: usize, + ) -> &mut Line<'diagnostic> { + self.lines.entry(line_index).or_insert_with(|| Line { + range: line_range, + number: line_number, + single_labels: vec![], + multi_labels: vec![], + // This has to be false by default so we know if it must be rendered by another condition already. + must_render: false, + }) + } +} + +struct Line<'diagnostic> { + number: usize, + range: Range, + // TODO: How do we reuse these allocations? + single_labels: Vec>, + multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>, + must_render: bool, +} + +#[cfg(test)] +mod tests { + use crate::codespan::SourceFile; + use crate::{self as rome_console, BufferConsole, Console, Message}; + use crate::{ + codespan::{Codespan, Label, LabelStyle, Location, Locus, Severity}, + markup, + }; + + #[test] + fn test_codespan() { + const SOURCE: &str = "Lorem ipsum dolor sit amet, +consectetur adipiscing elit, +sed do eiusmod tempor incididunt ut +labore et dolore magna aliqua"; + + const DIAGNOSTIC: &str = " ┌─ file_name:2:12 + │ +2 │ consectetur adipiscing elit, + │ ^^^^^^^^^^^^^^^ Important message +3 │ sed do eiusmod tempor incididunt ut + │ ╭──────────────' +4 │ │ labore et dolore magna aliqua + │ │ --------- Secondary message + │ ╰──────' Multiline message +"; + + let source = SourceFile::new(SOURCE); + + let mut codespan = Codespan::new( + &source, + Severity::Error, + Some(Locus::FileLocation { + name: String::from("file_name"), + location: Location { + line_number: 2, + column_number: 12, + }, + }), + ); + + codespan + .add_label(Label { + style: LabelStyle::Primary, + range: 40..55, + message: markup! { + "Important"" message" + }, + }) + .unwrap(); + + codespan + .add_label(Label { + style: LabelStyle::Secondary, + range: 71..99, + message: markup! { + "Multiline message" + }, + }) + .unwrap(); + + codespan + .add_label(Label { + style: LabelStyle::Secondary, + range: 100..109, + message: markup! { + "Secondary message" + }, + }) + .unwrap(); + + let mut console = BufferConsole::default(); + console.message(markup! { + {codespan} + }); + + let mut iter = console.buffer.into_iter(); + + let message = match iter.next() { + Some(Message::Message(msg)) => msg, + other => panic!("unexpected message {other:?}"), + }; + + assert_eq!(message, DIAGNOSTIC); + + assert!(iter.next().is_none()); + } +} diff --git a/crates/rome_console/src/codespan/render.rs b/crates/rome_console/src/codespan/render.rs new file mode 100644 index 00000000000..c7c00f44520 --- /dev/null +++ b/crates/rome_console/src/codespan/render.rs @@ -0,0 +1,1093 @@ +use std::io; +use std::{io::Error, ops::Range}; + +use crate::fmt::Display; +use crate::{self as rome_console, MarkupNode}; +use crate::{ + codespan::{LabelStyle, Locus, Severity}, + fmt::Formatter, + markup, Markup, MarkupElement, +}; + +const MAX_LINE_LENGTH: usize = 250; + +const SOURCE_BORDER_TOP_LEFT: char = '┌'; +const SOURCE_BORDER_TOP: char = '─'; +const SOURCE_BORDER_LEFT: char = '│'; +const SOURCE_BORDER_LEFT_BREAK: char = '·'; + +const SINGLE_PRIMARY_CARET: char = '^'; +const SINGLE_SECONDARY_CARET: char = '-'; + +const MULTI_PRIMARY_CARET_START: char = '^'; +const MULTI_SECONDARY_CARET_START: char = '\''; +const MULTI_TOP_LEFT: char = '╭'; +const MULTI_TOP: char = '─'; +const MULTI_BOTTOM_LEFT: char = '╰'; +const MULTI_BOTTOM: char = '─'; +const MULTI_LEFT: char = '│'; + +const POINTER_LEFT: char = '│'; + +/// The style used to mark a primary or secondary label at a given severity. +fn label_element(severity: Severity, label_style: LabelStyle) -> MarkupElement { + match (label_style, severity) { + (LabelStyle::Primary, Severity::Bug) => MarkupElement::Error, + (LabelStyle::Primary, Severity::Error) => MarkupElement::Error, + (LabelStyle::Primary, Severity::Warning) => MarkupElement::Warn, + (LabelStyle::Primary, Severity::Note) => MarkupElement::Success, + (LabelStyle::Primary, Severity::Help) => MarkupElement::Info, + (LabelStyle::Secondary, _) => MarkupElement::Info, + } +} + +/// Single-line label, with an optional message. +/// +/// ```text +/// ^^^^^^^^^ blah blah +/// ``` +pub(super) type SingleLabel<'diagnostic> = (LabelStyle, Range, Markup<'diagnostic>); + +/// A multi-line label to render. +/// +/// Locations are relative to the start of where the source code is rendered. +pub(super) enum MultiLabel<'diagnostic> { + /// Multi-line label top. + /// The contained value indicates where the label starts. + /// + /// ```text + /// ╭────────────^ + /// ``` + /// + /// Can also be rendered at the beginning of the line + /// if there is only whitespace before the label starts. + /// + /// /// ```text + /// ╭ + /// ``` + Top(usize), + /// Left vertical labels for multi-line labels. + /// + /// ```text + /// │ + /// ``` + Left, + /// Multi-line label bottom, with an optional message. + /// The first value indicates where the label ends. + /// + /// ```text + /// ╰────────────^ blah blah + /// ``` + Bottom(usize, Markup<'diagnostic>), +} + +#[derive(Copy, Clone)] +enum VerticalBound { + Top, + Bottom, +} + +type Underline = (LabelStyle, VerticalBound); + +/// A renderer of display list entries. +/// +/// The following diagram gives an overview of each of the parts of the renderer's output: +/// +/// ```text +/// ┌ outer gutter +/// │ ┌ left border +/// │ │ ┌ inner gutter +/// │ │ │ ┌─────────────────────────── source ─────────────────────────────┐ +/// │ │ │ │ │ +/// ┌──────────────────────────────────────────────────────────────────────────── +/// snippet start ── │ ┌─ test:9:0 +/// snippet empty ── │ │ +/// snippet line ── │ 9 │ ╭ Cupcake ipsum dolor. Sit amet marshmallow topping cheesecake +/// snippet line ── │ 10 │ │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly +/// │ │ ╭─│─────────^ +/// snippet break ── │ · │ │ +/// snippet line ── │ 33 │ │ │ Muffin danish chocolate soufflé pastry icing bonbon oat cake. +/// snippet line ── │ 34 │ │ │ Powder cake jujubes oat cake. Lemon drops tootsie roll marshmallow +/// │ │ │ ╰─────────────────────────────^ blah blah +/// snippet break ── │ · │ +/// snippet line ── │ 38 │ │ Brownie lemon drops chocolate jelly-o candy canes. Danish marzipan +/// snippet line ── │ 39 │ │ jujubes soufflé carrot cake marshmallow tiramisu caramels candy canes. +/// │ │ │ ^^^^^^^^^^^^^^^^^^^ -------------------- blah blah +/// │ │ │ │ +/// │ │ │ blah blah +/// │ │ │ note: this is a note +/// snippet line ── │ 40 │ │ Fruitcake jelly-o danish toffee. Tootsie roll pastry cheesecake +/// snippet line ── │ 41 │ │ soufflé marzipan. Chocolate bar oat cake jujubes lollipop pastry +/// snippet line ── │ 42 │ │ cupcake. Candy canes cupcake toffee gingerbread candy canes muffin +/// │ │ │ ^^^^^^^^^^^^^^^^^^ blah blah +/// │ │ ╰──────────^ blah blah +/// snippet break ── │ · +/// snippet line ── │ 82 │ gingerbread toffee chupa chups chupa chups jelly-o cotton candy. +/// │ │ ^^^^^^ ------- blah blah +/// snippet empty ── │ │ +/// snippet note ── │ = blah blah +/// snippet note ── │ = blah blah blah +/// │ blah blah +/// snippet note ── │ = blah blah blah +/// │ blah blah +/// empty ── │ +/// ``` +/// +/// Filler text from http://www.cupcakeipsum.com +pub(super) struct Renderer<'render, 'fmt> { + writer: &'render mut Formatter<'fmt>, +} + +impl<'render, 'fmt> Renderer<'render, 'fmt> { + /// Construct a renderer from the given writer and config. + pub(super) fn new(writer: &'render mut Formatter<'fmt>) -> Renderer<'render, 'fmt> { + Renderer { writer } + } + + /// Top left border and locus. + /// + /// ```text + /// ┌─ test:2:9 + /// ``` + pub(super) fn render_snippet_start( + &mut self, + outer_padding: usize, + locus: &Locus, + ) -> Result<(), Error> { + self.outer_gutter(outer_padding)?; + + self.writer.write_markup(markup! { + {SOURCE_BORDER_TOP_LEFT}{SOURCE_BORDER_TOP} + })?; + + write!(self.writer, " ")?; + self.snippet_locus(locus)?; + + writeln!(self.writer)?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn render_snippet_source_impl( + &mut self, + outer_padding: usize, + line_number: usize, + source: &str, + severity: Severity, + single_labels: &[SingleLabel<'_>], + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + // Trim trailing newlines, linefeeds, and null chars from source, if they exist. + // FIXME: Use the number of trimmed placeholders when rendering single line carets + let source = source.trim_end_matches(['\n', '\r', '\0'].as_ref()); + + // Write source line + // + // ```text + // 10 │ │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly + // ``` + { + // Write outer gutter (with line number) and border + self.outer_gutter_number(line_number, outer_padding)?; + self.border_left()?; + + // Write inner gutter (with multi-line continuations on the left if necessary) + let mut multi_labels_iter = multi_labels.iter().peekable(); + for label_column in 0..num_multi_labels { + match multi_labels_iter.peek() { + Some((label_index, label_style, label)) if *label_index == label_column => { + match label { + MultiLabel::Top(start) + if *start <= source.len() - source.trim_start().len() => + { + self.label_multi_top_left(severity, *label_style)?; + } + MultiLabel::Top(..) => self.inner_gutter_space()?, + MultiLabel::Left | MultiLabel::Bottom(..) => { + self.label_multi_left(severity, *label_style, None)?; + } + } + multi_labels_iter.next(); + } + Some((_, _, _)) | None => self.inner_gutter_space()?, + } + } + + // Write source text + write!(self.writer, " ")?; + for (metrics, ch) in self.char_metrics(source.char_indices()) { + let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8()); + + // Check if we are overlapping a primary label + let is_primary = single_labels.iter().any(|(ls, range, _)| { + *ls == LabelStyle::Primary && is_overlapping(range, &column_range) + }) || multi_labels.iter().any(|(_, ls, label)| { + *ls == LabelStyle::Primary + && match label { + MultiLabel::Top(start) => column_range.start >= *start, + MultiLabel::Left => true, + MultiLabel::Bottom(start, _) => column_range.end <= *start, + } + }); + + match ch { + '\t' => { + (0..metrics.unicode_width).try_for_each(|_| write!(self.writer, " "))? + } + _ => { + // Set the source color if we are in a primary label + if is_primary { + let style = match severity { + Severity::Bug | Severity::Error => MarkupElement::Error, + Severity::Warning => MarkupElement::Warn, + Severity::Note => MarkupElement::Info, + Severity::Help => MarkupElement::Info, + }; + + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[style], + content: &ch, + }]))? + } else { + write!(self.writer, "{}", ch)? + } + } + } + } + writeln!(self.writer)?; + } + + // Write single labels underneath source + // + // ```text + // │ - ---- ^^^ second mutable borrow occurs here + // │ │ │ + // │ │ first mutable borrow occurs here + // │ first borrow later used by call + // │ help: some help here + // ``` + if !single_labels.is_empty() { + // Our plan is as follows: + // + // 1. Do an initial scan to find: + // - The number of non-empty messages. + // - The right-most start and end positions of labels. + // - A candidate for a trailing label (where the label's message + // is printed to the left of the caret). + // 2. Check if the trailing label candidate overlaps another label - + // if so we print it underneath the carets with the other labels. + // 3. Print a line of carets, and (possibly) the trailing message + // to the left. + // 4. Print vertical lines pointing to the carets, and the messages + // for those carets. + // + // We try our best avoid introducing new dynamic allocations, + // instead preferring to iterate over the labels multiple times. It + // is unclear what the performance tradeoffs are however, so further + // investigation may be required. + + // The number of non-empty messages to print. + let mut num_messages = 0; + // The right-most start position, eg: + // + // ```text + // -^^^^---- ^^^^^^^ + // │ + // right-most start position + // ``` + let mut max_label_start = 0; + // The right-most end position, eg: + // + // ```text + // -^^^^---- ^^^^^^^ + // │ + // right-most end position + // ``` + let mut max_label_end = 0; + // A trailing message, eg: + // + // ```text + // ^^^ second mutable borrow occurs here + // ``` + let mut trailing_label = None; + + for (label_index, label) in single_labels.iter().enumerate() { + let (_, range, message) = label; + if !message.is_empty() { + num_messages += 1; + } + max_label_start = std::cmp::max(max_label_start, range.start); + max_label_end = std::cmp::max(max_label_end, range.end); + // This is a candidate for the trailing label, so let's record it. + if range.end == max_label_end { + if message.is_empty() { + trailing_label = None; + } else { + trailing_label = Some((label_index, label)); + } + } + } + + if let Some((trailing_label_index, (_, trailing_range, _))) = trailing_label { + // Check to see if the trailing label candidate overlaps any of + // the other labels on the current line. + if single_labels + .iter() + .enumerate() + .filter(|(label_index, _)| *label_index != trailing_label_index) + .any(|(_, (_, range, _))| is_overlapping(trailing_range, range)) + { + // If it does, we'll instead want to render it below the + // carets along with the other hanging labels. + trailing_label = None; + } + } + + // Write a line of carets + // + // ```text + // │ ^^^^^^ -------^^^^^^^^^-------^^^^^----- ^^^^ trailing label message + // ``` + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + write!(self.writer, " ")?; + + let placeholder_metrics = Metrics { + byte_index: source.len(), + unicode_width: 1, + }; + for (metrics, ch) in self + .char_metrics(source.char_indices()) + // Add a placeholder source column at the end to allow for + // printing carets at the end of lines, eg: + // + // ```text + // 1 │ Hello world! + // │ ^ + // ``` + .chain(std::iter::once((placeholder_metrics, '\0'))) + { + // Find the current label style at this column + let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8()); + let current_label_style = single_labels + .iter() + .filter(|(_, range, _)| is_overlapping(range, &column_range)) + .map(|(label_style, _, _)| *label_style) + .max_by_key(label_priority_key); + + let caret_ch = match current_label_style { + Some(LabelStyle::Primary) => Some(SINGLE_PRIMARY_CARET), + Some(LabelStyle::Secondary) => Some(SINGLE_SECONDARY_CARET), + // Only print padding if we are before the end of the last single line caret + None if metrics.byte_index < max_label_end => Some(' '), + None => None, + }; + + match (current_label_style, caret_ch) { + (_, None) => {} + (None, Some(caret_ch)) => { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width) + .try_for_each(|_| write!(self.writer, "{}", caret_ch))?; + } + (Some(label_style), Some(caret_ch)) => { + let style = label_element(severity, label_style); + for _ in 0..metrics.unicode_width { + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[style], + content: &caret_ch, + }]))? + } + } + } + } + // Write first trailing label message + if let Some((_, (label_style, _, message))) = trailing_label { + write!(self.writer, " ")?; + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, *label_style)], + content: message, + }]))?; + } + writeln!(self.writer)?; + + // Write hanging labels pointing to carets + // + // ```text + // │ │ │ + // │ │ first mutable borrow occurs here + // │ first borrow later used by call + // │ help: some help here + // ``` + if num_messages > trailing_label.iter().count() { + // Write first set of vertical lines before hanging labels + // + // ```text + // │ │ │ + // ``` + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + write!(self.writer, " ")?; + self.caret_pointers( + severity, + max_label_start, + single_labels, + trailing_label, + source.char_indices(), + )?; + writeln!(self.writer)?; + + // Write hanging labels pointing to carets + // + // ```text + // │ │ first mutable borrow occurs here + // │ first borrow later used by call + // │ help: some help here + // ``` + for (label_style, range, message) in + hanging_labels(single_labels, trailing_label).rev() + { + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + write!(self.writer, " ")?; + self.caret_pointers( + severity, + max_label_start, + single_labels, + trailing_label, + source + .char_indices() + .take_while(|(byte_index, _)| *byte_index < range.start), + )?; + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, *label_style)], + content: message, + }]))?; + writeln!(self.writer)?; + } + } + } + + // Write top or bottom label carets underneath source + // + // ```text + // │ ╰───│──────────────────^ woops + // │ ╭─│─────────^ + // ``` + for (multi_label_index, (_, label_style, label)) in multi_labels.iter().enumerate() { + let (label_style, range, bottom_message) = match label { + MultiLabel::Left => continue, // no label caret needed + // no label caret needed if this can be started in front of the line + MultiLabel::Top(start) if *start <= source.len() - source.trim_start().len() => { + continue + } + MultiLabel::Top(range) => (*label_style, range, None), + MultiLabel::Bottom(range, message) => (*label_style, range, Some(message)), + }; + + self.outer_gutter(outer_padding)?; + self.border_left()?; + + // Write inner gutter. + // + // ```text + // │ ╭─│───│ + // ``` + let mut underline = None; + let mut multi_labels_iter = multi_labels.iter().enumerate().peekable(); + for label_column in 0..num_multi_labels { + match multi_labels_iter.peek() { + Some((i, (label_index, ls, label))) if *label_index == label_column => { + match label { + MultiLabel::Left => { + self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?; + } + MultiLabel::Top(..) if multi_label_index > *i => { + self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?; + } + MultiLabel::Bottom(..) if multi_label_index < *i => { + self.label_multi_left(severity, *ls, underline.map(|(s, _)| s))?; + } + MultiLabel::Top(..) if multi_label_index == *i => { + underline = Some((*ls, VerticalBound::Top)); + self.label_multi_top_left(severity, label_style)? + } + MultiLabel::Bottom(..) if multi_label_index == *i => { + underline = Some((*ls, VerticalBound::Bottom)); + self.label_multi_bottom_left(severity, label_style)?; + } + MultiLabel::Top(..) | MultiLabel::Bottom(..) => { + self.inner_gutter_column(severity, underline)?; + } + } + multi_labels_iter.next(); + } + Some((_, _)) | None => self.inner_gutter_column(severity, underline)?, + } + } + + // Finish the top or bottom caret + match bottom_message { + None => self.label_multi_top_caret(severity, label_style, source, *range)?, + Some(message) => { + self.label_multi_bottom_caret(severity, label_style, source, *range, *message)? + } + } + } + + Ok(()) + } + + fn render_snippet_source_inside_of_long_line( + &mut self, + line_number: usize, + line_range: Range, + severity: Severity, + single_labels: &mut Vec<(LabelStyle, Range, Markup)>, + outer_padding: usize, + source: &str, + ) -> Result<(), Error> { + let labels_start = single_labels + .first() + .map_or(line_range.start, |x| x.1.start); + let labels_end = single_labels.last().map_or(line_range.end, |x| x.1.end); + + // If labels width are larger then max_line_length, we will + // trim the label + let labels_width = (labels_end - labels_start).min(MAX_LINE_LENGTH); + + let spacing = (MAX_LINE_LENGTH - labels_width) / 2; + + // We will try to center the interesting part of the line + let interesting_part_start = labels_start.saturating_sub(spacing); + let interesting_part_end = labels_end.saturating_add(spacing); + let interesting_part_range = interesting_part_start..interesting_part_end; + + // labels range are relative to the start of the line, now we + // need the range relative to the file start. + let mut new_code_range = line_range + .start + .saturating_add(interesting_part_range.start) + ..line_range.start.saturating_add(interesting_part_range.end); + + // We need to adjust all labels ranges to be relative to the start + // of the interesting part + for label in single_labels.iter_mut() { + label.1.start -= interesting_part_range.start; + label.1.end -= interesting_part_range.start; + + // We need to limit the width of the range + label.1.end = label + .1 + .end + .min(interesting_part_range.start + MAX_LINE_LENGTH); + } + + // and the width of what we are going to print + new_code_range.end = new_code_range + .end + .min(new_code_range.start + MAX_LINE_LENGTH); + + let source = source + .get(new_code_range) + .unwrap_or_else(|| &source[line_range]); + + self.render_snippet_source_impl( + outer_padding, + line_number, + source, + severity, + single_labels.as_slice(), + 0, + &[], + )?; + + Ok(()) + } + + /// A line of source code. + /// + /// ```text + /// 10 │ │ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly + /// │ ╭─│─────────^ + /// ``` + #[allow(clippy::too_many_arguments)] + pub(super) fn render_snippet_source( + &mut self, + outer_padding: usize, + line_number: usize, + line_range: Range, + source: &str, + severity: Severity, + single_labels: &[SingleLabel<'_>], + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + // if the line is smaller than max_line_length, we print it entirely... + // we also print it entirely if there are multi_labels + let line_candidate = &source[line_range.clone()]; + if (line_candidate.len() < MAX_LINE_LENGTH) || !multi_labels.is_empty() { + return self.render_snippet_source_impl( + outer_padding, + line_number, + line_candidate, + severity, + single_labels, + num_multi_labels, + multi_labels, + ); + } else { + // ... if not, we try to fit as many single_labels as possible + // showing only the interesting part of the line. + let mut candidates = vec![]; + for single_label in single_labels.iter() { + candidates.push((*single_label).clone()); + + // We need to know which part of the long line we are going to display + let labels_start = candidates.first().map_or(line_range.start, |x| x.1.start); + let labels_end = candidates.last().map_or(line_range.end, |x| x.1.end); + let labels_width = labels_end - labels_start; + + if labels_width >= MAX_LINE_LENGTH { + self.render_snippet_source_inside_of_long_line( + line_number, + line_range.clone(), + severity, + &mut candidates, + outer_padding, + source, + )?; + candidates.clear(); + } + } + + if !candidates.is_empty() { + self.render_snippet_source_inside_of_long_line( + line_number, + line_range, + severity, + &mut candidates, + outer_padding, + source, + )?; + } + } + + Ok(()) + } + + /// An empty source line, for providing additional whitespace to source snippets. + /// + /// ```text + /// │ │ │ + /// ``` + pub(super) fn render_snippet_empty( + &mut self, + outer_padding: usize, + severity: Severity, + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + self.outer_gutter(outer_padding)?; + self.border_left()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + writeln!(self.writer)?; + Ok(()) + } + + /// A broken source line, for labeling skipped sections of source. + /// + /// ```text + /// · │ │ + /// ``` + pub(super) fn render_snippet_break( + &mut self, + outer_padding: usize, + severity: Severity, + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + self.outer_gutter(outer_padding)?; + self.border_left_break()?; + self.inner_gutter(severity, num_multi_labels, multi_labels)?; + writeln!(self.writer)?; + Ok(()) + } + + /// Adds tab-stop aware unicode-width computations to an iterator over + /// character indices. Assumes that the character indices begin at the start + /// of the line. + fn char_metrics( + &self, + char_indices: impl Iterator + Clone, + ) -> impl Iterator + Clone { + use unicode_width::UnicodeWidthChar; + + let tab_width = 4; + let mut unicode_column = 0; + + char_indices.map(move |(byte_index, ch)| { + let metrics = Metrics { + byte_index, + unicode_width: match (ch, tab_width) { + ('\t', 0) => 0, // Guard divide-by-zero + ('\t', _) => tab_width - (unicode_column % tab_width), + (ch, _) => ch.width().unwrap_or(0), + }, + }; + unicode_column += metrics.unicode_width; + + (metrics, ch) + }) + } + + /// Location focus. + fn snippet_locus(&mut self, locus: &Locus) -> Result<(), Error> { + match locus { + Locus::File { name } => write!(self.writer, "{name}",)?, + Locus::FileLocation { name, location } => write!( + self.writer, + "{name}:{line_number}:{column_number}", + name = name, + line_number = location.line_number, + column_number = location.column_number, + )?, + } + Ok(()) + } + + /// The outer gutter of a source line. + fn outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error> { + write!( + self.writer, + "{space: >width$} ", + space = "", + width = outer_padding + )?; + Ok(()) + } + + /// The outer gutter of a source line, with line number. + fn outer_gutter_number( + &mut self, + line_number: usize, + outer_padding: usize, + ) -> Result<(), Error> { + self.writer.write_markup(markup! { + + {format_args!( + "{line_number: >width$}", + line_number = line_number, + width = outer_padding + )} + + })?; + write!(self.writer, " ")?; + Ok(()) + } + + /// The left-hand border of a source line. + fn border_left(&mut self) -> Result<(), Error> { + self.writer.write_markup(markup! { + {SOURCE_BORDER_LEFT} + })?; + Ok(()) + } + + /// The broken left-hand border of a source line. + fn border_left_break(&mut self) -> Result<(), Error> { + self.writer.write_markup(markup! { + {SOURCE_BORDER_LEFT_BREAK} + })?; + Ok(()) + } + + /// Write vertical lines pointing to carets. + fn caret_pointers( + &mut self, + severity: Severity, + max_label_start: usize, + single_labels: &[SingleLabel<'_>], + trailing_label: Option<(usize, &SingleLabel<'_>)>, + char_indices: impl Iterator + Clone, + ) -> Result<(), Error> { + for (metrics, ch) in self.char_metrics(char_indices) { + let column_range = metrics.byte_index..(metrics.byte_index + ch.len_utf8()); + let label_style = hanging_labels(single_labels, trailing_label) + .filter(|(_, range, _)| column_range.contains(&range.start)) + .map(|(label_style, _, _)| *label_style) + .max_by_key(label_priority_key); + + let mut spaces = match label_style { + None => 0..metrics.unicode_width, + Some(label_style) => { + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, label_style)], + content: &POINTER_LEFT, + }]))?; + 1..metrics.unicode_width + } + }; + // Only print padding if we are before the end of the last single line caret + if metrics.byte_index <= max_label_start { + spaces.try_for_each(|_| write!(self.writer, " "))?; + } + } + + Ok(()) + } + + /// The left of a multi-line label. + /// + /// ```text + /// │ + /// ``` + fn label_multi_left( + &mut self, + severity: Severity, + label_style: LabelStyle, + underline: Option, + ) -> Result<(), Error> { + match underline { + None => write!(self.writer, " ")?, + // Continue an underline horizontally + Some(label_style) => { + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, label_style)], + content: &MULTI_TOP, + }]))?; + } + } + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, label_style)], + content: &MULTI_LEFT, + }]))?; + Ok(()) + } + + /// The top-left of a multi-line label. + /// + /// ```text + /// ╭ + /// ``` + fn label_multi_top_left( + &mut self, + severity: Severity, + label_style: LabelStyle, + ) -> Result<(), Error> { + write!(self.writer, " ")?; + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, label_style)], + content: &MULTI_TOP_LEFT, + }]))?; + Ok(()) + } + + /// The bottom left of a multi-line label. + /// + /// ```text + /// ╰ + /// ``` + fn label_multi_bottom_left( + &mut self, + severity: Severity, + label_style: LabelStyle, + ) -> Result<(), Error> { + write!(self.writer, " ")?; + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, label_style)], + content: &MULTI_BOTTOM_LEFT, + }]))?; + Ok(()) + } + + /// Multi-line label top. + /// + /// ```text + /// ─────────────^ + /// ``` + fn label_multi_top_caret( + &mut self, + severity: Severity, + label_style: LabelStyle, + source: &str, + start: usize, + ) -> Result<(), Error> { + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, label_style)], + content: &MultiCaret { + label_style, + message: Markup(&[]), + caret: MULTI_TOP, + char_metrics: self + .char_metrics(source.char_indices()) + .take_while(|(metrics, _)| metrics.byte_index < start), + }, + }]))?; + + writeln!(self.writer)?; + Ok(()) + } + + /// Multi-line label bottom, with a message. + /// + /// ```text + /// ─────────────^ expected `Int` but found `String` + /// ``` + fn label_multi_bottom_caret( + &mut self, + severity: Severity, + label_style: LabelStyle, + source: &str, + start: usize, + message: Markup, + ) -> Result<(), Error> { + self.writer.write_markup(Markup(&[MarkupNode { + elements: &[label_element(severity, label_style)], + content: &MultiCaret { + label_style, + message, + caret: MULTI_BOTTOM, + char_metrics: self + .char_metrics(source.char_indices()) + .take_while(|(metrics, _)| metrics.byte_index < start), + }, + }]))?; + + writeln!(self.writer)?; + Ok(()) + } + + /// Writes an empty gutter space, or continues an underline horizontally. + fn inner_gutter_column( + &mut self, + severity: Severity, + underline: Option, + ) -> Result<(), Error> { + match underline { + None => self.inner_gutter_space(), + Some((label_style, vertical_bound)) => { + let ch = match vertical_bound { + VerticalBound::Top => MULTI_TOP, + VerticalBound::Bottom => MULTI_BOTTOM, + }; + let element = label_element(severity, label_style); + self.writer.write_markup(Markup(&[ + MarkupNode { + elements: &[element], + content: &ch, + }, + MarkupNode { + elements: &[element], + content: &ch, + }, + ]))?; + Ok(()) + } + } + } + + /// Writes an empty gutter space. + fn inner_gutter_space(&mut self) -> Result<(), Error> { + write!(self.writer, " ")?; + Ok(()) + } + + /// Writes an inner gutter, with the left lines if necessary. + fn inner_gutter( + &mut self, + severity: Severity, + num_multi_labels: usize, + multi_labels: &[(usize, LabelStyle, MultiLabel<'_>)], + ) -> Result<(), Error> { + let mut multi_labels_iter = multi_labels.iter().peekable(); + for label_column in 0..num_multi_labels { + match multi_labels_iter.peek() { + Some((label_index, ls, label)) if *label_index == label_column => match label { + MultiLabel::Left | MultiLabel::Bottom(..) => { + self.label_multi_left(severity, *ls, None)?; + multi_labels_iter.next(); + } + MultiLabel::Top(..) => { + self.inner_gutter_space()?; + multi_labels_iter.next(); + } + }, + Some((_, _, _)) | None => self.inner_gutter_space()?, + } + } + + Ok(()) + } +} + +struct MultiCaret<'a, I> { + label_style: LabelStyle, + message: Markup<'a>, + caret: char, + char_metrics: I, +} + +impl<'a, I> Display for MultiCaret<'a, I> +where + I: Iterator + Clone, +{ + fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { + for (metrics, _) in self.char_metrics.clone() { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width).try_for_each(|_| write!(fmt, "{}", self.caret))?; + } + + let caret_end = match self.label_style { + LabelStyle::Primary => MULTI_PRIMARY_CARET_START, + LabelStyle::Secondary => MULTI_SECONDARY_CARET_START, + }; + + write!(fmt, "{}", caret_end)?; + + if !self.message.is_empty() { + fmt.write_str(" ")?; + fmt.write_markup(self.message)?; + } + + Ok(()) + } +} +struct Metrics { + byte_index: usize, + unicode_width: usize, +} + +/// Check if two ranges overlap +fn is_overlapping(range0: &Range, range1: &Range) -> bool { + let start = std::cmp::max(range0.start, range1.start); + let end = std::cmp::min(range0.end, range1.end); + start < end +} + +/// For prioritizing primary labels over secondary labels when rendering carets. +fn label_priority_key(label_style: &LabelStyle) -> u8 { + match label_style { + LabelStyle::Secondary => 0, + LabelStyle::Primary => 1, + } +} + +/// Return an iterator that yields the labels that require hanging messages +/// rendered underneath them. +fn hanging_labels<'labels, 'diagnostic>( + single_labels: &'labels [SingleLabel<'diagnostic>], + trailing_label: Option<(usize, &'labels SingleLabel<'diagnostic>)>, +) -> impl 'labels + DoubleEndedIterator> { + single_labels + .iter() + .enumerate() + .filter(|(_, (_, _, message))| !message.is_empty()) + .filter(move |(i, _)| trailing_label.map_or(true, |(j, _)| *i != j)) + .map(|(_, label)| label) +} diff --git a/crates/rome_console/src/fmt.rs b/crates/rome_console/src/fmt.rs index 672c49f252c..ca98fb42b9f 100644 --- a/crates/rome_console/src/fmt.rs +++ b/crates/rome_console/src/fmt.rs @@ -155,6 +155,13 @@ impl Display for String { } } +// Implement Display for Markup and Rust format Arguments +impl<'a> Display for Markup<'a> { + fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { + fmt.write_markup(*self) + } +} + impl<'a> Display for std::fmt::Arguments<'a> { fn fmt(&self, fmt: &mut Formatter) -> io::Result<()> { fmt.write_fmt(*self) @@ -173,6 +180,7 @@ macro_rules! impl_std_display { }; } +impl_std_display!(char); impl_std_display!(i8); impl_std_display!(i16); impl_std_display!(i32); diff --git a/crates/rome_console/src/lib.rs b/crates/rome_console/src/lib.rs index dd345f430e9..caaad7dc1d9 100644 --- a/crates/rome_console/src/lib.rs +++ b/crates/rome_console/src/lib.rs @@ -3,6 +3,7 @@ use std::panic::RefUnwindSafe; use rome_diagnostics::{file::Files, Diagnostic, Emitter}; use termcolor::{ColorChoice, NoColor, StandardStream, WriteColor}; +pub mod codespan; pub mod diff; pub mod fmt; mod markup; diff --git a/crates/rome_console/src/markup.rs b/crates/rome_console/src/markup.rs index 9d1205aa294..63ca1b05872 100644 --- a/crates/rome_console/src/markup.rs +++ b/crates/rome_console/src/markup.rs @@ -71,4 +71,11 @@ pub struct MarkupNode<'fmt> { /// Text nodes are formatted lazily by storing an [fmt::Arguments] struct, this /// means [Markup] shares the same restriction as the values returned by /// [format_args] and can't be stored in a `let` binding for instance +#[derive(Copy, Clone)] pub struct Markup<'fmt>(pub &'fmt [MarkupNode<'fmt>]); + +impl<'fmt> Markup<'fmt> { + pub(crate) fn is_empty(&self) -> bool { + self.0.is_empty() + } +} From fd2da3cf91af3a80e2674701df196a0669ab8abb Mon Sep 17 00:00:00 2001 From: l3ops Date: Fri, 1 Apr 2022 18:21:32 +0200 Subject: [PATCH 2/2] improve documentation --- crates/rome_console/src/codespan/LICENSE | 201 +++++++++++++++++++++++ crates/rome_console/src/codespan/mod.rs | 9 +- website/src/credits.md | 4 + 3 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 crates/rome_console/src/codespan/LICENSE diff --git a/crates/rome_console/src/codespan/LICENSE b/crates/rome_console/src/codespan/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/crates/rome_console/src/codespan/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/rome_console/src/codespan/mod.rs b/crates/rome_console/src/codespan/mod.rs index febe9fc7b4c..e14757031a5 100644 --- a/crates/rome_console/src/codespan/mod.rs +++ b/crates/rome_console/src/codespan/mod.rs @@ -1,5 +1,5 @@ -///! This module if a fork of https://github.com/brendanzab/codespan, -/// adapted to use the `rome_console` markup for formatting +//! This module is a fork of https://github.com/brendanzab/codespan +//! adapted to use the `rome_console` markup for formatting use std::collections::BTreeMap; use std::io; use std::ops::Range; @@ -77,7 +77,12 @@ pub struct Location { pub struct Codespan<'diagnostic> { severity: Severity, locus: Option, + /// Source code and line indices for the file being annotated source_file: &'diagnostic SourceFile<'diagnostic>, + /// Cached annotated line contents and layout + /// + /// This holds only the lines of the file that have labels attached, + /// with precaculated positions for each label to make the printing efficient labeled_file: Option>, outer_padding: usize, } diff --git a/website/src/credits.md b/website/src/credits.md index 57a9a3b2042..a2699b79069 100644 --- a/website/src/credits.md +++ b/website/src/credits.md @@ -425,6 +425,10 @@ substantially rewritten. - **Original**: [`rslint/rslint_errors`](https://github.com/rslint/rslint/tree/master/crates/rslint_errors) - **License**: MIT +- [`crates/rome_console/src/codespan`](https://github.com/rome/tools/tree/main/crates/rome_console/src/codespan) + - **Original**: [`brendanzab/codespan`](https://github.com/brendanzab/codespan) + - **License**: Apache License, Version 2.0 + - [`crates/rome_js_parser`](https://github.com/rome/tools/tree/main/crates/rome_js_parser) - **Original**: [`rslint/rslint_parser`](https://github.com/rslint/rslint/tree/master/crates/rslint_parser) - **License**: MIT