diff options
Diffstat (limited to 'alacritty/src/string.rs')
-rw-r--r-- | alacritty/src/string.rs | 314 |
1 files changed, 314 insertions, 0 deletions
diff --git a/alacritty/src/string.rs b/alacritty/src/string.rs new file mode 100644 index 00000000..092c10e9 --- /dev/null +++ b/alacritty/src/string.rs @@ -0,0 +1,314 @@ +use std::cmp::Ordering; +use std::iter::Skip; +use std::str::Chars; + +use unicode_width::UnicodeWidthChar; + +/// The action performed by [`StrShortener`]. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum TextAction { + /// Yield a spacer. + Spacer, + /// Terminate state reached. + Terminate, + /// Yield a shortener. + Shortener, + /// Yield a character. + Char, +} + +/// The direction which we should shorten. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ShortenDirection { + /// Shorten to the start of the string. + Left, + + /// Shorten to the end of the string. + Right, +} + +/// Iterator that yield shortened version of the text. +pub struct StrShortener<'a> { + chars: Skip<Chars<'a>>, + accumulted_len: usize, + max_width: usize, + direction: ShortenDirection, + shortener: Option<char>, + text_action: TextAction, +} + +impl<'a> StrShortener<'a> { + pub fn new( + text: &'a str, + max_width: usize, + direction: ShortenDirection, + mut shortener: Option<char>, + ) -> Self { + if text.is_empty() { + // If we don't have any text don't produce a shortener for it. + let _ = shortener.take(); + } + + if direction == ShortenDirection::Right { + return Self { + chars: text.chars().skip(0), + accumulted_len: 0, + text_action: TextAction::Char, + max_width, + direction, + shortener, + }; + } + + let mut offset = 0; + let mut current_len = 0; + + let mut iter = text.chars().rev().enumerate(); + + while let Some((idx, ch)) = iter.next() { + let ch_width = ch.width().unwrap_or(1); + current_len += ch_width; + + match current_len.cmp(&max_width) { + // We can only be here if we've faced wide character or we've already + // handled equality situation. Anyway, break. + Ordering::Greater => break, + Ordering::Equal => { + if shortener.is_some() && iter.clone().next().is_some() { + // We have one more character after, shortener will accumulate for + // the `current_len`. + break; + } else { + // The match is exact, consume shortener. + let _ = shortener.take(); + } + }, + Ordering::Less => (), + } + + offset = idx + 1; + } + + // Consume the iterator to count the number of characters in it. + let num_chars = iter.last().map(|(idx, _)| idx + 1).unwrap_or(offset); + let skip_chars = num_chars - offset; + + let text_action = if num_chars <= max_width || shortener.is_none() { + TextAction::Char + } else { + TextAction::Shortener + }; + + let chars = text.chars().skip(skip_chars); + + Self { chars, accumulted_len: 0, text_action, max_width, direction, shortener } + } +} + +impl<'a> Iterator for StrShortener<'a> { + type Item = char; + + fn next(&mut self) -> Option<Self::Item> { + match self.text_action { + TextAction::Spacer => { + self.text_action = TextAction::Char; + Some(' ') + }, + TextAction::Terminate => { + // We've reached the termination state. + None + }, + TextAction::Shortener => { + // When we shorten from the left we yield the shortener first and process the rest. + self.text_action = if self.direction == ShortenDirection::Left { + TextAction::Char + } else { + TextAction::Terminate + }; + + // Consume the shortener to avoid yielding it later when shortening left. + self.shortener.take() + }, + TextAction::Char => { + let ch = self.chars.next()?; + let ch_width = ch.width().unwrap_or(1); + + // Advance width. + self.accumulted_len += ch_width; + + if self.accumulted_len > self.max_width { + self.text_action = TextAction::Terminate; + return self.shortener; + } else if self.accumulted_len == self.max_width && self.shortener.is_some() { + // Check if we have a next char. + let has_next = self.chars.clone().next().is_some(); + + // We should terminate after that. + self.text_action = TextAction::Terminate; + + return has_next.then(|| self.shortener.unwrap()).or(Some(ch)); + } + + // Add a spacer for wide character. + if ch_width == 2 { + self.text_action = TextAction::Spacer; + } + + Some(ch) + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn into_shortened_with_shortener() { + let s = "Hello"; + let len = s.chars().count(); + assert_eq!( + "", + StrShortener::new("", 1, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + assert_eq!( + ".", + StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>() + ); + + assert_eq!( + ".", + StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + assert_eq!( + "H.", + StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>() + ); + + assert_eq!( + ".o", + StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + assert_eq!( + s, + &StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>() + ); + + assert_eq!( + s, + &StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + let s = "こJんにちはP"; + let len = 2 + 1 + 2 + 2 + 2 + 2 + 1; + assert_eq!( + ".", + &StrShortener::new(s, 1, ShortenDirection::Right, Some('.')).collect::<String>() + ); + + assert_eq!( + &".", + &StrShortener::new(s, 1, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + assert_eq!( + ".", + &StrShortener::new(s, 2, ShortenDirection::Right, Some('.')).collect::<String>() + ); + + assert_eq!( + ".P", + &StrShortener::new(s, 2, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + assert_eq!( + "こ .", + &StrShortener::new(s, 3, ShortenDirection::Right, Some('.')).collect::<String>() + ); + + assert_eq!( + ".P", + &StrShortener::new(s, 3, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + assert_eq!( + "こ Jん に ち は P", + &StrShortener::new(s, len * 2, ShortenDirection::Left, Some('.')).collect::<String>() + ); + + assert_eq!( + "こ Jん に ち は P", + &StrShortener::new(s, len * 2, ShortenDirection::Right, Some('.')).collect::<String>() + ); + } + + #[test] + fn into_shortened_without_shortener() { + let s = "Hello"; + assert_eq!("", StrShortener::new("", 1, ShortenDirection::Left, None).collect::<String>()); + + assert_eq!( + "H", + &StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>() + ); + + assert_eq!("o", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>()); + + assert_eq!( + "He", + &StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>() + ); + + assert_eq!( + "lo", + &StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>() + ); + + assert_eq!( + &s, + &StrShortener::new(s, s.len(), ShortenDirection::Right, None).collect::<String>() + ); + + assert_eq!( + &s, + &StrShortener::new(s, s.len(), ShortenDirection::Left, None).collect::<String>() + ); + + let s = "こJんにちはP"; + let len = 2 + 1 + 2 + 2 + 2 + 2 + 1; + assert_eq!("", &StrShortener::new(s, 1, ShortenDirection::Right, None).collect::<String>()); + + assert_eq!("P", &StrShortener::new(s, 1, ShortenDirection::Left, None).collect::<String>()); + + assert_eq!( + "こ ", + &StrShortener::new(s, 2, ShortenDirection::Right, None).collect::<String>() + ); + + assert_eq!("P", &StrShortener::new(s, 2, ShortenDirection::Left, None).collect::<String>()); + + assert_eq!( + "こ J", + &StrShortener::new(s, 3, ShortenDirection::Right, None).collect::<String>() + ); + + assert_eq!( + "は P", + &StrShortener::new(s, 3, ShortenDirection::Left, None).collect::<String>() + ); + + assert_eq!( + "こ Jん に ち は P", + &StrShortener::new(s, len, ShortenDirection::Left, None).collect::<String>() + ); + + assert_eq!( + "こ Jん に ち は P", + &StrShortener::new(s, len, ShortenDirection::Right, None).collect::<String>() + ); + } +} |