use std::{collections::VecDeque, fmt};
use crossterm::style::{Attribute, ContentStyle};
use unicode_width::UnicodeWidthChar;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StyledGrapheme {
ch: char,
width: usize,
style: ContentStyle,
}
impl From<char> for StyledGrapheme {
fn from(ch: char) -> Self {
Self {
ch,
width: UnicodeWidthChar::width(ch).unwrap_or(0),
style: ContentStyle::default(),
}
}
}
impl fmt::Display for StyledGraphemes {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for styled_grapheme in self.iter() {
write!(f, "{}", styled_grapheme.ch)?;
}
Ok(())
}
}
impl StyledGrapheme {
pub fn new(ch: char, style: ContentStyle) -> Self {
Self {
ch,
width: UnicodeWidthChar::width(ch).unwrap_or(0),
style,
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn character(&self) -> char {
self.ch
}
pub fn apply_style(&mut self, style: ContentStyle) {
self.style = style;
}
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct StyledGraphemes(pub VecDeque<StyledGrapheme>);
impl FromIterator<StyledGraphemes> for StyledGraphemes {
fn from_iter<I: IntoIterator<Item = StyledGraphemes>>(iter: I) -> Self {
let concatenated = iter
.into_iter()
.flat_map(|g| g.0.into_iter())
.collect::<VecDeque<StyledGrapheme>>();
StyledGraphemes(concatenated)
}
}
impl<'a> FromIterator<&'a StyledGraphemes> for StyledGraphemes {
fn from_iter<I: IntoIterator<Item = &'a StyledGraphemes>>(iter: I) -> Self {
let concatenated = iter
.into_iter()
.flat_map(|g| g.0.iter().cloned())
.collect::<VecDeque<StyledGrapheme>>();
StyledGraphemes(concatenated)
}
}
impl FromIterator<StyledGrapheme> for StyledGraphemes {
fn from_iter<I: IntoIterator<Item = StyledGrapheme>>(iter: I) -> Self {
let mut g = StyledGraphemes::default();
for i in iter {
g.push_back(i);
}
g
}
}
impl<S: AsRef<str>> From<S> for StyledGraphemes {
fn from(string: S) -> Self {
Self::from_str(string, ContentStyle::default())
}
}
impl fmt::Debug for StyledGraphemes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for styled_grapheme in self.iter() {
write!(f, "{}", styled_grapheme.ch)?;
}
Ok(())
}
}
impl StyledGraphemes {
pub fn from_str<S: AsRef<str>>(string: S, style: ContentStyle) -> Self {
string
.as_ref()
.chars()
.map(|ch| StyledGrapheme::new(ch, style))
.collect()
}
pub fn from_lines<I>(lines: I) -> Self
where
I: IntoIterator<Item = StyledGraphemes>,
{
let mut merged = StyledGraphemes::default();
let mut lines = lines.into_iter().peekable();
while let Some(mut line) = lines.next() {
merged.append(&mut line);
if lines.peek().is_some() {
merged.push_back(StyledGrapheme::from('\n'));
}
}
merged
}
pub fn iter(&self) -> impl Iterator<Item = &StyledGrapheme> {
self.0.iter()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn chars(&self) -> Vec<char> {
self.0.iter().map(|grapheme| grapheme.ch).collect()
}
pub fn widths(&self) -> usize {
self.0.iter().map(|grapheme| grapheme.width).sum()
}
pub fn widths_to(&self, index: usize) -> usize {
self.0
.iter()
.take(index)
.map(|grapheme| grapheme.width)
.sum()
}
pub fn logical_lines(&self) -> Vec<StyledGraphemes> {
if self.is_empty() {
return Vec::new();
}
let mut lines = Vec::new();
let mut line = StyledGraphemes::default();
let mut last_was_newline = false;
for styled in self.iter() {
if styled.ch == '\n' {
lines.push(line);
line = StyledGraphemes::default();
last_was_newline = true;
} else {
line.push_back(styled.clone());
last_was_newline = false;
}
}
if !line.is_empty() || last_was_newline {
lines.push(line);
}
lines
}
pub fn styled_display(&self) -> StyledGraphemesDisplay<'_> {
StyledGraphemesDisplay {
styled_graphemes: self,
}
}
pub fn get_mut(&mut self, idx: usize) -> Option<&mut StyledGrapheme> {
self.0.get_mut(idx)
}
pub fn push_back(&mut self, grapheme: StyledGrapheme) {
self.0.push_back(grapheme);
}
pub fn pop_back(&mut self) -> Option<StyledGrapheme> {
self.0.pop_back()
}
pub fn append(&mut self, other: &mut Self) {
self.0.append(&mut other.0);
}
pub fn insert(&mut self, idx: usize, grapheme: StyledGrapheme) {
self.0.insert(idx, grapheme);
}
pub fn remove(&mut self, idx: usize) -> Option<StyledGrapheme> {
self.0.remove(idx)
}
pub fn drain(
&mut self,
range: std::ops::Range<usize>,
) -> std::collections::vec_deque::Drain<'_, StyledGrapheme> {
self.0.drain(range)
}
pub fn apply_style(mut self, style: ContentStyle) -> Self {
for grapheme in &mut self.0 {
grapheme.apply_style(style);
}
self
}
pub fn apply_style_at(mut self, idx: usize, style: ContentStyle) -> Self {
if let Some(grapheme) = self.0.get_mut(idx) {
grapheme.apply_style(style);
}
self
}
pub fn apply_attribute(mut self, attr: Attribute) -> Self {
for styled_grapheme in &mut self.0 {
styled_grapheme.style.attributes.set(attr);
}
self
}
pub fn find_all<S: AsRef<str>>(&self, query: S) -> Vec<usize> {
let query_str = query.as_ref();
if query_str.is_empty() {
return Vec::new();
}
let mut indices = Vec::new();
let mut pos = 0;
let query_chars: Vec<char> = query_str.chars().collect();
let query_len = query_chars.len();
while pos + query_len <= self.0.len() {
let mut match_found = true;
for (i, query_char) in query_chars.iter().enumerate() {
if self.0[pos + i].ch != *query_char {
match_found = false;
break;
}
}
if match_found {
indices.push(pos);
pos += 1; } else {
pos += 1; }
}
indices
}
pub fn highlight<S: AsRef<str>>(mut self, query: S, style: ContentStyle) -> Option<Self> {
let query_str = query.as_ref();
if query_str.is_empty() {
return Some(self);
}
let indices = self.find_all(query_str);
if indices.is_empty() {
return None;
}
let query_len = query_str.chars().count();
for &start_index in &indices {
for i in start_index..start_index + query_len {
if let Some(grapheme) = self.0.get_mut(i) {
grapheme.apply_style(style);
}
}
}
Some(self)
}
pub fn replace<S: AsRef<str>>(mut self, from: S, to: S) -> Self {
let from_len = from.as_ref().chars().count();
let to_len = to.as_ref().chars().count();
let mut offset = 0;
let diff = from_len.abs_diff(to_len);
let pos = self.find_all(from);
for p in pos {
let adjusted_pos = if to_len > from_len {
p + offset
} else {
p.saturating_sub(offset)
};
self.replace_range(adjusted_pos..adjusted_pos + from_len, &to);
offset += diff;
}
self
}
pub fn replace_range<S: AsRef<str>>(&mut self, range: std::ops::Range<usize>, replacement: S) {
for _ in range.clone() {
self.0.remove(range.start);
}
let replacement_graphemes: StyledGraphemes = replacement.as_ref().into();
for grapheme in replacement_graphemes.0.iter().rev() {
self.0.insert(range.start, grapheme.clone());
}
}
pub fn wrapped_lines(&self, width: usize) -> Vec<StyledGraphemes> {
if width == 0 {
return vec![];
}
let mut rows = Vec::new();
let mut row = StyledGraphemes::default();
let mut row_width = 0;
let mut last_was_newline = false;
for styled in self.iter() {
if styled.ch == '\n' {
rows.push(row);
row = StyledGraphemes::default();
row_width = 0;
last_was_newline = true;
continue;
}
last_was_newline = false;
if styled.width > width {
continue;
}
if !row.is_empty() && row_width + styled.width > width {
rows.push(row);
row = StyledGraphemes::default();
row_width = 0;
}
row.push_back(styled.clone());
row_width += styled.width;
}
if !row.is_empty() || last_was_newline {
rows.push(row);
}
rows
}
pub fn truncated_line_with_ellipsis(
&self,
width: usize,
ellipsis: &StyledGraphemes,
) -> StyledGraphemes {
if self.widths() <= width {
return self.clone();
}
if width == 0 {
return StyledGraphemes::default();
}
let ellipsis_width = ellipsis.widths();
if width <= ellipsis_width {
return ellipsis.clone();
}
let mut truncated = StyledGraphemes::default();
let mut current_width = 0;
for g in self.iter() {
if current_width + g.width() + ellipsis_width > width {
break;
}
truncated.push_back(g.clone());
current_width += g.width();
}
vec![truncated, ellipsis.clone()].into_iter().collect()
}
}
pub struct StyledGraphemesDisplay<'a> {
styled_graphemes: &'a StyledGraphemes,
}
impl fmt::Display for StyledGraphemesDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for styled_grapheme in self.styled_graphemes.iter() {
write!(f, "{}", styled_grapheme.style.apply(styled_grapheme.ch))?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
mod styled_graphemes {
use super::*;
mod from_str {
use super::*;
#[test]
fn creates_graphemes_with_the_given_style() {
let style = ContentStyle::default();
let graphemes = StyledGraphemes::from_str("abc", style);
assert_eq!(3, graphemes.0.len());
assert!(graphemes.0.iter().all(|g| g.style == style));
}
}
mod from_lines {
use super::*;
#[test]
fn empty_input_returns_empty_graphemes() {
let g = StyledGraphemes::from_lines(Vec::new());
assert!(g.is_empty());
}
#[test]
fn inserts_newlines_between_lines() {
let g = StyledGraphemes::from_lines(vec![
StyledGraphemes::from("abc"),
StyledGraphemes::from("def"),
]);
assert_eq!("abc\ndef", g.to_string());
}
}
mod chars {
use super::*;
#[test]
fn returns_the_characters() {
let graphemes = StyledGraphemes::from("abc");
let chars = graphemes.chars();
assert_eq!(vec!['a', 'b', 'c'], chars);
}
}
mod widths {
use super::*;
#[test]
fn sums_display_widths() {
let graphemes = StyledGraphemes::from("a b");
assert_eq!(3, graphemes.widths()); }
}
mod styled_display {
use super::*;
#[test]
fn renders_characters_with_default_styles() {
let graphemes = StyledGraphemes::from("abc");
let display = graphemes.styled_display();
assert_eq!(format!("{}", display), "abc"); }
}
mod apply_style {
use super::*;
use crossterm::style::Color;
#[test]
fn applies_the_style_to_every_grapheme() {
let mut graphemes = StyledGraphemes::from("abc");
let new_style = ContentStyle {
foreground_color: Some(Color::Green),
..Default::default()
};
graphemes = graphemes.apply_style(new_style);
assert!(graphemes.iter().all(|g| g.style == new_style));
}
}
mod apply_style_at {
use super::*;
use crossterm::style::Color;
#[test]
fn applies_the_style_at_the_given_index() {
let mut graphemes = StyledGraphemes::from("abc");
let new_style = ContentStyle {
foreground_color: Some(Color::Green),
..Default::default()
};
graphemes = graphemes.apply_style_at(1, new_style);
assert_eq!(graphemes.0[1].style, new_style);
assert_ne!(graphemes.0[0].style, new_style);
assert_ne!(graphemes.0[2].style, new_style);
}
#[test]
fn ignores_an_out_of_bounds_index() {
let mut graphemes = StyledGraphemes::from("abc");
let new_style = ContentStyle {
foreground_color: Some(Color::Green),
..Default::default()
};
graphemes = graphemes.apply_style_at(5, new_style); assert_eq!(graphemes.0.len(), 3); }
}
mod apply_attribute {
use super::*;
#[test]
fn applies_the_attribute_to_every_grapheme() {
let mut graphemes = StyledGraphemes::from("abc");
graphemes = graphemes.apply_attribute(Attribute::Bold);
assert!(
graphemes
.iter()
.all(|g| g.style.attributes.has(Attribute::Bold))
);
}
}
mod find_all {
use super::*;
#[test]
fn empty_query_returns_no_matches() {
let graphemes = StyledGraphemes::from("Hello, world!");
let indices = graphemes.find_all("");
assert!(
indices.is_empty(),
"Should return an empty vector for an empty query string"
);
}
#[test]
fn finds_repeated_substrings() {
let graphemes = StyledGraphemes::from("Hello, world! Hello, universe!");
let indices = graphemes.find_all("Hello");
assert_eq!(
indices,
vec![0, 14],
"Should find all starting indices of 'Hello'"
);
}
#[test]
fn missing_substring_returns_no_matches() {
let graphemes = StyledGraphemes::from("Hello, world!");
let indices = graphemes.find_all("xyz");
assert!(
indices.is_empty(),
"Should return an empty vector for a non-existent substring"
);
}
#[test]
fn handles_multibyte_characters() {
let graphemes = StyledGraphemes::from("µs µs µs");
let indices = graphemes.find_all("s");
assert_eq!(
indices,
vec![1, 4, 7],
"Should correctly find indices of substring 'µs'"
);
}
#[test]
fn finds_single_characters() {
let graphemes = StyledGraphemes::from("abcabcabc");
let indices = graphemes.find_all("b");
assert_eq!(
indices,
vec![1, 4, 7],
"Should find all indices of character 'b'"
);
}
#[test]
fn matches_the_entire_input() {
let graphemes = StyledGraphemes::from("Hello");
let indices = graphemes.find_all("Hello");
assert_eq!(indices, vec![0], "Should match the entire string");
}
#[test]
fn finds_overlapping_matches() {
let graphemes = StyledGraphemes::from("ababa");
let indices = graphemes.find_all("aba");
assert_eq!(
indices,
vec![0, 2],
"Should handle overlapping matches correctly"
);
}
}
mod highlight {
use super::*;
#[test]
fn empty_query_returns_the_input_unchanged() {
let graphemes = StyledGraphemes::from("Hello, world!");
let expected = graphemes.clone();
let highlighted = graphemes.highlight("", ContentStyle::default());
assert_eq!(highlighted.unwrap(), expected);
}
}
mod replace {
use super::*;
#[test]
fn replaces_all_occurrences() {
let graphemes = StyledGraphemes::from("banana");
assert_eq!("bonono", graphemes.replace("a", "o").to_string());
}
#[test]
fn missing_pattern_leaves_the_input_unchanged() {
let graphemes = StyledGraphemes::from("Hello World");
assert_eq!("Hello World", graphemes.replace("x", "o").to_string());
}
#[test]
fn empty_replacement_removes_matches() {
let graphemes = StyledGraphemes::from("Hello World");
assert_eq!("Hell Wrld", graphemes.replace("o", "").to_string());
}
#[test]
fn longer_replacement_expands_matches() {
let graphemes = StyledGraphemes::from("Hello World");
assert_eq!("Hellabc Wabcrld", graphemes.replace("o", "abc").to_string());
}
}
mod replace_range {
use super::*;
#[test]
fn replaces_the_given_range() {
let mut graphemes = StyledGraphemes::from("Hello");
graphemes.replace_range(1..5, "i");
assert_eq!("Hi", graphemes.to_string());
}
}
mod wrapped_lines {
use super::*;
#[test]
fn empty_input_returns_no_lines() {
let input = StyledGraphemes::default();
let rows = input.wrapped_lines(10);
assert_eq!(rows.len(), 0);
}
#[test]
fn wraps_at_the_display_width() {
let input = StyledGraphemes::from("123456");
let rows = input.wrapped_lines(3);
assert_eq!(rows.len(), 2);
assert_eq!("123", rows[0].to_string());
assert_eq!("456", rows[1].to_string());
}
#[test]
fn splits_at_explicit_newlines() {
let input = StyledGraphemes::from("ab\ncd");
let rows = input.wrapped_lines(10);
assert_eq!(rows.len(), 2);
assert_eq!("ab", rows[0].to_string());
assert_eq!("cd", rows[1].to_string());
}
#[test]
fn preserves_a_trailing_empty_line() {
let input = StyledGraphemes::from("ab\n");
let rows = input.wrapped_lines(10);
assert_eq!(rows.len(), 2);
assert_eq!("ab", rows[0].to_string());
assert_eq!("", rows[1].to_string());
}
}
mod truncated_line_with_ellipsis {
use super::*;
#[test]
fn returns_the_input_when_it_fits() {
let input = StyledGraphemes::from("abc");
let ellipsis = StyledGraphemes::from("…");
let output = input.truncated_line_with_ellipsis(10, &ellipsis);
assert_eq!("abc", output.to_string());
}
#[test]
fn zero_width_returns_empty_graphemes() {
let input = StyledGraphemes::from("abc");
let ellipsis = StyledGraphemes::from("…");
let output = input.truncated_line_with_ellipsis(0, &ellipsis);
assert_eq!("", output.to_string());
}
#[test]
fn returns_only_the_ellipsis_when_no_content_fits() {
let input = StyledGraphemes::from("abc");
let ellipsis = StyledGraphemes::from("…");
let output = input.truncated_line_with_ellipsis(1, &ellipsis);
assert_eq!("…", output.to_string());
}
#[test]
fn truncates_content_and_appends_the_ellipsis() {
let input = StyledGraphemes::from("abcdef");
let ellipsis = StyledGraphemes::from("…");
let output = input.truncated_line_with_ellipsis(4, &ellipsis);
assert_eq!("abc…", output.to_string());
}
}
}
}