Skip to content

Commit fd77257

Browse files
authoredNov 15, 2023
feat(graphical): Expose additional textwrap options (#321)
1 parent c7ba5b7 commit fd77257

File tree

4 files changed

+285
-8
lines changed

4 files changed

+285
-8
lines changed
 

‎src/handler.rs

+34
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ pub struct MietteHandlerOpts {
5555
pub(crate) context_lines: Option<usize>,
5656
pub(crate) tab_width: Option<usize>,
5757
pub(crate) with_cause_chain: Option<bool>,
58+
pub(crate) break_words: Option<bool>,
59+
pub(crate) word_separator: Option<textwrap::WordSeparator>,
60+
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
5861
}
5962

6063
impl MietteHandlerOpts {
@@ -86,6 +89,27 @@ impl MietteHandlerOpts {
8689
self
8790
}
8891

92+
/// If true, long words can be broken when wrapping.
93+
///
94+
/// If false, long words will not be broken when they exceed the width.
95+
///
96+
/// Defaults to true.
97+
pub fn break_words(mut self, break_words: bool) -> Self {
98+
self.break_words = Some(break_words);
99+
self
100+
}
101+
102+
/// Sets the `textwrap::WordSeparator` to use when determining wrap points.
103+
pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
104+
self.word_separator = Some(word_separator);
105+
self
106+
}
107+
108+
/// Sets the `textwrap::WordSplitter` to use when determining wrap points.
109+
pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
110+
self.word_splitter = Some(word_splitter);
111+
self
112+
}
89113
/// Include the cause chain of the top-level error in the report.
90114
pub fn with_cause_chain(mut self) -> Self {
91115
self.with_cause_chain = Some(true);
@@ -233,6 +257,16 @@ impl MietteHandlerOpts {
233257
if let Some(w) = self.tab_width {
234258
handler = handler.tab_width(w);
235259
}
260+
if let Some(b) = self.break_words {
261+
handler = handler.with_break_words(b)
262+
}
263+
if let Some(s) = self.word_separator {
264+
handler = handler.with_word_separator(s)
265+
}
266+
if let Some(s) = self.word_splitter {
267+
handler = handler.with_word_splitter(s)
268+
}
269+
236270
MietteHandler {
237271
inner: Box::new(handler),
238272
}

‎src/handlers/graphical.rs

+66-8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pub struct GraphicalReportHandler {
3030
pub(crate) context_lines: usize,
3131
pub(crate) tab_width: usize,
3232
pub(crate) with_cause_chain: bool,
33+
pub(crate) break_words: bool,
34+
pub(crate) word_separator: Option<textwrap::WordSeparator>,
35+
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
3336
}
3437

3538
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -51,6 +54,9 @@ impl GraphicalReportHandler {
5154
context_lines: 1,
5255
tab_width: 4,
5356
with_cause_chain: true,
57+
break_words: true,
58+
word_separator: None,
59+
word_splitter: None,
5460
}
5561
}
5662

@@ -64,6 +70,9 @@ impl GraphicalReportHandler {
6470
context_lines: 1,
6571
tab_width: 4,
6672
with_cause_chain: true,
73+
break_words: true,
74+
word_separator: None,
75+
word_splitter: None,
6776
}
6877
}
6978

@@ -122,6 +131,24 @@ impl GraphicalReportHandler {
122131
self
123132
}
124133

