simple errors for lang-dev
more errors on one line
| -rw-r--r-- | .github/example.png | bin | 5230 -> 4547 bytes | |||
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | src/lib.rs | 163 |
5 files changed, 147 insertions, 35 deletions
diff --git a/.github/example.png b/.github/example.png Binary files differindex 5ec619f..fdf6cb9 100644 --- a/.github/example.png +++ b/.github/example.png @@ -4,7 +4,7 @@ version = 3 [[package]] name = "lerr" -version = "0.1.1" +version = "0.1.2" dependencies = [ "unicode-width", ] @@ -1,6 +1,6 @@ [package] name = "lerr" -version = "0.1.1" +version = "0.1.2" edition = "2021" license = "MIT" readme = "README.md" @@ -11,12 +11,15 @@ feel free to add coloring with your favorite coloring crate, or just use raw ans ```rust use lerr::Error; -let out = Error::new("Strin::new()") - .label((0..5, "a 'strin' you say")) - .note("i think you meant String") - .to_string(); -eprintln!("{out}"); +let mut e = Error::new("Strin::nouveau().i_like_tests(3.14158)"); +e.label((0..5, "you probably meant String")) + .label((7..16, "use new()")) + .label((17..18, "caps: I")) + .label((30..37, "your π is bad")); +eprintln!("{e}"); +// dont mind this +assert_eq!(e.to_string(), "\n\u{1b}[1;34;30m0 │ \u{1b}[0mStrin::nouveau().i_like_tests(3.14158)\n\u{1b}[1;34;30m ¦ \u{1b}[0m\u{1b}[1;34;31m──┬──\u{1b}[0m \u{1b}[1;34;31m────┬────\u{1b}[0m \u{1b}[1;34;31m^\u{1b}[0m caps: I \u{1b}[1;34;31m^^^^^^^\u{1b}[0m your π is bad\n\u{1b}[1;34;30m ¦ \u{1b}[0m \u{1b}[1;34;31m│\u{1b}[0m \u{1b}[1;34;31m╰\u{1b}[0m use new()\n\u{1b}[1;34;30m ¦ \u{1b}[0m \u{1b}[1;34;31m╰\u{1b}[0m you probably meant String\n"); ``` -Please note that only one label per line is currently supported, and multiline labels are not yet supported. +Please note that multiline labels are not yet supported. If that doesnt work for you, use something like [ariadne](https://crates.io/crates/ariadne).
\ No newline at end of file @@ -16,7 +16,7 @@ use unicode_width::UnicodeWidthStr; /// Span of bytes in the source pub type Span = Range<usize>; /// Label around a [`Span`] -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Label { /// The span that this label will draw at pub span: Span, @@ -70,6 +70,7 @@ pub struct Error<'s> { impl<'s> Error<'s> { /// Create a new error with source code attached + #[must_use = "The error doesnt print itself"] pub fn new(source: &'s str) -> Self { Self { labels: vec![], @@ -104,45 +105,144 @@ impl<'s> Error<'s> { } } +macro_rules! wrpeat { + ($to:ident, $n:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + for _ in 0..$n { write!($to, $fmt $(, $arg)*)? } + }; +} + impl<'s> std::fmt::Display for Error<'s> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "{}", self.message)?; let lines = self.source.0.lines().count(); let width = lines.ilog10() as usize + 1; let space = " "; - let mut found = vec![]; - for (line, (code, span)) in self.source.spans().enumerate() { - for label in &self.labels { - if span.end >= label.span.start && span.start <= label.span.start { - found.push(label); + let mut labels = self.labels.clone(); + // label, width of message, width of ^^^ + let mut found: Vec<(Label, usize, usize)> = vec![]; + for (line, (code, line_span)) in self.source.spans().enumerate() { + let mut i = 0; + while i < labels.len() { + if line_span.end >= labels[i].span.start && line_span.start <= labels[i].span.start + { + let candidate = labels.swap_remove(i); + + for (Label { span, .. }, ..) in &found { + if span.contains(&candidate.span.start) { + todo!("erorrs may not overlap") + } + } + // ^^^ length + let mut point = UnicodeWidthStr::width( + &self.source.0[candidate.span.start - line_span.start + ..candidate.span.end - line_span.start], + ); + if candidate.span.end == candidate.span.start { + point += 1; + } + // ^^^ [<this part length>] + let msglen = UnicodeWidthStr::width(candidate.message.as_str()); + found.push((candidate, msglen, point)); + } else { + i += 1; } } - if found.len() > 1 { - todo!("only one error per line supported"); - } - let Some(&label) = found.first() else { + if found.is_empty() { continue; - }; - writeln!(f, "[1;34;30m{line:width$} | \x1b[0m{code}")?; - let about = UnicodeWidthStr::width( - &self.source.0[label.span.start - span.start..label.span.end - span.start], - ); - let padding = UnicodeWidthStr::width(&self.source.0[span.start..label.span.start]); - write!(f, "\x1b[1;34;30m{space:width$} ¦ \x1b[0m",)?; - for _ in 0..padding { - write!(f, " ")?; } - write!(f, "\x1b[1;34;31m")?; - for _ in 0..about { - write!(f, "^")?; + writeln!(f, "\x1b[1;34;30m{line:width$} │ \x1b[0m{code}")?; + write!(f, "\x1b[1;34;30m{space:width$} ¦ \x1b[0m")?; + + // sort by width + found.sort_unstable_by(|(a, ..), (b, ..)| match a.span.start.cmp(&b.span.start) { + core::cmp::Ordering::Equal => a.span.end.cmp(&b.span.end), + ord => ord, + }); + // keeps track of how many chars we have printed + let mut position = 0; + let mut middles = vec![]; + for (i, (l, msglen, about)) in found.iter().map(|(v, a, b)| (v, *a, *b)).enumerate() { + let padding = UnicodeWidthStr::width( + &self.source.0[line_span.start + position..l.span.start], + ); + wrpeat!(f, padding, " "); + position += padding; + + if found + .iter() + .skip(i + 1) + // will this label "but into" any of the future ones if i place it here + .any(|(b, ..)| l.span.start + about + msglen + 1 > b.span.start) + { + let p = about.saturating_sub(1); + let middle = (p + 1) / 2; + write!(f, "\x1b[1;34;31m")?; + wrpeat!(f, middle, "─"); + write!(f, "┬")?; + wrpeat!(f, p - middle, "─"); + write!(f, "\x1b[0m")?; + middles.push((l, middle, msglen)); + position += about; + continue; + } + write!(f, "\x1b[1;34;31m")?; + wrpeat!(f, about, "^"); + position += about; + write!(f, "\x1b[0m ")?; + position += 1; + write!(f, "{}", l.message)?; + position += msglen; } - if label.span.end == label.span.start { - write!(f, "^")?; + writeln!(f)?; + extras(self, middles, line_span, f, width)?; + fn extras( + e: &Error, + mut unfinished: Vec<(&Label, usize, usize)>, + line_span: Span, + f: &mut std::fmt::Formatter<'_>, + width: usize, + ) -> std::fmt::Result { + if unfinished.is_empty() { + return Ok(()); + } + write!(f, "\x1b[1;34;30m{:width$} ¦ \x1b[0m", " ")?; + let mut position = 0; + let mut i = 0; + while i < unfinished.len() { + // connection is where we are expected to put our ╰ + let (l, connection, msglen) = unfinished[i]; + + let padding = UnicodeWidthStr::width( + &e.source.0[line_span.start + position..l.span.start + connection], + ); + wrpeat!(f, padding, " "); + position += padding; + + if unfinished + .iter() + .skip(i + 1) + // will this label "but into" any of the future ones if i place it here + .any(|(b, ..)| l.span.start + connection + msglen + 2 > b.span.start) + { + // if it will, leave it for the next line (this is a recursive fn) + write!(f, "\x1b[1;34;31m│\x1b[0m ")?; + position += 2; + i += 1; + continue; + } + write!(f, "\x1b[1;34;31m╰\x1b[0m ")?; + position += 2; + write!(f, "{}", l.message)?; + position += msglen; + unfinished.remove(i); + } + writeln!(f)?; + extras(e, unfinished, line_span, f, width) } - write!(f, "\x1b[0m ")?; - writeln!(f, "{}", label.message)?; + found.clear(); } + for note in &self.notes { writeln!(f, "{space:width$} \x1b[1;34;30m>\x1b[0m {}", note.message)?; } @@ -158,5 +258,14 @@ fn display() { .note("\x1b[1;34;34mnote\x1b[0m: maybe python would be better for you") .to_string(); println!("{out}"); - assert_eq!(out, "\n\u{1b}[1;34;30m0 | \u{1b}[0mvoid fn x(void) -> four {\n\u{1b}[1;34;30m ¦ \u{1b}[0m \u{1b}[1;34;31m^^^^\u{1b}[0m what is 'four'?\n \u{1b}[1;34;30m>\u{1b}[0m \u{1b}[1;34;32mhelp\u{1b}[0m: change it to 4\n \u{1b}[1;34;30m>\u{1b}[0m \u{1b}[1;34;34mnote\u{1b}[0m: maybe python would be better for you\n"); + assert_eq!(out, "\n\u{1b}[1;34;30m0 │ \u{1b}[0mvoid fn x(void) -> four {\n\u{1b}[1;34;30m ¦ \u{1b}[0m \u{1b}[1;34;31m^^^^\u{1b}[0m what is 'four'?\n \u{1b}[1;34;30m>\u{1b}[0m \u{1b}[1;34;32mhelp\u{1b}[0m: change it to 4\n \u{1b}[1;34;30m>\u{1b}[0m \u{1b}[1;34;34mnote\u{1b}[0m: maybe python would be better for you\n"); +} +#[test] +fn inline() { + let out = Error::new("im out of this worl") + .label((15..19, "forgot d")) + .label((0..2, r#"forgot '"#)) + .to_string(); + println!("{out}"); + assert_eq!(out, "\n\u{1b}[1;34;30m0 │ \u{1b}[0mim out of this worl\n\u{1b}[1;34;30m ¦ \u{1b}[0m\u{1b}[1;34;31m^^\u{1b}[0m forgot ' \u{1b}[1;34;31m^^^^\u{1b}[0m forgot d\n"); } |