summaryrefslogtreecommitdiffstats
path: root/third_party/rust/codespan-reporting/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/rust/codespan-reporting/src
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/codespan-reporting/src')
-rw-r--r--third_party/rust/codespan-reporting/src/diagnostic.rs209
-rw-r--r--third_party/rust/codespan-reporting/src/files.rs443
-rw-r--r--third_party/rust/codespan-reporting/src/lib.rs7
-rw-r--r--third_party/rust/codespan-reporting/src/term.rs121
-rw-r--r--third_party/rust/codespan-reporting/src/term/config.rs321
-rw-r--r--third_party/rust/codespan-reporting/src/term/renderer.rs1020
-rw-r--r--third_party/rust/codespan-reporting/src/term/views.rs478
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(())
+ }
+}