use crate::compat::String;
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(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");
}
}