Skip to content

Commit

Permalink
feat(graphical): Expose additional textwrap options (#321)
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb authored Nov 15, 2023
1 parent c7ba5b7 commit fd77257
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 8 deletions.
34 changes: 34 additions & 0 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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),
}
Expand Down
74 changes: 66 additions & 8 deletions src/handlers/graphical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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,
}
}

Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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))?;

Expand Down Expand Up @@ -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();
Expand All @@ -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(())
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@
//! .unicode(false)
//! .context_lines(3)
//! .tab_width(4)
//! .break_words(true)
//! .build(),
//! )
//! }))
Expand Down
184 changes: 184 additions & 0 deletions tests/graphical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down

0 comments on commit fd77257

Please sign in to comment.