Skip to content

Commit 245c8ba

Browse files
authored
Merge pull request #6149 from epage/wrap
fix(help): Correctly calculate wrap points with ANSI escape codes
2 parents c9a39a5 + dd17a41 commit 245c8ba

File tree

6 files changed

+66
-14
lines changed

6 files changed

+66
-14
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clap_builder/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ unicode-width = { version = "0.2.0", optional = true }
7171
static_assertions = "1.1.0"
7272
unic-emoji-char = "0.9.0"
7373
color-print = "0.3.6"
74+
snapbox = { version = "0.6.16" }
7475

7576
[lints]
7677
workspace = true

clap_builder/src/builder/styled_str.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,46 @@ impl std::fmt::Display for StyledStr {
209209
Ok(())
210210
}
211211
}
212+
213+
#[cfg(test)]
214+
#[cfg(feature = "wrap_help")]
215+
mod wrap_tests {
216+
use super::*;
217+
218+
use snapbox::assert_data_eq;
219+
use snapbox::str;
220+
221+
#[test]
222+
#[cfg(feature = "wrap_help")]
223+
fn wrap_unstyled() {
224+
let style = anstyle::Style::new();
225+
let input = format!("{style}12345{style:#} {style}12345{style:#} {style}12345{style:#} {style}12345{style:#}");
226+
let mut actual = StyledStr::new();
227+
actual.push_string(input);
228+
actual.wrap(20);
229+
assert_data_eq!(
230+
actual.ansi().to_string(),
231+
str![[r#"
232+
12345 12345 12345
233+
12345
234+
"#]]
235+
);
236+
}
237+
238+
#[test]
239+
#[cfg(feature = "wrap_help")]
240+
fn wrap_styled() {
241+
let style = anstyle::Style::new().bold();
242+
let input = format!("{style}12345{style:#} {style}12345{style:#} {style}12345{style:#} {style}12345{style:#}");
243+
let mut actual = StyledStr::new();
244+
actual.push_string(input);
245+
actual.wrap(20);
246+
assert_data_eq!(
247+
actual.ansi().to_string(),
248+
str![[r#"
249+
12345 12345 12345 
250+
12345
251+
"#]]
252+
);
253+
}
254+
}

clap_builder/src/output/textwrap/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ mod test {
9292
// will be empty. This is because the string is split into
9393
// words like [" ", "foobar ", "baz"], which puts "foobar " on
9494
// the second line. We never output trailing whitespace
95-
assert_eq!(wrap(" foobar baz", 6), vec!["", " foobar", " baz"]);
95+
assert_eq!(wrap(" foobar baz", 6), vec![" foobar", " baz"]);
9696
}
9797

9898
#[test]

clap_builder/src/output/textwrap/wrap_algorithms.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,32 @@ use super::core::display_width;
44
pub(crate) struct LineWrapper<'w> {
55
hard_width: usize,
66
line_width: usize,
7-
carryover: Option<&'w str>,
7+
indentation: Option<&'w str>,
88
}
99

1010
impl<'w> LineWrapper<'w> {
1111
pub(crate) fn new(hard_width: usize) -> Self {
1212
Self {
1313
hard_width,
1414
line_width: 0,
15-
carryover: None,
15+
indentation: None,
1616
}
1717
}
1818

1919
pub(crate) fn reset(&mut self) {
2020
self.line_width = 0;
21-
self.carryover = None;
21+
self.indentation = None;
2222
}
2323

2424
pub(crate) fn wrap(&mut self, mut words: Vec<&'w str>) -> Vec<&'w str> {
25-
if self.carryover.is_none() {
25+
let mut first_word = false;
26+
if self.indentation.is_none() {
27+
first_word = true;
2628
if let Some(word) = words.first() {
2729
if word.trim().is_empty() {
28-
self.carryover = Some(*word);
30+
self.indentation = Some(*word);
2931
} else {
30-
self.carryover = Some("");
32+
self.indentation = Some("");
3133
}
3234
}
3335
}
@@ -38,19 +40,22 @@ impl<'w> LineWrapper<'w> {
3840
let trimmed = word.trim_end();
3941
let word_width = display_width(trimmed);
4042
let trimmed_delta = word.len() - trimmed.len();
41-
if i != 0 && self.hard_width < self.line_width + word_width {
43+
if first_word && 0 < word_width {
44+
// Never try to wrap the first word
45+
first_word = false;
46+
} else if self.hard_width < self.line_width + word_width {
4247
if 0 < i {
43-
let last = i - 1;
44-
let trimmed = words[last].trim_end();
45-
words[last] = trimmed;
48+
let prev = i - 1;
49+
let trimmed = words[prev].trim_end();
50+
words[prev] = trimmed;
4651
}
4752

4853
self.line_width = 0;
4954
words.insert(i, "\n");
5055
i += 1;
51-
if let Some(carryover) = self.carryover {
52-
words.insert(i, carryover);
53-
self.line_width += carryover.len();
56+
if let Some(indentation) = self.indentation {
57+
words.insert(i, indentation);
58+
self.line_width += indentation.len();
5459
i += 1;
5560
}
5661
}

src/bin/stdio-fixture.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ fn main() {
55
let mut cmd = Command::new("stdio-fixture")
66
.version("1.0")
77
.long_version("1.0 - a2132c")
8+
.term_width(0)
9+
.max_term_width(0)
810
.arg_required_else_help(true)
911
.subcommand(Command::new("more"))
1012
.subcommand(

0 commit comments

Comments
 (0)