use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub struct Error {
pub msg: String,
pub line: usize,
pub col: Option<usize>,
pub echo: String,
}
impl Error {
pub fn new(msg: impl Into<String>, line: usize, echo: impl Into<String>) -> Self {
Self { msg: msg.into(), line, col: None, echo: echo.into() }
}
pub fn with_col(mut self, col: Option<usize>) -> Self {
self.col = col;
self
}
pub fn data(msg: impl Into<String>, echo: impl Into<String>) -> Self {
Self::new(msg, 0, echo)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let echo: String = self.echo.chars().take(240).collect();
let cut = self.echo.chars().count() > 240;
if self.line == 0 {
if echo.is_empty() {
write!(f, "{}", self.msg)
} else if cut {
write!(f, "{}\n{}…", self.msg, echo)
} else {
write!(f, "{}\n{}", self.msg, echo)
}
} else {
writeln!(f, "{}", self.msg)?;
writeln!(f, " --> line {}", self.line)?;
if let Some(c) = self.col {
writeln!(f, "{}^", " ".repeat(c.saturating_sub(1)))?;
}
if cut {
write!(f, "{}…", echo)
} else {
write!(f, "{}", echo)
}
}
}
}
impl std::error::Error for Error {}
pub fn col_of(line: &str, byte_idx: usize) -> usize {
line[..byte_idx.min(line.len())].chars().count() + 1
}
#[macro_export]
macro_rules! err {
($line:expr, $echo:expr, $($t:tt)*) => {
$crate::error::Error::new(format!($($t)*), $line, $echo)
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_shows_msg_line_echo() {
let e = Error::new("bad statement", 12, "loop x");
let s = e.to_string();
assert!(s.contains("bad statement"), "{s}");
assert!(s.contains(" --> line 12"), "{s}");
assert!(s.contains("loop x"), "{s}");
}
#[test]
fn data_error_omits_line() {
let e = Error::data("ragged values", "[1, 2]");
let s = e.to_string();
assert!(!s.contains("line 0"), "{s}");
assert!(s.contains("ragged values") && s.contains("[1, 2]"), "{s}");
}
#[test]
fn long_echo_truncates() {
let e = Error::new("m", 1, "x".repeat(300));
assert!(e.to_string().ends_with('…'));
}
}