Repository: zkat/miette
Pull request #319 (2023-11-09 23:55:32)

add example to README and docs
Closing #96 

This is just a tiny docs change I that @esoterra proposed in an old issue. I thought it was still relevant, so I added an example for it. Let me know if this is how you like it or not :)



Patch:
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/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 8cec88b..35e9c79 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -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(())
diff --git a/src/lib.rs b/src/lib.rs
index 3cb021b..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(),
 //!     )
 //! }))


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index d24879a..536efcd 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)]