blob: 16889baa97b0a6ca874f3c35023cff87b90465e3 [file] [log] [blame]
use std::{
cmp,
fmt::{self, Display, Write},
iter::once,
};
pub mod style;
use self::style::{Style, StyleClass, Stylesheet};
#[cfg(feature = "color")]
use crate::stylesheets::color::AnsiTermStylesheet;
use crate::{display_list::*, stylesheets::no_color::NoColorStylesheet};
fn format_repeat_char(c: char, n: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for _ in 0..n {
f.write_char(c)?;
}
Ok(())
}
#[inline]
fn is_annotation_empty(annotation: &Annotation<'_>) -> bool {
annotation
.label
.iter()
.all(|fragment| fragment.content.is_empty())
}
#[cfg(feature = "color")]
#[inline]
pub fn get_term_style(color: bool) -> Box<dyn Stylesheet> {
if color {
Box::new(AnsiTermStylesheet)
} else {
Box::new(NoColorStylesheet)
}
}
#[cfg(not(feature = "color"))]
#[inline]
pub fn get_term_style(_color: bool) -> Box<dyn Stylesheet> {
Box::new(NoColorStylesheet)
}
impl<'a> fmt::Display for DisplayList<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lineno_width = self.body.iter().fold(0, |max, line| match line {
DisplayLine::Source {
lineno: Some(lineno),
..
} => {
// The largest line is the largest width.
cmp::max(*lineno, max)
}
_ => max,
});
let lineno_width = if lineno_width == 0 {
lineno_width
} else if self.anonymized_line_numbers {
Self::ANONYMIZED_LINE_NUM.len()
} else {
((lineno_width as f64).log10().floor() as usize) + 1
};
let inline_marks_width = self.body.iter().fold(0, |max, line| match line {
DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max),
_ => max,
});
for (i, line) in self.body.iter().enumerate() {
self.format_line(line, lineno_width, inline_marks_width, f)?;
if i + 1 < self.body.len() {
f.write_char('\n')?;
}
}
Ok(())
}
}
impl<'a> DisplayList<'a> {
const ANONYMIZED_LINE_NUM: &'static str = "LL";
const ERROR_TXT: &'static str = "error";
const HELP_TXT: &'static str = "help";
const INFO_TXT: &'static str = "info";
const NOTE_TXT: &'static str = "note";
const WARNING_TXT: &'static str = "warning";
#[inline]
fn format_annotation_type(
annotation_type: &DisplayAnnotationType,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
match annotation_type {
DisplayAnnotationType::Error => f.write_str(Self::ERROR_TXT),
DisplayAnnotationType::Help => f.write_str(Self::HELP_TXT),
DisplayAnnotationType::Info => f.write_str(Self::INFO_TXT),
DisplayAnnotationType::Note => f.write_str(Self::NOTE_TXT),
DisplayAnnotationType::Warning => f.write_str(Self::WARNING_TXT),
DisplayAnnotationType::None => Ok(()),
}
}
fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize {
match annotation_type {
DisplayAnnotationType::Error => Self::ERROR_TXT.len(),
DisplayAnnotationType::Help => Self::HELP_TXT.len(),
DisplayAnnotationType::Info => Self::INFO_TXT.len(),
DisplayAnnotationType::Note => Self::NOTE_TXT.len(),
DisplayAnnotationType::Warning => Self::WARNING_TXT.len(),
DisplayAnnotationType::None => 0,
}
}
fn get_annotation_style(&self, annotation_type: &DisplayAnnotationType) -> Box<dyn Style> {
self.stylesheet.get_style(match annotation_type {
DisplayAnnotationType::Error => StyleClass::Error,
DisplayAnnotationType::Warning => StyleClass::Warning,
DisplayAnnotationType::Info => StyleClass::Info,
DisplayAnnotationType::Note => StyleClass::Note,
DisplayAnnotationType::Help => StyleClass::Help,
DisplayAnnotationType::None => StyleClass::None,
})
}
fn format_label(
&self,
label: &[DisplayTextFragment<'_>],
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
let emphasis_style = self.stylesheet.get_style(StyleClass::Emphasis);
for fragment in label {
match fragment.style {
DisplayTextStyle::Regular => fragment.content.fmt(f)?,
DisplayTextStyle::Emphasis => emphasis_style.paint(fragment.content, f)?,
}
}
Ok(())
}
fn format_annotation(
&self,
annotation: &Annotation<'_>,
continuation: bool,
in_source: bool,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
let color = self.get_annotation_style(&annotation.annotation_type);
let formatted_len = if let Some(id) = &annotation.id {
2 + id.len() + Self::annotation_type_len(&annotation.annotation_type)
} else {
Self::annotation_type_len(&annotation.annotation_type)
};
if continuation {
format_repeat_char(' ', formatted_len + 2, f)?;
return self.format_label(&annotation.label, f);
}
if formatted_len == 0 {
self.format_label(&annotation.label, f)
} else {
color.paint_fn(
Box::new(|f| {
Self::format_annotation_type(&annotation.annotation_type, f)?;
if let Some(id) = &annotation.id {
f.write_char('[')?;
f.write_str(id)?;
f.write_char(']')?;
}
Ok(())
}),
f,
)?;
if !is_annotation_empty(annotation) {
if in_source {
color.paint_fn(
Box::new(|f| {
f.write_str(": ")?;
self.format_label(&annotation.label, f)
}),
f,
)?;
} else {
f.write_str(": ")?;
self.format_label(&annotation.label, f)?;
}
}
Ok(())
}
}
#[inline]
fn format_source_line(
&self,
line: &DisplaySourceLine<'_>,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
match line {
DisplaySourceLine::Empty => Ok(()),
DisplaySourceLine::Content { text, .. } => {
f.write_char(' ')?;
if let Some(margin) = self.margin {
let line_len = text.chars().count();
let mut left = margin.left(line_len);
let right = margin.right(line_len);
if margin.was_cut_left() {
// We have stripped some code/whitespace from the beginning, make it clear.
"...".fmt(f)?;
left += 3;
}
// On long lines, we strip the source line, accounting for unicode.
let mut taken = 0;
let cut_right = if margin.was_cut_right(line_len) {
taken += 3;
true
} else {
false
};
// Specifies that it will end on the next character, so it will return
// until the next one to the final condition.
let mut ended = false;
let range = text
.char_indices()
.skip(left)
// Complete char iterator with final character
.chain(once((text.len(), '\0')))
// Take until the next one to the final condition
.take_while(|(_, ch)| {
// Fast return to iterate over final byte position
if ended {
return false;
}
// Make sure that the trimming on the right will fall within the terminal width.
// FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is.
// For now, just accept that sometimes the code line will be longer than desired.
taken += unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1);
if taken > right - left {
ended = true;
}
true
})
// Reduce to start and end byte position
.fold((None, 0), |acc, (i, _)| {
if acc.0.is_some() {
(acc.0, i)
} else {
(Some(i), i)
}
});
// Format text with margins
text[range.0.expect("One character at line")..range.1].fmt(f)?;
if cut_right {
// We have stripped some code after the right-most span end, make it clear we did so.
"...".fmt(f)?;
}
Ok(())
} else {
text.fmt(f)
}
}
DisplaySourceLine::Annotation {
range,
annotation,
annotation_type,
annotation_part,
} => {
let indent_char = match annotation_part {
DisplayAnnotationPart::Standalone => ' ',
DisplayAnnotationPart::LabelContinuation => ' ',
DisplayAnnotationPart::Consequitive => ' ',
DisplayAnnotationPart::MultilineStart => '_',
DisplayAnnotationPart::MultilineEnd => '_',
};
let mark = match annotation_type {
DisplayAnnotationType::Error => '^',
DisplayAnnotationType::Warning => '-',
DisplayAnnotationType::Info => '-',
DisplayAnnotationType::Note => '-',
DisplayAnnotationType::Help => '-',
DisplayAnnotationType::None => ' ',
};
let color = self.get_annotation_style(annotation_type);
let indent_length = match annotation_part {
DisplayAnnotationPart::LabelContinuation => range.1,
DisplayAnnotationPart::Consequitive => range.1,
_ => range.0,
};
color.paint_fn(
Box::new(|f| {
format_repeat_char(indent_char, indent_length + 1, f)?;
format_repeat_char(mark, range.1 - indent_length, f)
}),
f,
)?;
if !is_annotation_empty(annotation) {
f.write_char(' ')?;
color.paint_fn(
Box::new(|f| {
self.format_annotation(
annotation,
annotation_part == &DisplayAnnotationPart::LabelContinuation,
true,
f,
)
}),
f,
)?;
}
Ok(())
}
}
}
#[inline]
fn format_raw_line(
&self,
line: &DisplayRawLine<'_>,
lineno_width: usize,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
match line {
DisplayRawLine::Origin {
path,
pos,
header_type,
} => {
let header_sigil = match header_type {
DisplayHeaderType::Initial => "-->",
DisplayHeaderType::Continuation => ":::",
};
let lineno_color = self.stylesheet.get_style(StyleClass::LineNo);
if let Some((col, row)) = pos {
format_repeat_char(' ', lineno_width, f)?;
lineno_color.paint(header_sigil, f)?;
f.write_char(' ')?;
path.fmt(f)?;
f.write_char(':')?;
col.fmt(f)?;
f.write_char(':')?;
row.fmt(f)
} else {
format_repeat_char(' ', lineno_width, f)?;
lineno_color.paint(header_sigil, f)?;
f.write_char(' ')?;
path.fmt(f)
}
}
DisplayRawLine::Annotation {
annotation,
source_aligned,
continuation,
} => {
if *source_aligned {
if *continuation {
format_repeat_char(' ', lineno_width + 3, f)?;
} else {
let lineno_color = self.stylesheet.get_style(StyleClass::LineNo);
format_repeat_char(' ', lineno_width, f)?;
f.write_char(' ')?;
lineno_color.paint("=", f)?;
f.write_char(' ')?;
}
}
self.format_annotation(annotation, *continuation, false, f)
}
}
}
#[inline]
fn format_line(
&self,
dl: &DisplayLine<'_>,
lineno_width: usize,
inline_marks_width: usize,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
match dl {
DisplayLine::Source {
lineno,
inline_marks,
line,
} => {
let lineno_color = self.stylesheet.get_style(StyleClass::LineNo);
if self.anonymized_line_numbers && lineno.is_some() {
lineno_color.paint_fn(
Box::new(|f| {
f.write_str(Self::ANONYMIZED_LINE_NUM)?;
f.write_str(" |")
}),
f,
)?;
} else {
lineno_color.paint_fn(
Box::new(|f| {
match lineno {
Some(n) => write!(f, "{:>width$}", n, width = lineno_width),
None => format_repeat_char(' ', lineno_width, f),
}?;
f.write_str(" |")
}),
f,
)?;
}
if *line != DisplaySourceLine::Empty {
if !inline_marks.is_empty() || 0 < inline_marks_width {
f.write_char(' ')?;
self.format_inline_marks(inline_marks, inline_marks_width, f)?;
}
self.format_source_line(line, f)?;
} else if !inline_marks.is_empty() {
f.write_char(' ')?;
self.format_inline_marks(inline_marks, inline_marks_width, f)?;
}
Ok(())
}
DisplayLine::Fold { inline_marks } => {
f.write_str("...")?;
if !inline_marks.is_empty() || 0 < inline_marks_width {
format_repeat_char(' ', lineno_width, f)?;
self.format_inline_marks(inline_marks, inline_marks_width, f)?;
}
Ok(())
}
DisplayLine::Raw(line) => self.format_raw_line(line, lineno_width, f),
}
}
fn format_inline_marks(
&self,
inline_marks: &[DisplayMark],
inline_marks_width: usize,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
format_repeat_char(' ', inline_marks_width - inline_marks.len(), f)?;
for mark in inline_marks {
self.get_annotation_style(&mark.annotation_type).paint_fn(
Box::new(|f| {
f.write_char(match mark.mark_type {
DisplayMarkType::AnnotationThrough => '|',
DisplayMarkType::AnnotationStart => '/',
})
}),
f,
)?;
}
Ok(())
}
}