From 2a7e32cdc7bb366b0b6636d7987736902069e30f Mon Sep 17 00:00:00 2001 From: elkowar Date: Sun, 22 Dec 2024 22:53:14 +0100 Subject: [PATCH] feat: Add show_related_errors_as_nested --- src/handler.rs | 16 +++++ src/handlers/graphical.rs | 95 +++++++++++++++++++++++---- tests/test_diagnostic_source_macro.rs | 58 ++++++++++++++++ 3 files changed, 156 insertions(+), 13 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 10d8f650..4af59996 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -57,6 +57,7 @@ pub struct MietteHandlerOpts { pub(crate) word_separator: Option, pub(crate) word_splitter: Option, pub(crate) highlighter: Option, + pub(crate) show_related_as_nested: Option, } impl MietteHandlerOpts { @@ -167,6 +168,18 @@ impl MietteHandlerOpts { self } + /// Show related errors as siblings. + pub fn show_related_errors_as_siblings(mut self) -> Self { + self.show_related_as_nested = Some(false); + self + } + + /// Show related errors as nested errors. + pub fn show_related_errors_as_nested(mut self) -> Self { + self.show_related_as_nested = Some(true); + self + } + /// If true, colors will be used during graphical rendering, regardless /// of whether or not the terminal supports them. /// @@ -332,6 +345,9 @@ impl MietteHandlerOpts { if let Some(s) = self.word_splitter { handler = handler.with_word_splitter(s) } + if let Some(b) = self.show_related_as_nested { + handler = handler.with_show_related_as_nested(b) + } MietteHandler { inner: Box::new(handler), diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 9e0948c5..fcbec052 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -37,6 +37,7 @@ pub struct GraphicalReportHandler { pub(crate) word_splitter: Option, pub(crate) highlighter: MietteHighlighter, pub(crate) link_display_text: Option, + pub(crate) show_related_as_nested: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -64,6 +65,7 @@ impl GraphicalReportHandler { word_splitter: None, highlighter: MietteHighlighter::default(), link_display_text: None, + show_related_as_nested: false, } } @@ -83,6 +85,7 @@ impl GraphicalReportHandler { word_splitter: None, highlighter: MietteHighlighter::default(), link_display_text: None, + show_related_as_nested: false, } } @@ -177,6 +180,12 @@ impl GraphicalReportHandler { self } + /// Sets whether to render related errors as nested errors. + pub fn with_show_related_as_nested(mut self, show_related_as_nested: bool) -> Self { + self.show_related_as_nested = show_related_as_nested; + self + } + /// Enable syntax highlighting for source code snippets, using the given /// [`Highlighter`]. See the [highlighters](crate::highlighters) crate /// for more details. @@ -414,23 +423,83 @@ impl GraphicalReportHandler { diagnostic: &(dyn Diagnostic), parent_src: Option<&dyn SourceCode>, ) -> fmt::Result { + let src = diagnostic.source_code().or(parent_src); + if let Some(related) = diagnostic.related() { + let severity_style = match diagnostic.severity() { + Some(Severity::Error) | None => self.theme.styles.error, + Some(Severity::Warning) => self.theme.styles.warning, + Some(Severity::Advice) => self.theme.styles.advice, + }; + let mut inner_renderer = self.clone(); // Re-enable the printing of nested cause chains for related errors inner_renderer.with_cause_chain = true; - for rel in related { - writeln!(f)?; - match rel.severity() { - Some(Severity::Error) | None => write!(f, "Error: ")?, - Some(Severity::Warning) => write!(f, "Warning: ")?, - Some(Severity::Advice) => write!(f, "Advice: ")?, - }; - inner_renderer.render_header(f, rel)?; - let src = rel.source_code().or(parent_src); - inner_renderer.render_causes(f, rel, src)?; - inner_renderer.render_snippets(f, rel, src)?; - inner_renderer.render_footer(f, rel)?; - inner_renderer.render_related(f, rel, src)?; + if self.show_related_as_nested { + let width = self.termwidth.saturating_sub(2); + let mut related = related.peekable(); + while let Some(rel) = related.next() { + let is_last = related.peek().is_none(); + let char = if !is_last { + self.theme.characters.lcross + } else { + self.theme.characters.lbot + }; + let initial_indent = format!( + " {}{}{} ", + char, self.theme.characters.hbar, self.theme.characters.rarrow + ) + .style(severity_style) + .to_string(); + let rest_indent = format!( + " {} ", + if is_last { + ' ' + } else { + self.theme.characters.vbar + } + ) + .style(severity_style) + .to_string(); + + let mut opts = textwrap::Options::new(width) + .initial_indent(&initial_indent) + .subsequent_indent(&rest_indent) + .break_words(self.break_words); + if let Some(word_separator) = self.word_separator { + opts = opts.word_separator(word_separator); + } + if let Some(word_splitter) = self.word_splitter.clone() { + opts = opts.word_splitter(word_splitter); + } + + let mut inner = String::new(); + + let mut inner_renderer = self.clone(); + inner_renderer.footer = None; + inner_renderer.with_cause_chain = false; + inner_renderer.termwidth -= rest_indent.width(); + inner_renderer.render_report_inner(&mut inner, rel, src)?; + + // If there was no header, remove the leading newline + let inner = inner.trim_matches('\n'); + writeln!(f, "{}", self.wrap(inner, opts))?; + } + } else { + for rel in related { + writeln!(f)?; + match rel.severity() { + Some(Severity::Error) | None => write!(f, "Error: ")?, + Some(Severity::Warning) => write!(f, "Warning: ")?, + Some(Severity::Advice) => write!(f, "Advice: ")?, + }; + inner_renderer.render_header(f, rel)?; + let src = rel.source_code().or(parent_src); + inner_renderer.render_causes(f, rel, src)?; + inner_renderer.render_snippets(f, rel, src)?; + inner_renderer.render_footer(f, rel)?; + inner_renderer.render_related(f, rel, src)?; + } } } Ok(()) diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index b21e091b..71429efb 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -300,6 +300,64 @@ fn test_nested_cause_chains_for_related_errors_are_output() { assert_eq!(expected, out); } +#[cfg(feature = "fancy-no-backtrace")] +#[test] +fn test_display_related_errors_as_nested() { + let inner_error = TestStructError { + asdf_inner_foo: SourceError { + code: String::from("This is another error"), + help: String::from("You should fix this"), + label: (3, 4), + }, + }; + let first_error = NestedError { + code: String::from("right here"), + label: (6, 4), + the_other_err: Box::new(inner_error), + }; + let second_error = SourceError { + code: String::from("You're actually a mess"), + help: String::from("Get a grip..."), + label: (3, 4), + }; + let diag = MultiError { + related_errs: vec![ + Box::new(MultiError { + related_errs: vec![Box::new(first_error), Box::new(AnErr)], + }), + Box::new(second_error), + ], + }; + + let mut out = String::new(); + miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor()) + .with_width(80) + .with_show_related_as_nested(true) + .render_report(&mut out, &diag) + .unwrap(); + println!("{}", out); + + let expected = r#" + × A multi-error happened + ├─▶ × A multi-error happened + │ ├─▶ × A nested error happened + │ │ ╭──── + │ │ 1 │ right here + │ │ · ──┬─ + │ │ · ╰── here + │ │ ╰──── + │ ╰─▶ × AnErr + ╰─▶ × A complex error happened + ╭──── + 1 │ You're actually a mess + · ──┬─ + · ╰── here + ╰──── + help: Get a grip... +"#; + assert_eq!(expected, out); +} + #[cfg(feature = "fancy-no-backtrace")] #[derive(Debug, miette::Diagnostic, thiserror::Error)] #[error("A case1 error happened")]