alacritty/alacritty/src/string.rs

315 lines
9.3 KiB
Rust

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>()
);
}
}