use std::{fmt, ops::Range};
use super::common::Location;
#[derive(Clone, Debug)]
pub struct RenderedError {
pub text: String,
pub snippets: Vec<Snippet>,
}
impl fmt::Display for RenderedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.text)?;
for s in self.snippets.iter() {
writeln!(f, "{}", s)?;
}
Ok(())
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Truncation {
None,
Start,
End,
Both,
}
#[derive(Clone, Debug)]
pub struct Snippet {
source: String,
truncation: Truncation,
location: Location,
offset: usize,
length: usize,
explain: Option<String>,
}
impl Snippet {
const MAX_SOURCE_DISPLAY_LEN: usize = 80;
const MAX_ERROR_LINE_OFFSET: usize = 50;
pub fn from_source_location(
source: &str,
location: Location,
explain: Option<&'static str>,
) -> Self {
let line = source.split('\n').nth(location.line - 1).unwrap();
let (line, truncation, offset) = Self::truncate_line(line, location.column - 1);
Snippet {
source: line.to_owned(),
truncation,
location,
offset,
length: 1,
explain: explain.map(|x| x.into()),
}
}
pub fn from_source_location_range(
source: &str,
location: Range<Location>,
explain: Option<&'static str>,
) -> Self {
let line = source.split('\n').nth(location.start.line - 1).unwrap();
let (line, truncation, offset) = Self::truncate_line(line, location.start.column - 1);
let length = if location.start.line == location.end.line {
location.end.column - location.start.column
} else {
1
};
Snippet {
source: line.to_owned(),
truncation,
location: location.start,
offset,
length,
explain: explain.map(|x| x.into()),
}
}
fn truncate_line(mut line: &str, target_col: usize) -> (&str, Truncation, usize) {
let mut offset = 0;
for (i, (idx, c)) in line.char_indices().enumerate() {
if i == target_col || !c.is_whitespace() {
line = &line[idx..];
offset = target_col - i;
break;
}
}
line = line.trim_end();
let mut truncation = Truncation::None;
if offset > Self::MAX_ERROR_LINE_OFFSET {
let too_much_offset = offset - 10;
let mut chars = line.chars();
for _ in 0..too_much_offset {
chars.next();
}
offset = 10;
line = chars.as_str();
truncation = Truncation::Start;
}
if line.chars().count() > Self::MAX_SOURCE_DISPLAY_LEN {
let mut size = Self::MAX_SOURCE_DISPLAY_LEN - 3;
if truncation == Truncation::Start {
truncation = Truncation::Both;
size -= 3;
} else {
truncation = Truncation::End
}
let truncate_index = line.char_indices().nth(size).unwrap().0;
line = &line[..truncate_index];
}
(line, truncation, offset)
}
}
impl fmt::Display for Snippet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let spacing = self.location.line.ilog10() as usize + 1;
writeln!(f, "{:>spacing$} |", "")?;
write!(f, "{:>spacing$} | ", self.location.line)?;
match self.truncation {
Truncation::None => {
writeln!(f, "{}", self.source)?;
}
Truncation::Start => {
writeln!(f, "...{}", self.source)?;
}
Truncation::End => {
writeln!(f, "{}...", self.source)?;
}
Truncation::Both => {
writeln!(f, "...{}...", self.source)?;
}
}
let error_offset = self.offset
+ if matches!(self.truncation, Truncation::Start | Truncation::Both) {
3
} else {
0
};
write!(f, "{:>spacing$} | {:>error_offset$} ", "", "",)?;
for _ in 0..self.length {
write!(f, "^")?;
}
write!(f, " ")?;
if let Some(ref explain) = self.explain {
write!(f, "{explain}")?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::{Snippet, Truncation};
use crate::syn::common::Location;
#[test]
fn truncate_whitespace() {
let source = "\n\n\n\t $ \t";
let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
let error = &source[offset..];
let location = Location::of_in(error, source);
let snippet = Snippet::from_source_location(source, location, None);
assert_eq!(snippet.truncation, Truncation::None);
assert_eq!(snippet.offset, 0);
assert_eq!(snippet.source.as_str(), "$");
}
#[test]
fn truncate_start() {
let source = " aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa $ \t";
let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
let error = &source[offset..];
let location = Location::of_in(error, source);
let snippet = Snippet::from_source_location(source, location, None);
assert_eq!(snippet.truncation, Truncation::Start);
assert_eq!(snippet.offset, 10);
assert_eq!(snippet.source.as_str(), "aaaaaaaaa $");
}
#[test]
fn truncate_end() {
let source = "\n\n a $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \t";
let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
let error = &source[offset..];
let location = Location::of_in(error, source);
let snippet = Snippet::from_source_location(source, location, None);
assert_eq!(snippet.truncation, Truncation::End);
assert_eq!(snippet.offset, 2);
assert_eq!(
snippet.source.as_str(),
"a $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
}
#[test]
fn truncate_both() {
let source = "\n\n\n\n aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \t";
let offset = source.char_indices().find(|(_, c)| *c == '$').unwrap().0;
let error = &source[offset..];
let location = Location::of_in(error, source);
let snippet = Snippet::from_source_location(source, location, None);
assert_eq!(snippet.truncation, Truncation::Both);
assert_eq!(snippet.offset, 10);
assert_eq!(
snippet.source.as_str(),
"aaaaaaaaa $ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
}
}