use std::{fmt::Debug,
ops::{Add, Deref, DerefMut}};
use smallvec::SmallVec;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::{ch, col, join, pad_fmt, seg_index, seg_width, usize, width, ByteIndex,
ChUnit, ColIndex, ColWidth, InlineString, InlineVecStr, Seg, SegIndex,
SegWidth};
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
pub struct GCString {
pub string: InlineString,
pub segments: sizing::SegmentArray,
pub display_width: ColWidth,
pub bytes_size: ChUnit,
}
mod iterator {
use super::{GCString, Seg};
#[derive(Debug)]
pub struct GCStringIterator<'a> {
gc_string: &'a GCString,
index: usize,
}
impl<'a> Iterator for GCStringIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
match self.gc_string.get_segment(self.index) {
Some(segment) => {
self.index += 1;
Some(segment)
}
None => None, }
}
}
impl GCString {
pub fn seg_iter(&self) -> impl Iterator<Item = &Seg> { self.segments.iter() }
#[must_use]
pub fn iter(&self) -> GCStringIterator<'_> {
GCStringIterator {
gc_string: self,
index: 0,
}
}
#[must_use]
pub fn get_segment(&self, index: usize) -> Option<&str> {
self.segments.get(index).map(|seg| seg.get_str(self))
}
}
impl<'a> IntoIterator for &'a GCString {
type Item = &'a str;
type IntoIter = GCStringIterator<'a>;
fn into_iter(self) -> Self::IntoIter { self.iter() }
}
}
#[cfg(test)]
mod tests_iterator {
use super::*;
#[test]
fn test_iterator() {
let gc_string = GCString::new("Hello, 世界🥞👨👩👧👦🙏🏽");
let mut iter = gc_string.iter();
assert_eq!(iter.next(), Some("H"));
assert_eq!(iter.next(), Some("e"));
assert_eq!(iter.next(), Some("l"));
assert_eq!(iter.next(), Some("l"));
assert_eq!(iter.next(), Some("o"));
assert_eq!(iter.next(), Some(","));
assert_eq!(iter.next(), Some(" "));
assert_eq!(iter.next(), Some("世"));
assert_eq!(iter.next(), Some("界"));
assert_eq!(iter.next(), Some("🥞"));
assert_eq!(iter.next(), Some("👨👩👧👦"));
assert_eq!(iter.next(), Some("🙏🏽"));
assert_eq!(iter.next(), None);
}
#[test]
fn test_into_iterator_implementation() {
let gc_string = GCString::new("Hello, 世界🥞");
let mut collected = Vec::new();
for segment in &gc_string {
collected.push(segment.to_string());
}
assert_eq!(collected.len(), 10);
assert_eq!(collected[0], "H");
assert_eq!(collected[1], "e");
assert_eq!(collected[6], " ");
assert_eq!(collected[7], "世");
assert_eq!(collected[8], "界");
assert_eq!(collected[9], "🥞");
let mut explicit_collected = Vec::new();
for segment in (&gc_string).into_iter() {
explicit_collected.push(segment.to_string());
}
assert_eq!(collected, explicit_collected);
let mut found_emoji = false;
for segment in &gc_string {
if segment == "🥞" {
found_emoji = true;
break;
}
}
assert!(found_emoji);
for (index, segment) in (&gc_string).into_iter().enumerate() {
match index {
0 => assert_eq!(segment, "H"),
1 => assert_eq!(segment, "e"),
7 => assert_eq!(segment, "世"),
8 => assert_eq!(segment, "界"),
9 => assert_eq!(segment, "🥞"),
_ => {} }
}
let mut ascii_count = 0;
let mut unicode_count = 0;
for segment in &gc_string {
if segment.is_ascii() {
ascii_count += 1;
} else {
unicode_count += 1;
}
}
assert_eq!(ascii_count, 7); assert_eq!(unicode_count, 3);
let iter_results: Vec<_> = gc_string.iter().map(ToString::to_string).collect();
assert_eq!(iter_results, collected);
}
}
pub fn grapheme_string(arg_from: impl Into<GCString>) -> GCString { arg_from.into() }
mod sizing {
use super::{ColWidth, GCString, Seg, SmallVec};
use crate::GetMemSize;
pub type SegmentArray = SmallVec<[Seg; VEC_SEGMENT_SIZE]>;
const VEC_SEGMENT_SIZE: usize = 28;
impl GetMemSize for GCString {
fn get_mem_size(&self) -> usize {
let string_size = self.bytes_size.as_usize();
let segments_size = self.segments.len() * std::mem::size_of::<Seg>();
let display_width_field_size = std::mem::size_of::<ColWidth>();
string_size + segments_size + display_width_field_size
}
}
}
mod basic {
use super::{ch, col, seg_width, sizing, width, ChUnit, ColWidth, Deref, DerefMut,
GCString, Seg, SegIndex, SegWidth, UnicodeSegmentation,
UnicodeWidthChar, UnicodeWidthStr};
impl AsRef<str> for GCString {
fn as_ref(&self) -> &str { &self.string }
}
impl Deref for GCString {
type Target = sizing::SegmentArray;
fn deref(&self) -> &Self::Target { &self.segments }
}
impl DerefMut for GCString {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.segments }
}
impl<S> From<&S> for GCString
where
S: AsRef<str> + ?Sized,
{
fn from(arg: &S) -> Self { Self::new(arg.as_ref()) }
}
impl GCString {
pub fn new(arg_str: impl AsRef<str>) -> GCString {
let str = arg_str.as_ref();
if str.is_ascii() {
let len = str.len();
let mut segments = sizing::SegmentArray::with_capacity(len);
for (i, _c) in str.chars().enumerate() {
segments.push(Seg {
start_byte_index: ch(i),
end_byte_index: ch(i + 1),
display_width: width(1),
seg_index: i.into(),
bytes_size: 1,
start_display_col_index: col(ch(i)),
});
}
return GCString {
string: str.into(),
segments,
display_width: width(len),
bytes_size: ch(len),
};
}
let mut total_byte_offset: ChUnit = ch(0);
let mut unicode_width_offset_acc: ChUnit = ch(0);
let iter = str.grapheme_indices(true).enumerate();
let size = iter.clone().count();
let mut unicode_string_segments = sizing::SegmentArray::with_capacity(size);
for (grapheme_cluster_index, (byte_offset, grapheme_cluster_str)) in iter {
let display_width = GCString::width(grapheme_cluster_str);
unicode_string_segments.push(Seg {
start_byte_index: ch(byte_offset),
end_byte_index: ch(byte_offset) + ch(grapheme_cluster_str.len()),
display_width,
seg_index: grapheme_cluster_index.into(),
bytes_size: grapheme_cluster_str.len(),
start_display_col_index: col(unicode_width_offset_acc), });
unicode_width_offset_acc += *display_width;
total_byte_offset = ch(byte_offset);
}
GCString {
string: str.into(),
segments: unicode_string_segments,
display_width: width(unicode_width_offset_acc),
bytes_size: if total_byte_offset > ch(0) {
total_byte_offset + 1
} else {
total_byte_offset
},
}
}
#[must_use]
pub fn len(&self) -> SegWidth { self.segments.len().into() }
#[must_use]
pub fn is_empty(&self) -> bool { self.len() == seg_width(0) }
#[must_use]
pub fn get_max_seg_index(&self) -> SegIndex { self.len().convert_to_seg_index() }
pub fn width(arg_str: impl AsRef<str>) -> ColWidth {
let str = arg_str.as_ref();
width(UnicodeWidthStr::width(str))
}
#[must_use]
pub fn width_char(c: char) -> ColWidth {
let value = UnicodeWidthChar::width(c).unwrap_or(0);
width(value)
}
pub fn get(&self, arg_seg_index: impl Into<SegIndex>) -> Option<Seg> {
let index: SegIndex = arg_seg_index.into();
self.segments.get(crate::usize(*index)).copied()
}
}
}
pub mod at_display_col_index {
use super::{ch, seg_index, ColIndex, GCString, Seg, SegString};
impl GCString {
pub fn check_is_in_middle_of_grapheme(
&self,
arg_col_index: impl Into<ColIndex>,
) -> Option<Seg> {
let col: ColIndex = arg_col_index.into();
let seg_index_at_col = (self + col)?;
let seg = self.get(seg_index_at_col)?;
if col != seg.start_display_col_index {
return Some(seg);
}
None
}
pub fn get_string_at(
&self,
arg_col_index: impl Into<ColIndex>,
) -> Option<SegString> {
let col: ColIndex = arg_col_index.into();
let seg_index_at_col = (self + col)?;
let seg = self.get(seg_index_at_col)?;
let seg_start_at = seg.start_display_col_index;
(col == seg_start_at).then(|| {
(seg, self).into()
})
}
pub fn get_string_at_right_of(
&self,
arg_col_index: impl Into<ColIndex>,
) -> Option<SegString> {
let col: ColIndex = arg_col_index.into();
let seg_index_at_col = (self + col)?;
let seg = self.get(seg_index_at_col)?;
(seg.seg_index < self.get_max_seg_index()).then(|| {
let right_neighbor_seg = self.get(*seg.seg_index + ch(1))?;
Some((right_neighbor_seg, self).into())
})?
}
pub fn get_string_at_left_of(
&self,
arg_col_index: impl Into<ColIndex>,
) -> Option<SegString> {
let col: ColIndex = arg_col_index.into();
let seg_index_at_col = (self + col)?;
let seg = self.get(seg_index_at_col)?;
(seg.seg_index > seg_index(0)).then(|| {
let left_neighbor_seg = self.get(*seg.seg_index - ch(1))?;
Some((left_neighbor_seg, self).into())
})?
}
#[must_use]
pub fn get_string_at_end(&self) -> Option<SegString> {
let seg = self.last()?;
Some((*seg, self).into())
}
}
}
#[derive(PartialEq, Eq)]
pub struct SegString {
pub string: GCString,
pub width: ColWidth,
pub start_at: ColIndex,
}
mod seg_string_result_impl {
use super::{grapheme_string, Debug, GCString, Seg, SegString};
impl From<(Seg, &GCString)> for SegString {
fn from((seg, gs): (Seg, &GCString)) -> SegString {
SegString {
string: grapheme_string(seg.get_str(gs)),
width: seg.display_width,
start_at: seg.start_display_col_index,
}
}
}
impl Debug for SegString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SegString {{ str: {:?} ┆ width: {:?} ┆ starts_at_col: {:?} }}",
self.string.string, self.width, self.start_at
)
}
}
}
mod convert {
use super::{seg_index, usize, Add, ByteIndex, ColIndex, GCString, SegIndex};
impl Add<ByteIndex> for &GCString {
type Output = Option<SegIndex>;
fn add(self, byte_index: ByteIndex) -> Self::Output {
let byte_index = *byte_index;
for seg in &self.segments {
let start = usize(seg.start_byte_index);
let end = usize(seg.end_byte_index);
if byte_index >= start && byte_index < end {
return Some(seg.seg_index);
}
}
None
}
}
impl Add<ColIndex> for &GCString {
type Output = Option<SegIndex>;
fn add(self, display_col_index: ColIndex) -> Self::Output {
self.segments
.iter()
.find(|seg| {
let seg_display_width = seg.display_width;
let seg_start = seg.start_display_col_index;
let seg_end = seg_start + seg_display_width;
display_col_index >= seg_start && display_col_index < seg_end
})
.map(|seg| seg_index(seg.seg_index))
}
}
impl Add<SegIndex> for &GCString {
type Output = Option<ColIndex>;
fn add(self, seg_index: SegIndex) -> Self::Output {
self.get(seg_index).map(|seg| seg.start_display_col_index)
}
}
}
pub mod wide_segments {
use super::{width, Debug, GCString};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ContainsWideSegments {
Yes,
No,
}
impl GCString {
#[must_use]
pub fn contains_wide_segments(&self) -> ContainsWideSegments {
if self.segments.iter().any(|seg| seg.display_width > width(1)) {
ContainsWideSegments::Yes
} else {
ContainsWideSegments::No
}
}
}
}
pub mod trunc_end {
use super::{ch, usize, ColWidth, GCString};
impl GCString {
pub fn trunc_end_to_fit(&self, arg_col_width: impl Into<ColWidth>) -> &str {
let mut avail_cols: ColWidth = arg_col_width.into();
let mut string_end_byte_index = 0;
for seg in self.seg_iter() {
let seg_display_width = seg.display_width;
if avail_cols < seg_display_width {
break;
}
string_end_byte_index += seg.bytes_size;
avail_cols -= seg_display_width;
}
&self.string[..string_end_byte_index]
}
pub fn trunc_end_by(&self, arg_col_width: impl Into<ColWidth>) -> &str {
let mut countdown_col_count: ColWidth = arg_col_width.into();
let mut string_end_byte_index = ch(0);
let rev_iter = self.segments.iter().rev();
for seg in rev_iter {
let seg_display_width = seg.display_width;
string_end_byte_index = seg.start_byte_index;
countdown_col_count -= seg_display_width;
if *countdown_col_count == ch(0) {
break;
}
}
&self.string[..usize(string_end_byte_index)]
}
}
}
pub mod trunc_start {
use super::{ch, ColWidth, GCString};
impl GCString {
pub fn trunc_start_by(&self, arg_col_width: impl Into<ColWidth>) -> &str {
let mut skip_col_count: ColWidth = arg_col_width.into();
let mut string_start_byte_index = 0;
for segment in self.seg_iter() {
let seg_display_width = segment.display_width;
if *skip_col_count == ch(0) {
break;
}
skip_col_count -= seg_display_width;
string_start_byte_index += segment.bytes_size;
}
&self.string[string_start_byte_index..]
}
}
}
mod pad {
use super::{pad_fmt, width, ColWidth, GCString, InlineString};
impl GCString {
pub fn pad_end_to_fit(
&self,
arg_pad_str: impl AsRef<str>,
arg_col_width: impl Into<ColWidth>,
) -> InlineString {
let pad_str = arg_pad_str.as_ref();
let max_display_width: ColWidth = arg_col_width.into();
let pad_count = max_display_width - self.display_width;
let self_str = self.string.as_str();
if pad_count > width(0) {
let mut acc = InlineString::from(self_str);
pad_fmt!(fmt: acc, pad_str: pad_str, repeat_count: **pad_count);
acc
} else {
self_str.into()
}
}
pub fn pad_start_to_fit(
&self,
arg_pad_str: impl AsRef<str>,
arg_col_width: impl Into<ColWidth>,
) -> InlineString {
let pad_str = arg_pad_str.as_ref();
let max_display_width: ColWidth = arg_col_width.into();
let pad_count = max_display_width - self.display_width;
let self_str = self.string.as_str();
if pad_count > width(0) {
let mut acc = InlineString::new();
pad_fmt!(fmt: acc, pad_str: pad_str, repeat_count: **pad_count);
acc.push_str(self_str);
acc
} else {
self_str.into()
}
}
pub fn try_get_postfix_padding_for(
&self,
arg_pad_str: impl AsRef<str>,
arg_col_width: impl Into<ColWidth>,
) -> Option<InlineString> {
let pad_str = arg_pad_str.as_ref();
let max_display_width: ColWidth = arg_col_width.into();
if self.display_width < max_display_width {
let pad_count = max_display_width - self.display_width;
let mut acc = InlineString::new();
pad_fmt!(fmt: acc, pad_str: pad_str, repeat_count: **pad_count);
Some(acc)
} else {
None
}
}
}
}
mod clip {
use super::{ch, ColIndex, ColWidth, GCString};
impl GCString {
pub fn clip(
&self,
arg_start_at_col_index: impl Into<ColIndex>,
arg_col_width: impl Into<ColWidth>,
) -> &str {
let start_display_col_index: ColIndex = arg_start_at_col_index.into();
let max_display_col_count: ColWidth = arg_col_width.into();
let string_start_byte_index = {
let mut it = 0;
let mut skip_col_count = start_display_col_index;
for seg in self.seg_iter() {
let seg_display_width = seg.display_width;
if *skip_col_count == ch(0) {
break;
}
skip_col_count -= seg_display_width;
it += seg.bytes_size;
}
it
};
let string_end_byte_index = {
let mut it = 0;
let mut avail_col_count = max_display_col_count;
let mut skip_col_count = start_display_col_index;
for seg in self.seg_iter() {
let seg_display_width = seg.display_width;
if *skip_col_count == ch(0) {
if avail_col_count < seg_display_width {
break;
}
it += seg.bytes_size;
avail_col_count -= seg_display_width;
} else {
skip_col_count -= seg_display_width;
it += seg.bytes_size;
}
}
it
};
&self.string[string_start_byte_index..string_end_byte_index]
}
}
}
mod mutate {
use super::{ch, join, seg_width, usize, width, ColIndex, ColWidth, GCString,
InlineString, InlineVecStr};
impl GCString {
pub fn insert_chunk_at_col(
&self,
arg_col_index: impl Into<ColIndex>,
arg_chunk: impl AsRef<str>,
) -> (InlineString, ColWidth) {
let chunk = arg_chunk.as_ref();
let mut vec = InlineVecStr::with_capacity(self.len().as_usize() + 1);
vec.extend(
self.seg_iter().map(|seg| seg.get_str(&self.string)),
);
let col: ColIndex = arg_col_index.into();
let seg_index_at_col = self + col;
match seg_index_at_col {
Some(seg_index) => vec.insert(usize(*seg_index), chunk),
None => vec.push(chunk),
}
(
join!(from: vec, each: item, delim: "", format: "{item}"),
GCString::width(chunk),
)
}
pub fn delete_char_at_col(
&self,
arg_col_index: impl Into<ColIndex>,
) -> Option<InlineString> {
if self.is_empty() {
return None;
}
if self.len() == seg_width(1) {
return Some("".into());
}
let col: ColIndex = arg_col_index.into();
let split_seg_index = (self + col)?;
let split_seg_index = usize(*split_seg_index);
let mut vec_left = InlineVecStr::with_capacity(self.len().as_usize());
let mut str_left_display_width = width(0);
{
for seg_index in 0..split_seg_index {
let seg = *self.segments.get(seg_index)?;
let string = seg.get_str(&self.string);
vec_left.push(string);
str_left_display_width += seg.display_width;
}
}
let mut vec_right = InlineVecStr::with_capacity(self.len().as_usize());
let mut str_right_display_width = width(0);
{
let max_seg_index = self.len();
for seg_index in (split_seg_index + 1)..max_seg_index.as_usize() {
let seg = *self.segments.get(seg_index)?;
let string = seg.get_str(&self.string);
vec_right.push(string);
str_right_display_width += seg.display_width;
}
}
vec_left.append(&mut vec_right);
Some(join!(from: vec_left, each: it, delim: "", format: "{it}"))
}
pub fn split_at_display_col(
&self,
arg_col_index: impl Into<ColIndex>,
) -> Option<(InlineString, InlineString)> {
let col: ColIndex = arg_col_index.into();
let split_seg_index = (self + col)?;
let split_seg_index = usize(*split_seg_index);
let mut acc_left = InlineVecStr::with_capacity(self.len().as_usize());
let mut str_left_display_width = width(0);
{
for seg_index in 0..split_seg_index {
let seg = *self.segments.get(seg_index)?;
acc_left.push(seg.get_str(&self.string));
str_left_display_width += seg.display_width;
}
}
let mut acc_right = InlineVecStr::with_capacity(self.len().as_usize());
let mut str_right_unicode_width = width(0);
{
let max_seg_index = self.len();
for seg_idx in split_seg_index..max_seg_index.as_usize() {
let seg = *self.segments.get(seg_idx)?;
acc_right.push(seg.get_str(&self.string));
str_right_unicode_width += seg.display_width;
}
}
(*str_right_unicode_width > ch(0) || *str_left_display_width > ch(0)).then(
|| {
(
join!(from: acc_left, each: it, delim: "", format: "{it}"),
join!(from: acc_right, each: it, delim: "", format: "{it}"),
)
},
)
}
}
}
#[cfg(test)]
mod tests {
use std::str;
use super::*;
use crate::{byte_index, gc_string::wide_segments::ContainsWideSegments};
fn ssr(
arg_gc_string: impl Into<GCString>,
width: ColWidth,
start_at: ColIndex,
) -> SegString {
SegString {
string: arg_gc_string.into(),
width,
start_at,
}
}
fn w(string: &str) -> ColWidth { GCString::width(string) }
const TEST_STR: &str = "Hi📦XelLo🙏🏽Bye";
#[test]
fn test_sanity_of_test_str() {
let gs = grapheme_string(TEST_STR);
assert!(!gs.is_empty());
assert!(gs.contains_wide_segments() == ContainsWideSegments::Yes);
assert_eq!(gs.display_width, width(14));
assert_eq!(gs.display_width.convert_to_col_index(), col(13));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(*gs.len() - ch(1)));
assert_eq!(gs.get_max_seg_index(), gs.len().convert_to_seg_index());
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
}
#[test]
fn test_insert_chunk_at_display_col() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
("🚀", col(00), "🚀Hi📦XelLo🙏🏽Bye", w("🚀")),
("🚀", col(01), "H🚀i📦XelLo🙏🏽Bye", w("🚀")),
("🚀", col(02), "Hi🚀📦XelLo🙏🏽Bye", w("🚀")),
("🚀", col(03), "Hi🚀📦XelLo🙏🏽Bye", w("🚀")),
("🚀", col(04), "Hi📦🚀XelLo🙏🏽Bye", w("🚀")),
("🚀", col(05), "Hi📦X🚀elLo🙏🏽Bye", w("🚀")),
("🚀", col(06), "Hi📦Xe🚀lLo🙏🏽Bye", w("🚀")),
("🚀", col(07), "Hi📦Xel🚀Lo🙏🏽Bye", w("🚀")),
("🚀", col(08), "Hi📦XelL🚀o🙏🏽Bye", w("🚀")),
("🚀", col(09), "Hi📦XelLo🚀🙏🏽Bye", w("🚀")),
("🚀", col(10), "Hi📦XelLo🚀🙏🏽Bye", w("🚀")),
("🚀", col(11), "Hi📦XelLo🙏🏽🚀Bye", w("🚀")),
("🚀", col(12), "Hi📦XelLo🙏🏽B🚀ye", w("🚀")),
("🚀", col(13), "Hi📦XelLo🙏🏽By🚀e", w("🚀")),
("🚀", col(14), "Hi📦XelLo🙏🏽Bye🚀", w("🚀")),
("🚀", col(15), "Hi📦XelLo🙏🏽Bye🚀", w("🚀")),
("🚀", col(16), "Hi📦XelLo🙏🏽Bye🚀", w("🚀")),
];
for (chunk, insert_at, expected_str, exp_chunk_width) in test_cases {
let (actual_str, actual_width) = gs.insert_chunk_at_col(insert_at, chunk);
assert_eq!(actual_str, expected_str);
assert_eq!(actual_width, exp_chunk_width);
}
}
#[test]
fn test_delete_char_at_display_col() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(col(00), Some("i📦XelLo🙏🏽Bye".into())),
(col(01), Some("H📦XelLo🙏🏽Bye".into())),
(col(02), Some("HiXelLo🙏🏽Bye".into())),
(col(03), Some("HiXelLo🙏🏽Bye".into())),
(col(04), Some("Hi📦elLo🙏🏽Bye".into())),
(col(05), Some("Hi📦XlLo🙏🏽Bye".into())),
(col(06), Some("Hi📦XeLo🙏🏽Bye".into())),
(col(07), Some("Hi📦Xelo🙏🏽Bye".into())),
(col(08), Some("Hi📦XelL🙏🏽Bye".into())),
(col(09), Some("Hi📦XelLoBye".into())),
(col(10), Some("Hi📦XelLoBye".into())),
(col(11), Some("Hi📦XelLo🙏🏽ye".into())),
(col(12), Some("Hi📦XelLo🙏🏽Be".into())),
(col(13), Some("Hi📦XelLo🙏🏽By".into())),
(col(14), None),
(col(15), None),
];
for (col_index, exp_result) in test_cases {
let result = gs.delete_char_at_col(col_index);
assert_eq!(exp_result, result);
}
}
#[test]
fn test_split_at_display_col() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
let test_cases = [
(col(0), Some(("".into(), "Hi📦XelLo🙏🏽Bye".into()))),
(col(1), Some(("H".into(), "i📦XelLo🙏🏽Bye".into()))),
(col(2), Some(("Hi".into(), "📦XelLo🙏🏽Bye".into()))),
(col(3), Some(("Hi".into(), "📦XelLo🙏🏽Bye".into()))),
(col(4), Some(("Hi📦".into(), "XelLo🙏🏽Bye".into()))),
(col(5), Some(("Hi📦X".into(), "elLo🙏🏽Bye".into()))),
(col(6), Some(("Hi📦Xe".into(), "lLo🙏🏽Bye".into()))),
(col(7), Some(("Hi📦Xel".into(), "Lo🙏🏽Bye".into()))),
(col(8), Some(("Hi📦XelL".into(), "o🙏🏽Bye".into()))),
(col(9), Some(("Hi📦XelLo".into(), "🙏🏽Bye".into()))),
(col(10), Some(("Hi📦XelLo".into(), "🙏🏽Bye".into()))),
(col(11), Some(("Hi📦XelLo🙏🏽".into(), "Bye".into()))),
(col(12), Some(("Hi📦XelLo🙏🏽B".into(), "ye".into()))),
(col(13), Some(("Hi📦XelLo🙏🏽By".into(), "e".into()))),
(col(14), None),
(col(15), None),
(col(16), None),
];
for (col_index, expected) in test_cases {
let result = gs.split_at_display_col(col_index);
assert_eq!(result, expected);
}
}
#[test]
fn test_get_string_at_end() {
let test_cases = [
(TEST_STR, Some(ssr("e", width(1), col(13)))),
("Hi", Some(ssr("i", width(1), col(1)))),
("H", Some(ssr("H", width(1), col(0)))),
("📦", Some(ssr("📦", width(2), col(0)))),
("🙏🏽", Some(ssr("🙏🏽", width(2), col(0)))),
("", None),
];
for (input, expected) in test_cases {
let gs = grapheme_string(input);
let end = gs.get_string_at_end();
assert_eq!(end, expected);
}
}
#[test]
fn test_get_string_at_left_of_display_col_index() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(col(00), None),
(col(01), Some(ssr("H", width(1), col(0)))),
(col(02), Some(ssr("i", width(1), col(1)))),
(col(03), Some(ssr("i", width(1), col(1)))),
(col(04), Some(ssr("📦", width(2), col(2)))),
(col(05), Some(ssr("X", width(1), col(4)))),
(col(06), Some(ssr("e", width(1), col(5)))),
(col(07), Some(ssr("l", width(1), col(6)))),
(col(08), Some(ssr("L", width(1), col(7)))),
(col(09), Some(ssr("o", width(1), col(8)))),
(col(10), Some(ssr("o", width(1), col(8)))),
(col(11), Some(ssr("🙏🏽", width(2), col(9)))),
(col(12), Some(ssr("B", width(1), col(11)))),
(col(13), Some(ssr("y", width(1), col(12)))),
(col(14), None),
(col(15), None),
(col(16), None),
];
for (display_col_index, expected) in test_cases {
let at_left = gs.get_string_at_left_of(display_col_index);
assert_eq!(at_left, expected);
}
}
#[test]
fn test_get_string_at_right_of() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(col(00), Some(ssr("i", width(1), col(1)))),
(col(01), Some(ssr("📦", width(2), col(2)))),
(col(02), Some(ssr("X", width(1), col(4)))),
(col(03), Some(ssr("X", width(1), col(4)))),
(col(04), Some(ssr("e", width(1), col(5)))),
(col(05), Some(ssr("l", width(1), col(6)))),
(col(06), Some(ssr("L", width(1), col(7)))),
(col(07), Some(ssr("o", width(1), col(8)))),
(col(08), Some(ssr("🙏🏽", width(2), col(9)))),
(col(09), Some(ssr("B", width(1), col(11)))),
(col(10), Some(ssr("B", width(1), col(11)))),
(col(11), Some(ssr("y", width(1), col(12)))),
(col(12), Some(ssr("e", width(1), col(13)))),
(col(13), None),
(col(14), None),
(col(15), None),
(col(16), None),
];
for (display_col_index, expected) in test_cases {
let result = gs.get_string_at_right_of(display_col_index);
assert_eq!(result, expected);
}
}
#[test]
fn test_in_middle_of_cluster() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
let test_cases = [
(col(0), None),
(col(1), None),
(col(2), None),
(col(3), gs.get(seg_index(2))),
(col(4), None),
(col(5), None),
(col(6), None),
(col(7), None),
(col(8), None),
(col(9), None),
(col(10), gs.get(seg_index(8))),
(col(11), None),
(col(12), None),
(col(13), None),
(col(14), None),
(col(15), None),
(col(16), None),
];
for (col_index, expected) in test_cases {
let seg = gs.check_is_in_middle_of_grapheme(col_index);
assert_eq!(seg, expected);
}
}
#[test]
fn test_get_string_at_col() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
let test_cases = [
(col(0), Some(("H", width(1), col(0)))),
(col(1), Some(("i", width(1), col(1)))),
(col(2), Some(("📦", width(2), col(2)))),
(col(3), None),
(col(4), Some(("X", width(1), col(4)))),
(col(5), Some(("e", width(1), col(5)))),
(col(6), Some(("l", width(1), col(6)))),
(col(7), Some(("L", width(1), col(7)))),
(col(8), Some(("o", width(1), col(8)))),
(col(9), Some(("🙏🏽", width(2), col(9)))),
(col(10), None),
(col(11), Some(("B", width(1), col(11)))),
(col(12), Some(("y", width(1), col(12)))),
(col(13), Some(("e", width(1), col(13)))),
(col(14), None),
(col(15), None),
(col(16), None),
];
for (given_display_col, expected) in test_cases {
let result = gs.get_string_at(given_display_col);
match expected {
Some((exp_str, exp_width, exp_col)) => {
let result = result.unwrap();
assert_eq!(result.string, grapheme_string(exp_str));
assert_eq!(result.width, exp_width);
assert_eq!(result.start_at, exp_col);
}
None => assert!(result.is_none()),
}
}
}
#[test]
fn test_clip() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(col(00), width(00), ""),
(col(00), width(01), "H"),
(col(00), width(02), "Hi"),
(col(00), width(03), "Hi"),
(col(00), width(04), "Hi📦"),
(col(00), width(05), "Hi📦X"),
(col(00), width(06), "Hi📦Xe"),
(col(00), width(07), "Hi📦Xel"),
(col(00), width(08), "Hi📦XelL"),
(col(00), width(09), "Hi📦XelLo"),
(col(00), width(10), "Hi📦XelLo"),
(col(00), width(11), "Hi📦XelLo🙏🏽"),
(col(00), width(12), "Hi📦XelLo🙏🏽B"),
(col(00), width(13), "Hi📦XelLo🙏🏽By"),
(col(00), width(14), "Hi📦XelLo🙏🏽Bye"),
(col(00), width(15), "Hi📦XelLo🙏🏽Bye"),
(col(00), width(16), "Hi📦XelLo🙏🏽Bye"),
];
for (start_at, width, expected) in test_cases {
let clipped_line = gs.clip(start_at, width);
assert_eq!(clipped_line, expected);
}
}
#[test]
fn test_try_get_postfix_padding_for() {
let gs = GCString::new("example");
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(" ", 11, Some(" ".into())),
(" ", 10, Some(" ".into())),
(" ", 09, Some(" ".into())),
(" ", 08, Some(" ".into())),
(" ", 07, None),
(" ", 06, None),
(" ", 05, None),
(" ", 04, None),
(" ", 03, None),
(" ", 02, None),
(" ", 01, None),
(" ", 00, None),
];
for (spacer, width, expected) in test_cases {
let padded_string = gs.try_get_postfix_padding_for(spacer, width);
assert_eq!(padded_string, expected);
}
}
#[test]
fn test_pad_start_to_fit() {
let gs = GCString::new("example");
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(" ", 10, " example"),
(" ", 09, " example"),
(" ", 08, " example"),
(" ", 07, "example"),
(" ", 06, "example"),
(" ", 05, "example"),
(" ", 04, "example"),
(" ", 03, "example"),
(" ", 02, "example"),
(" ", 01, "example"),
(" ", 00, "example"),
];
for (spacer, width, expected) in test_cases {
let padded_string = gs.pad_start_to_fit(spacer, width);
assert_eq!(padded_string, expected);
}
}
#[test]
fn test_pad_end_to_fit() {
let gs = GCString::new("example");
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(" ", 10, "example "),
(" ", 09, "example "),
(" ", 08, "example "),
(" ", 07, "example"),
(" ", 06, "example"),
(" ", 05, "example"),
(" ", 04, "example"),
(" ", 03, "example"),
(" ", 02, "example"),
(" ", 01, "example"),
(" ", 00, "example"),
];
for (spacer, width, expected) in test_cases {
let padded_string = gs.pad_end_to_fit(spacer, width);
assert_eq!(&padded_string, expected);
}
}
#[test]
fn test_trunc_start() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(width(00), "Hi📦XelLo🙏🏽Bye"),
(width(01), "i📦XelLo🙏🏽Bye"),
(width(02), "📦XelLo🙏🏽Bye"),
(width(03), "XelLo🙏🏽Bye"),
(width(04), "XelLo🙏🏽Bye"),
(width(05), "elLo🙏🏽Bye"),
(width(06), "lLo🙏🏽Bye"),
(width(07), "Lo🙏🏽Bye"),
(width(08), "o🙏🏽Bye"),
(width(09), "🙏🏽Bye"),
(width(10), "Bye"),
(width(11), "Bye"),
(width(12), "ye"),
(width(13), "e"),
(width(14), ""),
(width(15), ""),
(width(16), ""),
(width(17), ""),
];
for (input_width, expected) in &test_cases {
let truncated_line = gs.trunc_start_by(*input_width);
assert_eq!(truncated_line, *expected);
}
}
#[test]
fn test_trunc_end() {
let gs = grapheme_string(TEST_STR);
assert_eq!(w("📦"), width(2));
assert_eq!(w("🙏🏽"), width(2));
assert_eq!(w(TEST_STR), width(14));
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.get_max_seg_index(), seg_index(11));
#[allow(clippy::zero_prefixed_literal)]
let test_cases = [
(width(00), ""),
(width(01), "H"),
(width(02), "Hi"),
(width(03), "Hi"),
(width(04), "Hi📦"),
(width(05), "Hi📦X"),
(width(06), "Hi📦Xe"),
(width(07), "Hi📦Xel"),
(width(08), "Hi📦XelL"),
(width(09), "Hi📦XelLo"),
(width(10), "Hi📦XelLo"),
(width(11), "Hi📦XelLo🙏🏽"),
(width(12), "Hi📦XelLo🙏🏽B"),
(width(13), "Hi📦XelLo🙏🏽By"),
(width(14), "Hi📦XelLo🙏🏽Bye"),
(width(15), "Hi📦XelLo🙏🏽Bye"),
(width(16), "Hi📦XelLo🙏🏽Bye"),
(width(17), "Hi📦XelLo🙏🏽Bye"),
];
for (input_width, expected) in &test_cases {
let truncated_line = gs.trunc_end_to_fit(*input_width);
assert_eq!(truncated_line, *expected);
}
}
#[test]
fn test_add_grapheme_string_and_col_index() {
let gs = grapheme_string(TEST_STR);
assert_eq!("📦".len(), 4);
#[allow(clippy::zero_prefixed_literal)]
let valid_indices = [
(col(00), seg_index(00), "H"),
(col(01), seg_index(01), "i"),
(col(02), seg_index(02), "📦"),
(col(03), seg_index(02), "📦"),
(col(04), seg_index(03), "X"),
(col(05), seg_index(04), "e"),
(col(06), seg_index(05), "l"),
(col(07), seg_index(06), "L"),
(col(08), seg_index(07), "o"),
(col(09), seg_index(08), "🙏🏽"),
(col(10), seg_index(08), "🙏🏽"),
(col(11), seg_index(09), "B"),
(col(12), seg_index(10), "y"),
(col(13), seg_index(11), "e"),
];
for (given_col_idx, exp_seg_idx, exp_str) in valid_indices {
let result = (&gs + given_col_idx).unwrap();
assert_eq!(result, exp_seg_idx);
assert_eq!(gs.get(result).unwrap().get_str(&gs), exp_str);
}
let out_of_bounds_indices = [14, 15, 16, 17];
for &index in &out_of_bounds_indices {
assert_eq!((&gs + col(index)), None);
}
}
#[test]
fn test_add_grapheme_string_and_byte_index() {
let gs = grapheme_string(TEST_STR);
assert_eq!("📦".len(), 4);
#[allow(clippy::zero_prefixed_literal)]
let valid_indices = [
(byte_index(00), seg_index(00), "H"),
(byte_index(01), seg_index(01), "i"),
(byte_index(02), seg_index(02), "📦"),
(byte_index(03), seg_index(02), "📦"),
(byte_index(04), seg_index(02), "📦"),
(byte_index(05), seg_index(02), "📦"),
(byte_index(06), seg_index(03), "X"),
(byte_index(07), seg_index(04), "e"),
(byte_index(08), seg_index(05), "l"),
(byte_index(09), seg_index(06), "L"),
(byte_index(10), seg_index(07), "o"),
(byte_index(11), seg_index(08), "🙏🏽"),
(byte_index(12), seg_index(08), "🙏🏽"),
(byte_index(13), seg_index(08), "🙏🏽"),
(byte_index(14), seg_index(08), "🙏🏽"),
(byte_index(15), seg_index(08), "🙏🏽"),
(byte_index(16), seg_index(08), "🙏🏽"),
(byte_index(17), seg_index(08), "🙏🏽"),
(byte_index(18), seg_index(08), "🙏🏽"),
(byte_index(19), seg_index(09), "B"),
(byte_index(20), seg_index(10), "y"),
(byte_index(21), seg_index(11), "e"),
];
for (given_byte_idx, exp_seg_idx, exp_str) in valid_indices {
let result = (&gs + given_byte_idx).unwrap();
assert_eq!(result, seg_index(exp_seg_idx));
assert_eq!(gs.get(result).unwrap().get_str(&gs), exp_str);
}
let out_of_bounds_indices = [22, 23, 24, 25];
for &index in &out_of_bounds_indices {
assert_eq!((&gs + byte_index(index)), None);
}
}
#[test]
fn test_add_grapheme_string_and_seg_index() {
let gs = grapheme_string(TEST_STR);
let test_cases = [
(seg_index(0), Some(col(0))),
(seg_index(1), Some(col(1))),
(seg_index(2), Some(col(2))),
(seg_index(3), Some(col(4))),
(seg_index(4), Some(col(5))),
(seg_index(5), Some(col(6))),
(seg_index(6), Some(col(7))),
(seg_index(7), Some(col(8))),
(seg_index(8), Some(col(9))),
(seg_index(9), Some(col(11))),
(seg_index(10), Some(col(12))),
(seg_index(11), Some(col(13))),
(seg_index(12), None),
(seg_index(13), None),
];
for (seg_idx, expected_col_idx) in test_cases {
let display_col_index = &gs + seg_idx;
assert_eq!(display_col_index, expected_col_idx);
}
}
#[test]
fn test_get_at_seg_index() {
let gs = grapheme_string(TEST_STR);
for (i, seg) in gs.seg_iter().enumerate() {
assert_eq!(gs.get(seg_index(i)), Some(*seg));
}
}
#[test]
fn test_unicode_width() {
assert_eq!(w("a"), width(1));
assert_eq!(w("😀"), width(2));
assert_eq!(w("a"), width(1));
assert_eq!(w("😀"), width(2));
assert_eq!(GCString::width_char('a'), width(1));
assert_eq!(GCString::width_char('😀'), width(2));
assert_eq!(GCString::width_char('\0'), width(0));
}
#[test]
fn test_contains_wide_segments() {
let test_cases = [
(TEST_STR, ContainsWideSegments::Yes),
("Foo📦Bar", ContainsWideSegments::Yes),
("FooBarBaz", ContainsWideSegments::No),
];
for (input, expected) in &test_cases {
let gs = grapheme_string(input);
assert_eq!(gs.contains_wide_segments(), *expected);
}
}
#[test]
fn test_len_and_fields() {
let gs = grapheme_string(TEST_STR);
assert_eq!(gs.len(), seg_width(12));
assert_eq!(gs.display_width, width(14));
assert!(!gs.is_empty());
let gs = grapheme_string("");
assert_eq!(gs.len(), seg_width(0));
assert_eq!(gs.display_width, width(0));
assert!(gs.is_empty());
let gs = grapheme_string("a");
println!("{gs:#?}");
assert_eq!(gs.len(), seg_width(1));
assert_eq!(gs.display_width, width(1));
assert!(!gs.is_empty());
}
}
#[cfg(test)]
mod bench {
extern crate test;
use test::Bencher;
use super::*;
#[bench]
fn bench_gc_string_new_ascii_short(b: &mut Bencher) {
let text = "Hello, world!";
b.iter(|| {
let _gs = GCString::new(text);
});
}
#[bench]
fn bench_gc_string_new_ascii_long(b: &mut Bencher) {
let text = "The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet.";
b.iter(|| {
let _gs = GCString::new(text);
});
}
#[bench]
fn bench_gc_string_new_unicode_simple(b: &mut Bencher) {
let text = "Hello, 世界! こんにちは";
b.iter(|| {
let _gs = GCString::new(text);
});
}
#[bench]
fn bench_gc_string_new_unicode_complex(b: &mut Bencher) {
let text = "Hi📦XelLo🙏🏽Bye";
b.iter(|| {
let _gs = GCString::new(text);
});
}
#[bench]
fn bench_gc_string_new_emoji_heavy(b: &mut Bencher) {
let text = "😀😃😄😁😆😅😂🤣😊😇🙂🙃😉😌😍🥰😘😗😙😚";
b.iter(|| {
let _gs = GCString::new(text);
});
}
#[bench]
fn bench_gc_string_new_log_message(b: &mut Bencher) {
let text = "main_event_loop → Startup 🎉";
b.iter(|| {
let _gs = GCString::new(text);
});
}
}