simple errors for lang-dev
add charset config, and use comat instead of raaw ansi
bendn 2023-09-17
parent 5761b8e · commit bfdf46c
-rw-r--r--.github/example.pngbin14014 -> 20132 bytes
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml6
-rw-r--r--README.md12
-rw-r--r--src/config.rs81
-rw-r--r--src/lib.rs146
7 files changed, 200 insertions, 62 deletions
diff --git a/.github/example.png b/.github/example.png
index 0b48e46..9f574e3 100644
--- a/.github/example.png
+++ b/.github/example.png
Binary files differ
diff --git a/.gitignore b/.gitignore
index ea8c4bf..96ef6c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+Cargo.lock
diff --git a/Cargo.lock b/Cargo.lock
deleted file mode 100644
index 9eb39a4..0000000
--- a/Cargo.lock
+++ /dev/null
@@ -1,16 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "lerr"
-version = "0.1.2"
-dependencies = [
- "unicode-width",
-]
-
-[[package]]
-name = "unicode-width"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
diff --git a/Cargo.toml b/Cargo.toml
index c11022f..b7aa980 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "lerr"
-version = "0.1.2"
+version = "0.1.3"
edition = "2021"
license = "MIT"
readme = "README.md"
@@ -12,4 +12,8 @@ keywords = ["error", "lang-dev", "diagnostics"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+comat = "0.1.0"
unicode-width = "0.1.10"
+
+[dev-dependencies]
+anstream = { version = "0.5.0", default-features = false }
diff --git a/README.md b/README.md
index dc368ce..fba4762 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,15 @@ heres the code for the sample above.
feel free to add coloring with your favorite coloring crate, or just use raw ansi sequences.
```rust
+use comat::cformat as cmt;
use lerr::Error;
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"));
+e.message(cmt!(r#"{bold_red}error{reset}: unknown function {bold_red}String::new(){reset}"#))
+ .label((0..5, cmt!("you probably meant {black}String{reset}")))
+ .label((7..16, cmt!("use {green}new(){reset}")))
+ .label((17..18, cmt!("caps: {bold_cyan}I{reset}")))
+ .label((30..37, cmt!("your {bold_yellow}π{reset} 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 multiline labels are not yet supported.
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..dd7ffdb
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,81 @@
+//! allows configuration of how the error will appear.
+/// characters used in printing the error.
+#[derive(Debug, Clone, Copy)]
+pub struct Charset {
+ /// the line on the left
+ pub column_line: char,
+ /// the line on the left when theres a label there
+ pub column_broken_line: char,
+ /// the character shown below the error span for inline labels
+ /// ```text
+ /// 0 | problem
+ /// ^^^^^^^ issue
+ /// ^^^^^^^ these ones
+ /// ```
+ pub spanning: char,
+ /// the character shown about the span, when the error is moved to a next line
+ /// ```text
+ /// 0 | problem
+ /// ───┬───
+ /// ^^^ ^^^ these ones
+ /// ```
+ pub spanning_out: char,
+ /// the character shown in the middle of the span, when the error is moved to a next line, in the middle
+ /// ```text
+ /// 0 | problem
+ /// ───┬───
+ /// ^ this one
+ /// ```
+ pub spanning_mid: char,
+ /// the character used to extend the label to yet another line
+ /// ```text
+ /// 0 | problem
+ /// ───┬───
+ /// │ < this one
+ /// ```
+ pub out_extension: char,
+ /// the character used to end the label for a moved label
+ /// ```text
+ /// 0 | problem
+ /// ───┬───
+ /// ╰ issue
+ /// ^ this one
+ /// ```
+ pub out_end: char,
+ /// the character used for a note
+ /// ```text
+ /// 0 | problem
+ /// > btw i must say you use the same text in the example alot
+ /// ^ this one
+ /// ```
+ pub note: char,
+}
+
+impl Charset {
+ /// Produces a (pretty) unicode charset.
+ pub const fn unicode() -> Self {
+ Self {
+ column_line: '|',
+ column_broken_line: '¦',
+ spanning: '^',
+ spanning_out: '─',
+ spanning_mid: '┬',
+ out_extension: '│', // not a pipe btw
+ out_end: '╰',
+ note: '>',
+ }
+ }
+ /// Produces a (ugly) ascii charset.
+ pub const fn ascii() -> Self {
+ Self {
+ column_line: '|',
+ column_broken_line: ':',
+ spanning: '^',
+ spanning_out: '-',
+ spanning_mid: '.',
+ out_extension: '|',
+ out_end: '\\',
+ note: '>',
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index c7c5527..de08629 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,18 +1,19 @@
#![doc = include_str!("../README.md")]
+#![forbid(unsafe_code)]
#![warn(
- clippy::multiple_unsafe_ops_per_block,
clippy::missing_const_for_fn,
clippy::redundant_pub_crate,
- clippy::missing_safety_doc,
clippy::imprecise_flops,
- unsafe_op_in_unsafe_fn,
clippy::dbg_macro,
missing_docs
)]
-use std::ops::Range;
-
+use comat::{cwrite, cwriteln};
+use config::Charset;
+use std::{fmt::Write, ops::Range};
use unicode_width::UnicodeWidthStr;
+pub mod config;
+
/// Span of bytes in the source
pub type Span = Range<usize>;
/// Label around a [`Span`]
@@ -57,15 +58,18 @@ impl<'s> Source<'s> {
/// The error builder that this crate is all about
#[derive(Debug)]
+#[non_exhaustive]
pub struct Error<'s> {
+ /// The message
+ pub message: String,
/// Source text
pub source: Source<'s>,
/// Labels we hold
pub labels: Vec<Label>,
/// Notes
pub notes: Vec<Note>,
- /// The message
- pub message: String,
+ /// The config
+ pub charset: Charset,
}
impl<'s> Error<'s> {
@@ -77,9 +81,16 @@ impl<'s> Error<'s> {
source: Source(source),
notes: vec![],
message: String::new(),
+ charset: Charset::unicode(),
}
}
+ /// Sets the charset
+ pub fn charset(&mut self, charset: Charset) -> &mut Self {
+ self.charset = charset;
+ self
+ }
+
/// Add a message to this error
pub fn message(&mut self, message: impl ToString) -> &mut Self {
self.message = message.to_string();
@@ -89,9 +100,7 @@ impl<'s> Error<'s> {
/// Add a label to this error
pub fn label(&mut self, label: impl Into<Label>) -> &mut Self {
let l = label.into();
- if self.source.0.len() < l.span.end {
- panic!("label must be in bounds");
- }
+ assert!(self.source.0.len() >= l.span.end, "label must be in bounds");
self.labels.push(l);
self
}
@@ -103,17 +112,24 @@ impl<'s> Error<'s> {
});
self
}
+
+ #[cfg(test)]
+ fn monochrome(&self) -> String {
+ anstream::adapter::strip_str(&self.to_string()).to_string()
+ }
}
macro_rules! wrpeat {
- ($to:ident, $n:expr, $fmt:literal $(, $arg:expr)* $(,)?) => {
- for _ in 0..$n { write!($to, $fmt $(, $arg)*)? }
+ ($to:ident, $n:expr, $fmt:expr) => {
+ for _ in 0..$n {
+ write!($to, "{}", $fmt)?
+ }
};
}
impl<'s> std::fmt::Display for Error<'s> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- writeln!(f, "{}", self.message)?;
+ cwriteln!(f, "{:reset}", self.message)?;
let lines = self.source.0.lines().count();
let width = lines.ilog10() as usize + 1;
let space = " ";
@@ -150,8 +166,16 @@ impl<'s> std::fmt::Display for Error<'s> {
if found.is_empty() {
continue;
}
- writeln!(f, "\x1b[1;34;30m{line:width$} | \x1b[0m{code}")?;
- write!(f, "\x1b[1;34;30m{space:width$} ¦ \x1b[0m")?;
+ cwriteln!(
+ f,
+ "{bold_black}{line:width$} {} {reset}{code}",
+ self.charset.column_line
+ )?;
+ cwrite!(
+ f,
+ "{space:width$} {:bold_black} {reset}",
+ self.charset.column_broken_line
+ )?;
// sort by width
found.sort_unstable_by(|(a, ..), (b, ..)| match a.span.start.cmp(&b.span.start) {
@@ -176,36 +200,40 @@ impl<'s> std::fmt::Display for Error<'s> {
{
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")?;
+ cwrite!(f, "{bold_red}")?;
+ wrpeat!(f, middle, self.charset.spanning_out);
+ f.write_char(self.charset.spanning_mid)?;
+ wrpeat!(f, p - middle, self.charset.spanning_out);
+ cwrite!(f, "{reset}")?;
middles.push((l, middle, msglen));
position += about;
continue;
}
- write!(f, "\x1b[1;34;31m")?;
- wrpeat!(f, about, "^");
+ cwrite!(f, "{bold_red}")?;
+ wrpeat!(f, about, self.charset.spanning);
position += about;
- write!(f, "\x1b[0m ")?;
- position += 1;
- write!(f, "{}", l.message)?;
- position += msglen;
+ cwrite!(f, " {:reset}", l.message)?;
+ position += 1 + msglen;
}
writeln!(f)?;
- extras(self, middles, line_span, f, width)?;
+ extras(self, middles, line_span, f, width, self.charset)?;
fn extras(
e: &Error,
mut unfinished: Vec<(&Label, usize, usize)>,
line_span: Span,
f: &mut std::fmt::Formatter<'_>,
width: usize,
+ charset: Charset,
) -> std::fmt::Result {
if unfinished.is_empty() {
return Ok(());
}
- write!(f, "\x1b[1;34;30m{:width$} ¦ \x1b[0m", " ")?;
+ cwrite!(
+ f,
+ "{:width$} {:bold_black} ",
+ " ",
+ charset.column_broken_line
+ )?;
let mut position = 0;
let mut i = 0;
while i < unfinished.len() {
@@ -225,26 +253,26 @@ impl<'s> std::fmt::Display for Error<'s> {
.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 ")?;
+ cwrite!(f, "{:bold_red} ", charset.out_extension)?;
position += 2;
i += 1;
continue;
}
- write!(f, "\x1b[1;34;31m╰\x1b[0m ")?;
+ cwrite!(f, "{:bold_red} ", charset.out_end)?;
position += 2;
- write!(f, "{}", l.message)?;
+ cwrite!(f, "{:reset}", l.message)?;
position += msglen;
unfinished.remove(i);
}
writeln!(f)?;
- extras(e, unfinished, line_span, f, width)
+ extras(e, unfinished, line_span, f, width, charset)
}
found.clear();
}
for note in &self.notes {
- writeln!(f, "{space:width$} \x1b[1;34;30m>\x1b[0m {}", note.message)?;
+ cwriteln!(f, "{space:width$} {bold_black}>{reset} {}", note.message)?;
}
Ok(())
}
@@ -253,19 +281,59 @@ impl<'s> std::fmt::Display for Error<'s> {
#[test]
fn display() {
let out = Error::new("void fn x(void) -> four {\nwierd};")
+ .message("attempted to use string as type")
.label((19..23, "what is 'four'?"))
- .note("\x1b[1;34;32mhelp\x1b[0m: change it to 4")
- .note("\x1b[1;34;34mnote\x1b[0m: maybe python would be better for you")
- .to_string();
+ .note("help: change it to 4")
+ .note("note: maybe python would be better for you")
+ .charset(Charset::ascii())
+ .monochrome();
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,
+ r"attempted to use string as type
+0 | void fn x(void) -> four {
+ : ^^^^ what is 'four'?
+ > help: change it to 4
+ > note: maybe python would be better for you
+"
+ );
}
#[test]
fn inline() {
let out = Error::new("im out of this worl")
+ .message("such spelling")
.label((15..19, "forgot d"))
.label((0..2, r#"forgot '"#))
- .to_string();
+ .charset(Charset::ascii())
+ .monochrome();
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");
+ assert_eq!(
+ out,
+ r"such spelling
+0 | im out of this worl
+ : ^^ forgot ' ^^^^ forgot d
+"
+ );
+}
+
+#[test]
+fn outline() {
+ let e = Error::new("Strin::nouveau().i_like_tests(3.14158)")
+ .message("unknown method String::new")
+ .label((0..5, "you probably meant String"))
+ .label((7..16, "use new()"))
+ .label((17..18, "caps: I"))
+ .label((30..37, "your π is bad"))
+ .charset(Charset::ascii())
+ .monochrome();
+ println!("{e}");
+ assert_eq!(
+ e,
+ r"unknown method String::new
+0 | Strin::nouveau().i_like_tests(3.14158)
+ : --.-- ----.---- ^ caps: I ^^^^^^^ your π is bad
+ : | \ use new()
+ : \ you probably meant String
+"
+ );
}