use core::ops::Range;
use crate::compat::{Cow, String, Vec};
const ELLIPSIS: &str = "\u{2026}";
#[cfg(widgets_unstripped)]
pub(crate) fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return true;
}
if !needle.is_ascii() {
return haystack.to_lowercase().contains(needle);
}
let needle = needle.as_bytes();
let haystack = haystack.as_bytes();
needle.len() <= haystack.len()
&& haystack.windows(needle.len()).any(|window| window.eq_ignore_ascii_case(needle))
}
#[cfg(widgets_unstripped)]
pub(crate) fn starts_with_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return true;
}
if !needle.is_ascii() {
return haystack.to_lowercase().starts_with(needle);
}
let needle = needle.as_bytes();
match haystack.as_bytes().get(..needle.len()) {
Some(prefix) => prefix.eq_ignore_ascii_case(needle),
None => false,
}
}
pub fn floor_char_boundary(text: &str, index: usize) -> usize {
let len = text.len();
if index >= len {
return len;
}
let bytes = text.as_bytes();
let mut boundary = index;
while boundary > 0 && bytes[boundary] & 0xC0 == 0x80 {
boundary -= 1;
}
boundary
}
pub fn byte_index_of_char(text: &str, count: usize) -> usize {
match text.char_indices().nth(count) {
Some((index, _)) => index,
None => text.len(),
}
}
pub fn previous_char_boundary(text: &str, offset: usize) -> usize {
let boundary = floor_char_boundary(text, offset);
if boundary == 0 {
return 0;
}
floor_char_boundary(text, boundary - 1)
}
pub fn next_char_boundary(text: &str, offset: usize) -> usize {
let boundary = floor_char_boundary(text, offset);
match text[boundary..].chars().next() {
Some(ch) => boundary + ch.len_utf8(),
None => text.len(),
}
}
pub fn elide_end(text: &str, max_chars: usize) -> Cow<'_, str> {
let char_count = text.chars().count();
if char_count <= max_chars {
return Cow::Borrowed(text);
}
if max_chars <= 1 {
return Cow::Borrowed(ELLIPSIS);
}
let keep = max_chars - 1;
let mut out = String::with_capacity(text.len().min(max_chars.saturating_mul(4)));
out.extend(text.chars().take(keep));
out.push('\u{2026}');
Cow::Owned(out)
}
pub fn elide_middle(text: &str, max_chars: usize) -> Cow<'_, str> {
let char_count = text.chars().count();
if char_count <= max_chars {
return Cow::Borrowed(text);
}
if max_chars <= 1 {
return Cow::Borrowed(ELLIPSIS);
}
let keep = max_chars - 1;
let head = keep.div_ceil(2);
let tail = keep - head;
let head_end = byte_index_of_char(text, head);
let tail_start = byte_index_of_char(text, char_count - tail);
let mut out = String::with_capacity(text.len().min(max_chars.saturating_mul(4)));
out.push_str(&text[..head_end]);
out.push('\u{2026}');
out.push_str(&text[tail_start..]);
Cow::Owned(out)
}
impl ElideMode {
pub const fn removes_text(self) -> bool {
!matches!(self, Self::None)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ElideMode {
#[default]
End,
Middle,
Start,
None,
}
pub fn elide(text: &str, max_chars: usize, mode: ElideMode) -> Cow<'_, str> {
match mode {
ElideMode::End => elide_end(text, max_chars),
ElideMode::Middle => elide_middle(text, max_chars),
ElideMode::Start => elide_start(text, max_chars),
ElideMode::None => Cow::Borrowed(text),
}
}
pub fn elide_start(text: &str, max_chars: usize) -> Cow<'_, str> {
let char_count = text.chars().count();
if char_count <= max_chars {
return Cow::Borrowed(text);
}
if max_chars <= 1 {
return Cow::Borrowed(ELLIPSIS);
}
let keep = max_chars - 1;
let start = byte_index_of_char(text, char_count - keep);
let mut out = String::with_capacity(text.len().min(max_chars.saturating_mul(4)));
out.push('\u{2026}');
out.push_str(&text[start..]);
Cow::Owned(out)
}
pub fn elide_middle_to_width<'a>(
text: &'a str,
max_width: u32,
measure: &mut dyn FnMut(&str) -> u32,
) -> Cow<'a, str> {
if measure(text) <= max_width {
return Cow::Borrowed(text);
}
if max_width == 0 || measure(ELLIPSIS) > max_width {
return Cow::Borrowed("");
}
let char_count = text.chars().count();
let mut low = 0usize;
let mut high = char_count;
while low < high {
let mid = (low + high).div_ceil(2);
let head_chars = mid.div_ceil(2);
let tail_chars = mid - head_chars;
let head_end = byte_index_of_char(text, head_chars);
let tail_start = byte_index_of_char(text, char_count - tail_chars);
let mut candidate = String::with_capacity(head_end + (text.len() - tail_start) + 3);
candidate.push_str(&text[..head_end]);
candidate.push('\u{2026}');
candidate.push_str(&text[tail_start..]);
if measure(&candidate) <= max_width {
low = mid;
} else {
high = mid - 1;
}
}
if low == char_count {
return Cow::Borrowed(text);
}
let head_chars = low.div_ceil(2);
let tail_chars = low - head_chars;
let head_end = byte_index_of_char(text, head_chars);
let tail_start = byte_index_of_char(text, char_count - tail_chars);
let mut out = String::with_capacity(head_end + (text.len() - tail_start) + 3);
out.push_str(&text[..head_end]);
out.push('\u{2026}');
out.push_str(&text[tail_start..]);
Cow::Owned(out)
}
pub fn elide_to_width<'a>(
text: &'a str,
max_width: u32,
measure: &mut dyn FnMut(&str) -> u32,
) -> Cow<'a, str> {
if measure(text) <= max_width {
return Cow::Borrowed(text);
}
if max_width == 0 || measure(ELLIPSIS) > max_width {
return Cow::Borrowed("");
}
let char_count = text.chars().count();
let mut low = 0usize;
let mut high = char_count;
while low < high {
let mid = (low + high).div_ceil(2);
let end = byte_index_of_char(text, mid);
let mut candidate = String::with_capacity(end + ELLIPSIS.len());
candidate.push_str(&text[..end]);
candidate.push('\u{2026}');
if measure(&candidate) <= max_width {
low = mid;
} else {
high = mid - 1;
}
}
if low == char_count {
return Cow::Borrowed(text);
}
let end = byte_index_of_char(text, low);
let mut out = String::with_capacity(end + ELLIPSIS.len());
out.push_str(&text[..end]);
out.push('\u{2026}');
Cow::Owned(out)
}
pub fn wrap_char_ranges(text: &str, max_chars: usize) -> Vec<Range<usize>> {
let mut out = Vec::new();
if max_chars == 0 {
out.push(0..text.len());
return out;
}
let mut paragraph_start = 0usize;
for paragraph in text.split('\n') {
if paragraph.is_empty() {
out.push(paragraph_start..paragraph_start);
} else {
let mut line_start = 0usize;
let mut count = 0usize;
for (index, ch) in paragraph.char_indices() {
count += 1;
if count == max_chars {
let end = index + ch.len_utf8();
out.push(paragraph_start + line_start..paragraph_start + end);
line_start = end;
count = 0;
}
}
if line_start < paragraph.len() {
out.push(paragraph_start + line_start..paragraph_start + paragraph.len());
}
}
paragraph_start += paragraph.len() + 1;
}
out
}
pub fn wrap_to_width<'a>(
text: &'a str,
max_width: u32,
measure: &mut dyn FnMut(&str) -> u32,
) -> Vec<Range<usize>> {
let mut out = Vec::new();
if max_width == 0 {
out.push(0..text.len());
return out;
}
let mut paragraph_base = 0usize;
for paragraph in text.split('\n') {
if paragraph.is_empty() {
out.push(paragraph_base..paragraph_base);
} else {
wrap_paragraph(paragraph, paragraph_base, max_width, measure, &mut out);
}
paragraph_base += paragraph.len() + 1;
}
out
}
fn wrap_paragraph(
paragraph: &str,
paragraph_base: usize,
max_width: u32,
measure: &mut dyn FnMut(&str) -> u32,
out: &mut Vec<Range<usize>>,
) {
let mut line_start: Option<usize> = None;
let mut line_end = 0usize;
for word in word_ranges(paragraph) {
let (word_start, word_end) = (word.start, word.end);
if let Some(start) = line_start {
if measure(¶graph[start..word_end]) <= max_width {
line_end = word_end;
continue;
}
out.push(paragraph_base + start..paragraph_base + line_end);
}
if measure(¶graph[word_start..word_end]) <= max_width {
line_start = Some(word_start);
line_end = word_end;
continue;
}
let mut chunk_start = word_start;
let mut chunk_end = word_start;
for (offset, ch) in paragraph[word_start..word_end].char_indices() {
let end = word_start + offset + ch.len_utf8();
if chunk_end > chunk_start && measure(¶graph[chunk_start..end]) > max_width {
out.push(paragraph_base + chunk_start..paragraph_base + chunk_end);
chunk_start = word_start + offset;
}
chunk_end = end;
}
line_start = Some(chunk_start);
line_end = chunk_end;
}
if let Some(start) = line_start {
out.push(paragraph_base + start..paragraph_base + line_end);
}
}
fn word_ranges(text: &str) -> Vec<Range<usize>> {
let bytes = text.as_bytes();
let mut words = Vec::new();
let mut cursor = 0usize;
while cursor < bytes.len() {
while cursor < bytes.len() && bytes[cursor] == b' ' {
cursor += 1;
}
let start = cursor;
while cursor < bytes.len() && bytes[cursor] != b' ' {
cursor += 1;
}
if start < cursor {
words.push(start..cursor);
}
}
words
}
pub(crate) fn replace_selected_range(
text: &mut String,
selection: Option<(usize, usize)>,
) -> Option<usize> {
let (start, end) = selection?;
let len = text.len();
let start = floor_char_boundary(text, start.min(len));
let end = floor_char_boundary(text, end.min(len));
let (start, end) = if start < end { (start, end) } else { (end, start) };
text.replace_range(start..end, "");
Some(start)
}
pub(crate) fn first_visible_row_for(
caret_row: usize,
first_visible: usize,
visible: usize,
) -> usize {
if visible == 0 {
return first_visible;
}
if caret_row < first_visible {
caret_row
} else if caret_row >= first_visible + visible {
caret_row + 1 - visible
} else {
first_visible
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::Vec;
#[test]
fn ascii_boundaries_are_identity() {
let text = "hello";
for i in 0..=text.len() {
assert_eq!(floor_char_boundary(text, i), i);
}
}
#[test]
fn index_past_the_end_clamps_to_the_length() {
assert_eq!(floor_char_boundary("hello", 5), 5);
assert_eq!(floor_char_boundary("hello", 99), 5);
assert_eq!(floor_char_boundary("", 0), 0);
assert_eq!(floor_char_boundary("", 7), 0);
}
#[test]
fn interior_indices_land_on_a_real_boundary() {
let text = "aé世b";
assert_eq!(text.len(), 7);
for i in 0..=text.len() {
let boundary = floor_char_boundary(text, i);
assert!(boundary <= i, "must round down, got {boundary} for {i}");
assert!(text.is_char_boundary(boundary));
}
assert_eq!(floor_char_boundary(text, 2), 1); assert_eq!(floor_char_boundary(text, 3), 3); assert_eq!(floor_char_boundary(text, 5), 3); assert_eq!(floor_char_boundary(text, 6), 6); }
#[test]
fn astral_characters_are_not_split() {
let text = "🎉";
assert_eq!(text.len(), 4);
for i in 0..4 {
assert_eq!(floor_char_boundary(text, i), 0, "offset {i} is inside the emoji");
}
assert_eq!(floor_char_boundary(text, 4), 4);
}
#[test]
fn every_prefix_of_a_multibyte_string_is_sliceable() {
let text = "héllo wörld 世界 🎉";
for i in 0..=text.len() + 3 {
let _ = &text[..floor_char_boundary(text, i)];
}
}
#[test]
fn character_stepping_visits_every_boundary_exactly_once() {
let text = "aé世🎉b";
let boundaries: Vec<usize> =
text.char_indices().map(|(index, _)| index).chain([text.len()]).collect();
assert_eq!(boundaries, vec![0, 1, 3, 6, 10, 11], "the fixture's boundaries");
for pair in boundaries.windows(2) {
assert_eq!(next_char_boundary(text, pair[0]), pair[1], "forward from {}", pair[0]);
assert_eq!(previous_char_boundary(text, pair[1]), pair[0], "back from {}", pair[1]);
}
assert_eq!(previous_char_boundary(text, 0), 0);
assert_eq!(next_char_boundary(text, text.len()), text.len());
assert_eq!(next_char_boundary(text, text.len() + 99), text.len());
assert_eq!(previous_char_boundary(text, text.len() + 99), 10);
assert_eq!(previous_char_boundary(text, 2), 0, "offset 2 is inside 'é'");
assert_eq!(next_char_boundary(text, 2), 3, "and stepping forward leaves that character");
assert_eq!(previous_char_boundary("", 0), 0);
assert_eq!(next_char_boundary("", 0), 0);
}
#[test]
fn removing_a_selection_returns_the_caret_and_respects_boundaries() {
let mut text = String::from("aé世b");
assert_eq!(replace_selected_range(&mut text, Some((6, 1))), Some(1));
assert_eq!(text, "ab");
let mut untouched = String::from("abc");
assert_eq!(replace_selected_range(&mut untouched, None), None);
assert_eq!(untouched, "abc", "no selection leaves the value alone");
}
#[test]
fn the_caret_row_is_brought_into_the_viewport() {
assert_eq!(first_visible_row_for(2, 5, 3), 2, "above the viewport scrolls up");
assert_eq!(first_visible_row_for(9, 5, 3), 7, "below it scrolls just enough");
assert_eq!(first_visible_row_for(6, 5, 3), 5, "inside it leaves the origin alone");
assert_eq!(first_visible_row_for(6, 5, 0), 5, "a zero-height viewport keeps the origin");
}
#[test]
fn end_elision_respects_the_character_budget() {
assert!(matches!(elide_end("hello", 5), Cow::Borrowed("hello")));
assert_eq!(elide_end("hello world", 8), "hello w\u{2026}");
assert_eq!(elide_end("hello", 1), "\u{2026}");
assert_eq!(elide_end("hello", 0), "\u{2026}");
assert_eq!(
elide_end("\u{4e16}\u{754c}\u{4f60}\u{597d}\u{4e16}\u{754c}\u{4f60}", 5),
"\u{4e16}\u{754c}\u{4f60}\u{597d}\u{2026}"
);
assert_eq!(elide_end("\u{e9}\u{4e16}\u{1f389}", 2), "\u{e9}\u{2026}");
}
#[test]
fn middle_elision_keeps_both_ends() {
assert!(matches!(elide_middle("abc", 3), Cow::Borrowed("abc")));
assert_eq!(elide_middle("abcdefghij", 6), "abc\u{2026}ij");
assert_eq!(elide_middle("abcdefghij", 5), "ab\u{2026}ij");
assert_eq!(elide_middle("abc", 1), "\u{2026}");
assert_eq!(
elide_middle("/Users/alice/Desktop/workspace/file.rs", 14),
"/Users/\u{2026}ile.rs"
);
}
#[test]
fn width_elision_finds_the_largest_fitting_prefix() {
let mut measure = |value: &str| value.chars().count() as u32;
assert_eq!(elide_to_width("abcdef", 6, &mut measure), "abcdef");
assert_eq!(elide_to_width("abcdef", 4, &mut measure), "abc\u{2026}");
assert_eq!(elide_to_width("abcdef", 1, &mut measure), "\u{2026}");
assert_eq!(elide_to_width("abcdef", 0, &mut measure), "");
assert_eq!(
elide_to_width("\u{4e16}\u{754c}\u{4f60}\u{597d}", 3, &mut measure),
"\u{4e16}\u{754c}\u{2026}"
);
}
#[test]
fn wrapping_returns_byte_ranges_on_character_boundaries() {
assert_eq!(wrap_char_ranges("abcdef", 3), vec![0..3, 3..6]);
assert_eq!(wrap_char_ranges("ab\ncd", 2), vec![0..2, 3..5]);
assert_eq!(wrap_char_ranges("ab\n", 2), vec![0..2, 3..3]);
assert_eq!(wrap_char_ranges("", 4), vec![0..0]);
assert_eq!(wrap_char_ranges("a\u{e9}\u{4e16}\u{1f389}b", 2), vec![0..3, 3..10, 10..11]);
assert_eq!(wrap_char_ranges("ab\ncd", 0), vec![0..5]);
let ranges = wrap_char_ranges("\u{4e16}\u{754c}\u{4f60}\u{597d}", 2);
for range in ranges {
assert!("\u{4e16}\u{754c}\u{4f60}\u{597d}".is_char_boundary(range.start));
assert!("\u{4e16}\u{754c}\u{4f60}\u{597d}".is_char_boundary(range.end));
}
}
#[test]
fn start_elision_keeps_the_tail() {
assert!(matches!(elide_start("hello", 5), Cow::Borrowed("hello")));
assert_eq!(elide_start("hello world", 8), "\u{2026}o world");
assert_eq!(elide_start("hello", 1), "\u{2026}");
assert_eq!(elide_start("hello", 0), "\u{2026}");
assert_eq!(elide_start("a\u{e9}\u{4e16}\u{1f389}", 2), "\u{2026}\u{1f389}");
}
#[test]
fn the_elide_mode_dispatches_to_distinct_endings() {
let long = "abcdefghij";
assert_eq!(elide(long, 4, ElideMode::End), "abc\u{2026}");
assert_eq!(elide(long, 4, ElideMode::Middle), "ab\u{2026}j");
assert_eq!(elide(long, 4, ElideMode::Start), "\u{2026}hij");
assert!(matches!(elide(long, 4, ElideMode::None), Cow::Borrowed(value) if value == long));
assert!(ElideMode::End.removes_text());
assert!(ElideMode::Start.removes_text());
assert!(!ElideMode::None.removes_text());
}
#[test]
fn middle_width_elision_keeps_the_head_and_the_tail() {
let mut measure = |value: &str| value.chars().count() as u32;
assert_eq!(elide_middle_to_width("abcdef", 6, &mut measure), "abcdef");
assert_eq!(elide_middle_to_width("abcdef", 4, &mut measure), "ab\u{2026}f");
assert_eq!(elide_middle_to_width("abcdef", 1, &mut measure), "\u{2026}");
assert_eq!(elide_middle_to_width("abcdef", 0, &mut measure), "");
}
#[test]
fn width_wrapping_breaks_at_word_boundaries() {
let mut measure = |value: &str| value.chars().count() as u32;
let lines = |text: &str, width: u32, measure: &mut dyn FnMut(&str) -> u32| {
wrap_to_width(text, width, measure)
.into_iter()
.map(|range| String::from(&text[range]))
.collect::<Vec<_>>()
};
assert_eq!(
lines("one two three", 7, &mut measure),
vec!["one two", "three"],
"a word moves whole rather than being split at the break"
);
assert_eq!(lines("ab\ncd", 10, &mut measure), vec!["ab", "cd"], "explicit breaks survive");
assert_eq!(
lines("ab\n", 10, &mut measure),
vec!["ab", ""],
"a blank trailing line is kept"
);
assert_eq!(lines("", 10, &mut measure), vec![""], "an empty value is one empty line");
assert_eq!(wrap_to_width("ab\ncd", 0, &mut measure), vec![0..5], "zero means no wrapping");
assert_eq!(lines("a b", 10, &mut measure), vec!["a b"], "inner spaces are preserved");
for range in wrap_to_width("\u{4e16}\u{754c} \u{4f60}\u{597d}", 4, &mut measure) {
assert!("\u{4e16}\u{754c} \u{4f60}\u{597d}".is_char_boundary(range.start));
assert!("\u{4e16}\u{754c} \u{4f60}\u{597d}".is_char_boundary(range.end));
}
}
#[test]
fn an_over_long_word_is_split_on_character_boundaries() {
let mut measure = |value: &str| value.chars().count() as u32;
let text = "abcdefgh";
let lines: Vec<&str> =
wrap_to_width(text, 3, &mut measure).into_iter().map(|range| &text[range]).collect();
assert_eq!(lines, vec!["abc", "def", "gh"]);
let text = "\u{4e16}\u{754c}\u{4f60}\u{597d}";
let ranges = wrap_to_width(text, 2, &mut measure);
for range in &ranges {
assert!(text.is_char_boundary(range.start));
assert!(text.is_char_boundary(range.end));
assert!(range.end > range.start, "each line makes progress");
}
assert_eq!(ranges.len(), 2, "four characters at two per line is two lines");
}
}