Skip to content

Commit

Permalink
perf: Vendor textwrap parts we need
Browse files Browse the repository at this point in the history
The immediate benefit is binary size but this also makes us more
flexible on the implementation, like allowing wrapping of `StyledStr`.

This removed 12 KiB from `.text`

This helps towards clap-rs#1365 and probably clap-rs#2037
  • Loading branch information
epage committed Aug 25, 2022
1 parent 735d6fd commit 37f2efb
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 47 deletions.
25 changes: 2 additions & 23 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions Cargo.toml
Expand Up @@ -71,9 +71,9 @@ suggestions = ["dep:strsim"]
deprecated = ["clap_derive?/deprecated"] # Guided experience to prepare for next breaking release (at different stages of development, this may become default)
derive = ["clap_derive"]
cargo = [] # Disable if you're not using Cargo, enables Cargo-env-var-dependent macros
wrap_help = ["dep:terminal_size", "textwrap/terminal_size"]
wrap_help = ["dep:terminal_size"]
env = [] # Use environment variables during arg parsing
unicode = ["textwrap/unicode-width", "dep:unicode-width", "dep:unicase"] # Support for unicode characters in arguments and help messages
unicode = ["dep:unicode-width", "dep:unicase"] # Support for unicode characters in arguments and help messages
perf = [] # Optimize for runtime performance

