use crate::snippet;
use std::cmp::{max, min, Reverse};
use std::collections::HashMap;
use std::fmt::Display;
use std::ops::Range;
use std::{cmp, fmt};
use crate::renderer::styled_buffer::StyledBuffer;
use crate::renderer::{stylesheet::Stylesheet, Margin, Style, DEFAULT_TERM_WIDTH};
const ANONYMIZED_LINE_NUM: &str = "LL";
const ERROR_TXT: &str = "error";
const HELP_TXT: &str = "help";
const INFO_TXT: &str = "info";
const NOTE_TXT: &str = "note";
const WARNING_TXT: &str = "warning";
pub(crate) struct DisplayList<'a> {
pub(crate) body: Vec<DisplaySet<'a>>,
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(())
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct DisplaySet<'a> {
pub(crate) display_lines: Vec<DisplayLine<'a>>,
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)
}
}
}
#[inline]
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 {
let width_offset = lineno_width + 3;
let code_offset = if multiline_depth == 0 {
width_offset
} else {
width_offset + multiline_depth + 1
};
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.as_bytes().len();
let left = self.margin.left(line_len);
let right = self.margin.right(line_len);
let mut taken = 0;
let code: String = text
.chars()
.skip(left)
.take_while(|ch| {
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() {
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() {
if overlaps(next, annotation, 0)
&& annotation.has_label()
&& j > i
&& p == 0
{
if next.range.0 == annotation.range.0
&& next.range.1 == annotation.range.1
&& !next.has_label()
{
continue;
}
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::<Vec<_>>()
.join("")
.len()
+ 2;
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)
{
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()
{
for (pos, _) in &mut annotations_positions {
*pos = max_pos - *pos;
}
line_len = line_len.saturating_sub(1);
}
}
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);
}
if !annotations_positions.is_empty() {
for pos in 0..=line_len {
buffer.putc(
line_offset + pos + 1,
lineno_width + 1,
'|',
stylesheet.line_no,
);
}
}
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);
}
}
_ => {}
}
}
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);
}
}
_ => {}
}
}
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);
}
}
}
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();
}
}
}
annotations_positions.sort_by_key(|(_, ann)| {
Reverse(ann.len())
});
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)
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Annotation<'a> {
pub(crate) annotation_type: DisplayAnnotationType,
pub(crate) id: Option<&'a str>,
pub(crate) label: Vec<DisplayTextFragment<'a>>,
}
#[derive(Debug, PartialEq)]
pub(crate) enum DisplayLine<'a> {
Source {
lineno: Option<usize>,
inline_marks: Vec<DisplayMark>,
line: DisplaySourceLine<'a>,
annotations: Vec<DisplaySourceAnnotation<'a>>,
},
Fold { inline_marks: Vec<DisplayMark> },
Raw(DisplayRawLine<'a>),
}
#[derive(Debug, PartialEq)]
pub(crate) enum DisplaySourceLine<'a> {
Content {
text: &'a str,
range: (usize, usize), end_line: EndLine,
},
Empty,
}
#[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<'_> {
fn has_label(&self) -> bool {
!self
.annotation
.label
.iter()
.all(|label| label.content.is_empty())
}
fn len(&self) -> usize {
if self.range.1 > self.range.0 {
self.range.1 - self.range.0
} else {
self.range.0 - self.range.1
}
}
fn takes_space(&self) -> bool {
matches!(
self.annotation_part,
DisplayAnnotationPart::MultilineStart(_) | DisplayAnnotationPart::MultilineEnd(_)
)
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum DisplayRawLine<'a> {
Origin {
path: &'a str,
pos: Option<(usize, usize)>,
header_type: DisplayHeaderType,
},
Annotation {
annotation: Annotation<'a>,
source_aligned: bool,
continuation: bool,
},
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct DisplayTextFragment<'a> {
pub(crate) content: &'a str,
pub(crate) style: DisplayTextStyle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum DisplayTextStyle {
Regular,
Emphasis,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum DisplayAnnotationPart {
Standalone,
LabelContinuation,
MultilineStart(usize),
MultilineEnd(usize),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct DisplayMark {
pub(crate) mark_type: DisplayMarkType,
pub(crate) annotation_type: DisplayAnnotationType,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum DisplayMarkType {
AnnotationThrough(usize),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum DisplayAnnotationType {
None,
Error,
Warning,
Info,
Note,
Help,
}
impl From<snippet::Level> 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,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum DisplayHeaderType {
Initial,
Continuation,
}
struct CursorLines<'a>(&'a str);
impl CursorLines<'_> {
fn new(src: &str) -> CursorLines<'_> {
CursorLines(src)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum EndLine {
Eof,
Lf,
Crlf,
}
impl EndLine {
pub(crate) fn len(self) -> usize {
match self {
EndLine::Eof => 0,
EndLine::Lf => 1,
EndLine::Crlf => 2,
}
}
}
impl<'a> Iterator for CursorLines<'a> {
type Item = (&'a str, EndLine);
fn next(&mut self) -> Option<Self::Item> {
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
})
}
}
}
fn format_message(
message: snippet::Message<'_>,
term_width: usize,
anonymized_line_numbers: bool,
primary: bool,
) -> Vec<DisplaySet<'_>> {
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,
!footer.is_empty(),
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<DisplayLine<'a>> {
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<DisplayTextStyle>,
) -> Vec<DisplayTextFragment<'_>> {
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,
has_footer: 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,
has_footer,
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]
fn zip_opt<A, B>(a: Option<A>, b: Option<B>) -> Option<(A, B)> {
a.and_then(|a| b.map(|b| (a, b)))
}
fn format_header<'a>(
origin: Option<&'a str>,
main_range: Option<usize>,
body: &[DisplayLine<'_>],
is_first: bool,
) -> Option<DisplayLine<'a>> {
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<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
const INNER_CONTEXT: usize = 1;
const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1;
let mut lines = vec![];
let mut unhighlighed_lines = vec![];
for line in body {
match &line {
DisplayLine::Source { annotations, .. } => {
if annotations.is_empty() {
unhighlighed_lines.push(line);
} else {
if lines.is_empty() {
unhighlighed_lines.clear();
}
match unhighlighed_lines.len() {
0 => {}
n if n <= INNER_UNFOLD_SIZE => {
lines.append(&mut unhighlighed_lines);
}
_ => {
lines.extend(unhighlighed_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(),
});
unhighlighed_lines
.drain(..unhighlighed_lines.len().saturating_sub(INNER_CONTEXT));
lines.append(&mut unhighlighed_lines);
}
}
lines.push(line);
}
}
_ => {
unhighlighed_lines.push(line);
}
}
}
lines
}
fn format_body(
snippet: snippet::Snippet<'_>,
need_empty_header: bool,
has_footer: 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| {
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<usize, usize> = HashMap::new();
let mut current_depth = 0;
let mut annotations = snippet.annotations;
let ranges = annotations
.iter()
.map(|a| a.range.clone())
.collect::<Vec<_>>();
ranges.iter().enumerate().for_each(|(r_idx, range)| {
annotations
.iter_mut()
.enumerate()
.skip(r_idx + 1)
.for_each(|(ann_idx, ann)| {
if ann_idx != r_idx
&& snippet.source[ann.range.clone()].lines().count() > 1
&& 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::<Vec<_>>();
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 {
'\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;
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 {
Range { start, .. } if start > line_end_index + end_line_size => true,
Range { start, end }
if start >= line_start_index
&& 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::<usize>();
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::<usize>();
if annotation_start_col == annotation_end_col {
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
}
Range { start, end }
if start >= line_start_index
&& 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::<usize>();
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
}
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
}
Range { start, end }
if start < line_start_index
&& end >= line_start_index
&& 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::<usize>()
.saturating_sub(1);
let (end_mark, end_plus_one) = if end > line_end_index
|| (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,
}
});
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![],
},
);
}
if has_footer {
body.push(DisplayLine::Source {
lineno: None,
inline_marks: vec![],
line: DisplaySourceLine::Empty,
annotations: vec![],
});
} else if let Some(DisplayLine::Source { .. }) = body.last() {
body.push(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,
}
}
#[inline]
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 => "",
}
}
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,
}
}
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]
fn is_annotation_empty(annotation: &Annotation<'_>) -> bool {
annotation
.label
.iter()
.all(|fragment| fragment.content.is_empty())
}
const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
('\t', " "), ('\u{200D}', ""), ('\u{202A}', ""), ('\u{202B}', ""), ('\u{202D}', ""), ('\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(())
}