134+
/// Enables or disables breaking of words during wrapping.
135+
pub fn with_break_words(mut self, break_words: bool) -> Self {
136+
self.break_words = break_words;
137+
self
138+
}
139+
140+
/// Sets the word separator to use when wrapping.
141+
pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
142+
self.word_separator = Some(word_separator);
143+
self
144+
}
145+
146+
/// Sets the word splitter to usewhen wrapping.
147+
pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
148+
self.word_splitter = Some(word_splitter);
149+
self
150+
}
151+
125152
/// Sets the 'global' footer for this handler.
126153
pub fn with_footer(mut self, footer: String) -> Self {
127154
self.footer = Some(footer);
@@ -159,9 +186,17 @@ impl GraphicalReportHandler {
159186
if let Some(footer) = &self.footer {
160187
writeln!(f)?;
161188
let width = self.termwidth.saturating_sub(4);
162-
let opts = textwrap::Options::new(width)
189+
let mut opts = textwrap::Options::new(width)
163190
.initial_indent(" ")
164-
.subsequent_indent(" ");
191+
.subsequent_indent(" ")
192+
.break_words(self.break_words);
193+
if let Some(word_separator) = self.word_separator {
194+
opts = opts.word_separator(word_separator);
195+
}
196+
if let Some(word_splitter) = self.word_splitter.clone() {
197+
opts = opts.word_splitter(word_splitter);
198+
}
199+
165200
writeln!(f, "{}", textwrap::fill(footer, opts))?;
166201
}
167202
Ok(())
@@ -212,9 +247,16 @@ impl GraphicalReportHandler {
212247
let initial_indent = format!(" {} ", severity_icon.style(severity_style));
213248
let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
214249
let width = self.termwidth.saturating_sub(2);
215-
let opts = textwrap::Options::new(width)
250+
let mut opts = textwrap::Options::new(width)
216251
.initial_indent(&initial_indent)
217-
.subsequent_indent(&rest_indent);
252+
.subsequent_indent(&rest_indent)
253+
.break_words(self.break_words);
254+
if let Some(word_separator) = self.word_separator {
255+
opts = opts.word_separator(word_separator);
256+
}
257+
if let Some(word_splitter) = self.word_splitter.clone() {
258+
opts = opts.word_splitter(word_splitter);
259+
}
218260

219261
writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
220262

@@ -251,9 +293,17 @@ impl GraphicalReportHandler {
251293
)
252294
.style(severity_style)
253295
.to_string();
254-
let opts = textwrap::Options::new(width)
296+
let mut opts = textwrap::Options::new(width)
255297
.initial_indent(&initial_indent)
256-
.subsequent_indent(&rest_indent);
298+
.subsequent_indent(&rest_indent)
299+
.break_words(self.break_words);
300+
if let Some(word_separator) = self.word_separator {
301+
opts = opts.word_separator(word_separator);
302+
}
303+
if let Some(word_splitter) = self.word_splitter.clone() {
304+
opts = opts.word_splitter(word_splitter);
305+
}
306+
257307
match error {
258308
ErrorKind::Diagnostic(diag) => {
259309
let mut inner = String::new();
@@ -280,9 +330,17 @@ impl GraphicalReportHandler {
280330
if let Some(help) = diagnostic.help() {
281331
let width = self.termwidth.saturating_sub(4);
282332
let initial_indent = " help: ".style(self.theme.styles.help).to_string();
283-
let opts = textwrap::Options::new(width)
333+
let mut opts = textwrap::Options::new(width)
284334
.initial_indent(&initial_indent)
285-
.subsequent_indent(" ");
335+
.subsequent_indent(" ")
336+
.break_words(self.break_words);
337+
if let Some(word_separator) = self.word_separator {
338+
opts = opts.word_separator(word_separator);
339+
}
340+
if let Some(word_splitter) = self.word_splitter.clone() {
341+
opts = opts.word_splitter(word_splitter);
342+
}
343+
286344
writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
287345
}
288346
Ok(())

‎src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@
593593
//! .unicode(false)
594594
//! .context_lines(3)
595595
//! .tab_width(4)
596+
//! .break_words(true)
596597
//! .build(),
597598
//! )
598599
//! }))

‎tests/graphical.rs

+184
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,190 @@ fn fmt_report(diag: Report) -> String {
3434
out
3535
}
3636

37+
fn fmt_report_with_settings(
38+
diag: Report,
39+
with_settings: fn(GraphicalReportHandler) -> GraphicalReportHandler,
40+
) -> String {
41+
let mut out = String::new();
42+
43+
let handler = with_settings(GraphicalReportHandler::new_themed(
44+
GraphicalTheme::unicode_nocolor(),
45+
));
46+
47+
handler.render_report(&mut out, diag.as_ref()).unwrap();
48+
49+
println!("Error:\n```\n{}\n```", out);
50+
51+
out
52+
}
53+
54+
#[test]
55+
fn word_wrap_options() -> Result<(), MietteError> {
56+
// By default, a long word should not break
57+
let out =
58+
fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| handler);
59+
60+
let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string();
61+
assert_eq!(expected, out);
62+
63+
// A long word can break with a smaller width
64+
let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
65+
handler.with_width(10)
66+
});
67+
let expected = r#" × abcd
68+
│ efgh
69+
│ ijkl
70+
│ mnop
71+
│ qrst
72+
│ uvwx
73+
│ yz
74+
"#
75+
.to_string();
76+
assert_eq!(expected, out);
77+
78+
// Unless, word breaking is disabled
79+
let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
80+
handler.with_width(10).with_break_words(false)
81+
});
82+
let expected = " × abcdefghijklmnopqrstuvwxyz\n".to_string();
83+
assert_eq!(expected, out);
84+
85+
// Breaks should start at the boundary of each word if possible
86+
let out = fmt_report_with_settings(
87+
Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
88+
|handler| handler.with_width(10),
89+
);
90+
let expected = r#" × 12
91+
│ 123
92+
│ 1234
93+
│ 1234
94+
│ 5
95+
│ 1234
96+
│ 56
97+
│ 1234
98+
│ 567
99+
│ 1234
100+
│ 5678
101+
│ 90
102+
"#
103+
.to_string();
104+
assert_eq!(expected, out);
105+
106+
// But long words should not break if word breaking is disabled
107+
let out = fmt_report_with_settings(
108+
Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
109+
|handler| handler.with_width(10).with_break_words(false),
110+
);
111+
let expected = r#" × 12
112+
│ 123
113+
│ 1234
114+
│ 12345
115+
│ 123456
116+
│ 1234567
117+
│ 1234567890
118+
"#
119+
.to_string();
120+
assert_eq!(expected, out);
121+
122+
// Unless, of course, there are hyphens
123+
let out = fmt_report_with_settings(
124+
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"),
125+
|handler| handler.with_width(10).with_break_words(false),
126+
);
127+
let expected = r#" × a-b
128+
│ a-b-
129+
│ c a-
130+
│ b-c-
131+
│ d a-
132+
│ b-c-
133+
│ d-e
134+
│ a-b-
135+
│ c-d-
136+
│ e-f
137+
│ a-b-
138+
│ c-d-
139+
│ e-f-
140+
│ g a-
141+
│ b-c-
142+
│ d-e-
143+
│ f-g-
144+
│ h
145+
"#
146+
.to_string();
147+
assert_eq!(expected, out);
148+
149+
// Which requires an additional opt-out
150+
let out = fmt_report_with_settings(
151+
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"),
152+
|handler| {
153+
handler
154+
.with_width(10)
155+
.with_break_words(false)
156+
.with_word_splitter(textwrap::WordSplitter::NoHyphenation)
157+
},
158+
);
159+
let expected = r#" × a-b
160+
│ a-b-c
161+
│ a-b-c-d
162+
│ a-b-c-d-e
163+
│ a-b-c-d-e-f
164+
│ a-b-c-d-e-f-g
165+
│ a-b-c-d-e-f-g-h
166+
"#
167+
.to_string();
168+
assert_eq!(expected, out);
169+
170+
// Or if there are _other_ unicode word boundaries
171+
let out = fmt_report_with_settings(
172+
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"),
173+
|handler| handler.with_width(10).with_break_words(false),
174+
);
175+
let expected = r#" × a/b
176+
│ a/b/
177+
│ c a/
178+
│ b/c/
179+
│ d a/
180+
│ b/c/
181+
│ d/e
182+
│ a/b/
183+
│ c/d/
184+
│ e/f
185+
│ a/b/
186+
│ c/d/
187+
│ e/f/
188+
│ g a/
189+
│ b/c/
190+
│ d/e/
191+
│ f/g/
192+
│ h
193+
"#
194+
.to_string();
195+
assert_eq!(expected, out);
196+
197+
// Such things require you to opt-in to only breaking on ASCII whitespace
198+
let out = fmt_report_with_settings(
199+
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"),
200+
|handler| {
201+
handler
202+
.with_width(10)
203+
.with_break_words(false)
204+
.with_word_separator(textwrap::WordSeparator::AsciiSpace)
205+
},
206+
);
207+
let expected = r#" × a/b
208+
│ a/b/c
209+
│ a/b/c/d
210+
│ a/b/c/d/e
211+
│ a/b/c/d/e/f
212+
│ a/b/c/d/e/f/g
213+
│ a/b/c/d/e/f/g/h
214+
"#
215+
.to_string();
216+
assert_eq!(expected, out);
217+
218+
Ok(())
219+
}
220+
37221
#[test]
38222
fn empty_source() -> Result<(), MietteError> {
39223
#[derive(Debug, Diagnostic, Error)]

0 commit comments

Comments
 (0)
Please sign in to comment.