diff --git a/Cargo.lock b/Cargo.lock index 17ccc57..2cffe77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,16 +45,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" -[[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 = "askama" version = "0.12.1" @@ -256,12 +246,18 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "codesnake" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2205f7f6d3de68ecf4c291c789b3edf07b6569268abd0188819086f71ae42225" + [[package]] name = "cooklang" version = "0.13.2" dependencies = [ - "ariadne", "bitflags", + "codesnake", "criterion", "either", "emojis", @@ -283,7 +279,9 @@ dependencies = [ "toml 0.8.12", "tracing", "unicase", + "unicode-width", "url", + "yansi", ] [[package]] @@ -1357,9 +1355,9 @@ dependencies = [ [[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 = "uniffi" @@ -1702,6 +1700,6 @@ dependencies = [ [[package]] name = "yansi" -version = "0.5.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index 2affd22..a1c9ccf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,11 +25,13 @@ toml = { version = "0.8", optional = true } once_cell = "1" enum-map = { version = "2", features = ["serde"] } tracing = "0.1" -ariadne = "0.4" +codesnake = "0.2.1" +unicode-width = "0.1.13" either = "1.8" finl_unicode = { version = "1.2", features = ["categories"], default-features = false } smallvec = { version = "1" } unicase = "2.7.0" +yansi = "1.0.1" [dev-dependencies] serde_json = "1" diff --git a/examples/cook.rs b/examples/cook.rs index 4613894..798bf0e 100644 --- a/examples/cook.rs +++ b/examples/cook.rs @@ -1,9 +1,11 @@ +use std::io::Read; + fn main() -> Result<(), Box> { let mut args = std::env::args(); let bin = args.next().unwrap(); let in_file = match args.next() { Some(path) => path, - None => panic!("Usage: {bin} [output_file|STDOUT]"), + None => panic!("Usage: {bin} [|STDIN] [output_file|STDOUT]"), }; let out_file: Option> = match args.next().as_deref() { Some("STDOUT") => Some(Box::new(std::io::stdout().lock())), @@ -11,13 +13,20 @@ fn main() -> Result<(), Box> { None => None, }; - let input = std::fs::read_to_string(&in_file)?; + let input = match in_file.as_ref() { + "STDIN" => { + let mut buf = String::new(); + std::io::stdin().lock().read_to_string(&mut buf)?; + buf + } + path => std::fs::read_to_string(path)?, + }; match cooklang::parse(&input).into_result() { Ok((recipe, warnings)) => { warnings.eprint(&in_file, &input, true)?; if let Some(mut out) = out_file { - write!(out, "{:#?}", recipe)?; + writeln!(out, "{:#?}", recipe)?; } } Err(e) => { diff --git a/src/analysis/event_consumer.rs b/src/analysis/event_consumer.rs index e5c077a..288d0ea 100644 --- a/src/analysis/event_consumer.rs +++ b/src/analysis/event_consumer.rs @@ -217,9 +217,9 @@ impl<'i, 'c> RecipeCollector<'i, 'c> { let invalid_value = |possible| { error!( format!("Invalid value for config key '{key_t}': {value_t}"), - label!(value.span(), "this value is not supported") + label!(value.span(), "this value") ) - .label(label!(key.span(), "by this key")) + .label(label!(key.span(), "this key does not support")) .hint(format!("Possible values are: {possible:?}")) }; @@ -309,7 +309,7 @@ impl<'i, 'c> RecipeCollector<'i, 'c> { ), label!(value.span(), "this value"), ) - .label(label!(key.span(), "is not supported by this key")) + .label(label!(key.span(), "this key does not support")) .hint("It will be a regular metadata entry") .set_source(err), ); @@ -534,12 +534,14 @@ impl<'i, 'c> RecipeCollector<'i, 'c> { .map(|l| l.span()) .unwrap_or(new_q_loc.span()); - let (new_label, old_label) = match &e { + let (main_label, support_label) = match &e { crate::quantity::IncompatibleUnits::MissingUnit { found } => { - let m = "this is missing a unit"; - let f = "matching this one"; + let m = "value missing unit"; + let f = "found unit"; match found { + // new is mising either::Either::Left(_) => (label!(new, m), label!(old, f)), + // old is missing either::Either::Right(_) => (label!(new, f), label!(old, m)), } } @@ -550,16 +552,16 @@ impl<'i, 'c> RecipeCollector<'i, 'c> { (label!(new, b_q.to_string()), label!(old, a_q.to_string())) } crate::quantity::IncompatibleUnits::UnknownDifferentUnits { .. } => { - (label!(new, "this unit"), label!(old, "differs from this")) + (label!(new), label!(old)) } }; self.ctx.warn( warning!( "Incompatible units prevent calculating total amount", - new_label + main_label ) - .label(old_label) + .label(support_label) .set_source(e), ) } @@ -1283,11 +1285,11 @@ fn conflicting_reference_quantity_error( ) -> SourceDiag { let mut e = error!( "Conflicting component reference quantities", - label!(ref_quantity_span, "reference with quantity here") + label!(ref_quantity_span, "reference with quantity") ) .label(label!( def_span, - "definition with quantity outside a step here" + "definition with quantity outside a step" )) .hint("If the component is not defined in a step and has a quantity, its references cannot have a quantity"); if implicit { @@ -1303,12 +1305,9 @@ fn text_val_in_ref_warn( ) -> SourceDiag { let mut w = warning!( "Text value may prevent calculating total amount", - label!(text_quantity_span, "this text") + label!(text_quantity_span, "can't operate with text value") ) - .label(label!( - number_quantity_span, - "cannot be added to this value" - )) + .label(label!(number_quantity_span, "numeric value")) .hint("Use numeric values so they can be added together"); if implicit { w.add_hint(IMPLICIT_REF_WARN); diff --git a/src/error.rs b/src/error.rs index 6048c46..1fa7de8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -308,13 +308,13 @@ impl SourceReport { color: bool, w: &mut impl std::io::Write, ) -> std::io::Result<()> { - let mut cache = DummyCache::new(file_name, source_code); + let lidx = codesnake::LineIndex::new(source_code); for err in self.warnings() { - build_report(err, file_name, source_code, color).write(&mut cache, &mut *w)?; + write_report(&mut *w, err, &lidx, file_name, color)?; } for err in self.errors() { - build_report(err, file_name, source_code, color).write(&mut cache, &mut *w)?; + write_report(&mut *w, err, &lidx, file_name, color)?; } Ok(()) } @@ -454,107 +454,118 @@ pub fn write_rich_error( color: bool, w: impl std::io::Write, ) -> std::io::Result<()> { - let mut cache = DummyCache::new(file_name, source_code); - let report = build_report(error, file_name, source_code, color); - report.write(&mut cache, w) + let lidx = codesnake::LineIndex::new(source_code); + write_report(w, error, &lidx, file_name, color) } -fn build_report<'a>( - err: &'a dyn RichError, +#[derive(Default)] +struct ColorGenerator(usize); + +impl ColorGenerator { + const COLORS: &'static [yansi::Color] = &[ + yansi::Color::BrightMagenta, + yansi::Color::BrightGreen, + yansi::Color::BrightCyan, + yansi::Color::BrightBlue, + yansi::Color::BrightGreen, + yansi::Color::BrightYellow, + yansi::Color::BrightRed, + ]; + + fn next(&mut self) -> yansi::Color { + let c = Self::COLORS[self.0]; + if self.0 == Self::COLORS.len() - 1 { + self.0 = 0; + } else { + self.0 += 1; + } + c + } +} + +fn write_report( + mut w: impl std::io::Write, + err: &dyn RichError, + lidx: &codesnake::LineIndex, file_name: &str, - src_code: &str, color: bool, -) -> ariadne::Report<'a> { - use ariadne::{Color, ColorGenerator, Fmt, Label, Report}; +) -> std::io::Result<()> { + use yansi::Paint; - let labels = err.labels(); - let labels = labels - .iter() - .map(|(s, t)| (s.to_chars_span(src_code, file_name).range(), t)) - .collect::>(); + let cond = yansi::Condition::cached(color); - // The start of the first span - let offset = labels.first().map(|l| l.0.start).unwrap_or_default(); + let mut cg = ColorGenerator::default(); - let kind = match err.severity() { - Severity::Error => ariadne::ReportKind::Error, - Severity::Warning => ariadne::ReportKind::Warning, - }; + let mut labels = err.labels(); + // codesnake requires the labels to be sorted + labels.to_mut().sort_unstable_by_key(|l| l.0); - let mut r = - Report::build(kind, (), offset).with_config(ariadne::Config::default().with_color(color)); + let mut colored_labels = Vec::with_capacity(labels.len()); + for (s, t) in labels.iter() { + let c = cg.next(); + let mut l = codesnake::Label::new(s.range()) + .with_style(move |s| s.paint(c).whenever(cond).to_string()); + if let Some(text) = t { + l = l.with_text(text) + } + colored_labels.push(l); + } + let sev_color = match err.severity() { + Severity::Error => yansi::Color::Red, + Severity::Warning => yansi::Color::Yellow, + }; + match err.severity() { + Severity::Error => writeln!(w, "{} {err}", "Error:".paint(sev_color).whenever(cond))?, + Severity::Warning => writeln!(w, "{} {err}", "Warning:".paint(sev_color).whenever(cond))?, + } if let Some(source) = err.source() { - let arrow_color = if color { - match kind { - ariadne::ReportKind::Error => Color::Red, - ariadne::ReportKind::Warning => Color::Yellow, - ariadne::ReportKind::Advice => Color::Fixed(147), - ariadne::ReportKind::Custom(_, c) => c, - } - } else { - Color::Default - }; - let message = format!("{err}\n {} {source}", "╰▶ ".fg(arrow_color)); - r.set_message(message); - } else { - r.set_message(err); - } - - let mut c = ColorGenerator::new(); - r.add_labels(labels.into_iter().enumerate().map(|(order, (span, text))| { - let mut l = Label::new(span) - .with_order(order as i32) - .with_color(c.next()); - if let Some(text) = text { - l = l.with_message(text); - } - l - })); + writeln!(w, " {} {source}", "╰▶ ".paint(sev_color).whenever(cond))?; + } + + let Some(block) = codesnake::Block::new(lidx, colored_labels) else { + tracing::error!("Failed to format code span, this is a bug."); + return Ok(()); + }; + + let mut prev_empty = false; + let block = block.map_code(|s| { + let sub = usize::from(core::mem::replace(&mut prev_empty, s.is_empty())); + let s = s.replace('\t', " "); + let w = unicode_width::UnicodeWidthStr::width(&*s); + codesnake::CodeWidth::new(s, core::cmp::max(w, 1) - sub) + }); + + writeln!( + w, + "{}{}{}{}", + block.prologue(), + "[".dim().whenever(cond), + file_name, + "]".dim().whenever(cond) + )?; + write!(w, "{block}")?; + writeln!(w, "{}", block.epilogue())?; let hints = err.hints(); let mut hints = hints.iter(); if let Some(help) = hints.next() { - r.set_help(help); + writeln!(w, "{} {}", "Help:".green().whenever(cond), help)?; } if let Some(note) = hints.next() { - r.set_note(note); + writeln!(w, "{} {}", "Note:".green().whenever(cond), note)?; } #[cfg(debug_assertions)] if hints.next().is_some() { tracing::warn!( hints = ?err.hints(), - "this function only supports 2 hints, more will be ignored", + "the report builder only supports 2 hints, more will be ignored", ); } - - r.finish() -} - -// This is a ariadne cache that only supports one file. -// If needed it can be expanded to a full cache as the source id is already -// stored in CharsSpan (the ariadne::Span) -struct DummyCache<'a>(String, ariadne::Source<&'a str>); -impl<'a> DummyCache<'a> { - fn new(file_name: &str, src_code: &'a str) -> Self { - Self(file_name.into(), ariadne::Source::from(src_code)) - } -} -impl<'s> ariadne::Cache<()> for DummyCache<'s> { - type Storage = &'s str; - fn fetch( - &mut self, - _id: &(), - ) -> Result<&ariadne::Source, Box> { - Ok(&self.1) - } - - fn display<'a>(&self, _id: &'a ()) -> Option> { - Some(Box::new(self.0.clone())) - } + Ok(()) } /// Like [`Default`] but for situations where a default value does not make sense diff --git a/src/parser/quantity.rs b/src/parser/quantity.rs index 4c4d1ec..d92b493 100644 --- a/src/parser/quantity.rs +++ b/src/parser/quantity.rs @@ -72,10 +72,9 @@ fn parse_regular_quantity<'i>(bp: &mut BlockParser<'_, 'i>) -> ParsedQuantity<'i bp.warn( warning!( "Empty quantity unit", - label!(unit_text.span(), "add unit here") + label!(unit_separator.unwrap(), "remove this") ) - .label(label!(unit_separator.unwrap(), "or remove this")) - .hint("It will be as if the quantity has no unit"), + .hint("Add a unit or remove the separator"), ); unit = None; } diff --git a/src/parser/section.rs b/src/parser/section.rs index a30c34c..a003879 100644 --- a/src/parser/section.rs +++ b/src/parser/section.rs @@ -19,9 +19,8 @@ pub(crate) fn section<'i>(block: &mut BlockParser<'_, 'i>) -> Option> block.warn( warning!( "A section block is invalid and it will be a step", - label!(block.span()), + label!(tokens_span(block.rest()), "remove this"), ) - .label(label!(tokens_span(block.rest()), "remove this")) .hint("After the ending `=` the line must end for it to be a valid section"), ); return None; diff --git a/src/parser/step.rs b/src/parser/step.rs index 739e70d..d10b0a0 100644 --- a/src/parser/step.rs +++ b/src/parser/step.rs @@ -303,16 +303,16 @@ fn parse_alias<'i>( format!("Invalid {container}: multiple aliases"), label!(bad_bit, "more than one alias defined here"), ) - .hint("A component can only have one alias. Remove the extra '|'"), + .hint("A component can only have one alias"), ); None } else if alias_text.is_text_empty() { bp.error( error!( format!("Invalid {container}: empty alias"), - label!(alias_text.span(), "add alias here"), + label!(alias_sep.span, "remove this"), ) - .label(label!(alias_sep.span, "or remove this")), + .hint("Either remove the `|` or add an alias"), ); None } else { @@ -591,7 +591,7 @@ fn check_note(bp: &mut BlockParser, container: &'static str) { format!("A {container} cannot have a note, it will be text"), label!(Span::new(start, end)), ) - .label(label!(Span::pos(start), "add a space here")) + .label(label!(Span::pos(start - 1), "add a space here")) // this at least will be the marker character .hint("Notes are only available in ingredients and cookware items"), ); None::<()> // always backtrack diff --git a/src/quantity.rs b/src/quantity.rs index 654010a..25f670d 100644 --- a/src/quantity.rs +++ b/src/quantity.rs @@ -393,6 +393,8 @@ pub enum QuantityAddError { pub enum IncompatibleUnits { #[error("Missing unit: one unit is '{found}' but the other quantity is missing an unit")] MissingUnit { + /// `Left`: Missing on the left hand side quantity + /// `Right`: Missing on the right hand side quantity found: either::Either, }, #[error("Different physical quantity: '{a}' '{b}'")] diff --git a/src/span.rs b/src/span.rs index 5519e23..831e333 100644 --- a/src/span.rs +++ b/src/span.rs @@ -1,6 +1,6 @@ //! Utility to represent a location in the source code -use std::ops::{Deref, Range}; +use std::ops::Range; /// Location in the source code /// @@ -56,30 +56,6 @@ impl std::fmt::Debug for Span { } } -impl Span { - pub(crate) fn to_chars_span(self, all_source: &str, source_id: Id) -> CharsSpan { - let start = all_source[..self.start].chars().count(); - let len = all_source[self.range()].chars().count(); - CharsSpan { - span: Span::new(start, start + len), - source_id, - } - } -} - -pub(crate) struct CharsSpan { - span: Span, - source_id: Id, -} - -impl Deref for CharsSpan { - type Target = Span; - - fn deref(&self) -> &Self::Target { - &self.span - } -} - impl From> for Span { fn from(value: Range) -> Self { Self::new(value.start, value.end) @@ -103,22 +79,3 @@ impl crate::error::Recover for Span { Self::new(0, 0) } } - -impl ariadne::Span for CharsSpan -where - Id: ToOwned + PartialEq, -{ - type SourceId = Id; - - fn source(&self) -> &Self::SourceId { - &self.source_id - } - - fn start(&self) -> usize { - self.span.start - } - - fn end(&self) -> usize { - self.span.end - } -}