diff options
Diffstat (limited to 'third_party/rust/codespan-reporting/src')
-rw-r--r-- | third_party/rust/codespan-reporting/src/diagnostic.rs | 209 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/files.rs | 443 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/lib.rs | 7 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/term.rs | 121 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/term/config.rs | 321 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/term/renderer.rs | 1020 | ||||
-rw-r--r-- | third_party/rust/codespan-reporting/src/term/views.rs | 478 |
7 files changed, 2599 insertions, 0 deletions
diff --git a/third_party/rust/codespan-reporting/src/diagnostic.rs b/third_party/rust/codespan-reporting/src/diagnostic.rs new file mode 100644 index 0000000000..c1f98bd435 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/diagnostic.rs @@ -0,0 +1,209 @@ +//! Diagnostic data structures. + +#[cfg(feature = "serialization")] +use serde::{Deserialize, Serialize}; +use std::ops::Range; + +/// A severity level for diagnostic messages. +/// +/// These are ordered in the following way: +/// +/// ```rust +/// use codespan_reporting::diagnostic::Severity; +/// +/// assert!(Severity::Bug > Severity::Error); +/// assert!(Severity::Error > Severity::Warning); +/// assert!(Severity::Warning > Severity::Note); +/// assert!(Severity::Note > Severity::Help); +/// ``` +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +pub enum Severity { + /// An unexpected bug. + Bug, + /// An error. + Error, + /// A warning. + Warning, + /// A note. + Note, + /// A help message. + Help, +} + +impl Severity { + /// We want bugs to be the maximum severity, errors next, etc... + fn to_cmp_int(self) -> u8 { + match self { + Severity::Bug => 5, + Severity::Error => 4, + Severity::Warning => 3, + Severity::Note => 2, + Severity::Help => 1, + } + } +} + +impl PartialOrd for Severity { + fn partial_cmp(&self, other: &Severity) -> Option<std::cmp::Ordering> { + u8::partial_cmp(&self.to_cmp_int(), &other.to_cmp_int()) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd)] +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +pub enum LabelStyle { + /// Labels that describe the primary cause of a diagnostic. + Primary, + /// Labels that provide additional context for a diagnostic. + Secondary, +} + +/// A label describing an underlined region of code associated with a diagnostic. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +pub struct Label<FileId> { + /// The style of the label. + pub style: LabelStyle, + /// The file that we are labelling. + pub file_id: FileId, + /// The range in bytes we are going to include in the final snippet. + pub range: Range<usize>, + /// An optional message to provide some additional information for the + /// underlined code. These should not include line breaks. + pub message: String, +} + +impl<FileId> Label<FileId> { + /// Create a new label. + pub fn new( + style: LabelStyle, + file_id: FileId, + range: impl Into<Range<usize>>, + ) -> Label<FileId> { + Label { + style, + file_id, + range: range.into(), + message: String::new(), + } + } + + /// Create a new label with a style of [`LabelStyle::Primary`]. + /// + /// [`LabelStyle::Primary`]: LabelStyle::Primary + pub fn primary(file_id: FileId, range: impl Into<Range<usize>>) -> Label<FileId> { + Label::new(LabelStyle::Primary, file_id, range) + } + + /// Create a new label with a style of [`LabelStyle::Secondary`]. + /// + /// [`LabelStyle::Secondary`]: LabelStyle::Secondary + pub fn secondary(file_id: FileId, range: impl Into<Range<usize>>) -> Label<FileId> { + Label::new(LabelStyle::Secondary, file_id, range) + } + + /// Add a message to the diagnostic. + pub fn with_message(mut self, message: impl Into<String>) -> Label<FileId> { + self.message = message.into(); + self + } +} + +/// Represents a diagnostic message that can provide information like errors and +/// warnings to the user. +/// +/// The position of a Diagnostic is considered to be the position of the [`Label`] that has the earliest starting position and has the highest style which appears in all the labels of the diagnostic. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))] +pub struct Diagnostic<FileId> { + /// The overall severity of the diagnostic + pub severity: Severity, + /// An optional code that identifies this diagnostic. + pub code: Option<String>, + /// The main message associated with this diagnostic. + /// + /// These should not include line breaks, and in order support the 'short' + /// diagnostic display mod, the message should be specific enough to make + /// sense on its own, without additional context provided by labels and notes. + pub message: String, + /// Source labels that describe the cause of the diagnostic. + /// The order of the labels inside the vector does not have any meaning. + /// The labels are always arranged in the order they appear in the source code. + pub labels: Vec<Label<FileId>>, + /// Notes that are associated with the primary cause of the diagnostic. + /// These can include line breaks for improved formatting. + pub notes: Vec<String>, +} + +impl<FileId> Diagnostic<FileId> { + /// Create a new diagnostic. + pub fn new(severity: Severity) -> Diagnostic<FileId> { + Diagnostic { + severity, + code: None, + message: String::new(), + labels: Vec::new(), + notes: Vec::new(), + } + } + + /// Create a new diagnostic with a severity of [`Severity::Bug`]. + /// + /// [`Severity::Bug`]: Severity::Bug + pub fn bug() -> Diagnostic<FileId> { + Diagnostic::new(Severity::Bug) + } + + /// Create a new diagnostic with a severity of [`Severity::Error`]. + /// + /// [`Severity::Error`]: Severity::Error + pub fn error() -> Diagnostic<FileId> { + Diagnostic::new(Severity::Error) + } + + /// Create a new diagnostic with a severity of [`Severity::Warning`]. + /// + /// [`Severity::Warning`]: Severity::Warning + pub fn warning() -> Diagnostic<FileId> { + Diagnostic::new(Severity::Warning) + } + + /// Create a new diagnostic with a severity of [`Severity::Note`]. + /// + /// [`Severity::Note`]: Severity::Note + pub fn note() -> Diagnostic<FileId> { + Diagnostic::new(Severity::Note) + } + + /// Create a new diagnostic with a severity of [`Severity::Help`]. + /// + /// [`Severity::Help`]: Severity::Help + pub fn help() -> Diagnostic<FileId> { + Diagnostic::new(Severity::Help) + } + + /// Set the error code of the diagnostic. + pub fn with_code(mut self, code: impl Into<String>) -> Diagnostic<FileId> { + self.code = Some(code.into()); + self + } + + /// Set the message of the diagnostic. + pub fn with_message(mut self, message: impl Into<String>) -> Diagnostic<FileId> { + self.message = message.into(); + self + } + + /// Add some labels to the diagnostic. + pub fn with_labels(mut self, mut labels: Vec<Label<FileId>>) -> Diagnostic<FileId> { + self.labels.append(&mut labels); + self + } + + /// Add some notes to the diagnostic. + pub fn with_notes(mut self, mut notes: Vec<String>) -> Diagnostic<FileId> { + self.notes.append(&mut notes); + self + } +} diff --git a/third_party/rust/codespan-reporting/src/files.rs b/third_party/rust/codespan-reporting/src/files.rs new file mode 100644 index 0000000000..b25cd79788 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/files.rs @@ -0,0 +1,443 @@ +//! Source file support for diagnostic reporting. +//! +//! The main trait defined in this module is the [`Files`] trait, which provides +//! provides the minimum amount of functionality required for printing [`Diagnostics`] +//! with the [`term::emit`] function. +//! +//! Simple implementations of this trait are implemented: +//! +//! - [`SimpleFile`]: For single-file use-cases +//! - [`SimpleFiles`]: For multi-file use-cases +//! +//! These data structures provide a pretty minimal API, however, +//! so end-users are encouraged to create their own implementations for their +//! own specific use-cases, such as an implementation that accesses the file +//! system directly (and caches the line start locations), or an implementation +//! using an incremental compilation library like [`salsa`]. +//! +//! [`term::emit`]: crate::term::emit +//! [`Diagnostics`]: crate::diagnostic::Diagnostic +//! [`Files`]: Files +//! [`SimpleFile`]: SimpleFile +//! [`SimpleFiles`]: SimpleFiles +//! +//! [`salsa`]: https://crates.io/crates/salsa + +use std::ops::Range; + +/// An enum representing an error that happened while looking up a file or a piece of content in that file. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// A required file is not in the file database. + FileMissing, + /// The file is present, but does not contain the specified byte index. + IndexTooLarge { given: usize, max: usize }, + /// The file is present, but does not contain the specified line index. + LineTooLarge { given: usize, max: usize }, + /// The file is present and contains the specified line index, but the line does not contain the specified column index. + ColumnTooLarge { given: usize, max: usize }, + /// The given index is contained in the file, but is not a boundary of a UTF-8 code point. + InvalidCharBoundary { given: usize }, + /// There was a error while doing IO. + Io(std::io::Error), +} + +impl From<std::io::Error> for Error { + fn from(err: std::io::Error) -> Error { + Error::Io(err) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::FileMissing => write!(f, "file missing"), + Error::IndexTooLarge { given, max } => { + write!(f, "invalid index {}, maximum index is {}", given, max) + } + Error::LineTooLarge { given, max } => { + write!(f, "invalid line {}, maximum line is {}", given, max) + } + Error::ColumnTooLarge { given, max } => { + write!(f, "invalid column {}, maximum column {}", given, max) + } + Error::InvalidCharBoundary { .. } => write!(f, "index is not a code point boundary"), + Error::Io(err) => write!(f, "{}", err), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self { + Error::Io(err) => Some(err), + _ => None, + } + } +} + +/// A minimal interface for accessing source files when rendering diagnostics. +/// +/// A lifetime parameter `'a` is provided to allow any of the returned values to returned by reference. +/// This is to workaround the lack of higher kinded lifetime parameters. +/// This can be ignored if this is not needed, however. +pub trait Files<'a> { + /// A unique identifier for files in the file provider. This will be used + /// for rendering `diagnostic::Label`s in the corresponding source files. + type FileId: 'a + Copy + PartialEq; + /// The user-facing name of a file, to be displayed in diagnostics. + type Name: 'a + std::fmt::Display; + /// The source code of a file. + type Source: 'a + AsRef<str>; + + /// The user-facing name of a file. + fn name(&'a self, id: Self::FileId) -> Result<Self::Name, Error>; + + /// The source code of a file. + fn source(&'a self, id: Self::FileId) -> Result<Self::Source, Error>; + + /// The index of the line at the given byte index. + /// If the byte index is past the end of the file, returns the maximum line index in the file. + /// This means that this function only fails if the file is not present. + /// + /// # Note for trait implementors + /// + /// This can be implemented efficiently by performing a binary search over + /// a list of line starts that was computed by calling the [`line_starts`] + /// function that is exported from the [`files`] module. It might be useful + /// to pre-compute and cache these line starts. + /// + /// [`line_starts`]: crate::files::line_starts + /// [`files`]: crate::files + fn line_index(&'a self, id: Self::FileId, byte_index: usize) -> Result<usize, Error>; + + /// The user-facing line number at the given line index. + /// It is not necessarily checked that the specified line index + /// is actually in the file. + /// + /// # Note for trait implementors + /// + /// This is usually 1-indexed from the beginning of the file, but + /// can be useful for implementing something like the + /// [C preprocessor's `#line` macro][line-macro]. + /// + /// [line-macro]: https://en.cppreference.com/w/c/preprocessor/line + #[allow(unused_variables)] + fn line_number(&'a self, id: Self::FileId, line_index: usize) -> Result<usize, Error> { + Ok(line_index + 1) + } + + /// The user-facing column number at the given line index and byte index. + /// + /// # Note for trait implementors + /// + /// This is usually 1-indexed from the the start of the line. + /// A default implementation is provided, based on the [`column_index`] + /// function that is exported from the [`files`] module. + /// + /// [`files`]: crate::files + /// [`column_index`]: crate::files::column_index + fn column_number( + &'a self, + id: Self::FileId, + line_index: usize, + byte_index: usize, + ) -> Result<usize, Error> { + let source = self.source(id)?; + let line_range = self.line_range(id, line_index)?; + let column_index = column_index(source.as_ref(), line_range, byte_index); + + Ok(column_index + 1) + } + + /// Convenience method for returning line and column number at the given + /// byte index in the file. + fn location(&'a self, id: Self::FileId, byte_index: usize) -> Result<Location, Error> { + let line_index = self.line_index(id, byte_index)?; + + Ok(Location { + line_number: self.line_number(id, line_index)?, + column_number: self.column_number(id, line_index, byte_index)?, + }) + } + + /// The byte range of line in the source of the file. + fn line_range(&'a self, id: Self::FileId, line_index: usize) -> Result<Range<usize>, Error>; +} + +/// A user-facing location in a source file. +/// +/// Returned by [`Files::location`]. +/// +/// [`Files::location`]: Files::location +#[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, +} + +/// 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. +/// +/// # Example +/// +/// ```rust +/// use codespan_reporting::files; +/// +/// let source = "\n\n๐ปโ๐\n\n"; +/// +/// assert_eq!(files::column_index(source, 0..1, 0), 0); +/// assert_eq!(files::column_index(source, 2..13, 0), 0); +/// assert_eq!(files::column_index(source, 2..13, 2 + 0), 0); +/// assert_eq!(files::column_index(source, 2..13, 2 + 1), 0); +/// assert_eq!(files::column_index(source, 2..13, 2 + 4), 1); +/// assert_eq!(files::column_index(source, 2..13, 2 + 8), 2); +/// assert_eq!(files::column_index(source, 2..13, 2 + 10), 2); +/// assert_eq!(files::column_index(source, 2..13, 2 + 11), 3); +/// assert_eq!(files::column_index(source, 2..13, 2 + 12), 3); +/// ``` +pub fn column_index(source: &str, line_range: Range<usize>, 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() +} + +/// 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 +/// +/// # Example +/// +/// ```rust +/// use codespan_reporting::files; +/// +/// let source = "foo\nbar\r\n\nbaz"; +/// let line_starts: Vec<_> = files::line_starts(source).collect(); +/// +/// assert_eq!( +/// line_starts, +/// [ +/// 0, // "foo\n" +/// 4, // "bar\r\n" +/// 9, // "" +/// 10, // "baz" +/// ], +/// ); +/// +/// fn line_index(line_starts: &[usize], byte_index: usize) -> Option<usize> { +/// match line_starts.binary_search(&byte_index) { +/// Ok(line) => Some(line), +/// Err(next_line) => Some(next_line - 1), +/// } +/// } +/// +/// assert_eq!(line_index(&line_starts, 5), Some(1)); +/// ``` +// NOTE: this is copied in `codespan::file::line_starts` and should be kept in sync. +pub fn line_starts<'source>(source: &'source str) -> impl 'source + Iterator<Item = usize> { + std::iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1)) +} + +/// A file database that contains a single source file. +/// +/// Because there is only single file in this database we use `()` as a [`FileId`]. +/// +/// This is useful for simple language tests, but it might be worth creating a +/// custom implementation when a language scales beyond a certain size. +/// +/// [`FileId`]: Files::FileId +#[derive(Debug, Clone)] +pub struct SimpleFile<Name, Source> { + /// The name of the file. + name: Name, + /// The source code of the file. + source: Source, + /// The starting byte indices in the source code. + line_starts: Vec<usize>, +} + +impl<Name, Source> SimpleFile<Name, Source> +where + Name: std::fmt::Display, + Source: AsRef<str>, +{ + /// Create a new source file. + pub fn new(name: Name, source: Source) -> SimpleFile<Name, Source> { + SimpleFile { + name, + line_starts: line_starts(source.as_ref()).collect(), + source, + } + } + + /// Return the name of the file. + pub fn name(&self) -> &Name { + &self.name + } + + /// Return the source of the file. + pub fn source(&self) -> &Source { + &self.source + } + + /// 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<usize, Error> { + 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.as_ref().len()), + Ordering::Greater => Err(Error::LineTooLarge { + given: line_index, + max: self.line_starts.len() - 1, + }), + } + } +} + +impl<'a, Name, Source> Files<'a> for SimpleFile<Name, Source> +where + Name: 'a + std::fmt::Display + Clone, + Source: 'a + AsRef<str>, +{ + type FileId = (); + type Name = Name; + type Source = &'a str; + + fn name(&self, (): ()) -> Result<Name, Error> { + Ok(self.name.clone()) + } + + fn source(&self, (): ()) -> Result<&str, Error> { + Ok(self.source.as_ref()) + } + + fn line_index(&self, (): (), byte_index: usize) -> Result<usize, Error> { + Ok(self + .line_starts + .binary_search(&byte_index) + .unwrap_or_else(|next_line| next_line - 1)) + } + + fn line_range(&self, (): (), line_index: usize) -> Result<Range<usize>, Error> { + let line_start = self.line_start(line_index)?; + let next_line_start = self.line_start(line_index + 1)?; + + Ok(line_start..next_line_start) + } +} + +/// A file database that can store multiple source files. +/// +/// This is useful for simple language tests, but it might be worth creating a +/// custom implementation when a language scales beyond a certain size. +/// It is a glorified `Vec<SimpleFile>` that implements the `Files` trait. +#[derive(Debug, Clone)] +pub struct SimpleFiles<Name, Source> { + files: Vec<SimpleFile<Name, Source>>, +} + +impl<Name, Source> SimpleFiles<Name, Source> +where + Name: std::fmt::Display, + Source: AsRef<str>, +{ + /// Create a new files database. + pub fn new() -> SimpleFiles<Name, Source> { + SimpleFiles { files: Vec::new() } + } + + /// Add a file to the database, returning the handle that can be used to + /// refer to it again. + pub fn add(&mut self, name: Name, source: Source) -> usize { + let file_id = self.files.len(); + self.files.push(SimpleFile::new(name, source)); + file_id + } + + /// Get the file corresponding to the given id. + pub fn get(&self, file_id: usize) -> Result<&SimpleFile<Name, Source>, Error> { + self.files.get(file_id).ok_or(Error::FileMissing) + } +} + +impl<'a, Name, Source> Files<'a> for SimpleFiles<Name, Source> +where + Name: 'a + std::fmt::Display + Clone, + Source: 'a + AsRef<str>, +{ + type FileId = usize; + type Name = Name; + type Source = &'a str; + + fn name(&self, file_id: usize) -> Result<Name, Error> { + Ok(self.get(file_id)?.name().clone()) + } + + fn source(&self, file_id: usize) -> Result<&str, Error> { + Ok(self.get(file_id)?.source().as_ref()) + } + + fn line_index(&self, file_id: usize, byte_index: usize) -> Result<usize, Error> { + self.get(file_id)?.line_index((), byte_index) + } + + fn line_range(&self, file_id: usize, line_index: usize) -> Result<Range<usize>, Error> { + self.get(file_id)?.line_range((), line_index) + } +} + +#[cfg(test)] +mod test { + use super::*; + + const TEST_SOURCE: &str = "foo\nbar\r\n\nbaz"; + + #[test] + fn line_starts() { + let file = SimpleFile::new("test", TEST_SOURCE); + + assert_eq!( + file.line_starts, + [ + 0, // "foo\n" + 4, // "bar\r\n" + 9, // "" + 10, // "baz" + ], + ); + } + + #[test] + fn line_span_sources() { + let file = SimpleFile::new("test", TEST_SOURCE); + + let line_sources = (0..4) + .map(|line| { + let line_range = file.line_range((), line).unwrap(); + &file.source[line_range] + }) + .collect::<Vec<_>>(); + + assert_eq!(line_sources, ["foo\n", "bar\r\n", "\n", "baz"]); + } +} diff --git a/third_party/rust/codespan-reporting/src/lib.rs b/third_party/rust/codespan-reporting/src/lib.rs new file mode 100644 index 0000000000..28d7f24deb --- /dev/null +++ b/third_party/rust/codespan-reporting/src/lib.rs @@ -0,0 +1,7 @@ +//! Diagnostic reporting support for the codespan crate. + +#![forbid(unsafe_code)] + +pub mod diagnostic; +pub mod files; +pub mod term; diff --git a/third_party/rust/codespan-reporting/src/term.rs b/third_party/rust/codespan-reporting/src/term.rs new file mode 100644 index 0000000000..59baeb04e1 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/term.rs @@ -0,0 +1,121 @@ +//! Terminal back-end for emitting diagnostics. + +use std::str::FromStr; +use termcolor::{ColorChoice, WriteColor}; + +use crate::diagnostic::Diagnostic; +use crate::files::Files; + +mod config; +mod renderer; +mod views; + +pub use termcolor; + +pub use self::config::{Chars, Config, DisplayStyle, Styles}; + +/// A command line argument that configures the coloring of the output. +/// +/// This can be used with command line argument parsers like [`clap`] or [`structopt`]. +/// +/// [`clap`]: https://crates.io/crates/clap +/// [`structopt`]: https://crates.io/crates/structopt +/// +/// # Example +/// +/// ```rust +/// use codespan_reporting::term::termcolor::StandardStream; +/// use codespan_reporting::term::ColorArg; +/// use structopt::StructOpt; +/// +/// #[derive(Debug, StructOpt)] +/// #[structopt(name = "groovey-app")] +/// pub struct Opts { +/// /// Configure coloring of output +/// #[structopt( +/// long = "color", +/// default_value = "auto", +/// possible_values = ColorArg::VARIANTS, +/// case_insensitive = true, +/// )] +/// pub color: ColorArg, +/// } +/// +/// let opts = Opts::from_args(); +/// let writer = StandardStream::stderr(opts.color.into()); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ColorArg(pub ColorChoice); + +impl ColorArg { + /// Allowed values the argument. + /// + /// This is useful for generating documentation via [`clap`] or `structopt`'s + /// `possible_values` configuration. + /// + /// [`clap`]: https://crates.io/crates/clap + /// [`structopt`]: https://crates.io/crates/structopt + pub const VARIANTS: &'static [&'static str] = &["auto", "always", "ansi", "never"]; +} + +impl FromStr for ColorArg { + type Err = &'static str; + + fn from_str(src: &str) -> Result<ColorArg, &'static str> { + match src { + _ if src.eq_ignore_ascii_case("auto") => Ok(ColorArg(ColorChoice::Auto)), + _ if src.eq_ignore_ascii_case("always") => Ok(ColorArg(ColorChoice::Always)), + _ if src.eq_ignore_ascii_case("ansi") => Ok(ColorArg(ColorChoice::AlwaysAnsi)), + _ if src.eq_ignore_ascii_case("never") => Ok(ColorArg(ColorChoice::Never)), + _ => Err("valid values: auto, always, ansi, never"), + } + } +} + +impl Into<ColorChoice> for ColorArg { + fn into(self) -> ColorChoice { + self.0 + } +} + +/// Emit a diagnostic using the given writer, context, config, and files. +/// +/// The return value covers all error cases. These error case can arise if: +/// * a file was removed from the file database. +/// * a file was changed so that it is too small to have an index +/// * IO fails +pub fn emit<'files, F: Files<'files>>( + writer: &mut dyn WriteColor, + config: &Config, + files: &'files F, + diagnostic: &Diagnostic<F::FileId>, +) -> Result<(), super::files::Error> { + use self::renderer::Renderer; + use self::views::{RichDiagnostic, ShortDiagnostic}; + + let mut renderer = Renderer::new(writer, config); + match config.display_style { + DisplayStyle::Rich => RichDiagnostic::new(diagnostic, config).render(files, &mut renderer), + DisplayStyle::Medium => ShortDiagnostic::new(diagnostic, true).render(files, &mut renderer), + DisplayStyle::Short => ShortDiagnostic::new(diagnostic, false).render(files, &mut renderer), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::diagnostic::Label; + use crate::files::SimpleFiles; + + #[test] + fn unsized_emit() { + let mut files = SimpleFiles::new(); + + let id = files.add("test", ""); + let mut writer = termcolor::NoColor::new(Vec::<u8>::new()); + let diagnostic = Diagnostic::bug().with_labels(vec![Label::primary(id, 0..0)]); + + emit(&mut writer, &Config::default(), &files, &diagnostic).unwrap(); + } +} diff --git a/third_party/rust/codespan-reporting/src/term/config.rs b/third_party/rust/codespan-reporting/src/term/config.rs new file mode 100644 index 0000000000..c92a6b6047 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/term/config.rs @@ -0,0 +1,321 @@ +use termcolor::{Color, ColorSpec}; + +use crate::diagnostic::{LabelStyle, Severity}; + +/// Configures how a diagnostic is rendered. +#[derive(Clone, Debug)] +pub struct Config { + /// The display style to use when rendering diagnostics. + /// Defaults to: [`DisplayStyle::Rich`]. + /// + /// [`DisplayStyle::Rich`]: DisplayStyle::Rich + pub display_style: DisplayStyle, + /// Column width of tabs. + /// Defaults to: `4`. + pub tab_width: usize, + /// Styles to use when rendering the diagnostic. + pub styles: Styles, + /// Characters to use when rendering the diagnostic. + pub chars: Chars, + /// The minimum number of lines to be shown after the line on which a multiline [`Label`] begins. + /// + /// Defaults to: `3`. + /// + /// [`Label`]: crate::diagnostic::Label + pub start_context_lines: usize, + /// The minimum number of lines to be shown before the line on which a multiline [`Label`] ends. + /// + /// Defaults to: `1`. + /// + /// [`Label`]: crate::diagnostic::Label + pub end_context_lines: usize, +} + +impl Default for Config { + fn default() -> Config { + Config { + display_style: DisplayStyle::Rich, + tab_width: 4, + styles: Styles::default(), + chars: Chars::default(), + start_context_lines: 3, + end_context_lines: 1, + } + } +} + +/// The display style to use when rendering diagnostics. +#[derive(Clone, Debug)] +pub enum DisplayStyle { + /// Output a richly formatted diagnostic, with source code previews. + /// + /// ```text + /// error[E0001]: unexpected type in `+` application + /// โโ test:2:9 + /// โ + /// 2 โ (+ test "") + /// โ ^^ expected `Int` but found `String` + /// โ + /// = expected type `Int` + /// found type `String` + /// + /// error[E0002]: Bad config found + /// + /// ``` + Rich, + /// Output a condensed diagnostic, with a line number, severity, message and notes (if any). + /// + /// ```text + /// test:2:9: error[E0001]: unexpected type in `+` application + /// = expected type `Int` + /// found type `String` + /// + /// error[E0002]: Bad config found + /// ``` + Medium, + /// Output a short diagnostic, with a line number, severity, and message. + /// + /// ```text + /// test:2:9: error[E0001]: unexpected type in `+` application + /// error[E0002]: Bad config found + /// ``` + Short, +} + +/// Styles to use when rendering the diagnostic. +#[derive(Clone, Debug)] +pub struct Styles { + /// The style to use when rendering bug headers. + /// Defaults to `fg:red bold intense`. + pub header_bug: ColorSpec, + /// The style to use when rendering error headers. + /// Defaults to `fg:red bold intense`. + pub header_error: ColorSpec, + /// The style to use when rendering warning headers. + /// Defaults to `fg:yellow bold intense`. + pub header_warning: ColorSpec, + /// The style to use when rendering note headers. + /// Defaults to `fg:green bold intense`. + pub header_note: ColorSpec, + /// The style to use when rendering help headers. + /// Defaults to `fg:cyan bold intense`. + pub header_help: ColorSpec, + /// The style to use when the main diagnostic message. + /// Defaults to `bold intense`. + pub header_message: ColorSpec, + + /// The style to use when rendering bug labels. + /// Defaults to `fg:red`. + pub primary_label_bug: ColorSpec, + /// The style to use when rendering error labels. + /// Defaults to `fg:red`. + pub primary_label_error: ColorSpec, + /// The style to use when rendering warning labels. + /// Defaults to `fg:yellow`. + pub primary_label_warning: ColorSpec, + /// The style to use when rendering note labels. + /// Defaults to `fg:green`. + pub primary_label_note: ColorSpec, + /// The style to use when rendering help labels. + /// Defaults to `fg:cyan`. + pub primary_label_help: ColorSpec, + /// The style to use when rendering secondary labels. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub secondary_label: ColorSpec, + + /// The style to use when rendering the line numbers. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub line_number: ColorSpec, + /// The style to use when rendering the source code borders. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub source_border: ColorSpec, + /// The style to use when rendering the note bullets. + /// Defaults `fg:blue` (or `fg:cyan` on windows). + pub note_bullet: ColorSpec, +} + +impl Styles { + /// The style used to mark a header at a given severity. + pub fn header(&self, severity: Severity) -> &ColorSpec { + match severity { + Severity::Bug => &self.header_bug, + Severity::Error => &self.header_error, + Severity::Warning => &self.header_warning, + Severity::Note => &self.header_note, + Severity::Help => &self.header_help, + } + } + + /// The style used to mark a primary or secondary label at a given severity. + pub fn label(&self, severity: Severity, label_style: LabelStyle) -> &ColorSpec { + match (label_style, severity) { + (LabelStyle::Primary, Severity::Bug) => &self.primary_label_bug, + (LabelStyle::Primary, Severity::Error) => &self.primary_label_error, + (LabelStyle::Primary, Severity::Warning) => &self.primary_label_warning, + (LabelStyle::Primary, Severity::Note) => &self.primary_label_note, + (LabelStyle::Primary, Severity::Help) => &self.primary_label_help, + (LabelStyle::Secondary, _) => &self.secondary_label, + } + } + + #[doc(hidden)] + pub fn with_blue(blue: Color) -> Styles { + let header = ColorSpec::new().set_bold(true).set_intense(true).clone(); + + Styles { + header_bug: header.clone().set_fg(Some(Color::Red)).clone(), + header_error: header.clone().set_fg(Some(Color::Red)).clone(), + header_warning: header.clone().set_fg(Some(Color::Yellow)).clone(), + header_note: header.clone().set_fg(Some(Color::Green)).clone(), + header_help: header.clone().set_fg(Some(Color::Cyan)).clone(), + header_message: header, + + primary_label_bug: ColorSpec::new().set_fg(Some(Color::Red)).clone(), + primary_label_error: ColorSpec::new().set_fg(Some(Color::Red)).clone(), + primary_label_warning: ColorSpec::new().set_fg(Some(Color::Yellow)).clone(), + primary_label_note: ColorSpec::new().set_fg(Some(Color::Green)).clone(), + primary_label_help: ColorSpec::new().set_fg(Some(Color::Cyan)).clone(), + secondary_label: ColorSpec::new().set_fg(Some(blue)).clone(), + + line_number: ColorSpec::new().set_fg(Some(blue)).clone(), + source_border: ColorSpec::new().set_fg(Some(blue)).clone(), + note_bullet: ColorSpec::new().set_fg(Some(blue)).clone(), + } + } +} + +impl Default for Styles { + fn default() -> Styles { + // Blue is really difficult to see on the standard windows command line + #[cfg(windows)] + const BLUE: Color = Color::Cyan; + #[cfg(not(windows))] + const BLUE: Color = Color::Blue; + + Self::with_blue(BLUE) + } +} + +/// Characters to use when rendering the diagnostic. +/// +/// By using [`Chars::ascii()`] you can switch to an ASCII-only format suitable +/// for rendering on terminals that do not support box drawing characters. +#[derive(Clone, Debug)] +pub struct Chars { + /// The characters to use for the top-left border of the snippet. + /// Defaults to: `"โโ"` or `"-->"` with [`Chars::ascii()`]. + pub snippet_start: String, + /// The character to use for the left border of the source. + /// Defaults to: `'โ'` or `'|'` with [`Chars::ascii()`]. + pub source_border_left: char, + /// The character to use for the left border break of the source. + /// Defaults to: `'ยท'` or `'.'` with [`Chars::ascii()`]. + pub source_border_left_break: char, + + /// The character to use for the note bullet. + /// Defaults to: `'='`. + pub note_bullet: char, + + /// The character to use for marking a single-line primary label. + /// Defaults to: `'^'`. + pub single_primary_caret: char, + /// The character to use for marking a single-line secondary label. + /// Defaults to: `'-'`. + pub single_secondary_caret: char, + + /// The character to use for marking the start of a multi-line primary label. + /// Defaults to: `'^'`. + pub multi_primary_caret_start: char, + /// The character to use for marking the end of a multi-line primary label. + /// Defaults to: `'^'`. + pub multi_primary_caret_end: char, + /// The character to use for marking the start of a multi-line secondary label. + /// Defaults to: `'\''`. + pub multi_secondary_caret_start: char, + /// The character to use for marking the end of a multi-line secondary label. + /// Defaults to: `'\''`. + pub multi_secondary_caret_end: char, + /// The character to use for the top-left corner of a multi-line label. + /// Defaults to: `'โญ'` or `'/'` with [`Chars::ascii()`]. + pub multi_top_left: char, + /// The character to use for the top of a multi-line label. + /// Defaults to: `'โ'` or `'-'` with [`Chars::ascii()`]. + pub multi_top: char, + /// The character to use for the bottom-left corner of a multi-line label. + /// Defaults to: `'โฐ'` or `'\'` with [`Chars::ascii()`]. + pub multi_bottom_left: char, + /// The character to use when marking the bottom of a multi-line label. + /// Defaults to: `'โ'` or `'-'` with [`Chars::ascii()`]. + pub multi_bottom: char, + /// The character to use for the left of a multi-line label. + /// Defaults to: `'โ'` or `'|'` with [`Chars::ascii()`]. + pub multi_left: char, + + /// The character to use for the left of a pointer underneath a caret. + /// Defaults to: `'โ'` or `'|'` with [`Chars::ascii()`]. + pub pointer_left: char, +} + +impl Default for Chars { + fn default() -> Chars { + Chars::box_drawing() + } +} + +impl Chars { + /// A character set that uses Unicode box drawing characters. + pub fn box_drawing() -> Chars { + Chars { + snippet_start: "โโ".into(), + source_border_left: 'โ', + source_border_left_break: 'ยท', + + note_bullet: '=', + + single_primary_caret: '^', + single_secondary_caret: '-', + + multi_primary_caret_start: '^', + multi_primary_caret_end: '^', + multi_secondary_caret_start: '\'', + multi_secondary_caret_end: '\'', + multi_top_left: 'โญ', + multi_top: 'โ', + multi_bottom_left: 'โฐ', + multi_bottom: 'โ', + multi_left: 'โ', + + pointer_left: 'โ', + } + } + + /// A character set that only uses ASCII characters. + /// + /// This is useful if your terminal's font does not support box drawing + /// characters well and results in output that looks similar to rustc's + /// diagnostic output. + pub fn ascii() -> Chars { + Chars { + snippet_start: "-->".into(), + source_border_left: '|', + source_border_left_break: '.', + + note_bullet: '=', + + single_primary_caret: '^', + single_secondary_caret: '-', + + multi_primary_caret_start: '^', + multi_primary_caret_end: '^', + multi_secondary_caret_start: '\'', + multi_secondary_caret_end: '\'', + multi_top_left: '/', + multi_top: '-', + multi_bottom_left: '\\', + multi_bottom: '-', + multi_left: '|', + + pointer_left: '|', + } + } +} diff --git a/third_party/rust/codespan-reporting/src/term/renderer.rs b/third_party/rust/codespan-reporting/src/term/renderer.rs new file mode 100644 index 0000000000..eeb8965d25 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/term/renderer.rs @@ -0,0 +1,1020 @@ +use std::io::{self, Write}; +use std::ops::Range; +use termcolor::{ColorSpec, WriteColor}; + +use crate::diagnostic::{LabelStyle, Severity}; +use crate::files::{Error, Location}; +use crate::term::{Chars, Config, Styles}; + +/// The 'location focus' of a source code snippet. +pub struct Locus { + /// The user-facing name of the file. + pub name: String, + /// The location. + pub location: Location, +} + +/// Single-line label, with an optional message. +/// +/// ```text +/// ^^^^^^^^^ blah blah +/// ``` +pub type SingleLabel<'diagnostic> = (LabelStyle, Range<usize>, &'diagnostic str); + +/// A multi-line label to render. +/// +/// Locations are relative to the start of where the source code is rendered. +pub 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, &'diagnostic str), +} + +#[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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +/// โ โ โ โ โ +/// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +/// header โโ โ error[0001]: oh noes, a cupcake has occurred! +/// 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 struct Renderer<'writer, 'config> { + writer: &'writer mut dyn WriteColor, + config: &'config Config, +} + +impl<'writer, 'config> Renderer<'writer, 'config> { + /// Construct a renderer from the given writer and config. + pub fn new( + writer: &'writer mut dyn WriteColor, + config: &'config Config, + ) -> Renderer<'writer, 'config> { + Renderer { writer, config } + } + + fn chars(&self) -> &'config Chars { + &self.config.chars + } + + fn styles(&self) -> &'config Styles { + &self.config.styles + } + + /// Diagnostic header, with severity, code, and message. + /// + /// ```text + /// error[E0001]: unexpected type in `+` application + /// ``` + pub fn render_header( + &mut self, + locus: Option<&Locus>, + severity: Severity, + code: Option<&str>, + message: &str, + ) -> Result<(), Error> { + // Write locus + // + // ```text + // test:2:9: + // ``` + if let Some(locus) = locus { + self.snippet_locus(locus)?; + write!(self, ": ")?; + } + + // Write severity name + // + // ```text + // error + // ``` + self.set_color(self.styles().header(severity))?; + match severity { + Severity::Bug => write!(self, "bug")?, + Severity::Error => write!(self, "error")?, + Severity::Warning => write!(self, "warning")?, + Severity::Help => write!(self, "help")?, + Severity::Note => write!(self, "note")?, + } + + // Write error code + // + // ```text + // [E0001] + // ``` + if let Some(code) = &code.filter(|code| !code.is_empty()) { + write!(self, "[{}]", code)?; + } + + // Write diagnostic message + // + // ```text + // : unexpected type in `+` application + // ``` + self.set_color(&self.styles().header_message)?; + write!(self, ": {}", message)?; + self.reset()?; + + writeln!(self)?; + + Ok(()) + } + + /// Empty line. + pub fn render_empty(&mut self) -> Result<(), Error> { + writeln!(self)?; + Ok(()) + } + + /// Top left border and locus. + /// + /// ```text + /// โโ test:2:9 + /// ``` + pub fn render_snippet_start( + &mut self, + outer_padding: usize, + locus: &Locus, + ) -> Result<(), Error> { + self.outer_gutter(outer_padding)?; + + self.set_color(&self.styles().source_border)?; + write!(self, "{}", self.chars().snippet_start)?; + self.reset()?; + + write!(self, " ")?; + self.snippet_locus(&locus)?; + + writeln!(self)?; + + Ok(()) + } + + /// A line of source code. + /// + /// ```text + /// 10 โ โ muffin. Halvah croissant candy canes bonbon candy. Apple pie jelly + /// โ โญโโโโโโโโโโโ^ + /// ``` + pub fn render_snippet_source( + &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, " ")?; + let mut in_primary = false; + 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, + } + }); + + // Set the source color if we are in a primary label + if is_primary && !in_primary { + self.set_color(self.styles().label(severity, LabelStyle::Primary))?; + in_primary = true; + } else if !is_primary && in_primary { + self.reset()?; + in_primary = false; + } + + match ch { + '\t' => (0..metrics.unicode_width).try_for_each(|_| write!(self, " "))?, + _ => write!(self, "{}", ch)?, + } + } + if in_primary { + self.reset()?; + } + writeln!(self)?; + } + + // 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, " ")?; + + let mut previous_label_style = None; + 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); + + // Update writer style if necessary + if previous_label_style != current_label_style { + match current_label_style { + None => self.reset()?, + Some(label_style) => { + self.set_color(self.styles().label(severity, label_style))?; + } + } + } + + let caret_ch = match current_label_style { + Some(LabelStyle::Primary) => Some(self.chars().single_primary_caret), + Some(LabelStyle::Secondary) => Some(self.chars().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, + }; + if let Some(caret_ch) = caret_ch { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width).try_for_each(|_| write!(self, "{}", caret_ch))?; + } + + previous_label_style = current_label_style; + } + // Reset style if it was previously set + if previous_label_style.is_some() { + self.reset()?; + } + // Write first trailing label message + if let Some((_, (label_style, _, message))) = trailing_label { + write!(self, " ")?; + self.set_color(self.styles().label(severity, *label_style))?; + write!(self, "{}", message)?; + self.reset()?; + } + writeln!(self)?; + + // 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, " ")?; + self.caret_pointers( + severity, + max_label_start, + single_labels, + trailing_label, + source.char_indices(), + )?; + writeln!(self)?; + + // 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, " ")?; + self.caret_pointers( + severity, + max_label_start, + single_labels, + trailing_label, + source + .char_indices() + .take_while(|(byte_index, _)| *byte_index < range.start), + )?; + self.set_color(self.styles().label(severity, *label_style))?; + write!(self, "{}", message)?; + self.reset()?; + writeln!(self)?; + } + } + } + + // 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(()) + } + + /// An empty source line, for providing additional whitespace to source snippets. + /// + /// ```text + /// โ โ โ + /// ``` + pub 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)?; + Ok(()) + } + + /// A broken source line, for labeling skipped sections of source. + /// + /// ```text + /// ยท โ โ + /// ``` + pub 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)?; + Ok(()) + } + + /// Additional notes. + /// + /// ```text + /// = expected type `Int` + /// found type `String` + /// ``` + pub fn render_snippet_note( + &mut self, + outer_padding: usize, + message: &str, + ) -> Result<(), Error> { + for (note_line_index, line) in message.lines().enumerate() { + self.outer_gutter(outer_padding)?; + match note_line_index { + 0 => { + self.set_color(&self.styles().note_bullet)?; + write!(self, "{}", self.chars().note_bullet)?; + self.reset()?; + } + _ => write!(self, " ")?, + } + // Write line of message + writeln!(self, " {}", line)?; + } + + 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<Item = (usize, char)>, + ) -> impl Iterator<Item = (Metrics, char)> { + use unicode_width::UnicodeWidthChar; + + let tab_width = self.config.tab_width; + 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> { + write!( + self, + "{name}:{line_number}:{column_number}", + name = locus.name, + line_number = locus.location.line_number, + column_number = locus.location.column_number, + )?; + Ok(()) + } + + /// The outer gutter of a source line. + fn outer_gutter(&mut self, outer_padding: usize) -> Result<(), Error> { + write!(self, "{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.set_color(&self.styles().line_number)?; + write!( + self, + "{line_number: >width$}", + line_number = line_number, + width = outer_padding, + )?; + self.reset()?; + write!(self, " ")?; + Ok(()) + } + + /// The left-hand border of a source line. + fn border_left(&mut self) -> Result<(), Error> { + self.set_color(&self.styles().source_border)?; + write!(self, "{}", self.chars().source_border_left)?; + self.reset()?; + Ok(()) + } + + /// The broken left-hand border of a source line. + fn border_left_break(&mut self) -> Result<(), Error> { + self.set_color(&self.styles().source_border)?; + write!(self, "{}", self.chars().source_border_left_break)?; + self.reset()?; + 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<Item = (usize, char)>, + ) -> 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.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().pointer_left)?; + self.reset()?; + 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, " "))?; + } + } + + Ok(()) + } + + /// The left of a multi-line label. + /// + /// ```text + /// โ + /// ``` + fn label_multi_left( + &mut self, + severity: Severity, + label_style: LabelStyle, + underline: Option<LabelStyle>, + ) -> Result<(), Error> { + match underline { + None => write!(self, " ")?, + // Continue an underline horizontally + Some(label_style) => { + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_top)?; + self.reset()?; + } + } + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_left)?; + self.reset()?; + 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, " ")?; + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_top_left)?; + self.reset()?; + 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, " ")?; + self.set_color(self.styles().label(severity, label_style))?; + write!(self, "{}", self.chars().multi_bottom_left)?; + self.reset()?; + 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.set_color(self.styles().label(severity, label_style))?; + + for (metrics, _) in self + .char_metrics(source.char_indices()) + .take_while(|(metrics, _)| metrics.byte_index < start + 1) + { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width) + .try_for_each(|_| write!(self, "{}", self.chars().multi_top))?; + } + + let caret_start = match label_style { + LabelStyle::Primary => self.config.chars.multi_primary_caret_start, + LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start, + }; + write!(self, "{}", caret_start)?; + self.reset()?; + writeln!(self)?; + 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: &str, + ) -> Result<(), Error> { + self.set_color(self.styles().label(severity, label_style))?; + + for (metrics, _) in self + .char_metrics(source.char_indices()) + .take_while(|(metrics, _)| metrics.byte_index < start) + { + // FIXME: improve rendering of carets between character boundaries + (0..metrics.unicode_width) + .try_for_each(|_| write!(self, "{}", self.chars().multi_bottom))?; + } + + let caret_end = match label_style { + LabelStyle::Primary => self.config.chars.multi_primary_caret_start, + LabelStyle::Secondary => self.config.chars.multi_secondary_caret_start, + }; + write!(self, "{}", caret_end)?; + if !message.is_empty() { + write!(self, " {}", message)?; + } + self.reset()?; + writeln!(self)?; + Ok(()) + } + + /// Writes an empty gutter space, or continues an underline horizontally. + fn inner_gutter_column( + &mut self, + severity: Severity, + underline: Option<Underline>, + ) -> Result<(), Error> { + match underline { + None => self.inner_gutter_space(), + Some((label_style, vertical_bound)) => { + self.set_color(self.styles().label(severity, label_style))?; + let ch = match vertical_bound { + VerticalBound::Top => self.config.chars.multi_top, + VerticalBound::Bottom => self.config.chars.multi_bottom, + }; + write!(self, "{0}{0}", ch)?; + self.reset()?; + Ok(()) + } + } + } + + /// Writes an empty gutter space. + fn inner_gutter_space(&mut self) -> Result<(), Error> { + write!(self, " ")?; + 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(()) + } +} + +impl<'writer, 'config> Write for Renderer<'writer, 'config> { + fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + self.writer.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.writer.flush() + } +} + +impl<'writer, 'config> WriteColor for Renderer<'writer, 'config> { + fn supports_color(&self) -> bool { + self.writer.supports_color() + } + + fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { + self.writer.set_color(spec) + } + + fn reset(&mut self) -> io::Result<()> { + self.writer.reset() + } + + fn is_synchronous(&self) -> bool { + self.writer.is_synchronous() + } +} + +struct Metrics { + byte_index: usize, + unicode_width: usize, +} + +/// Check if two ranges overlap +fn is_overlapping(range0: &Range<usize>, range1: &Range<usize>) -> 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<Item = &'labels SingleLabel<'diagnostic>> { + 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/third_party/rust/codespan-reporting/src/term/views.rs b/third_party/rust/codespan-reporting/src/term/views.rs new file mode 100644 index 0000000000..f09d9582a6 --- /dev/null +++ b/third_party/rust/codespan-reporting/src/term/views.rs @@ -0,0 +1,478 @@ +use std::ops::Range; + +use crate::diagnostic::{Diagnostic, LabelStyle}; +use crate::files::{Error, Files, Location}; +use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel}; +use crate::term::Config; + +/// 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 +} + +/// Output a richly formatted diagnostic, with source code previews. +pub struct RichDiagnostic<'diagnostic, 'config, FileId> { + diagnostic: &'diagnostic Diagnostic<FileId>, + config: &'config Config, +} + +impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId> +where + FileId: Copy + PartialEq, +{ + pub fn new( + diagnostic: &'diagnostic Diagnostic<FileId>, + config: &'config Config, + ) -> RichDiagnostic<'diagnostic, 'config, FileId> { + RichDiagnostic { diagnostic, config } + } + + pub fn render<'files>( + &self, + files: &'files impl Files<'files, FileId = FileId>, + renderer: &mut Renderer<'_, '_>, + ) -> Result<(), Error> + where + FileId: 'files, + { + use std::collections::BTreeMap; + + struct LabeledFile<'diagnostic, FileId> { + file_id: FileId, + start: usize, + name: String, + location: Location, + num_multi_labels: usize, + lines: BTreeMap<usize, Line<'diagnostic>>, + max_label_style: LabelStyle, + } + + impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> { + fn get_or_insert_line( + &mut self, + line_index: usize, + line_range: Range<usize>, + 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: std::ops::Range<usize>, + // TODO: How do we reuse these allocations? + single_labels: Vec<SingleLabel<'diagnostic>>, + multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>, + must_render: bool, + } + + // TODO: Make this data structure external, to allow for allocation reuse + let mut labeled_files = Vec::<LabeledFile<'_, _>>::new(); + // Keep track of the outer padding to use when rendering the + // snippets of source code. + let mut outer_padding = 0; + + // Group labels by file + for label in &self.diagnostic.labels { + let start_line_index = files.line_index(label.file_id, label.range.start)?; + let start_line_number = files.line_number(label.file_id, start_line_index)?; + let start_line_range = files.line_range(label.file_id, start_line_index)?; + let end_line_index = files.line_index(label.file_id, label.range.end)?; + let end_line_number = files.line_number(label.file_id, end_line_index)?; + let end_line_range = files.line_range(label.file_id, end_line_index)?; + + outer_padding = std::cmp::max(outer_padding, count_digits(start_line_number)); + outer_padding = std::cmp::max(outer_padding, count_digits(end_line_number)); + + // NOTE: This could be made more efficient by using an associative + // data structure like a hashmap or B-tree, but we use a vector to + // preserve the order that unique files appear in the list of labels. + let labeled_file = match labeled_files + .iter_mut() + .find(|labeled_file| label.file_id == labeled_file.file_id) + { + Some(labeled_file) => { + // another diagnostic also referenced this file + 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 = files.location(label.file_id, label.range.start)?; + labeled_file.max_label_style = label.style; + } + labeled_file + } + None => { + // no other diagnostic referenced this file yet + labeled_files.push(LabeledFile { + file_id: label.file_id, + start: label.range.start, + name: files.name(label.file_id)?.to_string(), + location: files.location(label.file_id, label.range.start)?, + num_multi_labels: 0, + lines: BTreeMap::new(), + max_label_style: label.style, + }); + // this unwrap should never fail because we just pushed an element + labeled_files + .last_mut() + .expect("just pushed an element that disappeared") + } + }; + + 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<usize>` 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.clone(), + 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 = files.line_range(label.file_id, line_index)?; + let line_number = files.line_number(label.file_id, line_index)?; + + outer_padding = std::cmp::max(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 <= self.config.start_context_lines + || + // Is this line part of the context before the end of the label? + end_line_index - line_index <= self.config.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; + } + } + + // Header and message + // + // ```text + // error[E0001]: unexpected type in `+` application + // ``` + renderer.render_header( + None, + self.diagnostic.severity, + self.diagnostic.code.as_deref(), + self.diagnostic.message.as_str(), + )?; + + // Source snippets + // + // ```text + // โโ test:2:9 + // โ + // 2 โ (+ test "") + // โ ^^ expected `Int` but found `String` + // โ + // ``` + let mut labeled_files = labeled_files.into_iter().peekable(); + while let Some(labeled_file) = labeled_files.next() { + let source = files.source(labeled_file.file_id)?; + let source = source.as_ref(); + + // Top left border and locus. + // + // ```text + // โโ test:2:9 + // ``` + if !labeled_file.lines.is_empty() { + renderer.render_snippet_start( + outer_padding, + &Locus { + name: labeled_file.name, + location: labeled_file.location, + }, + )?; + renderer.render_snippet_empty( + outer_padding, + self.diagnostic.severity, + labeled_file.num_multi_labels, + &[], + )?; + } + + let mut lines = labeled_file + .lines + .iter() + .filter(|(_, line)| line.must_render) + .peekable(); + + while let Some((line_index, line)) = lines.next() { + renderer.render_snippet_source( + outer_padding, + line.number, + &source[line.range.clone()], + self.diagnostic.severity, + &line.single_labels, + labeled_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) => { + // Write a source line + let file_id = labeled_file.file_id; + + // This line was not intended to be rendered initially. + // To render the line right, we have to get back the original labels. + let labels = labeled_file + .lines + .get(&(line_index + 1)) + .map_or(&[][..], |line| &line.multi_labels[..]); + + renderer.render_snippet_source( + outer_padding, + files.line_number(file_id, line_index + 1)?, + &source[files.line_range(file_id, line_index + 1)?], + self.diagnostic.severity, + &[], + labeled_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( + outer_padding, + self.diagnostic.severity, + labeled_file.num_multi_labels, + &line.multi_labels, + )?; + } + } + } + } + + // Check to see if we should render a trailing border after the + // final line of the snippet. + if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() { + // We don't render a border if we are at the final newline + // without trailing notes, because it would end up looking too + // spaced-out in combination with the final new line. + } else { + // Render the trailing snippet border. + renderer.render_snippet_empty( + outer_padding, + self.diagnostic.severity, + labeled_file.num_multi_labels, + &[], + )?; + } + } + + // Additional notes + // + // ```text + // = expected type `Int` + // found type `String` + // ``` + for note in &self.diagnostic.notes { + renderer.render_snippet_note(outer_padding, note)?; + } + renderer.render_empty() + } +} + +/// Output a short diagnostic, with a line number, severity, and message. +pub struct ShortDiagnostic<'diagnostic, FileId> { + diagnostic: &'diagnostic Diagnostic<FileId>, + show_notes: bool, +} + +impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId> +where + FileId: Copy + PartialEq, +{ + pub fn new( + diagnostic: &'diagnostic Diagnostic<FileId>, + show_notes: bool, + ) -> ShortDiagnostic<'diagnostic, FileId> { + ShortDiagnostic { + diagnostic, + show_notes, + } + } + + pub fn render<'files>( + &self, + files: &'files impl Files<'files, FileId = FileId>, + renderer: &mut Renderer<'_, '_>, + ) -> Result<(), Error> + where + FileId: 'files, + { + // Located headers + // + // ```text + // test:2:9: error[E0001]: unexpected type in `+` application + // ``` + let mut primary_labels_encountered = 0; + let labels = self.diagnostic.labels.iter(); + for label in labels.filter(|label| label.style == LabelStyle::Primary) { + primary_labels_encountered += 1; + + renderer.render_header( + Some(&Locus { + name: files.name(label.file_id)?.to_string(), + location: files.location(label.file_id, label.range.start)?, + }), + self.diagnostic.severity, + self.diagnostic.code.as_deref(), + self.diagnostic.message.as_str(), + )?; + } + + // Fallback to printing a non-located header if no primary labels were encountered + // + // ```text + // error[E0002]: Bad config found + // ``` + if primary_labels_encountered == 0 { + renderer.render_header( + None, + self.diagnostic.severity, + self.diagnostic.code.as_deref(), + self.diagnostic.message.as_str(), + )?; + } + + if self.show_notes { + // Additional notes + // + // ```text + // = expected type `Int` + // found type `String` + // ``` + for note in &self.diagnostic.notes { + renderer.render_snippet_note(0, note)?; + } + } + + Ok(()) + } +} |