diff --git a/src/renderer/display/constants.rs b/src/renderer/display/constants.rs new file mode 100644 index 0000000..6fed36a --- /dev/null +++ b/src/renderer/display/constants.rs @@ -0,0 +1,6 @@ +pub(crate) const ANONYMIZED_LINE_NUM: &str = "LL"; +pub(crate) const ERROR_TXT: &str = "error"; +pub(crate) const HELP_TXT: &str = "help"; +pub(crate) const INFO_TXT: &str = "info"; +pub(crate) const NOTE_TXT: &str = "note"; +pub(crate) const WARNING_TXT: &str = "warning"; diff --git a/src/renderer/display/cursor_line.rs b/src/renderer/display/cursor_line.rs new file mode 100644 index 0000000..45040a2 --- /dev/null +++ b/src/renderer/display/cursor_line.rs @@ -0,0 +1,40 @@ +use super::end_line::EndLine; + +pub(crate) struct CursorLines<'a>(&'a str); + +impl CursorLines<'_> { + pub(crate) fn new(src: &str) -> CursorLines<'_> { + CursorLines(src) + } +} + +impl<'a> Iterator for CursorLines<'a> { + type Item = (&'a str, EndLine); + + fn next(&mut self) -> Option { + if self.0.is_empty() { + None + } else { + self.0 + .find('\n') + .map(|x| { + let ret = if 0 < x { + if self.0.as_bytes()[x - 1] == b'\r' { + (&self.0[..x - 1], EndLine::Crlf) + } else { + (&self.0[..x], EndLine::Lf) + } + } else { + ("", EndLine::Lf) + }; + self.0 = &self.0[x + 1..]; + ret + }) + .or_else(|| { + let ret = Some((self.0, EndLine::Eof)); + self.0 = ""; + ret + }) + } + } +} diff --git a/src/renderer/display/display_annotations.rs b/src/renderer/display/display_annotations.rs new file mode 100644 index 0000000..5a25ba6 --- /dev/null +++ b/src/renderer/display/display_annotations.rs @@ -0,0 +1,140 @@ +use anstyle::Style; + +use crate::{renderer::stylesheet::Stylesheet, snippet}; + +use super::{ + constants::{ERROR_TXT, HELP_TXT, INFO_TXT, NOTE_TXT, WARNING_TXT}, + display_text::DisplayTextFragment, +}; + +/// A type of the `Annotation` which may impact the sigils, style or text displayed. +/// +/// There are several ways to uses this information when formatting the `DisplayList`: +/// +/// * An annotation may display the name of the type like `error` or `info`. +/// * An underline for `Error` may be `^^^` while for `Warning` it could be `---`. +/// * `ColorStylesheet` may use different colors for different annotations. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum DisplayAnnotationType { + None, + Error, + Warning, + Info, + Note, + Help, +} + +/// An inline text +/// An indicator of what part of the annotation a given `Annotation` is. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum DisplayAnnotationPart { + /// A standalone, single-line annotation. + Standalone, + /// A continuation of a multi-line label of an annotation. + LabelContinuation, + /// A line starting a multiline annotation. + MultilineStart(usize), + /// A line ending a multiline annotation. + MultilineEnd(usize), +} + +/// Inline annotation which can be used in either Raw or Source line. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct Annotation<'a> { + pub(crate) annotation_type: DisplayAnnotationType, + pub(crate) id: Option<&'a str>, + pub(crate) label: Vec>, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DisplaySourceAnnotation<'a> { + pub(crate) annotation: Annotation<'a>, + pub(crate) range: (usize, usize), + pub(crate) annotation_type: DisplayAnnotationType, + pub(crate) annotation_part: DisplayAnnotationPart, +} + +impl DisplaySourceAnnotation<'_> { + pub(crate) fn has_label(&self) -> bool { + !self + .annotation + .label + .iter() + .all(|label| label.content.is_empty()) + } + + // Length of this annotation as displayed in the stderr output + pub(crate) fn len(&self) -> usize { + // Account for usize underflows + if self.range.1 > self.range.0 { + self.range.1 - self.range.0 + } else { + self.range.0 - self.range.1 + } + } + + pub(crate) fn takes_space(&self) -> bool { + // Multiline annotations always have to keep vertical space. + matches!( + self.annotation_part, + DisplayAnnotationPart::MultilineStart(_) | DisplayAnnotationPart::MultilineEnd(_) + ) + } +} + +impl From for DisplayAnnotationType { + fn from(at: snippet::Level) -> Self { + match at { + snippet::Level::Error => DisplayAnnotationType::Error, + snippet::Level::Warning => DisplayAnnotationType::Warning, + snippet::Level::Info => DisplayAnnotationType::Info, + snippet::Level::Note => DisplayAnnotationType::Note, + snippet::Level::Help => DisplayAnnotationType::Help, + } + } +} + +#[inline] +pub(crate) fn annotation_type_str(annotation_type: &DisplayAnnotationType) -> &'static str { + match annotation_type { + DisplayAnnotationType::Error => ERROR_TXT, + DisplayAnnotationType::Help => HELP_TXT, + DisplayAnnotationType::Info => INFO_TXT, + DisplayAnnotationType::Note => NOTE_TXT, + DisplayAnnotationType::Warning => WARNING_TXT, + DisplayAnnotationType::None => "", + } +} + +pub(crate) fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { + match annotation_type { + DisplayAnnotationType::Error => ERROR_TXT.len(), + DisplayAnnotationType::Help => HELP_TXT.len(), + DisplayAnnotationType::Info => INFO_TXT.len(), + DisplayAnnotationType::Note => NOTE_TXT.len(), + DisplayAnnotationType::Warning => WARNING_TXT.len(), + DisplayAnnotationType::None => 0, + } +} + +pub(crate) fn get_annotation_style<'a>( + annotation_type: &DisplayAnnotationType, + stylesheet: &'a Stylesheet, +) -> &'a Style { + match annotation_type { + DisplayAnnotationType::Error => stylesheet.error(), + DisplayAnnotationType::Warning => stylesheet.warning(), + DisplayAnnotationType::Info => stylesheet.info(), + DisplayAnnotationType::Note => stylesheet.note(), + DisplayAnnotationType::Help => stylesheet.help(), + DisplayAnnotationType::None => stylesheet.none(), + } +} + +#[inline] +pub(crate) fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { + annotation + .label + .iter() + .all(|fragment| fragment.content.is_empty()) +} diff --git a/src/renderer/display/display_header.rs b/src/renderer/display/display_header.rs new file mode 100644 index 0000000..968c9f7 --- /dev/null +++ b/src/renderer/display/display_header.rs @@ -0,0 +1,11 @@ +/// Information whether the header is the initial one or a consequitive one +/// for multi-slice cases. +// TODO: private +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum DisplayHeaderType { + /// Initial header is the first header in the snippet. + Initial, + + /// Continuation marks all headers of following slices in the snippet. + Continuation, +} diff --git a/src/renderer/display/display_line.rs b/src/renderer/display/display_line.rs new file mode 100644 index 0000000..b50f5f2 --- /dev/null +++ b/src/renderer/display/display_line.rs @@ -0,0 +1,64 @@ +use super::{ + display_annotations::{Annotation, DisplaySourceAnnotation}, + display_header::DisplayHeaderType, + display_mark::DisplayMark, + end_line::EndLine, +}; + +/// A single line used in `DisplayList`. +#[derive(Debug, PartialEq)] +pub(crate) enum DisplayLine<'a> { + /// A line with `lineno` portion of the slice. + Source { + lineno: Option, + inline_marks: Vec, + line: DisplaySourceLine<'a>, + annotations: Vec>, + }, + + /// A line indicating a folded part of the slice. + Fold { inline_marks: Vec }, + + /// A line which is displayed outside of slices. + Raw(DisplayRawLine<'a>), +} + +/// A source line. +#[derive(Debug, PartialEq)] +pub(crate) enum DisplaySourceLine<'a> { + /// A line with the content of the Snippet. + Content { + text: &'a str, + range: (usize, usize), // meta information for annotation placement. + end_line: EndLine, + }, + /// An empty source line. + Empty, +} + +/// Raw line - a line which does not have the `lineno` part and is not considered +/// a part of the snippet. +#[derive(Debug, PartialEq)] +pub(crate) enum DisplayRawLine<'a> { + /// A line which provides information about the location of the given + /// slice in the project structure. + Origin { + path: &'a str, + pos: Option<(usize, usize)>, + header_type: DisplayHeaderType, + }, + + /// An annotation line which is not part of any snippet. + Annotation { + annotation: Annotation<'a>, + + /// If set to `true`, the annotation will be aligned to the + /// lineno delimiter of the snippet. + source_aligned: bool, + /// If set to `true`, only the label of the `Annotation` will be + /// displayed. It allows for a multiline annotation to be aligned + /// without displaying the meta information (`type` and `id`) to be + /// displayed on each line. + continuation: bool, + }, +} diff --git a/src/renderer/display/display_list.rs b/src/renderer/display/display_list.rs new file mode 100644 index 0000000..df92a9e --- /dev/null +++ b/src/renderer/display/display_list.rs @@ -0,0 +1,120 @@ +use std::{ + cmp, + fmt::{self, Display}, +}; + +use crate::{ + renderer::{ + display::{ + constants::ANONYMIZED_LINE_NUM, display_annotations::DisplayAnnotationPart, + display_line::DisplayLine, + }, + styled_buffer::StyledBuffer, + stylesheet::Stylesheet, + }, + snippet, +}; + +use super::{display_set::DisplaySet, format_message}; + +/// List of lines to be displayed. +pub(crate) struct DisplayList<'a> { + pub(crate) body: Vec>, + pub(crate) stylesheet: &'a Stylesheet, + pub(crate) anonymized_line_numbers: bool, +} + +impl PartialEq for DisplayList<'_> { + fn eq(&self, other: &Self) -> bool { + self.body == other.body && self.anonymized_line_numbers == other.anonymized_line_numbers + } +} + +impl fmt::Debug for DisplayList<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DisplayList") + .field("body", &self.body) + .field("anonymized_line_numbers", &self.anonymized_line_numbers) + .finish() + } +} + +impl Display for DisplayList<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let lineno_width = self.body.iter().fold(0, |max, set| { + set.display_lines.iter().fold(max, |max, line| match line { + DisplayLine::Source { lineno, .. } => cmp::max(lineno.unwrap_or(0), max), + _ => max, + }) + }); + let lineno_width = if lineno_width == 0 { + lineno_width + } else if self.anonymized_line_numbers { + ANONYMIZED_LINE_NUM.len() + } else { + ((lineno_width as f64).log10().floor() as usize) + 1 + }; + + let multiline_depth = self.body.iter().fold(0, |max, set| { + set.display_lines.iter().fold(max, |max2, line| match line { + DisplayLine::Source { annotations, .. } => cmp::max( + annotations.iter().fold(max2, |max3, line| { + cmp::max( + match line.annotation_part { + DisplayAnnotationPart::Standalone => 0, + DisplayAnnotationPart::LabelContinuation => 0, + DisplayAnnotationPart::MultilineStart(depth) => depth + 1, + DisplayAnnotationPart::MultilineEnd(depth) => depth + 1, + }, + max3, + ) + }), + max, + ), + _ => max2, + }) + }); + let mut buffer = StyledBuffer::new(); + for set in self.body.iter() { + self.format_set(set, lineno_width, multiline_depth, &mut buffer)?; + } + write!(f, "{}", buffer.render(self.stylesheet)?) + } +} + +impl<'a> DisplayList<'a> { + pub(crate) fn new( + message: snippet::Message<'a>, + stylesheet: &'a Stylesheet, + anonymized_line_numbers: bool, + term_width: usize, + ) -> DisplayList<'a> { + let body = format_message(message, term_width, anonymized_line_numbers, true); + + Self { + body, + stylesheet, + anonymized_line_numbers, + } + } + + fn format_set( + &self, + set: &DisplaySet<'_>, + lineno_width: usize, + multiline_depth: usize, + buffer: &mut StyledBuffer, + ) -> fmt::Result { + for line in &set.display_lines { + set.format_line( + line, + lineno_width, + multiline_depth, + self.stylesheet, + self.anonymized_line_numbers, + buffer, + )?; + } + Ok(()) + } +} diff --git a/src/renderer/display/display_mark.rs b/src/renderer/display/display_mark.rs new file mode 100644 index 0000000..6c3a9c1 --- /dev/null +++ b/src/renderer/display/display_mark.rs @@ -0,0 +1,15 @@ +use super::display_annotations::DisplayAnnotationType; + +/// A visual mark used in `inline_marks` field of the `DisplaySourceLine`. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct DisplayMark { + pub(crate) mark_type: DisplayMarkType, + pub(crate) annotation_type: DisplayAnnotationType, +} + +/// A type of the `DisplayMark`. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum DisplayMarkType { + /// A mark indicating a multiline annotation going through the current line. + AnnotationThrough(usize), +} diff --git a/src/renderer/display/display_set.rs b/src/renderer/display/display_set.rs new file mode 100644 index 0000000..219783b --- /dev/null +++ b/src/renderer/display/display_set.rs @@ -0,0 +1,616 @@ +use core::fmt; +use std::cmp::{max, Reverse}; + +use anstyle::Style; + +use crate::renderer::{ + display::display_annotations::DisplayAnnotationPart, margin::Margin, + styled_buffer::StyledBuffer, stylesheet::Stylesheet, +}; + +use super::{ + constants::ANONYMIZED_LINE_NUM, + display_annotations::{ + annotation_type_len, annotation_type_str, get_annotation_style, is_annotation_empty, + Annotation, DisplayAnnotationType, + }, + display_header::DisplayHeaderType, + display_line::{DisplayRawLine, DisplaySourceLine}, + display_mark::DisplayMarkType, + display_text::{DisplayTextFragment, DisplayTextStyle}, + format_inline_marks, normalize_whitespace, overlaps, DisplayLine, +}; + +#[derive(Debug, PartialEq)] +pub(crate) struct DisplaySet<'a> { + pub(crate) display_lines: Vec>, + pub(crate) margin: Margin, +} + +impl DisplaySet<'_> { + fn format_label( + &self, + line_offset: usize, + label: &[DisplayTextFragment<'_>], + stylesheet: &Stylesheet, + buffer: &mut StyledBuffer, + ) -> fmt::Result { + for fragment in label { + let style = match fragment.style { + DisplayTextStyle::Regular => stylesheet.none(), + DisplayTextStyle::Emphasis => stylesheet.emphasis(), + }; + buffer.append(line_offset, fragment.content, *style); + } + Ok(()) + } + fn format_annotation( + &self, + line_offset: usize, + annotation: &Annotation<'_>, + continuation: bool, + stylesheet: &Stylesheet, + buffer: &mut StyledBuffer, + ) -> fmt::Result { + let color = get_annotation_style(&annotation.annotation_type, stylesheet); + let formatted_len = if let Some(id) = &annotation.id { + 2 + id.len() + annotation_type_len(&annotation.annotation_type) + } else { + annotation_type_len(&annotation.annotation_type) + }; + + if continuation { + for _ in 0..formatted_len + 2 { + buffer.append(line_offset, " ", Style::new()); + } + return self.format_label(line_offset, &annotation.label, stylesheet, buffer); + } + if formatted_len == 0 { + self.format_label(line_offset, &annotation.label, stylesheet, buffer) + } else { + let id = match &annotation.id { + Some(id) => format!("[{id}]"), + None => String::new(), + }; + buffer.append( + line_offset, + &format!("{}{}", annotation_type_str(&annotation.annotation_type), id), + *color, + ); + + if !is_annotation_empty(annotation) { + buffer.append(line_offset, ": ", stylesheet.none); + self.format_label(line_offset, &annotation.label, stylesheet, buffer)?; + } + Ok(()) + } + } + + #[inline] + fn format_raw_line( + &self, + line_offset: usize, + line: &DisplayRawLine<'_>, + lineno_width: usize, + stylesheet: &Stylesheet, + buffer: &mut StyledBuffer, + ) -> fmt::Result { + match line { + DisplayRawLine::Origin { + path, + pos, + header_type, + } => { + let header_sigil = match header_type { + DisplayHeaderType::Initial => "-->", + DisplayHeaderType::Continuation => ":::", + }; + let lineno_color = stylesheet.line_no(); + buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color); + buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none); + if let Some((col, row)) = pos { + buffer.append(line_offset, ":", stylesheet.none); + buffer.append(line_offset, col.to_string().as_str(), stylesheet.none); + buffer.append(line_offset, ":", stylesheet.none); + buffer.append(line_offset, row.to_string().as_str(), stylesheet.none); + } + Ok(()) + } + DisplayRawLine::Annotation { + annotation, + source_aligned, + continuation, + } => { + if *source_aligned { + if *continuation { + for _ in 0..lineno_width + 3 { + buffer.append(line_offset, " ", stylesheet.none); + } + } else { + let lineno_color = stylesheet.line_no(); + for _ in 0..lineno_width + 1 { + buffer.append(line_offset, " ", stylesheet.none); + } + buffer.append(line_offset, "=", *lineno_color); + buffer.append(line_offset, " ", *lineno_color); + } + } + self.format_annotation(line_offset, annotation, *continuation, stylesheet, buffer) + } + } + } + + // Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211 + #[inline] + pub(crate) fn format_line( + &self, + dl: &DisplayLine<'_>, + lineno_width: usize, + multiline_depth: usize, + stylesheet: &Stylesheet, + anonymized_line_numbers: bool, + buffer: &mut StyledBuffer, + ) -> fmt::Result { + let line_offset = buffer.num_lines(); + match dl { + DisplayLine::Source { + lineno, + inline_marks, + line, + annotations, + } => { + let lineno_color = stylesheet.line_no(); + if anonymized_line_numbers && lineno.is_some() { + let num = format!("{ANONYMIZED_LINE_NUM:>lineno_width$} |"); + buffer.puts(line_offset, 0, &num, *lineno_color); + } else { + match lineno { + Some(n) => { + let num = format!("{n:>lineno_width$} |"); + buffer.puts(line_offset, 0, &num, *lineno_color); + } + None => { + buffer.putc(line_offset, lineno_width + 1, '|', *lineno_color); + } + }; + } + if let DisplaySourceLine::Content { text, .. } = line { + // The width of the line number, a space, pipe, and a space + // `123 | ` is `lineno_width + 3`. + let width_offset = lineno_width + 3; + let code_offset = if multiline_depth == 0 { + width_offset + } else { + width_offset + multiline_depth + 1 + }; + + // Add any inline marks to the code line + if !inline_marks.is_empty() || 0 < multiline_depth { + format_inline_marks( + line_offset, + inline_marks, + lineno_width, + stylesheet, + buffer, + )?; + } + + let text = normalize_whitespace(text); + let line_len = text.len(); + let left = self.margin.left(line_len); + let right = self.margin.right(line_len); + + // On long lines, we strip the source line, accounting for unicode. + let mut taken = 0; + let code: String = text + .chars() + .skip(left) + .take_while(|ch| { + // Make sure that the trimming on the right will fall within the terminal width. + // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` + // is. For now, just accept that sometimes the code line will be longer than + // desired. + let next = unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1); + if taken + next > right - left { + return false; + } + taken += next; + true + }) + .collect(); + buffer.puts(line_offset, code_offset, &code, Style::new()); + if self.margin.was_cut_left() { + // We have stripped some code/whitespace from the beginning, make it clear. + buffer.puts(line_offset, code_offset, "...", *lineno_color); + } + if self.margin.was_cut_right(line_len) { + buffer.puts(line_offset, code_offset + taken - 3, "...", *lineno_color); + } + + let left: usize = text + .chars() + .take(left) + .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1)) + .sum(); + + let mut annotations = annotations.clone(); + annotations.sort_by_key(|a| Reverse(a.range.0)); + + let mut annotations_positions = vec![]; + let mut line_len: usize = 0; + let mut p = 0; + for (i, annotation) in annotations.iter().enumerate() { + for (j, next) in annotations.iter().enumerate() { + // This label overlaps with another one and both take space ( + // they have text and are not multiline lines). + if overlaps(next, annotation, 0) + && annotation.has_label() + && j > i + && p == 0 + // We're currently on the first line, move the label one line down + { + // If we're overlapping with an un-labelled annotation with the same span + // we can just merge them in the output + if next.range.0 == annotation.range.0 + && next.range.1 == annotation.range.1 + && !next.has_label() + { + continue; + } + + // This annotation needs a new line in the output. + p += 1; + break; + } + } + annotations_positions.push((p, annotation)); + for (j, next) in annotations.iter().enumerate() { + if j > i { + let l = next + .annotation + .label + .iter() + .map(|label| label.content) + .collect::>() + .join("") + .len() + + 2; + // Do not allow two labels to be in the same line if they + // overlap including padding, to avoid situations like: + // + // fn foo(x: u32) { + // -------^------ + // | | + // fn_spanx_span + // + // Both labels must have some text, otherwise they are not + // overlapping. Do not add a new line if this annotation or + // the next are vertical line placeholders. If either this + // or the next annotation is multiline start/end, move it + // to a new line so as not to overlap the horizontal lines. + if (overlaps(next, annotation, l) + && annotation.has_label() + && next.has_label()) + || (annotation.takes_space() && next.has_label()) + || (annotation.has_label() && next.takes_space()) + || (annotation.takes_space() && next.takes_space()) + || (overlaps(next, annotation, l) + && next.range.1 <= annotation.range.1 + && next.has_label() + && p == 0) + // Avoid #42595. + { + // This annotation needs a new line in the output. + p += 1; + break; + } + } + } + line_len = max(line_len, p); + } + + if line_len != 0 { + line_len += 1; + } + + if annotations_positions.iter().all(|(_, ann)| { + matches!( + ann.annotation_part, + DisplayAnnotationPart::MultilineStart(_) + ) + }) { + if let Some(max_pos) = + annotations_positions.iter().map(|(pos, _)| *pos).max() + { + // Special case the following, so that we minimize overlapping multiline spans. + // + // 3 │ X0 Y0 Z0 + // │ ┏━━━━━┛ │ │ < We are writing these lines + // │ ┃┌───────┘ │ < by reverting the "depth" of + // │ ┃│┌─────────┘ < their multilne spans. + // 4 │ ┃││ X1 Y1 Z1 + // 5 │ ┃││ X2 Y2 Z2 + // │ ┃│└────╿──│──┘ `Z` label + // │ ┃└─────│──┤ + // │ ┗━━━━━━┥ `Y` is a good letter too + // ╰╴ `X` is a good letter + for (pos, _) in &mut annotations_positions { + *pos = max_pos - *pos; + } + // We know then that we don't need an additional line for the span label, saving us + // one line of vertical space. + line_len = line_len.saturating_sub(1); + } + } + + // This is a special case where we have a multiline + // annotation that is at the start of the line disregarding + // any leading whitespace, and no other multiline + // annotations overlap it. In this case, we want to draw + // + // 2 | fn foo() { + // | _^ + // 3 | | + // 4 | | } + // | |_^ test + // + // we simplify the output to: + // + // 2 | / fn foo() { + // 3 | | + // 4 | | } + // | |_^ test + if multiline_depth == 1 + && annotations_positions.len() == 1 + && annotations_positions + .first() + .map_or(false, |(_, annotation)| { + matches!( + annotation.annotation_part, + DisplayAnnotationPart::MultilineStart(_) + ) && text + .chars() + .take(annotation.range.0) + .all(|c| c.is_whitespace()) + }) + { + let (_, ann) = annotations_positions.remove(0); + let style = get_annotation_style(&ann.annotation_type, stylesheet); + buffer.putc(line_offset, 3 + lineno_width, '/', *style); + } + + // Draw the column separator for any extra lines that were + // created + // + // After this we will have: + // + // 2 | fn foo() { + // | + // | + // | + // 3 | + // 4 | } + // | + if !annotations_positions.is_empty() { + for pos in 0..=line_len { + buffer.putc( + line_offset + pos + 1, + lineno_width + 1, + '|', + stylesheet.line_no, + ); + } + } + + // Write the horizontal lines for multiline annotations + // (only the first and last lines need this). + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | + // | + // 3 | + // 4 | } + // | _ + for &(pos, annotation) in &annotations_positions { + let style = get_annotation_style(&annotation.annotation_type, stylesheet); + let pos = pos + 1; + match annotation.annotation_part { + DisplayAnnotationPart::MultilineStart(depth) + | DisplayAnnotationPart::MultilineEnd(depth) => { + for col in width_offset + depth + ..(code_offset + annotation.range.0).saturating_sub(left) + { + buffer.putc(line_offset + pos, col + 1, '_', *style); + } + } + _ => {} + } + } + + // Write the vertical lines for labels that are on a different line as the underline. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | | + // | | + // 3 | | + // 4 | | } + // | |_ + for &(pos, annotation) in &annotations_positions { + let style = get_annotation_style(&annotation.annotation_type, stylesheet); + let pos = pos + 1; + if pos > 1 && (annotation.has_label() || annotation.takes_space()) { + for p in line_offset + 2..=line_offset + pos { + buffer.putc( + p, + (code_offset + annotation.range.0).saturating_sub(left), + '|', + *style, + ); + } + } + match annotation.annotation_part { + DisplayAnnotationPart::MultilineStart(depth) => { + for p in line_offset + pos + 1..line_offset + line_len + 2 { + buffer.putc(p, width_offset + depth, '|', *style); + } + } + DisplayAnnotationPart::MultilineEnd(depth) => { + for p in line_offset..=line_offset + pos { + buffer.putc(p, width_offset + depth, '|', *style); + } + } + _ => {} + } + } + + // Add in any inline marks for any extra lines that have + // been created. Output should look like above. + for inline_mark in inline_marks { + let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type; + let style = get_annotation_style(&inline_mark.annotation_type, stylesheet); + if annotations_positions.is_empty() { + buffer.putc(line_offset, width_offset + depth, '|', *style); + } else { + for p in line_offset..=line_offset + line_len + 1 { + buffer.putc(p, width_offset + depth, '|', *style); + } + } + } + + // Write the labels on the annotations that actually have a label. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _ test + for &(pos, annotation) in &annotations_positions { + if !is_annotation_empty(&annotation.annotation) { + let style = + get_annotation_style(&annotation.annotation_type, stylesheet); + let mut formatted_len = if let Some(id) = &annotation.annotation.id { + 2 + id.len() + + annotation_type_len(&annotation.annotation.annotation_type) + } else { + annotation_type_len(&annotation.annotation.annotation_type) + }; + let (pos, col) = if pos == 0 { + (pos + 1, (annotation.range.1 + 1).saturating_sub(left)) + } else { + (pos + 2, annotation.range.0.saturating_sub(left)) + }; + if annotation.annotation_part + == DisplayAnnotationPart::LabelContinuation + { + formatted_len = 0; + } else if formatted_len != 0 { + formatted_len += 2; + let id = match &annotation.annotation.id { + Some(id) => format!("[{id}]"), + None => String::new(), + }; + buffer.puts( + line_offset + pos, + col + code_offset, + &format!( + "{}{}: ", + annotation_type_str(&annotation.annotation_type), + id + ), + *style, + ); + } else { + formatted_len = 0; + } + let mut before = 0; + for fragment in &annotation.annotation.label { + let inner_col = before + formatted_len + col + code_offset; + buffer.puts(line_offset + pos, inner_col, fragment.content, *style); + before += fragment.content.len(); + } + } + } + + // Sort from biggest span to smallest span so that smaller spans are + // represented in the output: + // + // x | fn foo() + // | ^^^---^^ + // | | | + // | | something about `foo` + // | something about `fn foo()` + annotations_positions.sort_by_key(|(_, ann)| { + // Decreasing order. When annotations share the same length, prefer `Primary`. + Reverse(ann.len()) + }); + + // Write the underlines. + // + // After this we will have: + // + // 2 | fn foo() { + // | ____-_____^ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _^ test + for &(_, annotation) in &annotations_positions { + let mark = match annotation.annotation_type { + DisplayAnnotationType::Error => '^', + DisplayAnnotationType::Warning => '-', + DisplayAnnotationType::Info => '-', + DisplayAnnotationType::Note => '-', + DisplayAnnotationType::Help => '-', + DisplayAnnotationType::None => ' ', + }; + let style = get_annotation_style(&annotation.annotation_type, stylesheet); + for p in annotation.range.0..annotation.range.1 { + buffer.putc( + line_offset + 1, + (code_offset + p).saturating_sub(left), + mark, + *style, + ); + } + } + } else if !inline_marks.is_empty() { + format_inline_marks( + line_offset, + inline_marks, + lineno_width, + stylesheet, + buffer, + )?; + } + Ok(()) + } + DisplayLine::Fold { inline_marks } => { + buffer.puts(line_offset, 0, "...", *stylesheet.line_no()); + if !inline_marks.is_empty() || 0 < multiline_depth { + format_inline_marks( + line_offset, + inline_marks, + lineno_width, + stylesheet, + buffer, + )?; + } + Ok(()) + } + DisplayLine::Raw(line) => { + self.format_raw_line(line_offset, line, lineno_width, stylesheet, buffer) + } + } + } +} diff --git a/src/renderer/display/display_text.rs b/src/renderer/display/display_text.rs new file mode 100644 index 0000000..b845cc2 --- /dev/null +++ b/src/renderer/display/display_text.rs @@ -0,0 +1,15 @@ +/// An inline text fragment which any label is composed of. +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DisplayTextFragment<'a> { + pub(crate) content: &'a str, + pub(crate) style: DisplayTextStyle, +} + +/// A style for the `DisplayTextFragment` which can be visually formatted. +/// +/// This information may be used to emphasis parts of the label. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum DisplayTextStyle { + Regular, + Emphasis, +} diff --git a/src/renderer/display/end_line.rs b/src/renderer/display/end_line.rs new file mode 100644 index 0000000..e09f279 --- /dev/null +++ b/src/renderer/display/end_line.rs @@ -0,0 +1,17 @@ +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) enum EndLine { + Eof, + Lf, + Crlf, +} + +impl EndLine { + /// The number of characters this line ending occupies in bytes. + pub(crate) fn len(self) -> usize { + match self { + EndLine::Eof => 0, + EndLine::Lf => 1, + EndLine::Crlf => 2, + } + } +} diff --git a/src/renderer/display/mod.rs b/src/renderer/display/mod.rs new file mode 100644 index 0000000..36f525f --- /dev/null +++ b/src/renderer/display/mod.rs @@ -0,0 +1,757 @@ +//! `display_list` module stores the output model for the snippet. +//! +//! `DisplayList` is a central structure in the crate, which contains +//! the structured list of lines to be displayed. +//! +//! It is made of two types of lines: `Source` and `Raw`. All `Source` lines +//! are structured using four columns: +//! +//! ```text +//! /------------ (1) Line number column. +//! | /--------- (2) Line number column delimiter. +//! | | /------- (3) Inline marks column. +//! | | | /--- (4) Content column with the source and annotations for slices. +//! | | | | +//! ============================================================================= +//! error[E0308]: mismatched types +//! --> src/format.rs:51:5 +//! | +//! 151 | / fn test() -> String { +//! 152 | | return "test"; +//! 153 | | } +//! | |___^ error: expected `String`, for `&str`. +//! ``` +//! +//! The first two lines of the example above are `Raw` lines, while the rest +//! are `Source` lines. +//! +//! `DisplayList` does not store column alignment information, and those are +//! only calculated by the implementation of `std::fmt::Display` using information such as +//! styling. +//! +//! The above snippet has been built out of the following structure: + +mod constants; +mod cursor_line; +mod display_annotations; +mod display_header; +mod display_line; +mod display_list; +mod display_mark; +mod display_set; +mod display_text; +mod end_line; + +use crate::snippet; +use std::cmp::{max, min}; +use std::collections::HashMap; +use std::fmt; +use std::ops::Range; + +use crate::renderer::styled_buffer::StyledBuffer; +use crate::renderer::{stylesheet::Stylesheet, Margin, DEFAULT_TERM_WIDTH}; + +use constants::ANONYMIZED_LINE_NUM; +use cursor_line::CursorLines; +use display_annotations::{ + get_annotation_style, Annotation, DisplayAnnotationPart, DisplayAnnotationType, + DisplaySourceAnnotation, +}; +use display_header::DisplayHeaderType; +use display_line::{DisplayLine, DisplayRawLine, DisplaySourceLine}; +use display_mark::{DisplayMark, DisplayMarkType}; +use display_set::DisplaySet; +use display_text::{DisplayTextFragment, DisplayTextStyle}; + +pub(crate) use display_list::DisplayList; + +pub(crate) fn format_message( + message: snippet::Message<'_>, + term_width: usize, + anonymized_line_numbers: bool, + primary: bool, +) -> Vec> { + let snippet::Message { + level, + id, + title, + footer, + snippets, + } = message; + + let mut sets = vec![]; + let body = if !snippets.is_empty() || primary { + vec![format_title(level, id, title)] + } else { + format_footer(level, id, title) + }; + + for (idx, snippet) in snippets.into_iter().enumerate() { + let snippet = fold_prefix_suffix(snippet); + sets.push(format_snippet( + snippet, + idx == 0, + term_width, + anonymized_line_numbers, + )); + } + + if let Some(first) = sets.first_mut() { + for line in body { + first.display_lines.insert(0, line); + } + } else { + sets.push(DisplaySet { + display_lines: body, + margin: Margin::new(0, 0, 0, 0, DEFAULT_TERM_WIDTH, 0), + }); + } + + for annotation in footer { + sets.extend(format_message( + annotation, + term_width, + anonymized_line_numbers, + false, + )); + } + + sets +} + +fn format_title<'a>(level: crate::Level, id: Option<&'a str>, label: &'a str) -> DisplayLine<'a> { + DisplayLine::Raw(DisplayRawLine::Annotation { + annotation: Annotation { + annotation_type: DisplayAnnotationType::from(level), + id, + label: format_label(Some(label), Some(DisplayTextStyle::Emphasis)), + }, + source_aligned: false, + continuation: false, + }) +} + +fn format_footer<'a>( + level: crate::Level, + id: Option<&'a str>, + label: &'a str, +) -> Vec> { + let mut result = vec![]; + for (i, line) in label.lines().enumerate() { + result.push(DisplayLine::Raw(DisplayRawLine::Annotation { + annotation: Annotation { + annotation_type: DisplayAnnotationType::from(level), + id, + label: format_label(Some(line), None), + }, + source_aligned: true, + continuation: i != 0, + })); + } + result +} + +fn format_label( + label: Option<&str>, + style: Option, +) -> Vec> { + let mut result = vec![]; + if let Some(label) = label { + let element_style = style.unwrap_or(DisplayTextStyle::Regular); + result.push(DisplayTextFragment { + content: label, + style: element_style, + }); + } + result +} + +fn format_snippet( + snippet: snippet::Snippet<'_>, + is_first: bool, + term_width: usize, + anonymized_line_numbers: bool, +) -> DisplaySet<'_> { + let main_range = snippet.annotations.first().map(|x| x.range.start); + let origin = snippet.origin; + let need_empty_header = origin.is_some() || is_first; + let mut body = format_body( + snippet, + need_empty_header, + term_width, + anonymized_line_numbers, + ); + let header = format_header(origin, main_range, &body.display_lines, is_first); + + if let Some(header) = header { + body.display_lines.insert(0, header); + } + + body +} + +#[inline] +// TODO: option_zip +fn zip_opt(a: Option, b: Option) -> Option<(A, B)> { + a.and_then(|a| b.map(|b| (a, b))) +} + +fn format_header<'a>( + origin: Option<&'a str>, + main_range: Option, + body: &[DisplayLine<'_>], + is_first: bool, +) -> Option> { + let display_header = if is_first { + DisplayHeaderType::Initial + } else { + DisplayHeaderType::Continuation + }; + + if let Some((main_range, path)) = zip_opt(main_range, origin) { + let mut col = 1; + let mut line_offset = 1; + + for item in body { + if let DisplayLine::Source { + line: + DisplaySourceLine::Content { + text, + range, + end_line, + }, + lineno, + .. + } = item + { + if main_range >= range.0 && main_range < range.1 + max(*end_line as usize, 1) { + let char_column = text[0..(main_range - range.0).min(text.len())] + .chars() + .count(); + col = char_column + 1; + line_offset = lineno.unwrap_or(1); + break; + } + } + } + + return Some(DisplayLine::Raw(DisplayRawLine::Origin { + path, + pos: Some((line_offset, col)), + header_type: display_header, + })); + } + + if let Some(path) = origin { + return Some(DisplayLine::Raw(DisplayRawLine::Origin { + path, + pos: None, + header_type: display_header, + })); + } + + None +} + +fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_> { + if !snippet.fold { + return snippet; + } + + let ann_start = snippet + .annotations + .iter() + .map(|ann| ann.range.start) + .min() + .unwrap_or(0); + if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') { + let new_start = before_new_start + 1; + + let line_offset = newline_count(&snippet.source[..new_start]); + snippet.line_start += line_offset; + + snippet.source = &snippet.source[new_start..]; + + for ann in &mut snippet.annotations { + let range_start = ann.range.start - new_start; + let range_end = ann.range.end - new_start; + ann.range = range_start..range_end; + } + } + + let ann_end = snippet + .annotations + .iter() + .map(|ann| ann.range.end) + .max() + .unwrap_or(snippet.source.len()); + if let Some(end_offset) = snippet.source[ann_end..].find('\n') { + let new_end = ann_end + end_offset; + snippet.source = &snippet.source[..new_end]; + } + + snippet +} + +fn newline_count(body: &str) -> usize { + #[cfg(feature = "simd")] + { + memchr::memchr_iter(b'\n', body.as_bytes()).count() + } + #[cfg(not(feature = "simd"))] + { + body.lines().count() + } +} + +fn fold_body(body: Vec>) -> Vec> { + const INNER_CONTEXT: usize = 1; + const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1; + + let mut lines = vec![]; + let mut unhighlighted_lines = vec![]; + for line in body { + match &line { + DisplayLine::Source { annotations, .. } => { + if annotations.is_empty() { + unhighlighted_lines.push(line); + } else { + if lines.is_empty() { + // Ignore leading unhighlighted lines + unhighlighted_lines.clear(); + } + match unhighlighted_lines.len() { + 0 => {} + n if n <= INNER_UNFOLD_SIZE => { + // Rather than render `...`, don't fold + lines.append(&mut unhighlighted_lines); + } + _ => { + lines.extend(unhighlighted_lines.drain(..INNER_CONTEXT)); + let inline_marks = lines + .last() + .and_then(|line| { + if let DisplayLine::Source { + ref inline_marks, .. + } = line + { + let inline_marks = inline_marks.clone(); + Some(inline_marks) + } else { + None + } + }) + .unwrap_or_default(); + lines.push(DisplayLine::Fold { + inline_marks: inline_marks.clone(), + }); + unhighlighted_lines + .drain(..unhighlighted_lines.len().saturating_sub(INNER_CONTEXT)); + lines.append(&mut unhighlighted_lines); + } + } + lines.push(line); + } + } + _ => { + unhighlighted_lines.push(line); + } + } + } + + lines +} + +fn format_body( + snippet: snippet::Snippet<'_>, + need_empty_header: bool, + term_width: usize, + anonymized_line_numbers: bool, +) -> DisplaySet<'_> { + let source_len = snippet.source.len(); + if let Some(bigger) = snippet.annotations.iter().find_map(|x| { + // Allow highlighting one past the last character in the source. + if source_len + 1 < x.range.end { + Some(&x.range) + } else { + None + } + }) { + panic!("SourceAnnotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") + } + + let mut body = vec![]; + let mut current_line = snippet.line_start; + let mut current_index = 0; + + let mut whitespace_margin = usize::MAX; + let mut span_left_margin = usize::MAX; + let mut span_right_margin = 0; + let mut label_right_margin = 0; + let mut max_line_len = 0; + + let mut depth_map: HashMap = HashMap::new(); + let mut current_depth = 0; + let mut annotations = snippet.annotations; + let ranges = annotations + .iter() + .map(|a| a.range.clone()) + .collect::>(); + // We want to merge multiline annotations that have the same range into one + // multiline annotation to save space. This is done by making any duplicate + // multiline annotations into a single-line annotation pointing at the end + // of the range. + // + // 3 | X0 Y0 Z0 + // | _____^ + // | | ____| + // | || ___| + // | ||| + // 4 | ||| X1 Y1 Z1 + // 5 | ||| X2 Y2 Z2 + // | ||| ^ + // | |||____| + // | ||____`X` is a good letter + // | |____`Y` is a good letter too + // | `Z` label + // Should be + // error: foo + // --> test.rs:3:3 + // | + // 3 | / X0 Y0 Z0 + // 4 | | X1 Y1 Z1 + // 5 | | X2 Y2 Z2 + // | | ^ + // | |____| + // | `X` is a good letter + // | `Y` is a good letter too + // | `Z` label + // | + ranges.iter().enumerate().for_each(|(r_idx, range)| { + annotations + .iter_mut() + .enumerate() + .skip(r_idx + 1) + .for_each(|(ann_idx, ann)| { + // Skip if the annotation's index matches the range index + if ann_idx != r_idx + // We only want to merge multiline annotations + && snippet.source[ann.range.clone()].lines().count() > 1 + // We only want to merge annotations that have the same range + && ann.range.start == range.start + && ann.range.end == range.end + { + ann.range.start = ann.range.end.saturating_sub(1); + } + }); + }); + annotations.sort_by_key(|a| a.range.start); + let mut annotations = annotations.into_iter().enumerate().collect::>(); + + for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() { + let line_length: usize = line.len(); + let line_range = (current_index, current_index + line_length); + let end_line_size = end_line.len(); + body.push(DisplayLine::Source { + lineno: Some(current_line), + inline_marks: vec![], + line: DisplaySourceLine::Content { + text: line, + range: line_range, + end_line, + }, + annotations: vec![], + }); + + let leading_whitespace = line + .chars() + .take_while(|c| c.is_whitespace()) + .map(|c| { + match c { + // Tabs are displayed as 4 spaces + '\t' => 4, + _ => 1, + } + }) + .sum(); + if line.chars().any(|c| !c.is_whitespace()) { + whitespace_margin = min(whitespace_margin, leading_whitespace); + } + max_line_len = max(max_line_len, line_length); + + let line_start_index = line_range.0; + let line_end_index = line_range.1; + current_line += 1; + current_index += line_length + end_line_size; + + // It would be nice to use filter_drain here once it's stable. + annotations.retain(|(key, annotation)| { + let body_idx = idx; + let annotation_type = match annotation.level { + snippet::Level::Error => DisplayAnnotationType::None, + snippet::Level::Warning => DisplayAnnotationType::None, + _ => DisplayAnnotationType::from(annotation.level), + }; + let label_right = annotation.label.map_or(0, |label| label.len() + 1); + match annotation.range { + // This handles if the annotation is on the next line. We add + // the `end_line_size` to account for annotating the line end. + Range { start, .. } if start > line_end_index + end_line_size => true, + // This handles the case where an annotation is contained + // within the current line including any line-end characters. + Range { start, end } + if start >= line_start_index + // We add at least one to `line_end_index` to allow + // highlighting the end of a file + && end <= line_end_index + max(end_line_size, 1) => + { + if let DisplayLine::Source { + ref mut annotations, + .. + } = body[body_idx] + { + let annotation_start_col = line + [0..(start - line_start_index).min(line_length)] + .chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum::(); + let mut annotation_end_col = line + [0..(end - line_start_index).min(line_length)] + .chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum::(); + if annotation_start_col == annotation_end_col { + // At least highlight something + annotation_end_col += 1; + } + + span_left_margin = min(span_left_margin, annotation_start_col); + span_right_margin = max(span_right_margin, annotation_end_col); + label_right_margin = + max(label_right_margin, annotation_end_col + label_right); + + let range = (annotation_start_col, annotation_end_col); + annotations.push(DisplaySourceAnnotation { + annotation: Annotation { + annotation_type, + id: None, + label: format_label(annotation.label, None), + }, + range, + annotation_type: DisplayAnnotationType::from(annotation.level), + annotation_part: DisplayAnnotationPart::Standalone, + }); + } + false + } + // This handles the case where a multiline annotation starts + // somewhere on the current line, including any line-end chars + Range { start, end } + if start >= line_start_index + // The annotation can start on a line ending + && start <= line_end_index + end_line_size.saturating_sub(1) + && end > line_end_index => + { + if let DisplayLine::Source { + ref mut annotations, + .. + } = body[body_idx] + { + let annotation_start_col = line + [0..(start - line_start_index).min(line_length)] + .chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum::(); + let annotation_end_col = annotation_start_col + 1; + + span_left_margin = min(span_left_margin, annotation_start_col); + span_right_margin = max(span_right_margin, annotation_end_col); + label_right_margin = + max(label_right_margin, annotation_end_col + label_right); + + let range = (annotation_start_col, annotation_end_col); + annotations.push(DisplaySourceAnnotation { + annotation: Annotation { + annotation_type, + id: None, + label: vec![], + }, + range, + annotation_type: DisplayAnnotationType::from(annotation.level), + annotation_part: DisplayAnnotationPart::MultilineStart(current_depth), + }); + depth_map.insert(*key, current_depth); + current_depth += 1; + } + true + } + // This handles the case where a multiline annotation starts + // somewhere before this line and ends after it as well + Range { start, end } + if start < line_start_index && end > line_end_index + max(end_line_size, 1) => + { + if let DisplayLine::Source { + ref mut inline_marks, + .. + } = body[body_idx] + { + let depth = depth_map.get(key).cloned().unwrap_or_default(); + inline_marks.push(DisplayMark { + mark_type: DisplayMarkType::AnnotationThrough(depth), + annotation_type: DisplayAnnotationType::from(annotation.level), + }); + } + true + } + // This handles the case where a multiline annotation ends + // somewhere on the current line, including any line-end chars + Range { start, end } + if start < line_start_index + && end >= line_start_index + // We add at least one to `line_end_index` to allow + // highlighting the end of a file + && end <= line_end_index + max(end_line_size, 1) => + { + if let DisplayLine::Source { + ref mut annotations, + .. + } = body[body_idx] + { + let end_mark = line[0..(end - line_start_index).min(line_length)] + .chars() + .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) + .sum::() + .saturating_sub(1); + // If the annotation ends on a line-end character, we + // need to annotate one past the end of the line + let (end_mark, end_plus_one) = if end > line_end_index + // Special case for highlighting the end of a file + || (end == line_end_index + 1 && end_line_size == 0) + { + (end_mark + 1, end_mark + 2) + } else { + (end_mark, end_mark + 1) + }; + + span_left_margin = min(span_left_margin, end_mark); + span_right_margin = max(span_right_margin, end_plus_one); + label_right_margin = max(label_right_margin, end_plus_one + label_right); + + let range = (end_mark, end_plus_one); + let depth = depth_map.remove(key).unwrap_or(0); + annotations.push(DisplaySourceAnnotation { + annotation: Annotation { + annotation_type, + id: None, + label: format_label(annotation.label, None), + }, + range, + annotation_type: DisplayAnnotationType::from(annotation.level), + annotation_part: DisplayAnnotationPart::MultilineEnd(depth), + }); + } + false + } + _ => true, + } + }); + // Reset the depth counter, but only after we've processed all + // annotations for a given line. + let max = depth_map.len(); + if current_depth > max { + current_depth = max; + } + } + + if snippet.fold { + body = fold_body(body); + } + + if need_empty_header { + body.insert( + 0, + DisplayLine::Source { + lineno: None, + inline_marks: vec![], + line: DisplaySourceLine::Empty, + annotations: vec![], + }, + ); + } + + let max_line_num_len = if anonymized_line_numbers { + ANONYMIZED_LINE_NUM.len() + } else { + current_line.to_string().len() + }; + + let width_offset = 3 + max_line_num_len; + + if span_left_margin == usize::MAX { + span_left_margin = 0; + } + + let margin = Margin::new( + whitespace_margin, + span_left_margin, + span_right_margin, + label_right_margin, + term_width.saturating_sub(width_offset), + max_line_len, + ); + + DisplaySet { + display_lines: body, + margin, + } +} + +// We replace some characters so the CLI output is always consistent and underlines aligned. +const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ + ('\t', " "), // We do our own tab replacement + ('\u{200D}', ""), // Replace ZWJ with nothing for consistent terminal output of grapheme clusters. + ('\u{202A}', ""), // The following unicode text flow control characters are inconsistently + ('\u{202B}', ""), // supported across CLIs and can cause confusion due to the bytes on disk + ('\u{202D}', ""), // not corresponding to the visible source code, so we replace them always. + ('\u{202E}', ""), + ('\u{2066}', ""), + ('\u{2067}', ""), + ('\u{2068}', ""), + ('\u{202C}', ""), + ('\u{2069}', ""), +]; + +fn normalize_whitespace(str: &str) -> String { + let mut s = str.to_owned(); + for (c, replacement) in OUTPUT_REPLACEMENTS { + s = s.replace(*c, replacement); + } + s +} + +fn overlaps( + a1: &DisplaySourceAnnotation<'_>, + a2: &DisplaySourceAnnotation<'_>, + padding: usize, +) -> bool { + (a2.range.0..a2.range.1).contains(&a1.range.0) + || (a1.range.0..a1.range.1 + padding).contains(&a2.range.0) +} + +fn format_inline_marks( + line: usize, + inline_marks: &[DisplayMark], + lineno_width: usize, + stylesheet: &Stylesheet, + buf: &mut StyledBuffer, +) -> fmt::Result { + for mark in inline_marks.iter() { + let annotation_style = get_annotation_style(&mark.annotation_type, stylesheet); + match mark.mark_type { + DisplayMarkType::AnnotationThrough(depth) => { + buf.putc(line, 3 + lineno_width + depth, '|', *annotation_style); + } + }; + } + Ok(()) +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index b9edcc6..c396e87 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -10,14 +10,14 @@ //! let renderer = Renderer::styled(); //! println!("{}", renderer.render(snippet)); -mod display_list; +mod display; mod margin; mod styled_buffer; pub(crate) mod stylesheet; use crate::snippet::Message; pub use anstyle::*; -use display_list::DisplayList; +use display::DisplayList; use margin::Margin; use std::fmt::Display; use stylesheet::Stylesheet;