feat(graphical): Expose additional `textwrap` options (#321)

This commit is contained in:
Zanie Blue 2023-11-15 12:34:24 -06:00 committed by GitHub
parent c7ba5b7e52
commit fd77257cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 285 additions and 8 deletions

View File

@ -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),
}

View File

@ -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(())

View File

@ -593,6 +593,7 @@
//! .unicode(false)
//! .context_lines(3)
//! .tab_width(4)
//! .break_words(true)
//! .build(),
//! )
//! }))

View File

@ -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)]