# In-work features
Expand All @@ -88,7 +88,6 @@ bench = false
clap_derive = { path = "./clap_derive", version = "=4.0.0-alpha.0", optional = true }
clap_lex = { path = "./clap_lex", version = "0.2.2" }
bitflags = "1.2"
textwrap = { version = "0.15.0", default-features = false, features = [] }
unicase = { version = "2.6", optional = true }
strsim = { version = "0.10", optional = true }
atty = { version = "0.2", optional = true }
Expand Down
33 changes: 12 additions & 21 deletions src/output/help.rs
Expand Up @@ -10,6 +10,7 @@ use crate::builder::Str;
use crate::builder::StyledStr;
use crate::builder::{render_arg_val, Arg, Command};
use crate::output::display_width;
use crate::output::wrap;
use crate::output::Usage;
use crate::util::FlatSet;
use crate::ArgAction;
Expand Down Expand Up @@ -355,7 +356,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
self.cmd.get_before_help()
};
if let Some(output) = before_help {
self.none(text_wrapper(
self.none(wrap(
&output.unwrap_none().replace("{n}", "\n"),
self.term_w,
));
Expand All @@ -374,7 +375,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
};
if let Some(output) = after_help {
self.none("\n\n");
self.none(text_wrapper(
self.none(wrap(
&output.unwrap_none().replace("{n}", "\n"),
self.term_w,
));
Expand Down Expand Up @@ -415,7 +416,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
// Determine how many newlines we need to insert
let avail_chars = self.term_w - spaces;
debug!("Help::help: Usable space...{}", avail_chars);
help = text_wrapper(&help.replace("{n}", "\n"), avail_chars);
help = wrap(&help.replace("{n}", "\n"), avail_chars);
} else {
debug!("No");
}
Expand Down Expand Up @@ -498,7 +499,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
usize::MAX
};

let help = text_wrapper(help.unwrap_none(), avail_chars);
let help = wrap(help.unwrap_none(), avail_chars);
let mut help = help.lines();

self.none(help.next().unwrap_or_default());
Expand Down Expand Up @@ -657,7 +658,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
if before_new_line {
self.none("\n");
}
self.none(text_wrapper(output.unwrap_none(), self.term_w));
self.none(wrap(output.unwrap_none(), self.term_w));
if after_new_line {
self.none("\n");
}
Expand All @@ -669,7 +670,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
if before_new_line {
self.none("\n");
}
self.none(text_wrapper(author, self.term_w));
self.none(wrap(author, self.term_w));
if after_new_line {
self.none("\n");
}
Expand All @@ -682,7 +683,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
.get_version()
.or_else(|| self.cmd.get_long_version());
if let Some(output) = version {
self.none(text_wrapper(output, self.term_w));
self.none(wrap(output, self.term_w));
}
}
}
Expand Down Expand Up @@ -906,7 +907,7 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
fn write_display_name(&mut self) {
debug!("Help::write_display_name");

let display_name = text_wrapper(
let display_name = wrap(
&self
.cmd
.get_display_name()
Expand All @@ -926,10 +927,10 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
// In case we're dealing with subcommands i.e. git mv is translated to git-mv
bn.replace(' ', "-")
} else {
text_wrapper(&self.cmd.get_name().replace("{n}", "\n"), self.term_w)
wrap(&self.cmd.get_name().replace("{n}", "\n"), self.term_w)
}
} else {
text_wrapper(&self.cmd.get_name().replace("{n}", "\n"), self.term_w)
wrap(&self.cmd.get_name().replace("{n}", "\n"), self.term_w)
};
self.good(&bin_name);
}
Expand Down Expand Up @@ -1071,24 +1072,14 @@ fn should_show_subcommand(subcommand: &Command) -> bool {
!subcommand.is_hide_set()
}

fn text_wrapper(help: &str, width: usize) -> String {
let wrapper = textwrap::Options::new(width)
.break_words(false)
.word_splitter(textwrap::WordSplitter::NoHyphenation);
help.lines()
.map(|line| textwrap::fill(line, &wrapper))
.collect::<Vec<String>>()
.join("\n")
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn wrap_help_last_word() {
let help = String::from("foo bar baz");
assert_eq!(text_wrapper(&help, 5), "foo\nbar\nbaz");
assert_eq!(wrap(&help, 5), "foo\nbar\nbaz");
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions src/output/mod.rs
Expand Up @@ -6,4 +6,5 @@ pub(crate) mod fmt;

pub(crate) use self::help::Help;
pub(crate) use self::textwrap::core::display_width;
pub(crate) use self::textwrap::wrap;
pub(crate) use self::usage::Usage;
112 changes: 112 additions & 0 deletions src/output/textwrap/mod.rs
@@ -1 +1,113 @@
//! Fork of `textwrap` crate
//!
//! Benefits of forking:
//! - Pull in only what we need rather than relying on the compiler to remove what we don't need
//! - `LineWrapper` is able to incrementally wrap which will help with `StyledStr

pub(crate) mod core;
pub(crate) mod word_separators;
pub(crate) mod wrap_algorithms;

pub(crate) fn wrap(content: &str, hard_width: usize) -> String {
let mut wrapper = wrap_algorithms::LineWrapper::new(hard_width);
let mut total = Vec::new();
for line in content.split_inclusive('\n') {
wrapper.reset();
let line = word_separators::find_words_ascii_space(line).collect::<Vec<_>>();
total.extend(wrapper.wrap(line));
}
total.join("")
}

#[cfg(test)]
mod test {
/// Compatibility shim to keep textwrap's tests
fn wrap(content: &str, hard_width: usize) -> Vec<String> {
super::wrap(content, hard_width)
.trim_end()
.split('\n')
.map(|s| s.to_owned())
.collect::<Vec<_>>()
}

#[test]
fn no_wrap() {
assert_eq!(wrap("foo", 10), vec!["foo"]);
}

#[test]
fn wrap_simple() {
assert_eq!(wrap("foo bar baz", 5), vec!["foo", "bar", "baz"]);
}

#[test]
fn to_be_or_not() {
assert_eq!(
wrap("To be, or not to be, that is the question.", 10),
vec!["To be, or", "not to be,", "that is", "the", "question."]
);
}

#[test]
fn multiple_words_on_first_line() {
assert_eq!(wrap("foo bar baz", 10), vec!["foo bar", "baz"]);
}

#[test]
fn long_word() {
assert_eq!(wrap("foo", 0), vec!["foo"]);
}

#[test]
fn long_words() {
assert_eq!(wrap("foo bar", 0), vec!["foo", "bar"]);
}

#[test]
fn max_width() {
assert_eq!(wrap("foo bar", usize::MAX), vec!["foo bar"]);

let text = "Hello there! This is some English text. \
It should not be wrapped given the extents below.";
assert_eq!(wrap(text, usize::MAX), vec![text]);
}

#[test]
fn leading_whitespace() {
assert_eq!(wrap(" foo bar", 6), vec![" foo", "bar"]);
}

#[test]
fn leading_whitespace_empty_first_line() {
// If there is no space for the first word, the first line
// will be empty. This is because the string is split into
// words like [" ", "foobar ", "baz"], which puts "foobar " on
// the second line. We never output trailing whitespace
assert_eq!(wrap(" foobar baz", 6), vec!["", "foobar", "baz"]);
}

#[test]
fn trailing_whitespace() {
// Whitespace is only significant inside a line. After a line
// gets too long and is broken, the first word starts in
// column zero and is not indented.
assert_eq!(wrap("foo bar baz ", 5), vec!["foo", "bar", "baz"]);
}

#[test]
fn issue_99() {
// We did not reset the in_whitespace flag correctly and did
// not handle single-character words after a line break.
assert_eq!(
wrap("aaabbbccc x yyyzzzwww", 9),
vec!["aaabbbccc", "x", "yyyzzzwww"]
);
}

#[test]
fn issue_129() {
// The dash is an em-dash which takes up four bytes. We used
// to panic since we tried to index into the character.
assert_eq!(wrap("x – x", 1), vec!["x", "–", "x"]);
}
}

0 comments on commit 37f2efb

Please sign in to comment.