Skip to content

Feature request: Try wrapping text without actually allocating any space #545

@kkew3

Description

@kkew3

I roughly scan over the code, and don't find similar functionality. So this post can be regarded as a feature request, and I'm glad to PR if possible.

Proposal

I'm seeking for a function to try wrapping a paragraph, and return a vec of displayed widths. The signature of such function could be:

fn try_wrap<'a, Opt: Into<Options<'a>>>(text: &str, width_or_options: Opt) -> Vec<usize>;

An example use would be:

// assert_eq!(wrap("  foo bar", 4), vec!["", "foo", "bar"]);
assert_eq!(try_wrap("  foo bar", 4), vec![0, 3, 3]);

Use case

For example, one may want to find the smallest width such that any wrapped line won't protrude out, without the need to actually wrap the text into string.

Implementation

A naive implementation is to mimic this function:

textwrap/src/wrap.rs

Lines 215 to 292 in 6397036

pub(crate) fn wrap_single_line_slow_path<'a>(
line: &'a str,
options: &Options<'_>,
lines: &mut Vec<Cow<'a, str>>,
) {
let initial_width = options
.width
.saturating_sub(display_width(options.initial_indent));
let subsequent_width = options
.width
.saturating_sub(display_width(options.subsequent_indent));
let line_widths = [initial_width, subsequent_width];
let words = options.word_separator.find_words(line);
let split_words = split_words(words, &options.word_splitter);
let broken_words = if options.break_words {
let mut broken_words = break_words(split_words, line_widths[1]);
if !options.initial_indent.is_empty() {
// Without this, the first word will always go into the
// first line. However, since we break words based on the
// _second_ line width, it can be wrong to unconditionally
// put the first word onto the first line. An empty
// zero-width word fixed this.
broken_words.insert(0, Word::from(""));
}
broken_words
} else {
split_words.collect::<Vec<_>>()
};
let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
let mut idx = 0;
for words in wrapped_words {
let last_word = match words.last() {
None => {
lines.push(Cow::from(""));
continue;
}
Some(word) => word,
};
// We assume here that all words are contiguous in `line`.
// That is, the sum of their lengths should add up to the
// length of `line`.
let len = words
.iter()
.map(|word| word.len() + word.whitespace.len())
.sum::<usize>()
- last_word.whitespace.len();
// The result is owned if we have indentation, otherwise we
// can simply borrow an empty string.
let mut result = if lines.is_empty() && !options.initial_indent.is_empty() {
Cow::Owned(options.initial_indent.to_owned())
} else if !lines.is_empty() && !options.subsequent_indent.is_empty() {
Cow::Owned(options.subsequent_indent.to_owned())
} else {
// We can use an empty string here since string
// concatenation for `Cow` preserves a borrowed value when
// either side is empty.
Cow::from("")
};
result += &line[idx..idx + len];
if !last_word.penalty.is_empty() {
result.to_mut().push_str(last_word.penalty);
}
lines.push(result);
// Advance by the length of `result`, plus the length of
// `last_word.whitespace` -- even if we had a penalty, we need
// to skip over the whitespace.
idx += len + last_word.whitespace.len();
}
}

but replace all lines.push(x) with counts.push(textwrap::core::display_width(&x), where counts is the vec to return.
The downside is that there could be a lot of duplicate code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions