1use std::fmt;
17
18#[derive(Debug, Clone, PartialEq)]
20pub struct Error {
21 pub msg: String,
23 pub line: usize,
25 pub col: Option<usize>,
27 pub echo: String,
29}
30
31impl Error {
32 pub fn new(msg: impl Into<String>, line: usize, echo: impl Into<String>) -> Self {
33 Self { msg: msg.into(), line, col: None, echo: echo.into() }
34 }
35
36 pub fn with_col(mut self, col: Option<usize>) -> Self {
39 self.col = col;
40 self
41 }
42
43 pub fn data(msg: impl Into<String>, echo: impl Into<String>) -> Self {
45 Self::new(msg, 0, echo)
46 }
47}
48
49impl fmt::Display for Error {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 let echo: String = self.echo.chars().take(240).collect();
54 let cut = self.echo.chars().count() > 240;
55 if self.line == 0 {
56 if echo.is_empty() {
57 write!(f, "{}", self.msg)
58 } else if cut {
59 write!(f, "{}\n{}…", self.msg, echo)
60 } else {
61 write!(f, "{}\n{}", self.msg, echo)
62 }
63 } else {
64 writeln!(f, "{}", self.msg)?;
65 writeln!(f, " --> line {}", self.line)?;
66 if let Some(c) = self.col {
67 writeln!(f, "{}^", " ".repeat(c.saturating_sub(1)))?;
68 }
69 if cut {
70 write!(f, "{}…", echo)
71 } else {
72 write!(f, "{}", echo)
73 }
74 }
75 }
76}
77
78impl std::error::Error for Error {}
79
80pub fn col_of(line: &str, byte_idx: usize) -> usize {
84 line[..byte_idx.min(line.len())].chars().count() + 1
85}
86
87#[macro_export]
89macro_rules! err {
90 ($line:expr, $echo:expr, $($t:tt)*) => {
91 $crate::error::Error::new(format!($($t)*), $line, $echo)
92 };
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn display_shows_msg_line_echo() {
101 let e = Error::new("bad statement", 12, "loop x");
102 let s = e.to_string();
103 assert!(s.contains("bad statement"), "{s}");
104 assert!(s.contains(" --> line 12"), "{s}");
105 assert!(s.contains("loop x"), "{s}");
106 }
107
108 #[test]
109 fn data_error_omits_line() {
110 let e = Error::data("ragged values", "[1, 2]");
111 let s = e.to_string();
112 assert!(!s.contains("line 0"), "{s}");
113 assert!(s.contains("ragged values") && s.contains("[1, 2]"), "{s}");
114 }
115
116 #[test]
117 fn long_echo_truncates() {
118 let e = Error::new("m", 1, "x".repeat(300));
119 assert!(e.to_string().ends_with('…'));
120 }
121}