aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/string.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/string.rs')
-rw-r--r--alacritty/src/string.rs314
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>()
+ );
+ }
+}