use {
crate::format::CodeStr,
colored::{Colorize, control::SHOULD_COLORIZE},
pad::{Alignment, PadStr},
std::{
cmp::{max, min},
error, fmt,
path::Path,
rc::Rc,
},
};
#[derive(Clone, Debug)]
pub struct Error {
pub message: String,
pub reason: Option<Rc<dyn error::Error>>,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(reason) = &self.reason {
write!(
f,
"{}\n\n{} {}",
self.message,
"Reason:".blue().bold(),
reason,
)
} else {
write!(f, "{}", self.message)
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
self.reason.as_deref()
}
}
pub fn throw<T: error::Error + 'static>(
message: &str,
source_path: Option<&Path>,
listing: Option<&str>,
reason: Option<T>,
) -> Error {
#[allow(clippy::option_map_or_none)]
Error {
message: if let Some(path) = source_path {
if let Some(listing) = listing {
if listing.is_empty() {
format!(
"{} {} {}",
"[Error]".red().bold(),
format!("[{}]", path.to_string_lossy().code_str()).magenta(),
message,
)
} else {
format!(
"{} {} {}\n\n{}",
"[Error]".red().bold(),
format!("[{}]", path.to_string_lossy().code_str()).magenta(),
message,
listing,
)
}
} else {
format!(
"{} {} {}",
"[Error]".red().bold(),
format!("[{}]", path.to_string_lossy().code_str()).magenta(),
message,
)
}
} else if let Some(listing) = listing {
if listing.is_empty() {
format!("{} {}", "[Error]".red().bold(), message)
} else {
format!("{} {}\n\n{}", "[Error]".red().bold(), message, listing)
}
} else {
format!("{} {}", "[Error]".red().bold(), message)
},
reason: reason.map_or(None, |reason| Some(Rc::new(reason))),
}
}
#[derive(Clone, Copy, Debug)]
pub struct SourceRange {
pub start: usize, pub end: usize, }
pub fn listing(source_contents: &str, source_range: SourceRange) -> String {
let mut lines = vec![];
let mut pos = 0_usize;
for (i, line) in source_contents.split('\n').enumerate() {
let line_start = pos;
pos += line.len() + 1;
if line_start >= source_range.end {
break;
}
if pos <= source_range.start {
continue;
}
let trimmed_line = line.trim_end();
let (section_start, section_end) = if source_range.start > line_start {
(
min(source_range.start - line_start, trimmed_line.len()),
min(source_range.end - line_start, trimmed_line.len()),
)
} else {
let end = min(source_range.end - line_start, trimmed_line.len());
let start = trimmed_line
.find(|c: char| !c.is_whitespace())
.unwrap_or(end);
(start, end)
};
lines.push((
(i + 1).to_string(),
trimmed_line,
section_start,
section_end,
));
}
let gutter_width = lines.iter().fold(0_usize, |acc, (line_number, _, _, _)| {
max(acc, line_number.len())
});
let colorized = SHOULD_COLORIZE.should_colorize();
lines
.iter()
.enumerate()
.map(|(i, (line_number, line, section_start, section_end))| {
format!(
"{}{}{}{}{}",
format!(
"{} \u{2502} ",
line_number.pad(gutter_width, ' ', Alignment::Right, false),
)
.blue()
.bold(),
&line[..*section_start],
&line[*section_start..*section_end].red(),
&line[*section_end..],
if colorized {
String::new()
} else if section_start == section_end {
format!(
"\n{} {}",
" ".repeat(gutter_width),
if i == lines.len() - 1 {
" "
} else {
"\u{250a}"
},
)
} else {
format!(
"\n{} {} {}{}",
" ".repeat(gutter_width),
if i == lines.len() - 1 {
" "
} else {
"\u{250a}"
},
" ".repeat(*section_start),
"\u{203e}".repeat(section_end - section_start),
)
},
)
})
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use {
crate::{
assert_same,
error::{Error, SourceRange, listing, throw},
},
std::{path::Path, rc::Rc},
};
#[test]
fn error_no_reason_display() {
assert_eq!(
format!(
"{}",
Error {
message: "Something went wrong.".to_owned(),
reason: None,
},
),
"Something went wrong.",
);
}
#[test]
fn error_with_reason_display() {
assert_eq!(
format!(
"{}",
Error {
message: "Something went wrong.".to_owned(),
reason: Some(Rc::new(Error {
message: "Something deeper went wrong.".to_owned(),
reason: None,
})),
},
),
"\
Something went wrong.\n\
\n\
Reason: Something deeper went wrong.\
",
);
}
#[test]
fn throw_no_source_path_listing_reason() {
assert_same!(
throw::<Error>("An error occurred.", None, None, None),
Error {
message: "[Error] An error occurred.".to_owned(),
reason: None,
},
);
}
#[test]
fn throw_with_source_path_no_listing_reason() {
assert_same!(
throw::<Error>("An error occurred.", Some(Path::new("foo")), None, None),
Error {
message: "[Error] [`foo`] An error occurred.".to_owned(),
reason: None,
},
);
}
#[test]
fn throw_with_listing_no_source_path_reason() {
assert_same!(
throw::<Error>("An error occurred.", None, Some("It happened here."), None),
Error {
message: "\
[Error] An error occurred.\n\
\n\
It happened here.\
"
.to_owned(),
reason: None,
},
);
}
#[test]
fn throw_with_reason_no_source_path_listing() {
let reason = throw::<Error>("An deeper error occurred.", None, None, None);
assert_same!(
throw::<Error>("An error occurred.", None, None, Some(reason.clone())),
Error {
message: "[Error] An error occurred.".to_owned(),
reason: Some(Rc::new(reason)),
},
);
}
#[test]
fn throw_with_source_path_listing_no_reason() {
assert_same!(
throw::<Error>(
"An error occurred.",
Some(Path::new("foo")),
Some("It happened here."),
None,
),
Error {
message: "\
[Error] [`foo`] An error occurred.\n\
\n\
It happened here.\
"
.to_owned(),
reason: None,
},
);
}
#[test]
fn throw_with_listing_reason_no_source_path() {
let reason = throw::<Error>("An deeper error occurred.", None, None, None);
assert_same!(
throw::<Error>(
"An error occurred.",
None,
Some("It happened here."),
Some(reason.clone()),
),
Error {
message: "\
[Error] An error occurred.\n\
\n\
It happened here.\
"
.to_owned(),
reason: Some(Rc::new(reason)),
},
);
}
#[test]
fn throw_with_source_path_reason_no_listing() {
let reason = throw::<Error>("An deeper error occurred.", None, None, None);
assert_same!(
throw::<Error>(
"An error occurred.",
Some(Path::new("foo")),
None,
Some(reason.clone()),
),
Error {
message: "[Error] [`foo`] An error occurred.".to_owned(),
reason: Some(Rc::new(reason)),
},
);
}
#[test]
fn throw_with_source_path_listing_reason() {
let reason = throw::<Error>("An deeper error occurred.", None, None, None);
assert_same!(
throw::<Error>(
"An error occurred.",
Some(Path::new("foo")),
Some("It happened here."),
Some(reason.clone()),
),
Error {
message: "\
[Error] [`foo`] An error occurred.\n\
\n\
It happened here.\
"
.to_owned(),
reason: Some(Rc::new(reason)),
},
);
}
#[test]
fn listing_empty() {
assert_eq!(listing("", SourceRange { start: 0, end: 0 }), "");
}
#[test]
fn listing_single_line_full_range() {
assert_eq!(
listing("foo bar", SourceRange { start: 0, end: 7 }),
"1 \u{2502} foo bar\n \u{203e}\u{203e}\u{203e}\u{203e}\u{203e}\u{203e}\u{203e}",
);
}
#[test]
fn listing_single_line_partial_range() {
assert_eq!(
listing("foo bar", SourceRange { start: 1, end: 6 }),
"1 \u{2502} foo bar\n \u{203e}\u{203e}\u{203e}\u{203e}\u{203e}",
);
}
#[test]
fn listing_multiple_lines_full_range() {
assert_eq!(
listing("foo\nbar\nbaz\nqux", SourceRange { start: 0, end: 15 }),
"1 \u{2502} foo\n \u{250a} \u{203e}\u{203e}\u{203e}\n2 \u{2502} bar\n \u{250a} \
\u{203e}\u{203e}\u{203e}\n3 \u{2502} baz\n \u{250a} \u{203e}\u{203e}\u{203e}\n4 \
\u{2502} qux\n \u{203e}\u{203e}\u{203e}",
);
}
#[test]
fn listing_multiple_lines_partial_range() {
assert_eq!(
listing("foo\nbar\nbaz\nqux", SourceRange { start: 5, end: 9 }),
"2 \u{2502} bar\n \u{250a} \u{203e}\u{203e}\n3 \u{2502} baz\n \u{203e}",
);
}
#[test]
fn listing_many_lines_partial_range() {
assert_eq!(
listing(
"foo\nbar\nbaz\nqux\nfoo\nbar\nbaz\nqux\nfoo\nbar\nbaz\nqux",
SourceRange { start: 33, end: 42 },
),
" 9 \u{2502} foo\n \u{250a} \u{203e}\u{203e}\n10 \u{2502} bar\n \u{250a} \
\u{203e}\u{203e}\u{203e}\n11 \u{2502} baz\n \u{203e}\u{203e}",
);
}
}