Repository: zkat/miette (rust)

Issue:
Multiline support for long labels?
Suppose I'm rendering a type error that involves some large/gnarly types. Ideally, I'd like to format/pretty-print those types over multiple lines to make them readable. But afaict this isn't something supported by `miette` at the moment. Any advice? Am I missing something, or are there any plans to support it in future?

Pull request description:
Multiline label
Closes #85 

Now miette supports this!

![image](https://github.com/zkat/miette/assets/22172241/949a783d-54ab-482c-a55a-23d53f2d79b7)

And this:

![image](https://github.com/zkat/miette/assets/22172241/1f23e02d-9b40-4871-bc25-c17a946d3b90)


If you disagree about the specifics of how this is rendered, feel free to comment and I'll change it. If you don't like the way I programmed it as well :). Hope you like it!

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index dd6e584..f8c2cb5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
 
 [dependencies]
 thiserror = "1.0.40"
-miette-derive = { path = "miette-derive", version = "=5.10.0" }
+miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true }
 once_cell = "1.8.0"
 unicode-width = "0.1.9"
 
@@ -44,7 +44,8 @@ lazy_static = "1.4"
 serde_json = "1.0.64"
 
 [features]
-default = []
+default = ["derive"]
+derive = ["miette-derive"]
 no-format-args-capture = []
 fancy-no-backtrace = [
     "owo-colors",
diff --git a/README.md b/README.md
index a29b428..5f79c52 100644
--- a/README.md
+++ b/README.md
@@ -305,6 +305,23 @@ enabled:
 miette = { version = "X.Y.Z", features = ["fancy"] }
 ```
 
+Another way to display a diagnostic is by printing them using the debug formatter. 
+This is, in fact, what returning diagnostics from main ends up doing.
+To do it yourself, you can write the following:
+
+```rust
+use miette::{IntoDiagnostic, Result};
+use semver::Version;
+
+fn just_a_random_function() {
+    let version_result: Result<Version> = "1.2.x".parse().into_diagnostic();
+    match version_result {
+        Err(e) => println!("{:?}", e),
+        Ok(version) => println!("{}", version),
+    }
+}
+```
+
 #### ... diagnostic code URLs
 
 `miette` supports providing a URL for individual diagnostics. This URL will
diff --git a/src/error.rs b/src/error.rs
index 56041ca..4e57a78 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,27 +1,51 @@
-use std::io;
+use std::{fmt, io};
 
 use thiserror::Error;
 
-use crate::{self as miette, Diagnostic};
+use crate::Diagnostic;
 
 /**
 Error enum for miette. Used by certain operations in the protocol.
 */
-#[derive(Debug, Diagnostic, Error)]
+#[derive(Debug, Error)]
 pub enum MietteError {
     /// Wrapper around [`std::io::Error`]. This is returned when something went
     /// wrong while reading a [`SourceCode`](crate::SourceCode).
     #[error(transparent)]
-    #[diagnostic(code(miette::io_error), url(docsrs))]
     IoError(#[from] io::Error),
 
     /// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the
     /// bounds of a given [`SourceCode`](crate::SourceCode).
     #[error("The given offset is outside the bounds of its Source")]
-    #[diagnostic(
-        code(miette::span_out_of_bounds),
-        help("Double-check your spans. Do you have an off-by-one error?"),
-        url(docsrs)
-    )]
     OutOfBounds,
 }
+
+impl Diagnostic for MietteError {
+    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+        match self {
+            MietteError::IoError(_) => Some(Box::new("miette::io_error")),
+            MietteError::OutOfBounds => Some(Box::new("miette::span_out_of_bounds")),
+        }
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+        match self {
+            MietteError::IoError(_) => None,
+            MietteError::OutOfBounds => Some(Box::new(
+                "Double-check your spans. Do you have an off-by-one error?",
+            )),
+        }
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+        let crate_version = env!("CARGO_PKG_VERSION");
+        let variant = match self {
+            MietteError::IoError(_) => "#variant.IoError",
+            MietteError::OutOfBounds => "#variant.OutOfBounds",
+        };
+        Some(Box::new(format!(
+            "https://docs.rs/miette/{}/miette/enum.MietteError.html{}",
+            crate_version, variant,
+        )))
+    }
+}
diff --git a/src/handler.rs b/src/handler.rs
index e983a55..e32f3ef 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -55,6 +55,9 @@ pub struct MietteHandlerOpts {
     pub(crate) context_lines: Option<usize>,
     pub(crate) tab_width: Option<usize>,
     pub(crate) with_cause_chain: Option<bool>,
+    pub(crate) break_words: Option<bool>,
+    pub(crate) word_separator: Option<textwrap::WordSeparator>,
+    pub(crate) word_splitter: Option<textwrap::WordSplitter>,
 }
 
 impl MietteHandlerOpts {
@@ -86,6 +89,27 @@ impl MietteHandlerOpts {
         self
     }
 
+    /// If true, long words can be broken when wrapping.
+    ///
+    /// If false, long words will not be broken when they exceed the width.
+    ///
+    /// Defaults to true.
+    pub fn break_words(mut self, break_words: bool) -> Self {
+        self.break_words = Some(break_words);
+        self
+    }
+
+    /// Sets the `textwrap::WordSeparator` to use when determining wrap points.
+    pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
+        self.word_separator = Some(word_separator);
+        self
+    }
+
+    /// Sets the `textwrap::WordSplitter` to use when determining wrap points.
+    pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
+        self.word_splitter = Some(word_splitter);
+        self
+    }
     /// Include the cause chain of the top-level error in the report.
     pub fn with_cause_chain(mut self) -> Self {
         self.with_cause_chain = Some(true);
@@ -233,6 +257,16 @@ impl MietteHandlerOpts {
             if let Some(w) = self.tab_width {
                 handler = handler.tab_width(w);
             }
+            if let Some(b) = self.break_words {
+                handler = handler.with_break_words(b)
+            }
+            if let Some(s) = self.word_separator {
+                handler = handler.with_word_separator(s)
+            }
+            if let Some(s) = self.word_splitter {
+                handler = handler.with_word_splitter(s)
+            }
+
             MietteHandler {
                 inner: Box::new(handler),
             }
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 44d92b7..baadfb3 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -20,7 +20,7 @@ This printer can be customized by using [`new_themed()`](GraphicalReportHandler:
 
 See [`set_hook()`](crate::set_hook) for more details on customizing your global
 printer.
-*/
+ */
 #[derive(Debug, Clone)]
 pub struct GraphicalReportHandler {
     pub(crate) links: LinkStyle,
@@ -30,6 +30,9 @@ pub struct GraphicalReportHandler {
     pub(crate) context_lines: usize,
     pub(crate) tab_width: usize,
     pub(crate) with_cause_chain: bool,
+    pub(crate) break_words: bool,
+    pub(crate) word_separator: Option<textwrap::WordSeparator>,
+    pub(crate) word_splitter: Option<textwrap::WordSplitter>,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -51,6 +54,9 @@ impl GraphicalReportHandler {
             context_lines: 1,
             tab_width: 4,
             with_cause_chain: true,
+            break_words: true,
+            word_separator: None,
+            word_splitter: None,
         }
     }
 
@@ -64,6 +70,9 @@ impl GraphicalReportHandler {
             context_lines: 1,
             tab_width: 4,
             with_cause_chain: true,
+            break_words: true,
+            word_separator: None,
+            word_splitter: None,
         }
     }
 
@@ -122,6 +131,24 @@ impl GraphicalReportHandler {
         self
     }
 
+    /// Enables or disables breaking of words during wrapping.
+    pub fn with_break_words(mut self, break_words: bool) -> Self {
+        self.break_words = break_words;
+        self
+    }
+
+    /// Sets the word separator to use when wrapping.
+    pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
+        self.word_separator = Some(word_separator);
+        self
+    }
+
+    /// Sets the word splitter to usewhen wrapping.
+    pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
+        self.word_splitter = Some(word_splitter);
+        self
+    }
+
     /// Sets the 'global' footer for this handler.
     pub fn with_footer(mut self, footer: String) -> Self {
         self.footer = Some(footer);
@@ -159,9 +186,17 @@ impl GraphicalReportHandler {
         if let Some(footer) = &self.footer {
             writeln!(f)?;
             let width = self.termwidth.saturating_sub(4);
-            let opts = textwrap::Options::new(width)
+            let mut opts = textwrap::Options::new(width)
                 .initial_indent("  ")
-                .subsequent_indent("  ");
+                .subsequent_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);
+            }
+
             writeln!(f, "{}", textwrap::fill(footer, opts))?;
         }
         Ok(())
@@ -212,9 +247,16 @@ impl GraphicalReportHandler {
         let initial_indent = format!("  {} ", severity_icon.style(severity_style));
         let rest_indent = format!("  {} ", self.theme.characters.vbar.style(severity_style));
         let width = self.termwidth.saturating_sub(2);
-        let opts = textwrap::Options::new(width)
+        let mut opts = textwrap::Options::new(width)
             .initial_indent(&initial_indent)
-            .subsequent_indent(&rest_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);
+        }
 
         writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
 
@@ -251,9 +293,17 @@ impl GraphicalReportHandler {
                 )
                 .style(severity_style)
                 .to_string();
-                let opts = textwrap::Options::new(width)
+                let mut opts = textwrap::Options::new(width)
                     .initial_indent(&initial_indent)
-                    .subsequent_indent(&rest_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);
+                }
+
                 match error {
                     ErrorKind::Diagnostic(diag) => {
                         let mut inner = String::new();
@@ -280,9 +330,17 @@ impl GraphicalReportHandler {
         if let Some(help) = diagnostic.help() {
             let width = self.termwidth.saturating_sub(4);
             let initial_indent = "  help: ".style(self.theme.styles.help).to_string();
-            let opts = textwrap::Options::new(width)
+            let mut opts = textwrap::Options::new(width)
                 .initial_indent(&initial_indent)
-                .subsequent_indent("        ");
+                .subsequent_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);
+            }
+
             writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
         }
         Ok(())
@@ -487,7 +545,13 @@ impl GraphicalReportHandler {
                 // no line number!
                 self.write_no_linum(f, linum_width)?;
                 // gutter _again_
-                self.render_highlight_gutter(f, max_gutter, line, &labels)?;
+                self.render_highlight_gutter(
+                    f,
+                    max_gutter,
+                    line,
+                    &labels,
+                    LabelRenderMode::SingleLine,
+                )?;
                 self.render_single_line_highlights(
                     f,
                     line,
@@ -499,11 +563,7 @@ impl GraphicalReportHandler {
             }
             for hl in multi_line {
                 if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
-                    // no line number!
-                    self.write_no_linum(f, linum_width)?;
-                    // gutter _again_
-                    self.render_highlight_gutter(f, max_gutter, line, &labels)?;
-                    self.render_multi_line_end(f, hl)?;
+                    self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
                 }
             }
         }
@@ -517,6 +577,91 @@ impl GraphicalReportHandler {
         Ok(())
     }
 
+    fn render_multi_line_end(
+        &self,
+        f: &mut impl fmt::Write,
+        labels: &[FancySpan],
+        max_gutter: usize,
+        linum_width: usize,
+        line: &Line,
+        label: &FancySpan,
+    ) -> fmt::Result {
+        // no line number!
+        self.write_no_linum(f, linum_width)?;
+
+        if let Some(label_parts) = label.label_parts() {
+            // if it has a label, how long is it?
+            let (first, rest) = label_parts
+                .split_first()
+                .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
+
+            if rest.is_empty() {
+                // gutter _again_
+                self.render_highlight_gutter(
+                    f,
+                    max_gutter,
+                    line,
+                    &labels,
+                    LabelRenderMode::SingleLine,
+                )?;
+
+                self.render_multi_line_end_single(
+                    f,
+                    first,
+                    label.style,
+                    LabelRenderMode::SingleLine,
+                )?;
+            } else {
+                // gutter _again_
+                self.render_highlight_gutter(
+                    f,
+                    max_gutter,
+                    line,
+                    &labels,
+                    LabelRenderMode::MultiLineFirst,
+                )?;
+
+                self.render_multi_line_end_single(
+                    f,
+                    first,
+                    label.style,
+                    LabelRenderMode::MultiLineFirst,
+                )?;
+                for label_line in rest {
+                    // no line number!
+                    self.write_no_linum(f, linum_width)?;
+                    // gutter _again_
+                    self.render_highlight_gutter(
+                        f,
+                        max_gutter,
+                        line,
+                        &labels,
+                        LabelRenderMode::MultiLineRest,
+                    )?;
+                    self.render_multi_line_end_single(
+                        f,
+                        label_line,
+                        label.style,
+                        LabelRenderMode::MultiLineRest,
+                    )?;
+                }
+            }
+        } else {
+            // gutter _again_
+            self.render_highlight_gutter(
+                f,
+                max_gutter,
+                line,
+                &labels,
+                LabelRenderMode::SingleLine,
+            )?;
+            // has no label
+            writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
+        }
+
+        Ok(())
+    }
+
     fn render_line_gutter(
         &self,
         f: &mut impl fmt::Write,
@@ -585,6 +730,7 @@ impl GraphicalReportHandler {
         max_gutter: usize,
         line: &Line,
         highlights: &[FancySpan],
+        render_mode: LabelRenderMode,
     ) -> fmt::Result {
         if max_gutter == 0 {
             return Ok(());
@@ -594,15 +740,33 @@ impl GraphicalReportHandler {
         let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
         for (i, hl) in applicable.enumerate() {
             if !line.span_line_only(hl) && line.span_ends(hl) {
-                gutter.push_str(&chars.lbot.style(hl.style).to_string());
-                gutter.push_str(
-                    &chars
-                        .hbar
-                        .to_string()
-                        .repeat(max_gutter.saturating_sub(i) + 2)
-                        .style(hl.style)
-                        .to_string(),
-                );
+                if render_mode == LabelRenderMode::MultiLineRest {
+                    // this is to make multiline labels work. We want to make the right amount
+                    // of horizontal space for them, but not actually draw the lines
+                    for _ in 0..max_gutter.saturating_sub(i) + 2 {
+                        gutter.push(' ');
+                    }
+                } else {
+                    gutter.push_str(&chars.lbot.style(hl.style).to_string());
+
+                    gutter.push_str(
+                        &chars
+                            .hbar
+                            .to_string()
+                            .repeat(
+                                max_gutter.saturating_sub(i)
+                                    // if we are rendering a multiline label, then leave a bit of space for the
+                                    // rcross character
+                                    + if render_mode == LabelRenderMode::MultiLineFirst {
+                                        1
+                                    } else {
+                                        2
+                                    },
+                            )
+                            .style(hl.style)
+                            .to_string(),
+                    );
+                }
                 break;
             } else {
                 gutter.push_str(&chars.vbar.style(hl.style).to_string());
@@ -718,31 +882,33 @@ impl GraphicalReportHandler {
                 let byte_start = hl.offset();
                 let byte_end = hl.offset() + hl.len();
                 let start = self.visual_offset(line, byte_start, true).max(highest);
-                let end = self.visual_offset(line, byte_end, false).max(start + 1);
+                let end = if hl.len() == 0 {
+                    start + 1
+                } else {
+                    self.visual_offset(line, byte_end, false).max(start + 1)
+                };
 
                 let vbar_offset = (start + end) / 2;
                 let num_left = vbar_offset - start;
                 let num_right = end - vbar_offset - 1;
-                if start < end {
-                    underlines.push_str(
-                        &format!(
-                            "{:width$}{}{}{}",
-                            "",
-                            chars.underline.to_string().repeat(num_left),
-                            if hl.len() == 0 {
-                                chars.uarrow
-                            } else if hl.label().is_some() {
-                                chars.underbar
-                            } else {
-                                chars.underline
-                            },
-                            chars.underline.to_string().repeat(num_right),
-                            width = start.saturating_sub(highest),
-                        )
-                        .style(hl.style)
-                        .to_string(),
-                    );
-                }
+                underlines.push_str(
+                    &format!(
+                        "{:width$}{}{}{}",
+                        "",
+                        chars.underline.to_string().repeat(num_left),
+                        if hl.len() == 0 {
+                            chars.uarrow
+                        } else if hl.label().is_some() {
+                            chars.underbar
+                        } else {
+                            chars.underline
+                        },
+                        chars.underline.to_string().repeat(num_right),
+                        width = start.saturating_sub(highest),
+                    )
+                    .style(hl.style)
+                    .to_string(),
+                );
                 highest = std::cmp::max(highest, end);
 
                 (hl, vbar_offset)
@@ -751,27 +917,40 @@ impl GraphicalReportHandler {
         writeln!(f, "{}", underlines)?;
 
         for hl in single_liners.iter().rev() {
-            if let Some(label) = hl.label() {
-                self.write_no_linum(f, linum_width)?;
-                self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
-                let mut curr_offset = 1usize;
-                for (offset_hl, vbar_offset) in &vbar_offsets {
-                    while curr_offset < *vbar_offset + 1 {
-                        write!(f, " ")?;
-                        curr_offset += 1;
-                    }
-                    if *offset_hl != hl {
-                        write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
-                        curr_offset += 1;
-                    } else {
-                        let lines = format!(
-                            "{}{} {}",
-                            chars.lbot,
-                            chars.hbar.to_string().repeat(2),
-                            label,
-                        );
-                        writeln!(f, "{}", lines.style(hl.style))?;
-                        break;
+            if let Some(label) = hl.label_parts() {
+                if label.len() == 1 {
+                    self.write_label_text(
+                        f,
+                        line,
+                        linum_width,
+                        max_gutter,
+                        all_highlights,
+                        chars,
+                        &vbar_offsets,
+                        hl,
+                        &label[0],
+                        LabelRenderMode::SingleLine,
+                    )?;
+                } else {
+                    let mut first = true;
+                    for label_line in &label {
+                        self.write_label_text(
+                            f,
+                            line,
+                            linum_width,
+                            max_gutter,
+                            all_highlights,
+                            chars,
+                            &vbar_offsets,
+                            hl,
+                            label_line,
+                            if first {
+                                LabelRenderMode::MultiLineFirst
+                            } else {
+                                LabelRenderMode::MultiLineRest
+                            },
+                        )?;
+                        first = false;
                     }
                 }
             }
@@ -779,13 +958,80 @@ impl GraphicalReportHandler {
         Ok(())
     }
 
-    fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
-        writeln!(
+    // I know it's not good practice, but making this a function makes a lot of sense
+    // and making a struct for this does not...
+    #[allow(clippy::too_many_arguments)]
+    fn write_label_text(
+        &self,
+        f: &mut impl fmt::Write,
+        line: &Line,
+        linum_width: usize,
+        max_gutter: usize,
+        all_highlights: &[FancySpan],
+        chars: &ThemeCharacters,
+        vbar_offsets: &[(&&FancySpan, usize)],
+        hl: &&FancySpan,
+        label: &str,
+        render_mode: LabelRenderMode,
+    ) -> fmt::Result {
+        self.write_no_linum(f, linum_width)?;
+        self.render_highlight_gutter(
             f,
-            "{} {}",
-            self.theme.characters.hbar.style(hl.style),
-            hl.label().unwrap_or_else(|| "".into()),
+            max_gutter,
+            line,
+            all_highlights,
+            LabelRenderMode::SingleLine,
         )?;
+        let mut curr_offset = 1usize;
+        for (offset_hl, vbar_offset) in vbar_offsets {
+            while curr_offset < *vbar_offset + 1 {
+                write!(f, " ")?;
+                curr_offset += 1;
+            }
+            if *offset_hl != hl {
+                write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
+                curr_offset += 1;
+            } else {
+                let lines = match render_mode {
+                    LabelRenderMode::SingleLine => format!(
+                        "{}{} {}",
+                        chars.lbot,
+                        chars.hbar.to_string().repeat(2),
+                        label,
+                    ),
+                    LabelRenderMode::MultiLineFirst => {
+                        format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
+                    }
+                    LabelRenderMode::MultiLineRest => {
+                        format!("  {} {}", chars.vbar, label,)
+                    }
+                };
+                writeln!(f, "{}", lines.style(hl.style))?;
+                break;
+            }
+        }
+        Ok(())
+    }
+
+    fn render_multi_line_end_single(
+        &self,
+        f: &mut impl fmt::Write,
+        label: &str,
+        style: Style,
+        render_mode: LabelRenderMode,
+    ) -> fmt::Result {
+        match render_mode {
+            LabelRenderMode::SingleLine => {
+                writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
+            }
+            LabelRenderMode::MultiLineFirst => {
+                writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
+            }
+            LabelRenderMode::MultiLineRest => {
+                writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
+            }
+        }
+
         Ok(())
     }
 
@@ -864,6 +1110,16 @@ impl ReportHandler for GraphicalReportHandler {
 Support types
 */
 
+#[derive(PartialEq, Debug)]
+enum LabelRenderMode {
+    /// we're rendering a single line label (or not rendering in any special way)
+    SingleLine,
+    /// we're rendering a multiline label
+    MultiLineFirst,
+    /// we're rendering the rest of a multiline label
+    MultiLineRest,
+}
+
 #[derive(Debug)]
 struct Line {
     line_number: usize,
@@ -881,10 +1137,10 @@ impl Line {
         let spanlen = if span.len() == 0 { 1 } else { span.len() };
         // Span starts in this line
         (span.offset() >= self.offset && span.offset() < self.offset + self.length)
-        // Span passes through this line
-        || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
-        // Span ends on this line
-        || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
+            // Span passes through this line
+            || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
+            // Span ends on this line
+            || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
     }
 
     // A 'flyby' is a multi-line span that technically covers this line, but
@@ -914,7 +1170,10 @@ impl Line {
 
 #[derive(Debug, Clone)]
 struct FancySpan {
-    label: Option<String>,
+    /// this is deliberately an option of a vec because I wanted to be very explicit
+    /// that there can also be *no* label. If there is a label, it can have multiple
+    /// lines which is what the vec is for.
+    label: Option<Vec<String>>,
     span: SourceSpan,
     style: Style,
 }
@@ -925,9 +1184,17 @@ impl PartialEq for FancySpan {
     }
 }
 
+fn split_label(v: String) -> Vec<String> {
+    v.split('\n').map(|i| i.to_string()).collect()
+}
+
 impl FancySpan {
     fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
-        FancySpan { label, span, style }
+        FancySpan {
+            label: label.map(split_label),
+            span,
+            style,
+        }
     }
 
     fn style(&self) -> Style {
@@ -937,7 +1204,15 @@ impl FancySpan {
     fn label(&self) -> Option<String> {
         self.label
             .as_ref()
-            .map(|l| l.style(self.style()).to_string())
+            .map(|l| l.join("\n").style(self.style()).to_string())
+    }
+
+    fn label_parts(&self) -> Option<Vec<String>> {
+        self.label.as_ref().map(|l| {
+            l.iter()
+                .map(|i| i.style(self.style()).to_string())
+                .collect()
+        })
     }
 
     fn offset(&self) -> usize {
diff --git a/src/lib.rs b/src/lib.rs
index 20589ef..d80efa7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -304,6 +304,23 @@
 //! miette = { version = "X.Y.Z", features = ["fancy"] }
 //! ```
 //!
+//! Another way to display a diagnostic is by printing them using the debug formatter.
+//! This is, in fact, what returning diagnostics from main ends up doing.
+//! To do it yourself, you can write the following:
+//!
+//! ```rust
+//! use miette::{IntoDiagnostic, Result};
+//! use semver::Version;
+//!
+//! fn just_a_random_function() {
+//!     let version_result: Result<Version> = "1.2.x".parse().into_diagnostic();
+//!     match version_result {
+//!         Err(e) => println!("{:?}", e),
+//!         Ok(version) => println!("{}", version),
+//!     }
+//! }
+//! ```
+//!
 //! ### ... diagnostic code URLs
 //!
 //! `miette` supports providing a URL for individual diagnostics. This URL will
@@ -593,6 +610,7 @@
 //!             .unicode(false)
 //!             .context_lines(3)
 //!             .tab_width(4)
+//!             .break_words(true)
 //!             .build(),
 //!     )
 //! }))
@@ -652,6 +670,7 @@
 //! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also
 //! under the Apache License. Some code is taken from
 //! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
+#[cfg(feature = "derive")]
 pub use miette_derive::*;
 
 pub use error::*;


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index 31db4dc..f59f849 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -34,6 +34,190 @@ fn fmt_report(diag: Report) -> String {
     out
 }
 
+fn fmt_report_with_settings(
+    diag: Report,
+    with_settings: fn(GraphicalReportHandler) -> GraphicalReportHandler,
+) -> String {
+    let mut out = String::new();
+
+    let handler = with_settings(GraphicalReportHandler::new_themed(
+        GraphicalTheme::unicode_nocolor(),
+    ));
+
+    handler.render_report(&mut out, diag.as_ref()).unwrap();
+
+    println!("Error:\n```\n{}\n```", out);
+
+    out
+}
+
+#[test]
+fn word_wrap_options() -> Result<(), MietteError> {
+    // By default, a long word should not break
+    let out =
+        fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| handler);
+
+    let expected = "  × abcdefghijklmnopqrstuvwxyz\n".to_string();
+    assert_eq!(expected, out);
+
+    // A long word can break with a smaller width
+    let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
+        handler.with_width(10)
+    });
+    let expected = r#"  × abcd
+  │ efgh
+  │ ijkl
+  │ mnop
+  │ qrst
+  │ uvwx
+  │ yz
+"#
+    .to_string();
+    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 = "  × abcdefghijklmnopqrstuvwxyz\n".to_string();
+    assert_eq!(expected, out);
+
+    // Breaks should start at the boundary of each word if possible
+    let out = fmt_report_with_settings(
+        Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
+        |handler| handler.with_width(10),
+    );
+    let expected = r#"  × 12
+  │ 123
+  │ 1234
+  │ 1234
+  │ 5
+  │ 1234
+  │ 56
+  │ 1234
+  │ 567
+  │ 1234
+  │ 5678
+  │ 90
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    // But long words should not break if word breaking is disabled
+    let out = fmt_report_with_settings(
+        Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
+        |handler| handler.with_width(10).with_break_words(false),
+    );
+    let expected = r#"  × 12
+  │ 123
+  │ 1234
+  │ 12345
+  │ 123456
+  │ 1234567
+  │ 1234567890
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    // Unless, of course, there are hyphens
+    let out = fmt_report_with_settings(
+        Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
+        |handler| handler.with_width(10).with_break_words(false),
+    );
+    let expected = r#"  × a-b
+  │ a-b-
+  │ c a-
+  │ b-c-
+  │ d a-
+  │ b-c-
+  │ d-e
+  │ a-b-
+  │ c-d-
+  │ e-f
+  │ a-b-
+  │ c-d-
+  │ e-f-
+  │ g a-
+  │ b-c-
+  │ d-e-
+  │ f-g-
+  │ h
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    // Which requires an additional opt-out
+    let out = fmt_report_with_settings(
+        Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
+        |handler| {
+            handler
+                .with_width(10)
+                .with_break_words(false)
+                .with_word_splitter(textwrap::WordSplitter::NoHyphenation)
+        },
+    );
+    let expected = r#"  × a-b
+  │ a-b-c
+  │ a-b-c-d
+  │ a-b-c-d-e
+  │ a-b-c-d-e-f
+  │ a-b-c-d-e-f-g
+  │ a-b-c-d-e-f-g-h
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    // Or if there are _other_ unicode word boundaries
+    let out = fmt_report_with_settings(
+        Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
+        |handler| handler.with_width(10).with_break_words(false),
+    );
+    let expected = r#"  × a/b
+  │ a/b/
+  │ c a/
+  │ b/c/
+  │ d a/
+  │ b/c/
+  │ d/e
+  │ a/b/
+  │ c/d/
+  │ e/f
+  │ a/b/
+  │ c/d/
+  │ e/f/
+  │ g a/
+  │ b/c/
+  │ d/e/
+  │ f/g/
+  │ h
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    // Such things require you to opt-in to only breaking on ASCII whitespace
+    let out = fmt_report_with_settings(
+        Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
+        |handler| {
+            handler
+                .with_width(10)
+                .with_break_words(false)
+                .with_word_separator(textwrap::WordSeparator::AsciiSpace)
+        },
+    );
+    let expected = r#"  × a/b
+  │ a/b/c
+  │ a/b/c/d
+  │ a/b/c/d/e
+  │ a/b/c/d/e/f
+  │ a/b/c/d/e/f/g
+  │ a/b/c/d/e/f/g/h
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    Ok(())
+}
+
 #[test]
 fn empty_source() -> Result<(), MietteError> {
     #[derive(Debug, Diagnostic, Error)]
@@ -67,6 +251,52 @@ fn empty_source() -> Result<(), MietteError> {
     Ok(())
 }
 
+#[test]
+fn multiple_spans_multiline() {
+    #[derive(Error, Debug, Diagnostic)]
+    #[error("oops!")]
+    #[diagnostic(severity(Error))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label("big")]
+        big: SourceSpan,
+        #[label("small")]
+        small: SourceSpan,
+    }
+    let err = MyBad {
+        src: NamedSource::new(
+            "issue",
+            "\
+if true {
+    a
+} else {
+    b
+}",
+        ),
+        big: (0, 32).into(),
+        small: (14, 1).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+
+    let expected = r#"  × oops!
+   ╭─[issue:1:1]
+ 1 │ ╭─▶ if true {
+ 2 │ │╭▶     a
+   · ││    ┬
+   · ││    ╰── small
+ 3 │ │   } else {
+ 4 │ │       b
+ 5 │ ├─▶ }
+   · ╰──── big
+   ╰────
+"#
+    .to_string();
+
+    assert_eq!(expected, out);
+}
+
 #[test]
 fn single_line_highlight_span_full_line() {
     #[derive(Error, Debug, Diagnostic)]
@@ -541,6 +771,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
     Ok(())
 }
 
+#[test]
+fn multiline_label() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label("this bit here\nand\nthis\ntoo")]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\ntext\n  here".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight: (7, 4).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = r#"oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:2:1]
+ 1 │ source
+ 2 │ text
+   · ──┬─
+   ·   ╰─┤ this bit here
+   ·     │ and
+   ·     │ this
+   ·     │ too
+ 3 │   here
+   ╰────
+  help: try doing it better next time?
+"#
+    .trim_start()
+    .to_string();
+    assert_eq!(expected, out);
+    Ok(())
+}
+
+#[test]
+fn multiple_multi_line_labels() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label = "x\ny"]
+        highlight1: SourceSpan,
+        #[label = "z\nw"]
+        highlight2: SourceSpan,
+        #[label = "a\nb"]
+        highlight3: SourceSpan,
+    }
+
+    let src = "source\n  text text text text text\n    here".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight1: (9, 4).into(),
+        highlight2: (14, 4).into(),
+        highlight3: (24, 4).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = r#"oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:2:3]
+ 1 │ source
+ 2 │   text text text text text
+   ·   ──┬─ ──┬─      ──┬─
+   ·     │    │         ╰─┤ a
+   ·     │    │           │ b
+   ·     │    ╰─┤ z
+   ·     │      │ w
+   ·     ╰─┤ x
+   ·       │ y
+ 3 │     here
+   ╰────
+  help: try doing it better next time?
+"#
+    .trim_start()
+    .to_string();
+    assert_eq!(expected, out);
+    Ok(())
+}
+
 #[test]
 fn multiple_same_line_highlights() -> Result<(), MietteError> {
     #[derive(Debug, Diagnostic, Error)]
@@ -669,6 +987,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
     Ok(())
 }
 
