pub fn prev_char_boundary(buffer: &str, cursor: usize) -> usize {
if cursor == 0 {
return 0;
}
let cursor = cursor.min(buffer.len());
let mut pos = cursor - 1;
while pos > 0 && !buffer.is_char_boundary(pos) {
pos -= 1;
}
pos
}
pub fn next_char_boundary(buffer: &str, cursor: usize) -> usize {
if cursor >= buffer.len() {
return buffer.len();
}
let mut pos = cursor + 1;
while pos < buffer.len() && !buffer.is_char_boundary(pos) {
pos += 1;
}
pos
}
pub fn delete_char_before(buffer: &mut String, cursor: usize) -> usize {
if cursor == 0 {
return 0;
}
let prev = prev_char_boundary(buffer, cursor);
buffer.replace_range(prev..cursor, "");
prev
}
pub fn delete_word_before(buffer: &mut String, cursor: usize) -> usize {
if cursor == 0 {
return 0;
}
let mut pos = cursor;
while pos > 0 {
let prev = prev_char_boundary(buffer, pos);
if let Some(ch) = buffer[prev..pos].chars().next()
&& !ch.is_whitespace()
{
break;
}
pos = prev;
}
while pos > 0 {
let prev = prev_char_boundary(buffer, pos);
if let Some(ch) = buffer[prev..pos].chars().next()
&& ch.is_whitespace()
{
break;
}
pos = prev;
}
buffer.replace_range(pos..cursor, "");
pos
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_return_zero_when_at_start() {
let s = "hello";
let result = prev_char_boundary(s, 0);
assert_eq!(result, 0);
}
#[test]
fn should_find_prev_boundary_for_ascii() {
let s = "hello";
assert_eq!(prev_char_boundary(s, 5), 4);
assert_eq!(prev_char_boundary(s, 3), 2);
assert_eq!(prev_char_boundary(s, 1), 0);
}
#[test]
fn should_find_prev_boundary_for_multibyte_char() {
let s = "좋아";
assert_eq!(s.len(), 6);
assert_eq!(prev_char_boundary(s, 6), 3); assert_eq!(prev_char_boundary(s, 3), 0); }
#[test]
fn should_find_prev_boundary_for_emoji() {
let s = "🦀";
assert_eq!(s.len(), 4);
assert_eq!(prev_char_boundary(s, 4), 0);
}
#[test]
fn should_find_prev_boundary_for_mixed_content() {
let s = "a좋b";
assert_eq!(prev_char_boundary(s, 5), 4); assert_eq!(prev_char_boundary(s, 4), 1); assert_eq!(prev_char_boundary(s, 1), 0); }
#[test]
fn should_return_len_when_at_end() {
let s = "hello";
let result = next_char_boundary(s, 5);
assert_eq!(result, 5);
}
#[test]
fn should_find_next_boundary_for_ascii() {
let s = "hello";
assert_eq!(next_char_boundary(s, 0), 1);
assert_eq!(next_char_boundary(s, 2), 3);
assert_eq!(next_char_boundary(s, 4), 5);
}
#[test]
fn should_find_next_boundary_for_multibyte_char() {
let s = "좋아";
assert_eq!(next_char_boundary(s, 0), 3); assert_eq!(next_char_boundary(s, 3), 6); }
#[test]
fn should_find_next_boundary_for_emoji() {
let s = "🦀";
assert_eq!(next_char_boundary(s, 0), 4);
}
#[test]
fn should_delete_ascii_char() {
let mut s = String::from("hello");
let cursor = delete_char_before(&mut s, 5);
assert_eq!(s, "hell");
assert_eq!(cursor, 4);
}
#[test]
fn should_delete_multibyte_char() {
let mut s = String::from("좋아");
let cursor = delete_char_before(&mut s, 6);
assert_eq!(s, "좋");
assert_eq!(cursor, 3);
}
#[test]
fn should_delete_multibyte_char_from_middle() {
let mut s = String::from("좋아요");
let cursor = delete_char_before(&mut s, 6);
assert_eq!(s, "좋요");
assert_eq!(cursor, 3);
}
#[test]
fn should_not_delete_when_at_start() {
let mut s = String::from("hello");
let cursor = delete_char_before(&mut s, 0);
assert_eq!(s, "hello");
assert_eq!(cursor, 0);
}
#[test]
fn should_delete_emoji() {
let mut s = String::from("hi🦀");
let cursor = delete_char_before(&mut s, 6);
assert_eq!(s, "hi");
assert_eq!(cursor, 2);
}
#[test]
fn should_delete_ascii_word() {
let mut s = String::from("hello world");
let cursor = delete_word_before(&mut s, 11);
assert_eq!(s, "hello ");
assert_eq!(cursor, 6);
}
#[test]
fn should_delete_multibyte_word() {
let mut s = String::from("안녕 아가브라");
let cursor = delete_word_before(&mut s, 19);
assert_eq!(s, "안녕 ");
assert_eq!(cursor, 7);
}
#[test]
fn should_skip_trailing_whitespace_when_deleting_word() {
let mut s = String::from("hello ");
let cursor = delete_word_before(&mut s, 8);
assert_eq!(s, "");
assert_eq!(cursor, 0);
}
#[test]
fn should_not_delete_word_when_at_start() {
let mut s = String::from("hello");
let cursor = delete_word_before(&mut s, 0);
assert_eq!(s, "hello");
assert_eq!(cursor, 0);
}
#[test]
fn should_navigate_multibyte_string_correctly() {
let s = "좋아요";
let mut cursor = 0;
cursor = next_char_boundary(s, cursor);
assert_eq!(cursor, 3);
cursor = next_char_boundary(s, cursor);
assert_eq!(cursor, 6);
cursor = next_char_boundary(s, cursor);
assert_eq!(cursor, 9);
cursor = prev_char_boundary(s, cursor);
assert_eq!(cursor, 6);
cursor = prev_char_boundary(s, cursor);
assert_eq!(cursor, 3);
cursor = prev_char_boundary(s, cursor);
assert_eq!(cursor, 0);
}
#[test]
fn should_handle_insert_delete_roundtrip() {
let mut s = String::new();
let mut cursor = 0;
for c in "좋아".chars() {
s.insert(cursor, c);
cursor += c.len_utf8();
}
assert_eq!(s, "좋아");
assert_eq!(cursor, 6);
cursor = delete_char_before(&mut s, cursor);
assert_eq!(s, "좋");
cursor = delete_char_before(&mut s, cursor);
assert_eq!(s, "");
assert_eq!(cursor, 0);
}
}