diff --git a/Cargo.lock b/Cargo.lock index b4264ce31..6cf8f8cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,16 +30,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" -[[package]] -name = "ariadne" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd002a6223f12c7a95cdd4b1cb3a0149d22d37f7a9ecdb2cb691a071fe236c29" -dependencies = [ - "unicode-width", - "yansi", -] - [[package]] name = "atty" version = "0.2.14" @@ -136,6 +126,12 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "codesnake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdbcda08384319005fd4b79b08aa04728dbafa702d304d737b5ccbd556df331" + [[package]] name = "colored_json" version = "3.0.1" @@ -145,7 +141,7 @@ dependencies = [ "atty", "serde", "serde_json", - "yansi", + "yansi 0.5.1", ] [[package]] @@ -262,10 +258,10 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" name = "jaq" version = "1.4.0" dependencies = [ - "ariadne", "atty", "chumsky", "clap", + "codesnake", "colored_json", "env_logger", "hifijson", @@ -278,6 +274,8 @@ dependencies = [ "mimalloc", "serde_json", "tempfile", + "unicode-width", + "yansi 1.0.1", ] [[package]] @@ -324,8 +322,8 @@ name = "jaq-play" version = "0.1.0" dependencies = [ "aho-corasick", - "ariadne", "chumsky", + "codesnake", "console_log", "getrandom", "hifijson", @@ -336,6 +334,7 @@ dependencies = [ "jaq-syn", "js-sys", "log", + "unicode-width", "wasm-bindgen", "web-sys", ] @@ -645,9 +644,9 @@ checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "urlencoding" @@ -768,6 +767,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/jaq-play/Cargo.toml b/jaq-play/Cargo.toml index 6bdcbfbd4..17da27d0f 100644 --- a/jaq-play/Cargo.toml +++ b/jaq-play/Cargo.toml @@ -21,14 +21,15 @@ jaq-parse = { version = "1.0.0", path = "../jaq-parse" } jaq-interpret = { version = "1.2.0", path = "../jaq-interpret" } jaq-core = { version = "1.2.0", path = "../jaq-core" } jaq-std = { version = "1.2.0", path = "../jaq-std" } -ariadne = "0.4.0" +aho-corasick = "1.1.2" +codesnake = { version = "0.1" } chumsky = { version = "0.9.0", default-features = false } hifijson = "0.2" log = "0.4.17" +unicode-width = "0.1.13" console_log = { version = "1.0", features = ["color"] } getrandom = { version = "0.2", features = ["js"] } wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = ["DedicatedWorkerGlobalScope"] } js-sys = { version = "0.3" } -aho-corasick = "1.1.2" diff --git a/jaq-play/src/lib.rs b/jaq-play/src/lib.rs index 4fbb4781f..07f8cc680 100644 --- a/jaq-play/src/lib.rs +++ b/jaq-play/src/lib.rs @@ -193,16 +193,14 @@ pub fn run(filter: &str, input: &str, settings: &JsValue, scope: &Scope) { Ok(()) => (), Err(Error::Chumsky(errs)) => { for e in errs { - let mut buf = Vec::new(); - let cache = ariadne::Source::from(filter); - report(e).write(cache, &mut buf).unwrap(); - let s = String::from_utf8(buf).unwrap(); - scope.post_message(&format!("⚠️ Parse {s}").into()).unwrap(); + scope + .post_message(&format!("⚠️ Parse error: {}", report(&filter, &e)).into()) + .unwrap(); } } Err(Error::Hifijson(e)) => { scope - .post_message(&format!("⚠️ Parse Error: {e}").into()) + .post_message(&format!("⚠️ Parse error: {e}").into()) .unwrap(); } Err(Error::Jaq(e)) => { @@ -296,14 +294,33 @@ fn parse(filter_str: &str, vars: Vec) -> Result(e: chumsky::error::Simple) -> ariadne::Report<'a> { - use ariadne::{Color, Fmt, Label, Report, ReportKind}; +#[derive(Debug)] +struct Report<'a> { + code: &'a str, + message: String, + labels: Vec<(core::ops::Range, String, Color)>, +} + +#[derive(Clone, Debug)] +enum Color { + Yellow, + Red, +} + +impl Color { + fn apply(&self, d: impl Display) -> String { + let mut color = format!("{self:?}"); + color.make_ascii_lowercase(); + format!("{d}",) + } +} + +fn report<'a>(code: &'a str, e: &chumsky::error::Simple) -> Report<'a> { use chumsky::error::SimpleReason; - let (red, yellow) = (Color::Unset, Color::Unset); - let config = ariadne::Config::default().with_color(false); + let eof = || "end of input".to_string(); - let msg = if let SimpleReason::Custom(msg) = e.reason() { + let message = if let SimpleReason::Custom(msg) = e.reason() { msg.clone() } else { let found = if e.found().is_some() { @@ -319,13 +336,8 @@ fn report<'a>(e: chumsky::error::Simple) -> ariadne::Report<'a> { let expected = if e.expected().len() == 0 { "something else".to_string() } else { - e.expected() - .map(|expected| match expected { - Some(expected) => expected.to_string(), - None => "end of input".to_string(), - }) - .collect::>() - .join(", ") + let f = |e: &Option| e.as_ref().map_or_else(eof, |e| e.to_string()); + e.expected().map(f).collect::>().join(", ") }; format!("{found}{when}, expected {expected}",) }; @@ -333,27 +345,44 @@ fn report<'a>(e: chumsky::error::Simple) -> ariadne::Report<'a> { let label = if let SimpleReason::Custom(msg) = e.reason() { msg.clone() } else { - format!( - "Unexpected {}", - e.found() - .map(|c| format!("token {}", c.fg(red))) - .unwrap_or_else(|| "end of input".to_string()) - ) + let token = |c: &String| format!("token {}", Color::Red.apply(c)); + format!("Unexpected {}", e.found().map_or_else(eof, token)) }; - - let report = Report::build(ReportKind::Error, (), e.span().start) - .with_message(msg) - .with_label(Label::new(e.span()).with_message(label).with_color(red)); - - let report = match e.reason() { - SimpleReason::Unclosed { span, delimiter } => report.with_label( - Label::new(span.clone()) - .with_message(format!("Unclosed delimiter {}", delimiter.fg(yellow))) - .with_color(yellow), - ), - SimpleReason::Unexpected => report, - SimpleReason::Custom(_) => report, + // convert character indices to byte offsets + let char_to_byte = |i| { + code.char_indices() + .map(|(i, _c)| i) + .chain([code.len(), code.len()]) + .nth(i) + .unwrap() }; + let conv = |span: &core::ops::Range<_>| char_to_byte(span.start)..char_to_byte(span.end); + let mut labels = Vec::from([(conv(&e.span()), label, Color::Red)]); + + if let SimpleReason::Unclosed { span, delimiter } = e.reason() { + let text = format!("Unclosed delimiter {}", Color::Yellow.apply(delimiter)); + labels.insert(0, (conv(span), text, Color::Yellow)); + } + Report { + code, + message, + labels, + } +} - report.with_config(config).finish() +impl Display for Report<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use codesnake::{Block, CodeWidth, Label, LineIndex}; + let idx = LineIndex::new(self.code); + let labels = self.labels.clone().into_iter().map(|(range, text, color)| { + Label::new(range, text).with_style(move |s| color.apply(s).to_string()) + }); + let block = Block::new(&idx, labels).unwrap().map_code(|c| { + let c = c.replace('\t', " "); + let w = unicode_width::UnicodeWidthStr::width(&*c); + CodeWidth::new(c, core::cmp::max(w, 1)) + }); + writeln!(f, "{}", self.message)?; + write!(f, "{}\n{}{}", block.prologue(), block, block.epilogue()) + } } diff --git a/jaq-play/src/style.css b/jaq-play/src/style.css index a894db00e..d5035cb9d 100644 --- a/jaq-play/src/style.css +++ b/jaq-play/src/style.css @@ -104,6 +104,9 @@ textarea { .null { color: magenta; } .key { color: red; } +.red { color: #dc322f; } +.yellow { color: #b58900; } + .settings { display: inline-block; } diff --git a/jaq/Cargo.toml b/jaq/Cargo.toml index df14d9375..91af5adc0 100644 --- a/jaq/Cargo.toml +++ b/jaq/Cargo.toml @@ -20,9 +20,9 @@ jaq-parse = { version = "1.0.0", path = "../jaq-parse" } jaq-interpret = { version = "1.2.0", path = "../jaq-interpret" } jaq-core = { version = "1.2.0", path = "../jaq-core" } jaq-std = { version = "1.2.0", path = "../jaq-std" } -ariadne = "0.4.0" atty = "0.2" chumsky = { version = "0.9.0", default-features = false } +codesnake = { version = "0.1" } clap = { version = "4.0.0", features = ["derive"] } colored_json = "3.0.1" env_logger = { version = "0.10.0", default-features = false } @@ -31,3 +31,5 @@ memmap2 = "0.9" mimalloc = { version = "0.1.29", default-features = false, optional = true } serde_json = { version = "1.0.81", features = [ "arbitrary_precision", "preserve_order" ] } tempfile = "3.3.0" +unicode-width = "0.1.13" +yansi = "1.0.1" diff --git a/jaq/src/main.rs b/jaq/src/main.rs index 373aff029..b638651f7 100644 --- a/jaq/src/main.rs +++ b/jaq/src/main.rs @@ -1,4 +1,5 @@ use clap::{Parser, ValueEnum}; +use core::fmt::{self, Display, Formatter}; use jaq_interpret::{Ctx, Filter, FilterT, ParseCtx, RcIter, Val}; use std::io::{self, BufRead, Write}; use std::path::PathBuf; @@ -71,7 +72,7 @@ struct Cli { /// Color output #[arg(long, value_name = "WHEN", default_value = "auto")] - color: Color, + color: ColorWhen, /// Read filter from a file /// @@ -100,7 +101,7 @@ struct Cli { } #[derive(Clone, ValueEnum)] -enum Color { +enum ColorWhen { Always, Auto, Never, @@ -110,10 +111,10 @@ impl Cli { fn color_mode(&self) -> colored_json::ColorMode { use colored_json::{ColorMode, Output}; match self.color { - Color::Always => ColorMode::On, - Color::Auto if self.in_place => ColorMode::Off, - Color::Auto => ColorMode::Auto(Output::StdOut), - Color::Never => ColorMode::Off, + ColorWhen::Always => ColorMode::On, + ColorWhen::Auto if self.in_place => ColorMode::Off, + ColorWhen::Auto => ColorMode::Auto(Output::StdOut), + ColorWhen::Never => ColorMode::Off, } } } @@ -126,6 +127,7 @@ fn main() -> ExitCode { } fn real_main() -> Result { + // TODO: move into `main` let cli = Cli::parse(); use env_logger::Env; @@ -367,6 +369,12 @@ enum Error { impl Termination for Error { fn report(self) -> ExitCode { + if atty::is(atty::Stream::Stderr) { + yansi::enable(); + } else { + yansi::disable(); + }; + let exit = match self { Self::FalseOrNull => 1, Self::Io(prefix, e) => { @@ -381,11 +389,9 @@ impl Termination for Error { eprintln!("Error: {e}"); 2 } - Self::Chumsky(e) => { - for err in e { - report(err.error) - .eprint(ariadne::Source::from(err.filter)) - .unwrap(); + Self::Chumsky(errs) => { + for e in errs { + eprintln!("Error: {}", report(&e.filter, &e.error)); } 3 } @@ -486,20 +492,36 @@ fn with_stdout(f: impl FnOnce(&mut io::StdoutLock) -> Result) -> Re Ok(y) } -fn report<'a>(e: chumsky::error::Simple) -> ariadne::Report<'a> { - use ariadne::{Color, Fmt, Label, Report, ReportKind}; +#[derive(Debug)] +struct Report<'a> { + code: &'a str, + message: String, + labels: Vec<(core::ops::Range, String, Color)>, +} + +#[derive(Clone, Debug)] +enum Color { + Yellow, + Red, +} + +impl Color { + fn apply(&self, d: impl Display) -> String { + use yansi::{Color, Paint}; + let color = match self { + Self::Yellow => Color::Yellow, + Self::Red => Color::Red, + }; + d.fg(color).to_string() + } +} + +fn report<'a>(code: &'a str, e: &chumsky::error::Simple) -> Report<'a> { use chumsky::error::SimpleReason; - // color error messages only if we are on a tty - let isatty = atty::is(atty::Stream::Stderr); - let (red, yellow) = if isatty { - (Color::Red, Color::Yellow) - } else { - (Color::Unset, Color::Unset) - }; - let config = ariadne::Config::default().with_color(isatty); + let eof = || "end of input".to_string(); - let msg = if let SimpleReason::Custom(msg) = e.reason() { + let message = if let SimpleReason::Custom(msg) = e.reason() { msg.clone() } else { let found = if e.found().is_some() { @@ -515,13 +537,8 @@ fn report<'a>(e: chumsky::error::Simple) -> ariadne::Report<'a> { let expected = if e.expected().len() == 0 { "something else".to_string() } else { - e.expected() - .map(|expected| match expected { - Some(expected) => expected.to_string(), - None => "end of input".to_string(), - }) - .collect::>() - .join(", ") + let f = |e: &Option| e.as_ref().map_or_else(eof, |e| e.to_string()); + e.expected().map(f).collect::>().join(", ") }; format!("{found}{when}, expected {expected}",) }; @@ -529,29 +546,46 @@ fn report<'a>(e: chumsky::error::Simple) -> ariadne::Report<'a> { let label = if let SimpleReason::Custom(msg) = e.reason() { msg.clone() } else { - format!( - "Unexpected {}", - e.found() - .map(|c| format!("token {}", c.fg(red))) - .unwrap_or_else(|| "end of input".to_string()) - ) + let token = |c: &String| format!("token {}", Color::Red.apply(c)); + format!("Unexpected {}", e.found().map_or_else(eof, token)) }; - - let report = Report::build(ReportKind::Error, (), e.span().start) - .with_message(msg) - .with_label(Label::new(e.span()).with_message(label).with_color(red)); - - let report = match e.reason() { - SimpleReason::Unclosed { span, delimiter } => report.with_label( - Label::new(span.clone()) - .with_message(format!("Unclosed delimiter {}", delimiter.fg(yellow))) - .with_color(yellow), - ), - SimpleReason::Unexpected => report, - SimpleReason::Custom(_) => report, + // convert character indices to byte offsets + let char_to_byte = |i| { + code.char_indices() + .map(|(i, _c)| i) + .chain([code.len(), code.len()]) + .nth(i) + .unwrap() }; + let conv = |span: &core::ops::Range<_>| char_to_byte(span.start)..char_to_byte(span.end); + let mut labels = Vec::from([(conv(&e.span()), label, Color::Red)]); - report.with_config(config).finish() + if let SimpleReason::Unclosed { span, delimiter } = e.reason() { + let text = format!("Unclosed delimiter {}", Color::Yellow.apply(delimiter)); + labels.insert(0, (conv(span), text, Color::Yellow)); + } + Report { + code, + message, + labels, + } +} + +impl Display for Report<'_> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use codesnake::{Block, CodeWidth, Label, LineIndex}; + let idx = LineIndex::new(self.code); + let labels = self.labels.clone().into_iter().map(|(range, text, color)| { + Label::new(range, text).with_style(move |s| color.apply(s).to_string()) + }); + let block = Block::new(&idx, labels).unwrap().map_code(|c| { + let c = c.replace('\t', " "); + let w = unicode_width::UnicodeWidthStr::width(&*c); + CodeWidth::new(c, core::cmp::max(w, 1)) + }); + writeln!(f, "{}", self.message)?; + write!(f, "{}\n{}{}", block.prologue(), block, block.epilogue()) + } } fn run_test(test: jaq_syn::test::Test) -> Result<(Val, Val), Error> {