#[derive(Clone, Debug, Default)]
pub struct TextBuffer {
content: String,
chars: Vec<char>,
byte_indices: Vec<usize>,
cursor: usize,
selection_anchor: Option<usize>,
}
impl TextBuffer {
pub fn new() -> Self {
Self {
content: String::new(),
chars: Vec::new(),
byte_indices: vec![0],
cursor: 0,
selection_anchor: None,
}
}
pub fn with_content(text: impl Into<String>) -> Self {
let content = text.into();
let mut buffer = Self::new();
buffer.content = content.clone();
buffer.rebuild_cache();
buffer.cursor = buffer.chars.len();
buffer
}
fn rebuild_cache(&mut self) {
self.chars = self.content.chars().collect();
self.byte_indices = self.content.char_indices().map(|(i, _)| i).collect();
self.byte_indices.push(self.content.len());
}
#[inline]
pub fn text(&self) -> &str {
&self.content
}
#[inline]
pub fn cursor(&self) -> usize {
self.cursor
}
#[inline]
pub fn char_count(&self) -> usize {
self.chars.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.content.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.content.len()
}
#[inline]
pub fn char_to_byte(&self, char_idx: usize) -> usize {
self.byte_indices
.get(char_idx)
.copied()
.unwrap_or(self.content.len())
}
pub fn byte_to_char(&self, byte_idx: usize) -> usize {
self.byte_indices
.partition_point(|&i| i <= byte_idx)
.saturating_sub(1)
}
pub fn substring(&self, start: usize, end: usize) -> &str {
if start >= end {
return "";
}
let start_byte = self.char_to_byte(start);
let end_byte = self.char_to_byte(end);
&self.content[start_byte..end_byte]
}
pub fn char_at(&self, pos: usize) -> Option<char> {
self.chars.get(pos).copied()
}
pub fn insert_char(&mut self, ch: char) -> usize {
let byte_idx = self.char_to_byte(self.cursor);
self.content.insert(byte_idx, ch);
self.rebuild_cache();
self.cursor += 1;
self.cursor
}
pub fn insert_str(&mut self, s: &str) -> usize {
let byte_idx = self.char_to_byte(self.cursor);
self.content.insert_str(byte_idx, s);
self.rebuild_cache();
self.cursor += s.chars().count();
self.cursor
}
pub fn insert_char_at(&mut self, pos: usize, ch: char) -> usize {
let byte_idx = self.char_to_byte(pos);
self.content.insert(byte_idx, ch);
self.rebuild_cache();
pos + 1
}
pub fn insert_str_at(&mut self, pos: usize, s: &str) -> usize {
let byte_idx = self.char_to_byte(pos);
self.content.insert_str(byte_idx, s);
self.rebuild_cache();
pos + s.chars().count()
}
pub fn delete_char_before(&mut self) -> Option<char> {
if self.cursor == 0 {
return None;
}
self.cursor -= 1;
let ch = self.chars.get(self.cursor)?;
let byte_idx = self.byte_indices[self.cursor];
let byte_len = ch.len_utf8();
let ch_copy = *ch; self.content.drain(byte_idx..byte_idx + byte_len);
self.rebuild_cache();
Some(ch_copy)
}
pub fn delete_char_at(&mut self) -> Option<char> {
if self.cursor >= self.chars.len() {
return None;
}
let ch = self.chars.get(self.cursor)?;
let byte_idx = self.byte_indices[self.cursor];
let byte_len = ch.len_utf8();
let ch_copy = *ch; self.content.drain(byte_idx..byte_idx + byte_len);
self.rebuild_cache();
Some(ch_copy)
}
pub fn delete_range(&mut self, start: usize, end: usize) -> String {
let start_byte = self.char_to_byte(start);
let end_byte = self.char_to_byte(end);
let deleted: String = self.content.drain(start_byte..end_byte).collect();
self.rebuild_cache();
if self.cursor > end {
self.cursor -= end - start;
} else if self.cursor > start {
self.cursor = start;
}
deleted
}
pub fn set_content(&mut self, text: impl Into<String>) {
self.content = text.into();
self.rebuild_cache();
self.cursor = self.chars.len();
self.selection_anchor = None;
}
pub fn clear(&mut self) {
self.content.clear();
self.rebuild_cache();
self.cursor = 0;
self.selection_anchor = None;
}
pub fn set_cursor(&mut self, pos: usize) {
self.cursor = pos.min(self.char_count());
}
pub fn move_left(&mut self) -> bool {
if self.cursor > 0 {
self.cursor -= 1;
true
} else {
false
}
}
pub fn move_right(&mut self) -> bool {
if self.cursor < self.char_count() {
self.cursor += 1;
true
} else {
false
}
}
pub fn move_to_start(&mut self) {
self.cursor = 0;
}
pub fn move_to_end(&mut self) {
self.cursor = self.chars.len();
}
pub fn move_word_left(&mut self) {
if self.cursor == 0 {
return;
}
let byte_pos = self.char_to_byte(self.cursor);
let before_cursor = &self.content[..byte_pos];
let mut new_pos = self.cursor;
for ch in before_cursor.chars().rev() {
if ch.is_whitespace() {
new_pos -= 1;
} else {
break;
}
}
let byte_pos = self.char_to_byte(new_pos);
let before_new_pos = &self.content[..byte_pos];
for ch in before_new_pos.chars().rev() {
if !ch.is_whitespace() {
new_pos -= 1;
} else {
break;
}
}
self.cursor = new_pos;
}
pub fn move_word_right(&mut self) {
let char_len = self.chars.len();
if self.cursor >= char_len {
return;
}
let mut advance = 0;
for ch in self.chars.iter().skip(self.cursor) {
if !ch.is_whitespace() {
advance += 1;
} else {
break;
}
}
for ch in self.chars.iter().skip(self.cursor + advance) {
if ch.is_whitespace() {
advance += 1;
} else {
break;
}
}
self.cursor = (self.cursor + advance).min(char_len);
}
pub fn start_selection(&mut self) {
if self.selection_anchor.is_none() {
self.selection_anchor = Some(self.cursor);
}
}
pub fn clear_selection(&mut self) {
self.selection_anchor = None;
}
pub fn has_selection(&self) -> bool {
self.selection_anchor.is_some() && self.selection_anchor != Some(self.cursor)
}
pub fn selection(&self) -> Option<(usize, usize)> {
self.selection_anchor.map(|anchor| {
if anchor < self.cursor {
(anchor, self.cursor)
} else {
(self.cursor, anchor)
}
})
}
pub fn selected_text(&self) -> Option<&str> {
self.selection()
.map(|(start, end)| self.substring(start, end))
}
pub fn select_all(&mut self) {
self.selection_anchor = Some(0);
self.cursor = self.char_count();
}
pub fn delete_selection(&mut self) -> Option<String> {
if let Some((start, end)) = self.selection() {
let deleted = self.delete_range(start, end);
self.cursor = start;
self.selection_anchor = None;
Some(deleted)
} else {
None
}
}
pub fn delete_word_before(&mut self) -> String {
if self.cursor == 0 {
return String::new();
}
let end = self.cursor;
self.move_word_left();
let start = self.cursor;
self.delete_range(start, end)
}
pub fn delete_word_after(&mut self) -> String {
let start = self.cursor;
self.move_word_right();
let end = self.cursor;
self.cursor = start;
self.delete_range(start, end)
}
pub fn is_word_boundary(&self, pos: usize) -> bool {
if pos == 0 || pos >= self.chars.len() {
return true;
}
let prev = self.chars.get(pos - 1);
let curr = self.chars.get(pos);
match (prev, curr) {
(Some(p), Some(c)) => {
let p_word = !(*p).is_whitespace();
let c_word = !(*c).is_whitespace();
p_word != c_word
}
_ => true,
}
}
pub fn word_at_cursor(&self) -> (usize, usize) {
if self.is_empty() {
return (0, 0);
}
let char_len = self.chars.len();
let mut start = self.cursor.min(char_len.saturating_sub(1));
let mut end = start;
while start > 0 {
if let Some(ch) = self.chars.get(start - 1) {
if ch.is_whitespace() {
break;
}
start -= 1;
} else {
break;
}
}
while end < char_len {
if let Some(ch) = self.chars.get(end) {
if ch.is_whitespace() {
break;
}
end += 1;
} else {
break;
}
}
(start, end)
}
pub fn select_word(&mut self) {
let (start, end) = self.word_at_cursor();
self.selection_anchor = Some(start);
self.cursor = end;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let buf = TextBuffer::new();
assert!(buf.is_empty());
assert_eq!(buf.cursor(), 0);
assert_eq!(buf.char_count(), 0);
}
#[test]
fn test_with_content() {
let buf = TextBuffer::with_content("Hello");
assert_eq!(buf.text(), "Hello");
assert_eq!(buf.cursor(), 5);
assert_eq!(buf.char_count(), 5);
}
#[test]
fn test_utf8_emoji() {
let buf = TextBuffer::with_content("Hi 🎉!");
assert_eq!(buf.char_count(), 5); assert_eq!(buf.char_at(3), Some('🎉'));
assert_eq!(buf.substring(0, 2), "Hi");
assert_eq!(buf.substring(3, 4), "🎉");
}
#[test]
fn test_utf8_korean() {
let buf = TextBuffer::with_content("안녕하세요");
assert_eq!(buf.char_count(), 5);
assert_eq!(buf.char_at(0), Some('안'));
assert_eq!(buf.substring(1, 3), "녕하");
}
#[test]
fn test_char_to_byte() {
let buf = TextBuffer::with_content("A🎉B");
assert_eq!(buf.char_to_byte(0), 0); assert_eq!(buf.char_to_byte(1), 1); assert_eq!(buf.char_to_byte(2), 5); assert_eq!(buf.char_to_byte(3), 6); assert_eq!(buf.char_to_byte(100), 6); }
#[test]
fn test_insert_char() {
let mut buf = TextBuffer::new();
buf.insert_char('H');
buf.insert_char('i');
assert_eq!(buf.text(), "Hi");
assert_eq!(buf.cursor(), 2);
}
#[test]
fn test_insert_str() {
let mut buf = TextBuffer::new();
buf.insert_str("Hello");
assert_eq!(buf.text(), "Hello");
assert_eq!(buf.cursor(), 5);
buf.set_cursor(0);
buf.insert_str("Say ");
assert_eq!(buf.text(), "Say Hello");
}
#[test]
fn test_insert_emoji() {
let mut buf = TextBuffer::new();
buf.insert_str("Hi ");
buf.insert_char('🎉');
assert_eq!(buf.text(), "Hi 🎉");
assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_delete_char_before() {
let mut buf = TextBuffer::with_content("Hello");
let deleted = buf.delete_char_before();
assert_eq!(deleted, Some('o'));
assert_eq!(buf.text(), "Hell");
assert_eq!(buf.cursor(), 4);
}
#[test]
fn test_delete_char_before_emoji() {
let mut buf = TextBuffer::with_content("Hi🎉");
let deleted = buf.delete_char_before();
assert_eq!(deleted, Some('🎉'));
assert_eq!(buf.text(), "Hi");
assert_eq!(buf.cursor(), 2);
}
#[test]
fn test_delete_char_at() {
let mut buf = TextBuffer::with_content("Hello");
buf.set_cursor(1);
let deleted = buf.delete_char_at();
assert_eq!(deleted, Some('e'));
assert_eq!(buf.text(), "Hllo");
}
#[test]
fn test_delete_range() {
let mut buf = TextBuffer::with_content("Hello World");
let deleted = buf.delete_range(0, 6);
assert_eq!(deleted, "Hello ");
assert_eq!(buf.text(), "World");
}
#[test]
fn test_move_left_right() {
let mut buf = TextBuffer::with_content("Hello");
buf.set_cursor(3);
assert!(buf.move_left());
assert_eq!(buf.cursor(), 2);
assert!(buf.move_right());
assert_eq!(buf.cursor(), 3);
buf.set_cursor(0);
assert!(!buf.move_left());
buf.set_cursor(5);
assert!(!buf.move_right()); }
#[test]
fn test_move_word() {
let mut buf = TextBuffer::with_content("hello world test");
buf.set_cursor(0);
buf.move_word_right();
assert_eq!(buf.cursor(), 6);
buf.move_word_right();
assert_eq!(buf.cursor(), 12);
buf.move_word_left();
assert_eq!(buf.cursor(), 6);
buf.move_word_left();
assert_eq!(buf.cursor(), 0);
}
#[test]
fn test_selection() {
let mut buf = TextBuffer::with_content("Hello World");
buf.set_cursor(0);
buf.start_selection();
buf.set_cursor(5);
assert!(buf.has_selection());
assert_eq!(buf.selection(), Some((0, 5)));
assert_eq!(buf.selected_text(), Some("Hello"));
}
#[test]
fn test_selection_reverse() {
let mut buf = TextBuffer::with_content("Hello World");
buf.set_cursor(5);
buf.start_selection();
buf.set_cursor(0);
assert!(buf.has_selection());
assert_eq!(buf.selection(), Some((0, 5))); assert_eq!(buf.selected_text(), Some("Hello"));
}
#[test]
fn test_select_all() {
let mut buf = TextBuffer::with_content("Hello");
buf.select_all();
assert!(buf.has_selection());
assert_eq!(buf.selection(), Some((0, 5)));
assert_eq!(buf.selected_text(), Some("Hello"));
}
#[test]
fn test_delete_selection() {
let mut buf = TextBuffer::with_content("Hello World");
buf.set_cursor(0);
buf.start_selection();
buf.set_cursor(6);
let deleted = buf.delete_selection();
assert_eq!(deleted, Some("Hello ".to_string()));
assert_eq!(buf.text(), "World");
assert_eq!(buf.cursor(), 0);
}
#[test]
fn test_delete_word_before() {
let mut buf = TextBuffer::with_content("hello world");
buf.set_cursor(11);
let deleted = buf.delete_word_before();
assert_eq!(deleted, "world");
assert_eq!(buf.text(), "hello ");
}
#[test]
fn test_word_at_cursor() {
let buf = TextBuffer::with_content("hello world");
let mut buf2 = buf.clone();
buf2.set_cursor(2);
assert_eq!(buf2.word_at_cursor(), (0, 5));
let mut buf3 = buf.clone();
buf3.set_cursor(7);
assert_eq!(buf3.word_at_cursor(), (6, 11)); }
#[test]
fn test_select_word() {
let mut buf = TextBuffer::with_content("hello world");
buf.set_cursor(2);
buf.select_word();
assert_eq!(buf.selection(), Some((0, 5)));
assert_eq!(buf.selected_text(), Some("hello"));
}
#[test]
fn test_empty_operations() {
let mut buf = TextBuffer::new();
assert_eq!(buf.delete_char_before(), None);
assert_eq!(buf.delete_char_at(), None);
assert!(!buf.move_left());
assert!(!buf.move_right());
assert!(!buf.has_selection());
}
#[test]
fn test_set_cursor_clamped() {
let mut buf = TextBuffer::with_content("Hello");
buf.set_cursor(100);
assert_eq!(buf.cursor(), 5); }
#[test]
fn test_clear() {
let mut buf = TextBuffer::with_content("Hello");
buf.start_selection();
buf.clear();
assert!(buf.is_empty());
assert_eq!(buf.cursor(), 0);
assert!(!buf.has_selection());
}
}