use std::cmp::Ordering;
use std::fmt;
use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Copy, Clone)]
pub struct TruncatedStrView {
pub range: Option<TruncatedRange>,
available_space: isize,
}
#[derive(Debug, Copy, Clone)]
pub struct TruncatedRange {
pub start: usize,
pub end: usize,
pub showing_replacement_character: bool,
used_space: isize,
}
pub struct TruncatedStrSlice<'a, 'b> {
pub s: &'a str,
pub truncated_view: &'b TruncatedStrView,
}
#[derive(Clone, Debug)]
struct RangeAdjuster<'a> {
s: &'a str,
used_space: isize,
available_space: isize,
start: usize,
end: usize,
}
impl TruncatedRange {
fn adjuster<'a>(&self, s: &'a str, available_space: isize) -> RangeAdjuster<'a> {
let mut used_space = self.used_space;
if self.showing_replacement_character {
used_space -= 1;
}
RangeAdjuster {
s,
used_space,
available_space,
start: self.start,
end: self.end,
}
}
pub fn is_completely_elided(&self) -> bool {
self.used_space == 1 && self.start == self.end
}
pub fn is_truncated(&self, s: &str) -> bool {
self.start != 0 || self.end != s.len() || self.showing_replacement_character
}
pub fn print_leading_ellipsis(&self) -> bool {
self.start != 0
}
pub fn print_trailing_ellipsis(&self, s: &str) -> bool {
self.end != s.len()
}
}
impl TruncatedStrView {
pub fn can_str_fit_at_all(s: &str, available_space: isize) -> bool {
available_space > 0 || (available_space == 0 && s.is_empty())
}
pub fn init_start(s: &str, available_space: isize) -> TruncatedStrView {
if !Self::can_str_fit_at_all(s, available_space) {
return Self::init_no_view(available_space);
}
let mut adj = RangeAdjuster::init_start(s, available_space);
adj.fill_right();
adj.to_view()
}
pub fn init_back(s: &str, available_space: isize) -> TruncatedStrView {
if !Self::can_str_fit_at_all(s, available_space) {
return Self::init_no_view(available_space);
}
let mut adj = RangeAdjuster::init_back(s, available_space);
adj.fill_left();
adj.to_view()
}
fn init_no_view(available_space: isize) -> TruncatedStrView {
TruncatedStrView {
range: None,
available_space,
}
}
pub fn used_space(&self) -> Option<isize> {
self.range
.map(|TruncatedRange { used_space, .. }| used_space)
}
pub fn is_completely_elided(&self) -> bool {
self.range.map_or(false, |r| r.is_completely_elided())
}
pub fn any_contents_visible(&self) -> bool {
self.range.map_or(false, |r| !r.is_completely_elided())
}
fn range_adjuster<'a>(&self, s: &'a str) -> RangeAdjuster<'a> {
debug_assert!(self.range.is_some());
self.range.unwrap().adjuster(s, self.available_space)
}
pub fn scroll_right(&self, s: &str, count: usize) -> TruncatedStrView {
if self.range.is_none() {
return *self;
}
if self.available_space <= 2 {
return Self::init_back(s, self.available_space);
}
let mut adjuster = self.range_adjuster(s);
adjuster.expand_right(count);
adjuster.shrink_left_to_fit();
if adjuster.start != adjuster.end {
adjuster.fill_right();
}
adjuster.to_view()
}
pub fn scroll_left(&self, s: &str, count: usize) -> TruncatedStrView {
if self.range.is_none() {
return *self;
}
if self.available_space <= 2 {
return Self::init_start(s, self.available_space);
}
let mut adjuster = self.range_adjuster(s);
adjuster.expand_left(count);
adjuster.shrink_right_to_fit();
if adjuster.start != adjuster.end {
adjuster.fill_left();
}
adjuster.to_view()
}
pub fn jump_to_an_end(&self, s: &str) -> TruncatedStrView {
match self.range {
None => *self,
Some(range) => {
if range.end < s.len() {
TruncatedStrView::init_back(s, self.available_space)
} else {
TruncatedStrView::init_start(s, self.available_space)
}
}
}
}
pub fn resize(&self, s: &str, available_space: isize) -> TruncatedStrView {
if self.range.is_none() {
return TruncatedStrView::init_start(s, available_space);
}
match available_space.cmp(&self.available_space) {
Ordering::Less => {
if !Self::can_str_fit_at_all(s, available_space) {
Self::init_no_view(available_space)
} else {
self.shrink(s, available_space)
}
}
Ordering::Greater => self.expand(s, available_space),
Ordering::Equal => *self,
}
}
fn expand(&self, s: &str, available_space: isize) -> TruncatedStrView {
debug_assert!(available_space > self.available_space);
let mut adjuster = self.range_adjuster(s);
adjuster.available_space = available_space;
if adjuster.end == s.len() {
adjuster.fill_left();
} else {
adjuster.fill_right();
if adjuster.end == s.len() {
adjuster.fill_left();
}
}
adjuster.to_view()
}
fn shrink(&self, s: &str, available_space: isize) -> TruncatedStrView {
debug_assert!(available_space < self.available_space);
debug_assert!(self.range.is_some());
if available_space < 3 {
let TruncatedRange { start, end, .. } = self.range.unwrap();
if start > 0 && end == s.len() {
return Self::init_back(s, available_space);
} else {
return Self::init_start(s, available_space);
}
}
let mut adjuster = self.range_adjuster(s);
adjuster.available_space = available_space;
if adjuster.start > 0 && adjuster.end == s.len() {
adjuster.shrink_left_to_fit();
} else {
adjuster.shrink_right_to_fit();
}
adjuster.to_view()
}
pub fn focus(&self, s: &str, range: &Range<usize>) -> TruncatedStrView {
if self.range.is_none() {
return *self;
}
let Range { mut start, mut end } = *range;
while start != 0 && !s.is_char_boundary(start) {
start -= 1;
}
end = end.min(s.len());
let visible_range = self.range.unwrap();
if visible_range.start <= start && end <= visible_range.end {
return *self;
}
let mut adjuster = RangeAdjuster::init_at_index(s, self.available_space, start);
while adjuster.end < end && adjuster.used_space < self.available_space {
adjuster.expand_right(1);
}
adjuster.fill_from_both_sides();
adjuster.to_view()
}
}
impl<'a> RangeAdjuster<'a> {
pub fn init_start(s: &'a str, available_space: isize) -> Self {
RangeAdjuster::init_at_index(s, available_space, 0)
}
pub fn init_back(s: &'a str, available_space: isize) -> Self {
RangeAdjuster::init_at_index(s, available_space, s.len())
}
pub fn init_at_index(s: &'a str, available_space: isize, index: usize) -> Self {
let mut space_for_ellipses = 0;
if index > 0 {
space_for_ellipses += 1;
}
if index < s.len() {
space_for_ellipses += 1;
}
RangeAdjuster {
s,
used_space: space_for_ellipses,
available_space,
start: index,
end: index,
}
}
pub fn expand_right(&mut self, count: usize) {
let mut right_graphemes = self.s[self.end..].graphemes(true);
for _ in 0..count {
if let Some(grapheme) = right_graphemes.next() {
self.end += grapheme.len();
self.used_space += UnicodeWidthStr::width(grapheme) as isize;
if self.end == self.s.len() {
self.used_space -= 1;
}
} else {
break;
}
}
}
pub fn expand_left(&mut self, count: usize) {
let mut left_graphemes = self.s[..self.start].graphemes(true);
for _ in 0..count {
if let Some(grapheme) = left_graphemes.next_back() {
self.start -= grapheme.len();
self.used_space += UnicodeWidthStr::width(grapheme) as isize;
if self.start == 0 {
self.used_space -= 1;
}
} else {
break;
}
}
}
pub fn fill_right(&mut self) {
let right_graphemes = self.s[self.end..].graphemes(true);
for grapheme in right_graphemes {
if !self.add_grapheme_to_right_if_it_will_fit(grapheme) {
break;
}
}
}
fn add_grapheme_to_right_if_it_will_fit(&mut self, grapheme: &str) -> bool {
let new_end = self.end + grapheme.len();
let mut new_used_space = self.used_space + UnicodeWidthStr::width(grapheme) as isize;
if new_end == self.s.len() {
new_used_space -= 1;
}
if new_used_space > self.available_space {
return false;
}
self.end = new_end;
self.used_space = new_used_space;
true
}
pub fn fill_left(&mut self) {
let mut left_graphemes = self.s[..self.start].graphemes(true);
while let Some(grapheme) = left_graphemes.next_back() {
if !self.add_grapheme_to_left_if_it_will_fit(grapheme) {
break;
}
}
}
fn add_grapheme_to_left_if_it_will_fit(&mut self, grapheme: &str) -> bool {
let new_start = self.start - grapheme.len();
let mut new_used_space = self.used_space + UnicodeWidthStr::width(grapheme) as isize;
if new_start == 0 {
new_used_space -= 1;
}
if new_used_space > self.available_space {
return false;
}
self.start = new_start;
self.used_space = new_used_space;
true
}
pub fn fill_from_both_sides(&mut self) {
let mut left_graphemes = self.s[..self.start].graphemes(true);
let mut right_graphemes = self.s[self.end..].graphemes(true);
let mut width_added_to_left = 0;
let mut width_added_to_right = 0;
let mut more_on_left = true;
let mut more_on_right = true;
while self.used_space <= self.available_space {
let mut added_to_left = false;
let mut added_to_right = false;
while !more_on_left || width_added_to_right <= width_added_to_left {
if let Some(grapheme) = right_graphemes.next() {
let used_space_before = self.used_space;
if !self.add_grapheme_to_right_if_it_will_fit(grapheme) {
more_on_right = false;
break;
}
width_added_to_right += self.used_space - used_space_before;
added_to_right = true;
} else {
more_on_right = false;
break;
}
}
while !more_on_right || width_added_to_left < width_added_to_right {
if let Some(grapheme) = left_graphemes.next_back() {
let used_space_before = self.used_space;
if !self.add_grapheme_to_left_if_it_will_fit(grapheme) {
more_on_left = false;
break;
}
width_added_to_left += self.used_space - used_space_before;
added_to_left = true;
} else {
more_on_left = false;
break;
}
}
if !added_to_right && !added_to_left {
break;
}
}
}
pub fn shrink_right_to_fit(&mut self) {
let mut visible_graphemes = self.s[self.start..self.end].graphemes(true);
while self.used_space > self.available_space {
debug_assert!(self.start < self.end);
let rightmost_grapheme = visible_graphemes.next_back().unwrap();
if self.end == self.s.len() {
self.used_space += 1;
}
self.end -= rightmost_grapheme.len();
self.used_space -= UnicodeWidthStr::width(rightmost_grapheme) as isize;
}
}
pub fn shrink_left_to_fit(&mut self) {
let mut visible_graphemes = self.s[self.start..self.end].graphemes(true);
while self.used_space > self.available_space {
debug_assert!(self.start < self.end);
let leftmost_grapheme = visible_graphemes.next().unwrap();
if self.start == 0 {
self.used_space += 1;
}
self.start += leftmost_grapheme.len();
self.used_space -= UnicodeWidthStr::width(leftmost_grapheme) as isize;
}
}
pub fn to_view(&self) -> TruncatedStrView {
debug_assert!(TruncatedStrView::can_str_fit_at_all(
self.s,
self.available_space
));
let showing_replacement_character =
self.start == self.end &&
self.available_space > 1 &&
!self.s.is_empty();
let mut used_space = self.used_space;
if showing_replacement_character {
debug_assert!(used_space < self.available_space);
used_space += 1;
};
TruncatedStrView {
range: Some(TruncatedRange {
start: self.start,
end: self.end,
showing_replacement_character,
used_space,
}),
available_space: self.available_space,
}
}
}
impl<'a, 'b> fmt::Display for TruncatedStrSlice<'a, 'b> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.truncated_view.range.is_none() {
return Ok(());
}
let TruncatedRange {
start,
end,
showing_replacement_character,
..
} = self.truncated_view.range.unwrap();
if start != 0 {
f.write_str("…")?;
}
if showing_replacement_character {
f.write_str("�")?;
}
f.write_str(&self.s[start..end])?;
if end != self.s.len() {
f.write_str("…")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rendered(s: &str, truncated_view: &TruncatedStrView) -> String {
format!("{}", TruncatedStrSlice { s, truncated_view })
}
#[test]
fn test_init_start_and_init_back() {
#[track_caller]
fn assert_init_start(string: &str, space: isize, front: &str, used_space: Option<isize>) {
let init_state = TruncatedStrView::init_start(string, space);
assert_eq!(front, rendered(string, &init_state), "incorrect prefix");
assert_eq!(
used_space,
init_state.used_space(),
"incorrect prefix width"
);
}
#[track_caller]
fn assert_init_back(string: &str, space: isize, back: &str, used_space: Option<isize>) {
let init_state = TruncatedStrView::init_back(string, space);
assert_eq!(back, rendered(string, &init_state), "incorrect suffix");
assert_eq!(
used_space,
init_state.used_space(),
"incorrect suffix width"
);
}
#[track_caller]
fn assert_init_states(
string: &str,
space: isize,
front: &str,
back: &str,
used_space: Option<isize>,
) {
assert_init_start(string, space, front, used_space);
assert_init_back(string, space, back, used_space);
}
assert_init_states("abcde", -1, "", "", None);
assert_init_states("abcde", 0, "", "", None);
assert_init_states("", 0, "", "", Some(0));
assert_init_states("a", 1, "a", "a", Some(1));
assert_init_states("abc", 1, "…", "…", Some(1));
assert_init_states("🦀", 1, "…", "…", Some(1));
assert_init_states("abc", 2, "a…", "…c", Some(2));
assert_init_states("ab", 2, "ab", "ab", Some(2));
assert_init_states("🦀abc", 2, "�…", "…c", Some(2));
assert_init_states("abc🦀", 2, "a…", "…�", Some(2));
assert_init_states("abc", 3, "abc", "abc", Some(3));
assert_init_states("abcd", 3, "ab…", "…cd", Some(3));
assert_init_states("🦀🦀abc🦀🦀", 3, "🦀…", "…🦀", Some(3));
assert_init_states("🦀🦀abc🦀🦀", 5, "🦀🦀…", "…🦀🦀", Some(5));
assert_init_start("a🦀bc", 3, "a…", Some(2));
assert_init_back("a🦀bc", 3, "…bc", Some(3));
assert_init_start("ab🦀c", 3, "ab…", Some(3));
assert_init_back("ab🦀c", 3, "…c", Some(2));
}
#[test]
fn test_scroll_states() {
let s = "abcdef";
assert_scroll_states(s, 5, vec!["abcd…", "…cdef"]);
let s = "abcdefgh";
assert_scroll_states(s, 5, vec!["abcd…", "…cde…", "…def…", "…efgh"]);
let s = "🦀bcde";
assert_scroll_states(s, 5, vec!["🦀bc…", "…bcde"]);
let s = "🦀bcdef";
assert_scroll_states(s, 5, vec!["🦀bc…", "…bcd…", "…cdef"]);
let s = "abcd🦀efghi";
assert_scroll_states(s, 5, vec!["abcd…", "…d🦀…", "…🦀e…", "…efg…", "…fghi"]);
let s = "abc🦀def";
assert_scroll_states(s, 3, vec!["ab…", "…c…", "…�…", "…d…", "…ef"]);
let s = "🦀z";
assert_scroll_states(s, 2, vec!["�…", "…z"]);
let s = "a🦀";
assert_scroll_states(s, 2, vec!["a…", "…�"]);
}
#[track_caller]
fn assert_scroll_states(s: &str, available_space: isize, states: Vec<&str>) {
let mut curr_state = TruncatedStrView::init_start(s, available_space);
let mut prev_formatted = rendered(s, &curr_state);
assert_eq!(states[0], prev_formatted);
for expected_state in states.iter().skip(1) {
let next_state = curr_state.scroll_right(s, 1);
let formatted = rendered(s, &next_state);
assert_eq!(
expected_state, &formatted,
"expected scroll_right({}) to be {}",
&prev_formatted, &expected_state,
);
curr_state = next_state;
prev_formatted = formatted;
}
let mut curr_state = TruncatedStrView::init_back(s, available_space);
let mut prev_formatted = rendered(s, &curr_state);
assert_eq!(states.last().unwrap(), &prev_formatted);
for expected_state in states.iter().rev().skip(1) {
let next_state = curr_state.scroll_left(s, 1);
let formatted = rendered(s, &next_state);
assert_eq!(
expected_state, &formatted,
"expected scroll_left({}) to be {}",
&prev_formatted, &expected_state,
);
curr_state = next_state;
prev_formatted = formatted;
}
}
#[test]
fn test_expand() {
let s = "abcdefghij";
assert_expansions(
s,
TruncatedStrView::init_start(s, 5),
5,
vec![
"abcd…",
"abcde…",
"abcdef…",
"abcdefg…",
"abcdefgh…",
"abcdefghij",
],
);
let initial_state = TruncatedStrView::init_start(s, 5).scroll_right(s, 2);
assert_expansions(
s,
initial_state,
5,
vec![
"…def…",
"…defg…",
"…defgh…",
"…defghij",
"…cdefghij",
"abcdefghij",
],
);
let s = "a👍b👀c😱d";
assert_expansions(
s,
TruncatedStrView::init_start(s, 5),
5,
vec![
"a👍b…",
"a👍b…",
"a👍b👀…",
"a👍b👀c…",
"a👍b👀c…",
"a👍b👀c😱d",
],
);
let s = "a👍b👀c😱d";
assert_expansions(
s,
TruncatedStrView::init_start(s, 5).scroll_right(s, 2),
5,
vec![
"…👀c…",
"…👀c…",
"…👀c😱d",
"…b👀c😱d",
"…b👀c😱d",
"a👍b👀c😱d",
],
);
}
#[track_caller]
fn assert_expansions(
string: &str,
initial_state: TruncatedStrView,
mut available_space: isize,
states: Vec<&str>,
) {
let mut curr_state = initial_state;
let mut prev_formatted = rendered(string, &curr_state);
assert_eq!(states[0], prev_formatted);
for expansion in states.iter().skip(1) {
available_space += 1;
let next_state = curr_state.expand(string, available_space);
let formatted = rendered(string, &next_state);
assert_eq!(
expansion, &formatted,
"expected expand({}) to be {}",
&prev_formatted, &expansion,
);
curr_state = next_state;
prev_formatted = formatted;
}
}
#[test]
fn test_shrink() {
let s = "abcdefghij";
assert_shrinks(
s,
TruncatedStrView::init_start(s, 10),
10,
vec![
"abcdefghij",
"abcdefgh…",
"abcdefg…",
"abcdef…",
"abcde…",
"abcd…",
"abc…",
"ab…",
"a…",
"…",
],
);
assert_shrinks(
s,
TruncatedStrView::init_start(s, 9).scroll_right(s, 1),
9,
vec![
"…cdefghij",
"…defghij",
"…efghij",
"…fghij",
"…ghij",
"…hij",
"…ij",
"…j",
"…",
],
);
assert_shrinks(
s,
TruncatedStrView::init_start(s, 8).scroll_right(s, 1),
8,
vec![
"…cdefgh…",
"…cdefg…",
"…cdef…",
"…cde…",
"…cd…",
"…c…",
"a…",
],
);
let s = "ab👍c👀d😱efg";
assert_shrinks(
s,
TruncatedStrView::init_start(s, 11).scroll_right(s, 1),
11,
vec![
"…👍c👀d😱e…",
"…👍c👀d😱…",
"…👍c👀d…",
"…👍c👀d…",
"…👍c👀…",
"…👍c…",
"…👍c…",
"…👍…",
"…�…",
"a…",
],
);
let s = "🦀abc";
assert_shrinks(
s,
TruncatedStrView::init_start(s, 5),
5,
vec!["🦀abc", "🦀a…", "🦀…", "�…", "…"],
);
let s = "abc🦀";
assert_shrinks(
s,
TruncatedStrView::init_back(s, 4),
4,
vec!["…c🦀", "…🦀", "…�", "…"],
);
}
#[track_caller]
fn assert_shrinks(
string: &str,
initial_state: TruncatedStrView,
mut available_space: isize,
states: Vec<&str>,
) {
let mut curr_state = initial_state;
let mut prev_formatted = rendered(string, &curr_state);
assert_eq!(states[0], prev_formatted);
for shrunk in states.iter().skip(1) {
available_space -= 1;
let next_state = curr_state.shrink(string, available_space);
let formatted = rendered(string, &next_state);
assert_eq!(
shrunk, &formatted,
"expected shrink({}) to be {}",
&prev_formatted, &shrunk,
);
curr_state = next_state;
prev_formatted = formatted;
}
}
#[test]
fn test_focus() {
let s = "0123456789";
let tsv = TruncatedStrView::init_start(s, 10);
assert_focuses(
s,
tsv,
vec![
(&(0..1), "0123456789"),
(&(4..7), "0123456789"),
(&(8..12), "0123456789"),
],
);
let s = "0123456789abc";
let tsv = TruncatedStrView::init_start(s, 7).scroll_right(s, 2);
assert_focuses(
s,
tsv,
vec![
(&(0..1), "012345…"),
(&(3..4), "…34567…"),
(&(3..8), "…34567…"),
(&(7..8), "…34567…"),
(&(6..9), "…56789…"),
(&(2..9), "…23456…"),
(&(10..15), "…789abc"),
],
);
}
#[track_caller]
fn assert_focuses(
string: &str,
initial_state: TruncatedStrView,
ranges_and_expected: Vec<(&Range<usize>, &str)>,
) {
let initial_formatted = rendered(string, &initial_state);
for (i, (range, expected_focused)) in ranges_and_expected.into_iter().enumerate() {
let focused = initial_state.focus(string, range);
let formatted = rendered(string, &focused);
assert_eq!(
expected_focused, &formatted,
"Case {}: expected focus({}, {}..{}) to be {}",
i, &initial_formatted, range.start, range.end, &expected_focused,
);
}
}
}