Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to preserve trailing whitespace #503

Open
heiskane opened this issue Feb 10, 2023 · 2 comments
Open

Option to preserve trailing whitespace #503

heiskane opened this issue Feb 10, 2023 · 2 comments

Comments

@heiskane
Copy link

Currently working on something that requires me to keep track of a cursor in a message and wrap the message in a textbox with the cursor in the right place. This currently is not possible as wrapping trims the trailing whitespace making wrapped text different length from the original message misaligning any position i might track in the message.

Trimming trailing whitespace could be disabled in the options?

wanted behavior.

let text = "as     a hello";
let wrapped = textwrap::wrap(text, 5);
// ["as   ", "  a ", "hello"]
@mgeisler
Copy link
Owner

Hi @heiskane,

Currently working on something that requires me to keep track of a cursor in a message and wrap the message in a textbox with the cursor in the right place.

Interesting! Since you're doing something custom, have you considered using the Word struct directly? You might even want to implement Fragment for your own custom type.

All the wrapping logic operates on fragments: the "upper layer" simply splits the text into fragments using different options.

I think your use case could be handled by

  • creating a Fragment for every space character and
  • moving the whitespace to the "word" part of the Fragment instead of the "whitespace" part

Today, the string "as a hello" is turned into words and whitespace like this:

[("as", "     "), ("a", " "), ("hello". "")]

I'm suggesting instead turning it into

[("as", ""), (" ", ""), (" ", ""), (" ", ""), (" ", ""), (" ", ""), ("a", ""), (" ", ""), ("hello", "")]

That should do the trick. You should be able to do this with the WordSeparator::Custom setting.

@heiskane
Copy link
Author

heiskane commented Feb 15, 2023

Thanks for the reply. I experimented a bit with what you suggested but i am still having some issues.

The issue with this is that WordSeparator::Custom takes a closure that returns iterator over Words and Word::from() strips whitespace so you can't really do something like Word::from(" "). I can't construct Word manually either because it needs width which is a private field. Not sure how i can make this happen with these restrictions.

If WordSeparator::Custom took a closure that returns an iterator over any type that implements Fragment i feel like this would be simpler to solve. Although i do feel like it would be nice if the library offered a NoTrim option directly.

Update:
I implemented a simple method in textwrap to create Word without trimming the whitespace and that did allow me to do what i described in the issue.

the method:

impl<'a> Word<'a> {
    // snip....
    pub fn no_trim(word: &str) -> Word<'_> {
        Word {
            word,
            width: display_width(word),
            whitespace: "",
            penalty: "",
        }
    }
}

my (quick and dirty) solution to the issue:

use textwrap::{core::Word, Options, WordSeparator};

fn main() {
    let text = "as     a hello";

    let opts = Options::new(5).word_separator(WordSeparator::Custom(|line| {
        let mut start = 0;
        let mut prev_char = ' ';
        let mut char_indices = line.char_indices();

        println!("line: {:?}", line);
        Box::new(std::iter::from_fn(move || {
            for (idx, ch) in char_indices.by_ref() {
                if prev_char == ' ' && start != 0 {
                    let word = Word::no_trim(" ");
                    println!("word0: {:?}", word);
                    prev_char = ch;
                    start = idx;
                    return Some(word);
                }
                if prev_char != ' ' && ch == ' ' {
                    let word = Word::from(&line[start..idx]);
                    println!("word1: {:?}", word);
                    start = idx;
                    prev_char = ch;
                    return Some(word);
                }
                prev_char = ch;
            }

            if start < line.len() {
                let word = Word::no_trim(&line[start..]);
                println!("word2: {:?}", word);
                start = line.len();
                return Some(word);
            }

            None
        }))
    }));
    let wrapped = textwrap::wrap(text, opts);
    println!("{wrapped:?}");
}

prints:

line: "as     a hello"
word1: Word { word: "as", whitespace: "", penalty: "", width: 2 }
word0: Word { word: " ", whitespace: "", penalty: "", width: 1 }
word0: Word { word: " ", whitespace: "", penalty: "", width: 1 }
word0: Word { word: " ", whitespace: "", penalty: "", width: 1 }
word0: Word { word: " ", whitespace: "", penalty: "", width: 1 }
word0: Word { word: " ", whitespace: "", penalty: "", width: 1 }
word1: Word { word: "a", whitespace: "", penalty: "", width: 1 }
word0: Word { word: " ", whitespace: "", penalty: "", width: 1 }
word2: Word { word: "hello", whitespace: "", penalty: "", width: 5 }
["as  ", "   a ", "hello"]

Could this method (or something similar) be added to the crate so that wrapping without trimming would be possible? I can submit a PR if the method i created is fine (and maybe create a new WordSeparator type for this?). Let me know how you want to move forward with this 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants