#![forbid(unsafe_code)]
use crate::cursor::{CursorNavigator, CursorPosition};
use crate::rope::Rope;
#[derive(Debug, Clone)]
enum EditOp {
Insert {
byte_offset: usize,
text: String,
},
Delete {
byte_offset: usize,
text: String,
},
Replace {
byte_offset: usize,
deleted: String,
inserted: String,
},
}
impl EditOp {
fn inverse(&self) -> Self {
match self {
Self::Insert { byte_offset, text } => Self::Delete {
byte_offset: *byte_offset,
text: text.clone(),
},
Self::Delete { byte_offset, text } => Self::Insert {
byte_offset: *byte_offset,
text: text.clone(),
},
Self::Replace {
byte_offset,
deleted,
inserted,
} => Self::Replace {
byte_offset: *byte_offset,
deleted: inserted.clone(),
inserted: deleted.clone(),
},
}
}
fn byte_len(&self) -> usize {
match self {
Self::Insert { text, .. } => text.len(),
Self::Delete { text, .. } => text.len(),
Self::Replace {
deleted, inserted, ..
} => deleted.len() + inserted.len(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Selection {
pub anchor: CursorPosition,
pub head: CursorPosition,
}
impl Selection {
#[must_use]
pub fn byte_range(&self, nav: &CursorNavigator<'_>) -> (usize, usize) {
let a = nav.to_byte_index(self.anchor);
let b = nav.to_byte_index(self.head);
if a <= b { (a, b) } else { (b, a) }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.anchor == self.head
}
}
#[derive(Debug, Clone)]
pub struct Editor {
rope: Rope,
cursor: CursorPosition,
selection: Option<Selection>,
undo_stack: Vec<(EditOp, CursorPosition)>,
redo_stack: Vec<(EditOp, CursorPosition)>,
max_history: usize,
current_undo_size: usize,
max_undo_size: usize,
}
impl Default for Editor {
fn default() -> Self {
Self::new()
}
}
impl Editor {
#[must_use]
pub fn new() -> Self {
Self {
rope: Rope::new(),
cursor: CursorPosition::default(),
selection: None,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
max_history: 1000,
current_undo_size: 0,
max_undo_size: 10 * 1024 * 1024, }
}
#[must_use]
pub fn with_text(text: &str) -> Self {
let rope = Rope::from_text(text);
let nav = CursorNavigator::new(&rope);
let cursor = nav.document_end();
Self {
rope,
cursor,
selection: None,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
max_history: 1000,
current_undo_size: 0,
max_undo_size: 10 * 1024 * 1024,
}
}
pub fn set_max_history(&mut self, max: usize) {
self.max_history = max;
}
pub fn set_max_undo_size(&mut self, bytes: usize) {
self.max_undo_size = bytes;
while self.current_undo_size > self.max_undo_size && !self.undo_stack.is_empty() {
let (op, _) = self.undo_stack.remove(0);
self.current_undo_size -= op.byte_len();
}
}
#[must_use]
pub fn text(&self) -> String {
self.rope.to_string()
}
#[must_use]
pub fn rope(&self) -> &Rope {
&self.rope
}
#[must_use]
pub fn cursor(&self) -> CursorPosition {
self.cursor
}
pub fn set_cursor(&mut self, pos: CursorPosition) {
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.clamp(pos);
self.selection = None;
}
#[must_use]
pub fn selection(&self) -> Option<Selection> {
self.selection
}
#[must_use]
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
#[must_use]
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.rope.is_empty()
}
#[must_use]
pub fn line_count(&self) -> usize {
self.rope.len_lines()
}
#[must_use]
pub fn line_text(&self, line: usize) -> Option<String> {
self.rope.line(line).map(|cow| {
let s = cow.as_ref();
s.trim_end_matches('\n').trim_end_matches('\r').to_string()
})
}
pub fn insert_char(&mut self, ch: char) {
let mut buf = [0u8; 4];
let s = ch.encode_utf8(&mut buf);
self.insert_text(s);
}
pub fn insert_text(&mut self, text: &str) {
if text.is_empty() {
return;
}
let sanitized: String = text
.chars()
.filter(|&c| !c.is_control() || c == '\n' || c == '\t')
.collect();
if sanitized.is_empty() {
return;
}
if let Some((start_byte, deleted)) = self.extract_selection() {
let char_idx = self.rope.byte_to_char(start_byte);
self.push_undo(EditOp::Replace {
byte_offset: start_byte,
deleted,
inserted: sanitized.clone(),
});
self.rope.insert(char_idx, &sanitized);
let new_byte_idx = start_byte + sanitized.len();
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.from_byte_index(new_byte_idx);
} else {
let nav = CursorNavigator::new(&self.rope);
let byte_idx = nav.to_byte_index(self.cursor);
let char_idx = self.rope.byte_to_char(byte_idx);
self.push_undo(EditOp::Insert {
byte_offset: byte_idx,
text: sanitized.clone(),
});
self.rope.insert(char_idx, &sanitized);
let new_byte_idx = byte_idx + sanitized.len();
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.from_byte_index(new_byte_idx);
}
}
pub fn insert_newline(&mut self) {
self.insert_text("\n");
}
pub fn delete_backward(&mut self) -> bool {
if self.delete_selection_inner() {
return true;
}
let nav = CursorNavigator::new(&self.rope);
let old_pos = self.cursor;
let new_pos = nav.move_left(old_pos);
if new_pos == old_pos {
return false; }
let start_byte = nav.to_byte_index(new_pos);
let end_byte = nav.to_byte_index(old_pos);
let start_char = self.rope.byte_to_char(start_byte);
let end_char = self.rope.byte_to_char(end_byte);
let deleted = self.rope.slice(start_char..end_char).into_owned();
self.push_undo(EditOp::Delete {
byte_offset: start_byte,
text: deleted,
});
self.rope.remove(start_char..end_char);
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.from_byte_index(start_byte);
true
}
pub fn delete_forward(&mut self) -> bool {
if self.delete_selection_inner() {
return true;
}
let nav = CursorNavigator::new(&self.rope);
let old_pos = self.cursor;
let next_pos = nav.move_right(old_pos);
if next_pos == old_pos {
return false; }
let start_byte = nav.to_byte_index(old_pos);
let end_byte = nav.to_byte_index(next_pos);
let start_char = self.rope.byte_to_char(start_byte);
let end_char = self.rope.byte_to_char(end_byte);
let deleted = self.rope.slice(start_char..end_char).into_owned();
self.push_undo(EditOp::Delete {
byte_offset: start_byte,
text: deleted,
});
self.rope.remove(start_char..end_char);
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.clamp(self.cursor);
true
}
pub fn delete_word_backward(&mut self) -> bool {
if self.delete_selection_inner() {
return true;
}
let nav = CursorNavigator::new(&self.rope);
let old_pos = self.cursor;
let word_start = nav.move_word_left(old_pos);
if word_start == old_pos {
return false;
}
let start_byte = nav.to_byte_index(word_start);
let end_byte = nav.to_byte_index(old_pos);
let start_char = self.rope.byte_to_char(start_byte);
let end_char = self.rope.byte_to_char(end_byte);
let deleted = self.rope.slice(start_char..end_char).into_owned();
self.push_undo(EditOp::Delete {
byte_offset: start_byte,
text: deleted,
});
self.rope.remove(start_char..end_char);
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.from_byte_index(start_byte);
true
}
pub fn delete_word_forward(&mut self) -> bool {
if self.delete_selection_inner() {
return true;
}
let nav = CursorNavigator::new(&self.rope);
let old_pos = self.cursor;
let word_end = nav.move_word_right(old_pos);
if word_end == old_pos {
return false;
}
let start_byte = nav.to_byte_index(old_pos);
let end_byte = nav.to_byte_index(word_end);
let start_char = self.rope.byte_to_char(start_byte);
let end_char = self.rope.byte_to_char(end_byte);
let deleted = self.rope.slice(start_char..end_char).into_owned();
self.push_undo(EditOp::Delete {
byte_offset: start_byte,
text: deleted,
});
self.rope.remove(start_char..end_char);
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.clamp(self.cursor);
true
}
pub fn delete_to_end_of_line(&mut self) -> bool {
if self.delete_selection_inner() {
return true;
}
let nav = CursorNavigator::new(&self.rope);
let old_pos = self.cursor;
let line_end = nav.line_end(old_pos);
if line_end == old_pos {
return self.delete_forward();
}
let start_byte = nav.to_byte_index(old_pos);
let end_byte = nav.to_byte_index(line_end);
let start_char = self.rope.byte_to_char(start_byte);
let end_char = self.rope.byte_to_char(end_byte);
let deleted = self.rope.slice(start_char..end_char).into_owned();
self.push_undo(EditOp::Delete {
byte_offset: start_byte,
text: deleted,
});
self.rope.remove(start_char..end_char);
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.clamp(self.cursor);
true
}
fn push_undo(&mut self, op: EditOp) {
let op_len = op.byte_len();
self.undo_stack.push((op, self.cursor));
self.current_undo_size += op_len;
if self.undo_stack.len() > self.max_history {
if let Some((removed_op, _)) = self.undo_stack.first() {
self.current_undo_size =
self.current_undo_size.saturating_sub(removed_op.byte_len());
}
self.undo_stack.remove(0);
}
while self.current_undo_size > self.max_undo_size && !self.undo_stack.is_empty() {
let (removed_op, _) = self.undo_stack.remove(0);
self.current_undo_size = self.current_undo_size.saturating_sub(removed_op.byte_len());
}
self.redo_stack.clear();
}
pub fn undo(&mut self) -> bool {
let Some((op, cursor_before)) = self.undo_stack.pop() else {
return false;
};
self.current_undo_size = self.current_undo_size.saturating_sub(op.byte_len());
let inverse = op.inverse();
self.apply_op(&inverse);
self.redo_stack.push((inverse, self.cursor));
self.cursor = cursor_before;
self.selection = None;
true
}
pub fn redo(&mut self) -> bool {
let Some((op, cursor_before)) = self.redo_stack.pop() else {
return false;
};
let inverse = op.inverse();
self.apply_op(&inverse);
let op_len = inverse.byte_len();
self.undo_stack.push((inverse, self.cursor));
self.current_undo_size += op_len;
while self.current_undo_size > self.max_undo_size && !self.undo_stack.is_empty() {
let (removed_op, _) = self.undo_stack.remove(0);
self.current_undo_size = self.current_undo_size.saturating_sub(removed_op.byte_len());
}
self.cursor = cursor_before;
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.clamp(self.cursor);
true
}
fn apply_op(&mut self, op: &EditOp) {
match op {
EditOp::Insert { byte_offset, text } => {
let char_idx = self.rope.byte_to_char(*byte_offset);
self.rope.insert(char_idx, text);
}
EditOp::Delete { byte_offset, text } => {
let start_char = self.rope.byte_to_char(*byte_offset);
let end_char = self.rope.byte_to_char(*byte_offset + text.len());
self.rope.remove(start_char..end_char);
}
EditOp::Replace {
byte_offset,
deleted,
inserted,
} => {
let start_char = self.rope.byte_to_char(*byte_offset);
let end_char = self.rope.byte_to_char(*byte_offset + deleted.len());
self.rope.remove(start_char..end_char);
self.rope.insert(start_char, inserted);
}
}
}
fn extract_selection(&mut self) -> Option<(usize, String)> {
let sel = self.selection.take()?;
if sel.is_empty() {
return None;
}
let nav = CursorNavigator::new(&self.rope);
let (start_byte, end_byte) = sel.byte_range(&nav);
let start_char = self.rope.byte_to_char(start_byte);
let end_char = self.rope.byte_to_char(end_byte);
let deleted = self.rope.slice(start_char..end_char).into_owned();
self.rope.remove(start_char..end_char);
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.from_byte_index(start_byte);
Some((start_byte, deleted))
}
fn delete_selection_inner(&mut self) -> bool {
if let Some((start_byte, deleted)) = self.extract_selection() {
self.push_undo(EditOp::Delete {
byte_offset: start_byte,
text: deleted,
});
true
} else {
false
}
}
pub fn move_left(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.move_left(self.cursor);
}
pub fn move_right(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.move_right(self.cursor);
}
pub fn move_up(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.move_up(self.cursor);
}
pub fn move_down(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.move_down(self.cursor);
}
pub fn move_word_left(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.move_word_left(self.cursor);
}
pub fn move_word_right(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.move_word_right(self.cursor);
}
pub fn move_to_line_start(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.line_start(self.cursor);
}
pub fn move_to_line_end(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.line_end(self.cursor);
}
pub fn move_to_document_start(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.document_start();
}
pub fn move_to_document_end(&mut self) {
self.selection = None;
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.document_end();
}
pub fn select_left(&mut self) {
self.extend_selection(|nav, pos| nav.move_left(pos));
}
pub fn select_right(&mut self) {
self.extend_selection(|nav, pos| nav.move_right(pos));
}
pub fn select_up(&mut self) {
self.extend_selection(|nav, pos| nav.move_up(pos));
}
pub fn select_down(&mut self) {
self.extend_selection(|nav, pos| nav.move_down(pos));
}
pub fn select_word_left(&mut self) {
self.extend_selection(|nav, pos| nav.move_word_left(pos));
}
pub fn select_word_right(&mut self) {
self.extend_selection(|nav, pos| nav.move_word_right(pos));
}
pub fn select_all(&mut self) {
let nav = CursorNavigator::new(&self.rope);
let start = nav.document_start();
let end = nav.document_end();
self.selection = Some(Selection {
anchor: start,
head: end,
});
self.cursor = end;
}
pub fn clear_selection(&mut self) {
self.selection = None;
}
#[must_use]
pub fn selected_text(&self) -> Option<String> {
let sel = self.selection?;
if sel.is_empty() {
return None;
}
let nav = CursorNavigator::new(&self.rope);
let (start, end) = sel.byte_range(&nav);
let start_char = self.rope.byte_to_char(start);
let end_char = self.rope.byte_to_char(end);
Some(self.rope.slice(start_char..end_char).into_owned())
}
fn extend_selection(
&mut self,
f: impl FnOnce(&CursorNavigator<'_>, CursorPosition) -> CursorPosition,
) {
let nav = CursorNavigator::new(&self.rope);
let new_head = f(&nav, self.cursor);
self.extend_selection_to(new_head);
}
pub fn extend_selection_to(&mut self, new_head: CursorPosition) {
let anchor = match self.selection {
Some(sel) => sel.anchor,
None => self.cursor,
};
let nav = CursorNavigator::new(&self.rope);
let new_head = nav.clamp(new_head);
self.cursor = new_head;
self.selection = Some(Selection {
anchor,
head: new_head,
});
}
pub fn set_text(&mut self, text: &str) {
self.rope.replace(text);
let nav = CursorNavigator::new(&self.rope);
self.cursor = nav.document_end();
self.selection = None;
self.undo_stack.clear();
self.redo_stack.clear();
self.current_undo_size = 0;
}
pub fn clear(&mut self) {
self.rope.clear();
self.cursor = CursorPosition::default();
self.selection = None;
self.undo_stack.clear();
self.redo_stack.clear();
self.current_undo_size = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_editor_is_empty() {
let ed = Editor::new();
assert!(ed.is_empty());
assert_eq!(ed.text(), "");
assert_eq!(ed.cursor(), CursorPosition::default());
}
#[test]
fn with_text_cursor_at_end() {
let ed = Editor::with_text("hello");
assert_eq!(ed.text(), "hello");
assert_eq!(ed.cursor().line, 0);
assert_eq!(ed.cursor().grapheme, 5);
}
#[test]
fn insert_char_at_end() {
let mut ed = Editor::new();
ed.insert_char('a');
ed.insert_char('b');
ed.insert_char('c');
assert_eq!(ed.text(), "abc");
assert_eq!(ed.cursor().grapheme, 3);
}
#[test]
fn insert_text() {
let mut ed = Editor::new();
ed.insert_text("hello world");
assert_eq!(ed.text(), "hello world");
}
#[test]
fn insert_in_middle() {
let mut ed = Editor::with_text("helo");
ed.set_cursor(CursorPosition::new(0, 3, 3));
ed.insert_char('l');
assert_eq!(ed.text(), "hello");
}
#[test]
fn insert_newline() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 5, 5));
ed.insert_newline();
assert_eq!(ed.text(), "hello\n world");
assert_eq!(ed.cursor().line, 1);
assert_eq!(ed.line_count(), 2);
}
#[test]
fn delete_backward() {
let mut ed = Editor::with_text("hello");
assert!(ed.delete_backward());
assert_eq!(ed.text(), "hell");
}
#[test]
fn delete_backward_at_beginning() {
let mut ed = Editor::with_text("hello");
ed.set_cursor(CursorPosition::new(0, 0, 0));
assert!(!ed.delete_backward());
assert_eq!(ed.text(), "hello");
}
#[test]
fn delete_backward_joins_lines() {
let mut ed = Editor::with_text("hello\nworld");
ed.set_cursor(CursorPosition::new(1, 0, 0));
assert!(ed.delete_backward());
assert_eq!(ed.text(), "helloworld");
assert_eq!(ed.line_count(), 1);
}
#[test]
fn delete_forward() {
let mut ed = Editor::with_text("hello");
ed.set_cursor(CursorPosition::new(0, 0, 0));
assert!(ed.delete_forward());
assert_eq!(ed.text(), "ello");
}
#[test]
fn delete_forward_at_end() {
let mut ed = Editor::with_text("hello");
assert!(!ed.delete_forward());
assert_eq!(ed.text(), "hello");
}
#[test]
fn delete_forward_joins_lines() {
let mut ed = Editor::with_text("hello\nworld");
ed.set_cursor(CursorPosition::new(0, 5, 5));
assert!(ed.delete_forward());
assert_eq!(ed.text(), "helloworld");
}
#[test]
fn move_left_right() {
let mut ed = Editor::with_text("abc");
assert_eq!(ed.cursor().grapheme, 3);
ed.move_left();
assert_eq!(ed.cursor().grapheme, 2);
ed.move_left();
assert_eq!(ed.cursor().grapheme, 1);
ed.move_right();
assert_eq!(ed.cursor().grapheme, 2);
}
#[test]
fn move_left_at_start_is_noop() {
let mut ed = Editor::with_text("abc");
ed.set_cursor(CursorPosition::new(0, 0, 0));
ed.move_left();
assert_eq!(ed.cursor().grapheme, 0);
assert_eq!(ed.cursor().line, 0);
}
#[test]
fn move_right_at_end_is_noop() {
let mut ed = Editor::with_text("abc");
ed.move_right();
assert_eq!(ed.cursor().grapheme, 3);
}
#[test]
fn move_up_down() {
let mut ed = Editor::with_text("line 1\nline 2\nline 3");
assert_eq!(ed.cursor().line, 2);
ed.move_up();
assert_eq!(ed.cursor().line, 1);
ed.move_up();
assert_eq!(ed.cursor().line, 0);
ed.move_up();
assert_eq!(ed.cursor().line, 0);
ed.move_down();
assert_eq!(ed.cursor().line, 1);
}
#[test]
fn move_to_line_start_end() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 5, 5));
ed.move_to_line_start();
assert_eq!(ed.cursor().grapheme, 0);
ed.move_to_line_end();
assert_eq!(ed.cursor().grapheme, 11);
}
#[test]
fn move_to_document_start_end() {
let mut ed = Editor::with_text("line 1\nline 2\nline 3");
ed.move_to_document_start();
assert_eq!(ed.cursor().line, 0);
assert_eq!(ed.cursor().grapheme, 0);
ed.move_to_document_end();
assert_eq!(ed.cursor().line, 2);
}
#[test]
fn move_word_left_right() {
let mut ed = Editor::with_text("hello world foo");
let start = ed.cursor().grapheme;
ed.move_word_left();
let after_first = ed.cursor().grapheme;
assert!(after_first < start, "word_left should move cursor left");
ed.move_word_left();
let after_second = ed.cursor().grapheme;
assert!(
after_second < after_first,
"second word_left should move further left"
);
ed.move_word_right();
let after_right = ed.cursor().grapheme;
assert!(
after_right > after_second,
"word_right should move cursor right"
);
}
#[test]
fn delete_word_backward() {
let mut ed = Editor::with_text("hello world");
assert!(ed.delete_word_backward());
assert_eq!(ed.text(), "hello ");
}
#[test]
fn delete_word_forward() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 0, 0));
assert!(ed.delete_word_forward());
assert_eq!(ed.text(), "world");
}
#[test]
fn delete_to_end_of_line() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 5, 5));
assert!(ed.delete_to_end_of_line());
assert_eq!(ed.text(), "hello");
}
#[test]
fn delete_to_end_joins_when_at_line_end() {
let mut ed = Editor::with_text("hello\nworld");
ed.set_cursor(CursorPosition::new(0, 5, 5));
assert!(ed.delete_to_end_of_line());
assert_eq!(ed.text(), "helloworld");
}
#[test]
fn set_text_replaces_content() {
let mut ed = Editor::with_text("old");
ed.set_text("new content");
assert_eq!(ed.text(), "new content");
}
#[test]
fn clear_resets() {
let mut ed = Editor::with_text("hello");
ed.clear();
assert!(ed.is_empty());
assert_eq!(ed.cursor(), CursorPosition::default());
}
#[test]
fn line_text_works() {
let ed = Editor::with_text("line 0\nline 1\nline 2");
assert_eq!(ed.line_text(0), Some("line 0".to_string()));
assert_eq!(ed.line_text(1), Some("line 1".to_string()));
assert_eq!(ed.line_text(2), Some("line 2".to_string()));
assert_eq!(ed.line_text(3), None);
}
#[test]
fn cursor_stays_in_bounds_after_delete() {
let mut ed = Editor::with_text("a");
assert!(ed.delete_backward());
assert_eq!(ed.text(), "");
assert_eq!(ed.cursor(), CursorPosition::default());
assert!(!ed.delete_backward());
assert!(!ed.delete_forward());
}
#[test]
fn multiline_editing() {
let mut ed = Editor::new();
ed.insert_text("first");
ed.insert_newline();
ed.insert_text("second");
ed.insert_newline();
ed.insert_text("third");
assert_eq!(ed.text(), "first\nsecond\nthird");
assert_eq!(ed.line_count(), 3);
assert_eq!(ed.cursor().line, 2);
ed.move_up();
ed.move_to_line_start();
ed.insert_text(">> ");
assert_eq!(ed.line_text(1), Some(">> second".to_string()));
}
#[test]
fn undo_insert() {
let mut ed = Editor::new();
ed.insert_text("hello");
assert!(ed.can_undo());
assert!(ed.undo());
assert_eq!(ed.text(), "");
}
#[test]
fn undo_delete() {
let mut ed = Editor::with_text("hello");
ed.delete_backward();
assert_eq!(ed.text(), "hell");
assert!(ed.undo());
assert_eq!(ed.text(), "hello");
}
#[test]
fn redo_after_undo() {
let mut ed = Editor::new();
ed.insert_text("abc");
ed.undo();
assert_eq!(ed.text(), "");
assert!(ed.can_redo());
assert!(ed.redo());
assert_eq!(ed.text(), "abc");
}
#[test]
fn redo_cleared_on_new_edit() {
let mut ed = Editor::new();
ed.insert_text("abc");
ed.undo();
ed.insert_text("xyz");
assert!(!ed.can_redo());
}
#[test]
fn multiple_undo_redo() {
let mut ed = Editor::new();
ed.insert_text("a");
ed.insert_text("b");
ed.insert_text("c");
assert_eq!(ed.text(), "abc");
ed.undo();
assert_eq!(ed.text(), "ab");
ed.undo();
assert_eq!(ed.text(), "a");
ed.undo();
assert_eq!(ed.text(), "");
ed.redo();
assert_eq!(ed.text(), "a");
ed.redo();
assert_eq!(ed.text(), "ab");
}
#[test]
fn undo_restores_cursor() {
let mut ed = Editor::new();
let before = ed.cursor();
ed.insert_text("x");
ed.undo();
assert_eq!(ed.cursor(), before);
}
#[test]
fn max_history_respected() {
let mut ed = Editor::new();
ed.set_max_history(3);
for c in ['a', 'b', 'c', 'd', 'e'] {
ed.insert_text(&c.to_string());
}
assert!(ed.undo());
assert!(ed.undo());
assert!(ed.undo());
assert!(!ed.undo());
assert_eq!(ed.text(), "ab");
}
#[test]
fn set_text_clears_undo() {
let mut ed = Editor::new();
ed.insert_text("abc");
ed.set_text("new");
assert!(!ed.can_undo());
assert!(!ed.can_redo());
}
#[test]
fn clear_clears_undo() {
let mut ed = Editor::new();
ed.insert_text("abc");
ed.clear();
assert!(!ed.can_undo());
}
#[test]
fn select_right_creates_selection() {
let mut ed = Editor::with_text("hello");
ed.set_cursor(CursorPosition::new(0, 0, 0));
ed.select_right();
ed.select_right();
ed.select_right();
let sel = ed.selection().unwrap();
assert_eq!(sel.anchor, CursorPosition::new(0, 0, 0));
assert_eq!(sel.head.grapheme, 3);
assert_eq!(ed.selected_text(), Some("hel".to_string()));
}
#[test]
fn select_all_selects_everything() {
let mut ed = Editor::with_text("abc\ndef");
ed.select_all();
assert_eq!(ed.selected_text(), Some("abc\ndef".to_string()));
}
#[test]
fn insert_replaces_selection() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 0, 0));
for _ in 0..5 {
ed.select_right();
}
ed.insert_text("goodbye");
assert_eq!(ed.text(), "goodbye world");
assert!(ed.selection().is_none());
}
#[test]
fn delete_backward_removes_selection() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 0, 0));
for _ in 0..5 {
ed.select_right();
}
ed.delete_backward();
assert_eq!(ed.text(), " world");
}
#[test]
fn movement_clears_selection() {
let mut ed = Editor::with_text("hello");
ed.set_cursor(CursorPosition::new(0, 0, 0));
ed.select_right();
ed.select_right();
assert!(ed.selection().is_some());
ed.move_right();
assert!(ed.selection().is_none());
}
#[test]
fn undo_selection_delete() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 0, 0));
for _ in 0..5 {
ed.select_right();
}
ed.delete_backward();
assert_eq!(ed.text(), " world");
ed.undo();
assert_eq!(ed.text(), "hello world");
}
#[test]
fn insert_empty_text_is_noop() {
let mut ed = Editor::with_text("hello");
let before = ed.text();
ed.insert_text("");
assert_eq!(ed.text(), before);
assert!(!ed.can_undo());
}
#[test]
fn unicode_emoji_handling() {
let mut ed = Editor::new();
ed.insert_text("hello 🎉 world");
assert_eq!(ed.text(), "hello 🎉 world");
ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.move_left(); ed.delete_backward(); assert_eq!(ed.text(), "hello🎉 world");
}
#[test]
fn unicode_combining_character() {
let mut ed = Editor::new();
ed.insert_text("caf\u{0065}\u{0301}");
assert_eq!(ed.text(), "caf\u{0065}\u{0301}");
ed.delete_backward();
assert_eq!(ed.text(), "caf");
}
#[test]
fn unicode_zwj_sequence() {
let mut ed = Editor::new();
ed.insert_text("👩\u{200D}🚀");
let text = ed.text();
assert!(text.contains("👩"));
ed.move_left();
ed.insert_char('x');
assert!(ed.text().starts_with('x'));
}
#[test]
fn unicode_cjk_wide_chars() {
let mut ed = Editor::new();
ed.insert_text("世界");
assert_eq!(ed.text(), "世界");
ed.move_left();
assert_eq!(ed.cursor().grapheme, 1);
ed.move_left();
assert_eq!(ed.cursor().grapheme, 0);
}
#[test]
fn crlf_handling() {
let ed = Editor::with_text("hello\r\nworld");
assert_eq!(ed.line_count(), 2);
assert_eq!(ed.line_text(0), Some("hello".to_string()));
assert_eq!(ed.line_text(1), Some("world".to_string()));
}
#[test]
fn mixed_newlines() {
let ed = Editor::with_text("line1\nline2\r\nline3");
assert_eq!(ed.line_count(), 3);
assert_eq!(ed.line_text(0), Some("line1".to_string()));
assert_eq!(ed.line_text(1), Some("line2".to_string()));
assert_eq!(ed.line_text(2), Some("line3".to_string()));
}
#[test]
fn trailing_newline() {
let ed = Editor::with_text("hello\n");
assert_eq!(ed.line_count(), 2);
assert_eq!(ed.line_text(0), Some("hello".to_string()));
assert_eq!(ed.line_text(1), Some(String::new()));
}
#[test]
fn only_newlines() {
let ed = Editor::with_text("\n\n\n");
assert_eq!(ed.line_count(), 4);
for i in 0..4 {
assert_eq!(ed.line_text(i), Some(String::new()));
}
}
#[test]
fn delete_word_backward_at_start_is_noop() {
let mut ed = Editor::with_text("hello");
ed.set_cursor(CursorPosition::new(0, 0, 0));
assert!(!ed.delete_word_backward());
assert_eq!(ed.text(), "hello");
}
#[test]
fn delete_word_backward_multiple_spaces() {
let mut ed = Editor::with_text("hello world");
assert!(ed.delete_word_backward());
let remaining = ed.text();
assert!(remaining.starts_with("hello"));
}
#[test]
fn delete_to_end_at_document_end() {
let mut ed = Editor::with_text("hello");
assert!(!ed.delete_to_end_of_line());
assert_eq!(ed.text(), "hello");
}
#[test]
fn select_word_operations() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 0, 0));
ed.select_word_right();
assert_eq!(ed.selected_text(), Some("hello ".to_string()));
ed.clear_selection();
ed.move_to_line_end();
ed.select_word_left();
assert_eq!(ed.selected_text(), Some("world".to_string()));
}
#[test]
fn select_up_down() {
let mut ed = Editor::with_text("line1\nline2\nline3");
ed.set_cursor(CursorPosition::new(1, 3, 3));
ed.select_up();
let sel = ed.selection().expect("should have selection");
assert_eq!(sel.anchor.line, 1);
assert_eq!(sel.head.line, 0);
ed.select_down();
ed.select_down();
let sel = ed.selection().expect("should have selection");
assert_eq!(sel.head.line, 2);
}
#[test]
fn selection_extending_preserves_anchor() {
let mut ed = Editor::with_text("abcdef");
ed.set_cursor(CursorPosition::new(0, 2, 2));
ed.select_right();
ed.select_right();
ed.select_right();
let sel = ed.selection().unwrap();
assert_eq!(sel.anchor.grapheme, 2);
assert_eq!(sel.head.grapheme, 5);
ed.select_left();
let sel = ed.selection().unwrap();
assert_eq!(sel.anchor.grapheme, 2);
assert_eq!(sel.head.grapheme, 4);
}
#[test]
fn empty_selection_returns_none() {
let mut ed = Editor::with_text("hello");
ed.set_cursor(CursorPosition::new(0, 2, 2));
ed.select_right();
ed.select_left();
let sel = ed.selection().unwrap();
assert!(sel.is_empty());
assert_eq!(ed.selected_text(), None);
}
#[test]
fn cursor_clamp_after_set_text() {
let mut ed = Editor::with_text("very long text here");
ed.set_text("hi");
assert_eq!(ed.cursor().line, 0);
assert_eq!(ed.cursor().grapheme, 2);
}
#[test]
fn undo_redo_with_selection() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 6, 6));
for _ in 0..5 {
ed.select_right();
}
ed.insert_text("universe");
assert_eq!(ed.text(), "hello universe");
ed.undo();
assert_eq!(ed.text(), "hello world");
ed.redo();
assert_eq!(ed.text(), "hello universe");
}
#[test]
fn rapid_insert_delete_cycle() {
let mut ed = Editor::new();
for i in 0..100 {
ed.insert_char(char::from_u32('a' as u32 + (i % 26)).unwrap());
if i % 3 == 0 {
ed.delete_backward();
}
}
let cursor = ed.cursor();
assert!(cursor.line == 0);
assert!(cursor.grapheme <= ed.text().chars().count());
}
#[test]
fn multiline_select_all_and_replace() {
let mut ed = Editor::with_text("line1\nline2\nline3");
ed.select_all();
ed.insert_text("replaced");
assert_eq!(ed.text(), "replaced");
assert_eq!(ed.line_count(), 1);
}
#[test]
fn delete_forward_with_selection() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 0, 0));
for _ in 0..5 {
ed.select_right();
}
ed.delete_forward();
assert_eq!(ed.text(), " world");
}
#[test]
fn delete_word_backward_with_selection() {
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 6, 6));
for _ in 0..5 {
ed.select_right();
}
ed.delete_word_backward();
assert_eq!(ed.text(), "hello ");
}
#[test]
fn default_impl() {
let ed = Editor::default();
assert!(ed.is_empty());
assert_eq!(ed.cursor(), CursorPosition::default());
}
#[test]
fn line_text_out_of_bounds() {
let ed = Editor::with_text("hello");
assert_eq!(ed.line_text(0), Some("hello".to_string()));
assert_eq!(ed.line_text(1), None);
assert_eq!(ed.line_text(100), None);
}
#[test]
fn rope_accessor() {
let ed = Editor::with_text("test");
let rope = ed.rope();
assert_eq!(rope.len_bytes(), 4);
}
#[test]
fn insert_text_sanitizes_controls() {
let mut ed = Editor::new();
ed.insert_text("hello\x1bworld\x07\n\t!");
assert_eq!(ed.text(), "helloworld\n\t!");
}
#[test]
fn cursor_position_after_multiline_insert() {
let mut ed = Editor::new();
ed.insert_text("hello\nworld\nfoo");
assert_eq!(ed.cursor().line, 2);
assert_eq!(ed.line_count(), 3);
}
#[test]
fn delete_backward_across_lines() {
let mut ed = Editor::with_text("abc\ndef");
ed.set_cursor(CursorPosition::new(1, 0, 0));
ed.delete_backward();
assert_eq!(ed.text(), "abcdef");
assert_eq!(ed.cursor().line, 0);
assert_eq!(ed.cursor().grapheme, 3);
}
#[test]
fn very_long_line() {
let long_text: String = "a".repeat(10000);
let mut ed = Editor::with_text(&long_text);
assert_eq!(ed.text().len(), 10000);
ed.move_to_line_start();
assert_eq!(ed.cursor().grapheme, 0);
ed.move_to_line_end();
assert_eq!(ed.cursor().grapheme, 10000);
}
#[test]
fn many_lines() {
let text: String = (0..1000)
.map(|i| format!("line{i}"))
.collect::<Vec<_>>()
.join("\n");
let ed = Editor::with_text(&text);
assert_eq!(ed.line_count(), 1000);
assert_eq!(ed.line_text(999), Some("line999".to_string()));
}
#[test]
fn selection_byte_range_order() {
use crate::cursor::CursorNavigator;
let mut ed = Editor::with_text("hello world");
ed.set_cursor(CursorPosition::new(0, 8, 8));
ed.select_left();
ed.select_left();
ed.select_left();
let sel = ed.selection().unwrap();
let nav = CursorNavigator::new(ed.rope());
let (start, end) = sel.byte_range(&nav);
assert!(start <= end);
assert_eq!(end - start, 3);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
fn text_strategy() -> impl Strategy<Value = String> {
prop::string::string_regex("[a-zA-Z0-9 \n]{0,100}")
.unwrap()
.prop_filter("non-empty or empty", |_| true)
}
fn unicode_text_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(
prop_oneof![
Just("a".to_string()),
Just(" ".to_string()),
Just("\n".to_string()),
Just("é".to_string()),
Just("世".to_string()),
Just("🎉".to_string()),
],
0..50,
)
.prop_map(|v| v.join(""))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn cursor_always_in_bounds(text in text_strategy()) {
let mut ed = Editor::with_text(&text);
let c = ed.cursor();
prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
ed.move_left();
let c = ed.cursor();
prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
ed.move_right();
let c = ed.cursor();
prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
ed.move_up();
let c = ed.cursor();
prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
ed.move_down();
let c = ed.cursor();
prop_assert!(c.line < ed.line_count() || (c.line == 0 && ed.line_count() == 1));
ed.move_to_line_start();
let c = ed.cursor();
prop_assert_eq!(c.grapheme, 0);
ed.move_to_document_start();
let c = ed.cursor();
prop_assert_eq!(c.line, 0);
prop_assert_eq!(c.grapheme, 0);
}
#[test]
fn undo_insert_restores_text(base in text_strategy(), insert in "[a-z]{1,20}") {
let mut ed = Editor::with_text(&base);
let original = ed.text();
ed.insert_text(&insert);
prop_assert!(ed.can_undo());
ed.undo();
prop_assert_eq!(ed.text(), original);
}
#[test]
fn undo_delete_restores_text(text in "[a-zA-Z]{5,50}") {
let mut ed = Editor::with_text(&text);
let original = ed.text();
if ed.delete_backward() {
prop_assert!(ed.can_undo());
ed.undo();
prop_assert_eq!(ed.text(), original);
}
}
#[test]
fn redo_after_undo_restores(text in text_strategy(), insert in "[a-z]{1,10}") {
let mut ed = Editor::with_text(&text);
ed.insert_text(&insert);
let after_insert = ed.text();
ed.undo();
prop_assert!(ed.can_redo());
ed.redo();
prop_assert_eq!(ed.text(), after_insert);
}
#[test]
fn select_all_delete_empties(text in text_strategy()) {
let mut ed = Editor::with_text(&text);
ed.select_all();
ed.delete_backward();
prop_assert!(ed.is_empty());
}
#[test]
fn line_count_matches_newlines(text in text_strategy()) {
let ed = Editor::with_text(&text);
let newline_count = text.matches('\n').count();
prop_assert_eq!(ed.line_count(), newline_count + 1);
}
#[test]
fn set_text_roundtrip(text in text_strategy()) {
let mut ed = Editor::new();
ed.set_text(&text);
prop_assert_eq!(ed.text(), text);
}
#[test]
fn unicode_cursor_bounds(text in unicode_text_strategy()) {
let mut ed = Editor::with_text(&text);
for _ in 0..10 {
ed.move_left();
}
let c = ed.cursor();
prop_assert!(c.line < ed.line_count() || ed.line_count() == 1);
for _ in 0..10 {
ed.move_right();
}
let c = ed.cursor();
prop_assert!(c.line < ed.line_count() || ed.line_count() == 1);
}
#[test]
fn insert_delete_roundtrip(ch in prop::char::any().prop_filter("printable", |c| !c.is_control())) {
let mut ed = Editor::new();
ed.insert_char(ch);
ed.delete_backward();
prop_assert!(ed.is_empty());
}
#[test]
fn multiple_undos_safe(ops in prop::collection::vec(0..3u8, 0..20)) {
let mut ed = Editor::new();
for op in ops {
match op {
0 => { ed.insert_char('x'); }
1 => { ed.delete_backward(); }
_ => { ed.undo(); }
}
}
while ed.can_undo() {
prop_assert!(ed.undo());
}
prop_assert!(!ed.can_undo());
}
#[test]
fn selection_range_ordered(text in "[a-zA-Z]{10,50}") {
use crate::cursor::CursorNavigator;
let mut ed = Editor::with_text(&text);
ed.set_cursor(CursorPosition::new(0, 5, 5));
ed.select_left();
ed.select_left();
if let Some(sel) = ed.selection() {
let nav = CursorNavigator::new(ed.rope());
let (start, end) = sel.byte_range(&nav);
prop_assert!(start <= end);
}
ed.select_right();
ed.select_right();
ed.select_right();
ed.select_right();
if let Some(sel) = ed.selection() {
let nav = CursorNavigator::new(ed.rope());
let (start, end) = sel.byte_range(&nav);
prop_assert!(start <= end);
}
}
#[test]
fn word_movement_progress(text in "[a-zA-Z ]{5,50}") {
let mut ed = Editor::with_text(&text);
ed.set_cursor(CursorPosition::new(0, 0, 0));
let start = ed.cursor();
ed.move_word_right();
let after = ed.cursor();
prop_assert!(after.grapheme >= start.grapheme);
ed.move_to_line_end();
let end_pos = ed.cursor();
ed.move_word_left();
let after_left = ed.cursor();
prop_assert!(after_left.grapheme <= end_pos.grapheme);
}
#[test]
fn document_bounds(text in text_strategy()) {
let mut ed = Editor::with_text(&text);
ed.move_to_document_start();
prop_assert_eq!(ed.cursor().line, 0);
prop_assert_eq!(ed.cursor().grapheme, 0);
ed.move_to_document_end();
let c = ed.cursor();
let last_line = ed.line_count().saturating_sub(1);
prop_assert_eq!(c.line, last_line);
}
}
}