Repository: zkat/miette
Pull request #328 (2024-01-04 16:58:54)

Add `wrap_lines: bool` option allowing wrapping be disabled entirely
Hi!

I want to be able to disable line wrapping entirely for logging runs and for users that would rather rely on their terminal's line wrapping. This pull request introduces a `wrap_lines` option which disables wrapping. Since `textwrap::fill` is used insert prefixes as well, this requires a little implementation to preserve that behavior.



Patch:
diff --git a/src/handler.rs b/src/handler.rs
index e32f3ef..38c9fc8 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -56,6 +56,7 @@ pub struct MietteHandlerOpts {
     pub(crate) tab_width: Option<usize>,
     pub(crate) with_cause_chain: Option<bool>,
     pub(crate) break_words: Option<bool>,
+    pub(crate) wrap_lines: Option<bool>,
     pub(crate) word_separator: Option<textwrap::WordSeparator>,
     pub(crate) word_splitter: Option<textwrap::WordSplitter>,
 }
@@ -89,6 +90,16 @@ impl MietteHandlerOpts {
         self
     }
 
+    /// If true, long lines can be wrapped.
+    ///
+    /// If false, long lines will not be broken when they exceed the width.
+    ///
+    /// Defaults to true.
+    pub fn wrap_lines(mut self, wrap_lines: bool) -> Self {
+        self.wrap_lines = Some(wrap_lines);
+        self
+    }
+
     /// If true, long words can be broken when wrapping.
     ///
     /// If false, long words will not be broken when they exceed the width.
@@ -98,7 +109,6 @@ impl MietteHandlerOpts {
         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);
@@ -260,6 +270,9 @@ impl MietteHandlerOpts {
             if let Some(b) = self.break_words {
                 handler = handler.with_break_words(b)
             }
+            if let Some(b) = self.wrap_lines {
+                handler = handler.with_wrap_lines(b)
+            }
             if let Some(s) = self.word_separator {
                 handler = handler.with_word_separator(s)
             }
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 3193472..951303a 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -30,6 +30,7 @@ pub struct GraphicalReportHandler {
     pub(crate) context_lines: usize,
     pub(crate) tab_width: usize,
     pub(crate) with_cause_chain: bool,
+    pub(crate) wrap_lines: bool,
     pub(crate) break_words: bool,
     pub(crate) word_separator: Option<textwrap::WordSeparator>,
     pub(crate) word_splitter: Option<textwrap::WordSplitter>,
@@ -54,6 +55,7 @@ impl GraphicalReportHandler {
             context_lines: 1,
             tab_width: 4,
             with_cause_chain: true,
+            wrap_lines: true,
             break_words: true,
             word_separator: None,
             word_splitter: None,
@@ -69,6 +71,7 @@ impl GraphicalReportHandler {
             footer: None,
             context_lines: 1,
             tab_width: 4,
+            wrap_lines: true,
             with_cause_chain: true,
             break_words: true,
             word_separator: None,
@@ -131,6 +134,12 @@ impl GraphicalReportHandler {
         self
     }
 
+    /// Enables or disables wrapping of lines to fit the width.
+    pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
+        self.wrap_lines = wrap_lines;
+        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;
@@ -197,7 +206,7 @@ impl GraphicalReportHandler {
                 opts = opts.word_splitter(word_splitter);
             }
 
-            writeln!(f, "{}", textwrap::fill(footer, opts))?;
+            writeln!(f, "{}", self.wrap(footer, opts))?;
         }
         Ok(())
     }
@@ -258,7 +267,7 @@ impl GraphicalReportHandler {
             opts = opts.word_splitter(word_splitter);
         }
 
-        writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
+        writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?;
 
         if !self.with_cause_chain {
             return Ok(());
@@ -314,10 +323,10 @@ impl GraphicalReportHandler {
                         inner_renderer.with_cause_chain = false;
                         inner_renderer.render_report(&mut inner, diag)?;
 
-                        writeln!(f, "{}", textwrap::fill(&inner, opts))?;
+                        writeln!(f, "{}", self.wrap(&inner, opts))?;
                     }
                     ErrorKind::StdError(err) => {
-                        writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?;
+                        writeln!(f, "{}", self.wrap(&err.to_string(), opts))?;
                     }
                 }
             }
@@ -341,7 +350,7 @@ impl GraphicalReportHandler {
                 opts = opts.word_splitter(word_splitter);
             }
 
-            writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
+            writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
         }
         Ok(())
     }
@@ -810,6 +819,41 @@ impl GraphicalReportHandler {
         Ok(())
     }
 
+    fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
+        if self.wrap_lines {
+            textwrap::fill(text, opts)
+        } else {
+            // Format without wrapping, but retain the indentation options
+            // Implementation based on `textwrap::indent`
+            let mut result = String::with_capacity(2 * text.len());
+            let trimmed_indent = opts.subsequent_indent.trim_end();
+            for (idx, line) in text.split_terminator('\n').enumerate() {
+                if idx > 0 {
+                    result.push('\n');
+                }
+                if idx == 0 {
+                    if line.trim().is_empty() {
+                        result.push_str(opts.initial_indent.trim_end());
+                    } else {
+                        result.push_str(opts.initial_indent);
+                    }
+                } else {
+                    if line.trim().is_empty() {
+                        result.push_str(trimmed_indent);
+                    } else {
+                        result.push_str(opts.subsequent_indent);
+                    }
+                }
+                result.push_str(line);
+            }
+            if text.ends_with('\n') {
+                // split_terminator will have eaten the final '\n'.
+                result.push('\n');
+            }
+            result
+        }
+    }
+
     fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
         write!(
             f,


Test patch:
diff --git a/tests/graphical.rs b/tests/graphical.rs
index aabf167..21185f5 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -218,6 +218,50 @@ fn word_wrap_options() -> Result<(), MietteError> {
     Ok(())
 }
 
+#[test]
+fn wrap_option() -> Result<(), MietteError> {
+    // A line should break on the width
+    let out = fmt_report_with_settings(
+        Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"),
+        |handler| handler.with_width(15),
+    );
+    let expected = r#"  × abc def
+  │ ghi jkl
+  │ mno pqr
+  │ stu vwx
+  │ yz abc
+  │ def ghi
+  │ jkl mno
+  │ pqr stu
+  │ vwx yz
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    // Unless, wrapping is disabled
+    let out = fmt_report_with_settings(
+        Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"),
+        |handler| handler.with_width(15).with_wrap_lines(false),
+    );
+    let expected =
+        "  × 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
+    let out = fmt_report_with_settings(
+      Report::msg("abc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz"),
+      |handler| handler.with_width(15).with_wrap_lines(false),
+  );
+    let expected = r#"  × abc def ghi jkl mno pqr stu vwx yz
+  │ abc def ghi jkl mno pqr stu vwx yz
+  │ abc def ghi jkl mno pqr stu vwx yz
+"#
+    .to_string();
+    assert_eq!(expected, out);
+
+    Ok(())
+}
+
 #[test]
 fn empty_source() -> Result<(), MietteError> {
     #[derive(Debug, Diagnostic, Error)]