+#[test]
+fn multiline_highlight_multiline_label() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label = "these two lines\nare the problem"]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight: (9, 11).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = r#"oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:2:3]
+ 1 │     source
+ 2 │ ╭─▶   text
+ 3 │ ├─▶     here
+   · ╰──┤ these two lines
+   ·    │ are the problem
+   ╰────
+  help: try doing it better next time?
+"#
+    .trim_start()
+    .to_string();
+    assert_eq!(expected, out);
+    Ok(())
+}
+
 #[test]
 fn multiline_highlight_flyby() -> Result<(), MietteError> {
     #[derive(Debug, Diagnostic, Error)]
@@ -1321,3 +1676,40 @@ fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> {
     assert_eq!(expected, out);
     Ok(())
 }
+
+#[test]
+fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        #[source_code]
+        src: NamedSource,
+        #[label("this bit here")]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\n  👼🏼text\n    here".to_string();
+    let err = MyBad {
+        src: NamedSource::new("bad_file.rs", src),
+        highlight: (10, 0).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("Error: {}", out);
+    let expected = r#"oops::my::bad
+
+  × oops!
+   ╭─[bad_file.rs:2:4]
+ 1 │ source
+ 2 │   👼🏼text
+   ·   ▲
+   ·   ╰── this bit here
+ 3 │     here
+   ╰────
+  help: try doing it better next time?
+"#
+    .trim_start()
+    .to_string();
+    assert_eq!(expected, out);
+    Ok(())
+}