Repository: zkat/miette
Pull request #413 (2024-11-29 03:55:35)

docs: introduce primary param in label


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 277215d..3ce20c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # `miette` Release Changelog
 
+<a name="7.5.0"></a>
+## 7.5.0 (2025-02-01)
+
+### Features
+
+* **graphical:** support rendering related diagnostics as nested (#417) ([771a0751](https://github.com/zkat/miette/commit/771a07519f078b94aceb1a2d2532d786f09f350b))
+
+### Bug Fixes
+
+* **graphical:** prevent leading newline when no link/code (#418) ([1e1938a0](https://github.com/zkat/miette/commit/1e1938a099409969c69b9f070e0fb0d13d564527))
+
 <a name="7.4.0"></a>
 ## 7.4.0 (2024-11-27)
 
diff --git a/Cargo.toml b/Cargo.toml
index 2427667..896e940 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "miette"
-version = "7.4.0"
+version = "7.5.0"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers."
 categories = ["rust-patterns"]
@@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
 
 [dependencies]
 thiserror = "1.0.56"
-miette-derive = { path = "miette-derive", version = "=7.4.0", optional = true }
+miette-derive = { path = "miette-derive", version = "=7.5.0", optional = true }
 unicode-width = "0.1.11"
 cfg-if = "1.0.0"
 
diff --git a/README.md b/README.md
index 646047c..4829d03 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ diagnostic error code: ruget::api::bad_json
   - [... handler options](#-handler-options)
   - [... dynamic diagnostics](#-dynamic-diagnostics)
   - [... syntax highlighting](#-syntax-highlighting)
+  - [... primary label](#-primary-label)
   - [... collection of labels](#-collection-of-labels)
 - [Acknowledgements](#acknowledgements)
 - [License](#license)
@@ -100,7 +101,7 @@ You can derive a `Diagnostic` from any `std::error::Error` type.
 
 `thiserror` is a great way to define them, and plays nicely with `miette`!
 */
-use miette::{Diagnostic, SourceSpan};
+use miette::{Diagnostic, NamedSource, SourceSpan};
 use thiserror::Error;
 
 #[derive(Error, Debug, Diagnostic)]
@@ -127,7 +128,7 @@ Use this `Result` type (or its expanded version) as the return type
 throughout your app (but NOT your libraries! Those should always return
 concrete types!).
 */
-use miette::{NamedSource, Result};
+use miette::Result;
 fn this_fails() -> Result<()> {
     // You can use plain strings as a `Source`, or anything that implements
     // the one-method `Source` trait.
@@ -266,7 +267,7 @@ pub fn some_tool() -> Result<Version> {
 }
 ```
 
-To construct your own simple adhoc error use the [miette!] macro:
+To construct your own simple adhoc error use the [`miette!`] macro:
 ```rust
 // my_app/lib/my_internal_file.rs
 use miette::{miette, Result};
@@ -689,6 +690,37 @@ trait to [`MietteHandlerOpts`] by calling the
 [`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
 method. See the [`highlighters`] module docs for more details.
 
+#### ... primary label
+
+You can use the `primary` parameter to `label` to indicate that the label
+is the primary label.
+
+```rust
+#[derive(Debug, Diagnostic, Error)]
+#[error("oops!")]
+struct MyError {
+    #[label(primary, "main issue")]
+    primary_span: SourceSpan,
+
+    #[label("other label")]
+    other_span: SourceSpan,
+}
+```
+
+The `primary` parameter can be used at most once:
+
+```rust
+#[derive(Debug, Diagnostic, Error)]
+#[error("oops!")]
+struct MyError {
+    #[label(primary, "main issue")]
+    primary_span: SourceSpan,
+
+    #[label(primary, "other label")] // Error: Cannot have more than one primary label.
+    other_span: SourceSpan,
+}
+```
+
 #### ... collection of labels
 
 When the number of labels is unknown, you can use a collection of `SourceSpan`
diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml
index c055df7..9a27562 100644
--- a/miette-derive/Cargo.toml
+++ b/miette-derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "miette-derive"
-version = "7.4.0"
+version = "7.5.0"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 edition = "2018"
 license = "Apache-2.0"
diff --git a/src/diagnostic_chain.rs b/src/diagnostic_chain.rs
index 1e5e0c2..996fdba 100644
--- a/src/diagnostic_chain.rs
+++ b/src/diagnostic_chain.rs
@@ -74,7 +74,7 @@ impl<'a> ErrorKind<'a> {
     }
 }
 
-impl<'a> std::fmt::Debug for ErrorKind<'a> {
+impl std::fmt::Debug for ErrorKind<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             ErrorKind::Diagnostic(d) => d.fmt(f),
@@ -83,7 +83,7 @@ impl<'a> std::fmt::Debug for ErrorKind<'a> {
     }
 }
 
-impl<'a> std::fmt::Display for ErrorKind<'a> {
+impl std::fmt::Display for ErrorKind<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             ErrorKind::Diagnostic(d) => d.fmt(f),
diff --git a/src/eyreish/ptr.rs b/src/eyreish/ptr.rs
index fa954d1..40c9c7b 100644
--- a/src/eyreish/ptr.rs
+++ b/src/eyreish/ptr.rs
@@ -69,9 +69,9 @@ where
     lifetime: PhantomData<&'a T>,
 }
 
-impl<'a, T> Copy for Ref<'a, T> where T: ?Sized {}
+impl<T> Copy for Ref<'_, T> where T: ?Sized {}
 
-impl<'a, T> Clone for Ref<'a, T>
+impl<T> Clone for Ref<'_, T>
 where
     T: ?Sized,
 {
@@ -132,9 +132,9 @@ where
     lifetime: PhantomData<&'a mut T>,
 }
 
-impl<'a, T> Copy for Mut<'a, T> where T: ?Sized {}
+impl<T> Copy for Mut<'_, T> where T: ?Sized {}
 
-impl<'a, T> Clone for Mut<'a, T>
+impl<T> Clone for Mut<'_, T>
 where
     T: ?Sized,
 {
@@ -173,7 +173,7 @@ where
     }
 }
 
-impl<'a, T> Mut<'a, T> {
+impl<T> Mut<'_, T> {
     pub(crate) unsafe fn read(self) -> T {
         self.ptr.as_ptr().read()
     }
diff --git a/src/handler.rs b/src/handler.rs
index 10d8f65..4af5999 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -57,6 +57,7 @@ pub struct MietteHandlerOpts {
     pub(crate) word_separator: Option<textwrap::WordSeparator>,
     pub(crate) word_splitter: Option<textwrap::WordSplitter>,
     pub(crate) highlighter: Option<MietteHighlighter>,
+    pub(crate) show_related_as_nested: Option<bool>,
 }
 
 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 9e0948c..3e9c790 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -37,6 +37,7 @@ pub struct GraphicalReportHandler {
     pub(crate) word_splitter: Option<textwrap::WordSplitter>,
     pub(crate) highlighter: MietteHighlighter,
     pub(crate) link_display_text: Option<String>,
+    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.
@@ -228,7 +237,7 @@ impl GraphicalReportHandler {
         parent_src: Option<&dyn SourceCode>,
     ) -> fmt::Result {
         let src = diagnostic.source_code().or(parent_src);
-        self.render_header(f, diagnostic)?;
+        self.render_header(f, diagnostic, false)?;
         self.render_causes(f, diagnostic, src)?;
         self.render_snippets(f, diagnostic, src)?;
         self.render_footer(f, diagnostic)?;
@@ -252,13 +261,19 @@ impl GraphicalReportHandler {
         Ok(())
     }
 
-    fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
+    fn render_header(
+        &self,
+        f: &mut impl fmt::Write,
+        diagnostic: &(dyn Diagnostic),
+        is_nested: bool,
+    ) -> fmt::Result {
         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 header = String::new();
+        let mut need_newline = is_nested;
         if self.links == LinkStyle::Link && diagnostic.url().is_some() {
             let url = diagnostic.url().unwrap(); // safe
             let code = if let Some(code) = diagnostic.code() {
@@ -275,6 +290,7 @@ impl GraphicalReportHandler {
             );
             write!(header, "{}", link)?;
             writeln!(f, "{}", header)?;
+            need_newline = true;
         } else if let Some(code) = diagnostic.code() {
             write!(header, "{}", code.style(severity_style),)?;
             if self.links == LinkStyle::Text && diagnostic.url().is_some() {
@@ -282,8 +298,11 @@ impl GraphicalReportHandler {
                 write!(header, " ({})", url.style(self.theme.styles.link))?;
             }
             writeln!(f, "{}", header)?;
+            need_newline = true;
+        }
+        if need_newline {
+            writeln!(f)?;
         }
-        writeln!(f)?;
         Ok(())
     }
 
@@ -414,23 +433,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, true)?;
+                    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/src/lib.rs b/src/lib.rs
index 175c929..6396c9f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -266,7 +266,7 @@
 //! }
 //! ```
 //!
-//! To construct your own simple adhoc error use the [miette!] macro:
+//! To construct your own simple adhoc error use the [`miette!`] macro:
 //! ```rust
 //! // my_app/lib/my_internal_file.rs
 //! use miette::{miette, Result};
diff --git a/src/protocol.rs b/src/protocol.rs
index 7f09d4d..c2df294 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -134,7 +134,7 @@ impl From<&str> for Box<dyn Diagnostic> {
     }
 }
 
-impl<'a> From<&str> for Box<dyn Diagnostic + Send + Sync + 'a> {
+impl From<&str> for Box<dyn Diagnostic + Send + Sync + '_> {
     fn from(s: &str) -> Self {
         From::from(String::from(s))
     }
diff --git a/src/source_impls.rs b/src/source_impls.rs
index e362b4a..9c26b71 100644
--- a/src/source_impls.rs
+++ b/src/source_impls.rs
@@ -104,7 +104,7 @@ impl SourceCode for [u8] {
     }
 }
 
-impl<'src> SourceCode for &'src [u8] {
+impl SourceCode for &[u8] {
     fn read_span<'a>(
         &'a self,
         span: &SourceSpan,
@@ -143,7 +143,7 @@ impl SourceCode for str {
 }
 
 /// Makes `src: &'static str` or `struct S<'a> { src: &'a str }` usable.
-impl<'s> SourceCode for &'s str {
+impl SourceCode for &str {
     fn read_span<'a>(
         &'a self,
         span: &SourceSpan,


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index 3176812..b80e16d 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -59,7 +59,7 @@ fn word_wrap_options() -> Result<(), MietteError> {
     let out =
         fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| handler);
 
-    let expected = "\n  × abcdefghijklmnopqrstuvwxyz\n".to_string();
+    let expected = "  × abcdefghijklmnopqrstuvwxyz\n".to_string();
     assert_eq!(expected, out);
 
     // A long word can break with a smaller width
@@ -75,14 +75,14 @@ fn word_wrap_options() -> Result<(), MietteError> {
   │ uvwx
   │ yz
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     // Unless, word breaking is disabled
     let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
         handler.with_width(10).with_break_words(false)
     });
-    let expected = "\n  × abcdefghijklmnopqrstuvwxyz\n".to_string();
+    let expected = "  × abcdefghijklmnopqrstuvwxyz\n".to_string();
     assert_eq!(expected, out);
 
     // Breaks should start at the boundary of each word if possible
@@ -104,7 +104,7 @@ fn word_wrap_options() -> Result<(), MietteError> {
   │ 5678
   │ 90
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     // But long words should not break if word breaking is disabled
@@ -121,7 +121,7 @@ fn word_wrap_options() -> Result<(), MietteError> {
   │ 1234567
   │ 1234567890
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     // Unless, of course, there are hyphens
@@ -149,7 +149,7 @@ fn word_wrap_options() -> Result<(), MietteError> {
   │ f-g-
   │ h
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     // Which requires an additional opt-out
@@ -171,7 +171,7 @@ fn word_wrap_options() -> Result<(), MietteError> {
   │ a-b-c-d-e-f-g
   │ a-b-c-d-e-f-g-h
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     // Or if there are _other_ unicode word boundaries
@@ -199,7 +199,7 @@ fn word_wrap_options() -> Result<(), MietteError> {
   │ f/g/
   │ h
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     // Such things require you to opt-in to only breaking on ASCII whitespace
@@ -221,7 +221,7 @@ fn word_wrap_options() -> Result<(), MietteError> {
   │ a/b/c/d/e/f/g
   │ a/b/c/d/e/f/g/h
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     Ok(())
@@ -245,7 +245,7 @@ fn wrap_option() -> Result<(), MietteError> {
   │ pqr stu
   │ vwx yz
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     // Unless, wrapping is disabled
@@ -254,7 +254,7 @@ fn wrap_option() -> Result<(), MietteError> {
         |handler| handler.with_width(15).with_wrap_lines(false),
     );
     let expected =
-        "\n  × abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz\n".to_string();
+        "  × abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz\n".to_string();
     assert_eq!(expected, out);
 
     // Then, user-defined new lines should be preserved wrapping is disabled
@@ -267,7 +267,7 @@ fn wrap_option() -> Result<(), MietteError> {
   │ abc def ghi jkl mno pqr stu vwx yz
   │ abc def ghi jkl mno pqr stu vwx yz
 "#
-    .to_string();
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 
     Ok(())
@@ -485,8 +485,7 @@ if true {
    · ╰──── big
    ╰────
 "#
-    .to_string();
-
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 }
 
@@ -517,8 +516,7 @@ fn single_line_highlight_span_full_line() {
    ·   ╰── This bit here
    ╰────
 "#
-    .to_string();
-
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 }
 
@@ -1781,8 +1779,7 @@ fn zero_length_eol_span() {
    · ╰── This bit here
    ╰────
 "#
-    .to_string();
-
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 }
 
@@ -1817,8 +1814,7 @@ fn primary_label() {
    ·    ╰── nope
    ╰────
 "#
-    .to_string();
-
+    .trim_start_matches('\n');
     assert_eq!(expected, out);
 }
 
@@ -1968,7 +1964,8 @@ fn syntax_highlighter() {
    ·                  ╰── this is a label
  3 │ }
    ╰────
-"#;
+"#
+    .trim_start_matches('\n');
     assert!(out.contains("\u{1b}[38;2;180;142;173m"));
     assert_eq!(expected, strip_ansi_escapes::strip_str(out))
 }
@@ -2028,6 +2025,7 @@ fn syntax_highlighter_on_real_file() {
         l2 = line,
         l3 = line + 1
     );
+    let expected = expected.trim_start_matches('\n');
     assert!(out.contains("\u{1b}[38;2;180;142;173m"));
     assert_eq!(expected, strip_ansi_escapes::strip_str(out));
 }
diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs
index b21e091..e038830 100644
--- a/tests/test_diagnostic_source_macro.rs
+++ b/tests/test_diagnostic_source_macro.rs
@@ -118,7 +118,8 @@ fn test_diagnostic_source_pass_extra_info() {
 
   this is a footer
 "#
-    .to_string();
+    .trim_start_matches('\n');
+
     assert_eq!(expected, out);
 }
 
@@ -149,7 +150,8 @@ fn test_diagnostic_source_is_output() {
          ╰────
         help: That's where the error is!
       
-"#;
+"#
+    .trim_start_matches('\n');
 
     assert_eq!(expected, out);
 }
@@ -208,7 +210,8 @@ fn test_nested_diagnostic_source_is_output() {
    ╰────
 
   Yooo, a footer
-"#;
+"#
+    .trim_start_matches('\n');
 
     assert_eq!(expected, out);
 }
@@ -295,7 +298,68 @@ fn test_nested_cause_chains_for_related_errors_are_output() {
    ╰────
 
   Yooo, a footer
-"#;
+"#
+    .trim_start_matches('\n');
+
+    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...
+"#
+    .trim_start_matches('\n');
 
     assert_eq!(expected, out);
 }
@@ -345,7 +409,8 @@ fn source_is_inherited_to_causes() {
       
 
   Yooo, a footer
-"#;
+"#
+    .trim_start_matches('\n');
 
     assert_eq!(expected, out);
 }