#![allow(clippy::while_let_on_iterator)]
#![allow(clippy::option_if_let_else)]
use crate::highlight::HighlightedBuffer;
use crate::text::TextBuffer;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Cursor {
pub offset: usize,
pub row: usize,
pub col: usize,
}
impl Cursor {
#[must_use]
pub fn new(offset: usize, row: usize, col: usize) -> Self {
Self { offset, row, col }
}
#[must_use]
pub fn start() -> Self {
Self::default()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CursorPosition {
pub row: usize,
pub col: usize,
pub offset: usize,
}
#[derive(Clone, Debug)]
enum EditOp {
Insert { offset: usize, text: String },
Delete { offset: usize, text: String },
}
impl EditOp {
fn invert(&self) -> Self {
match self {
Self::Insert { offset, text } => Self::Delete {
offset: *offset,
text: text.clone(),
},
Self::Delete { offset, text } => Self::Insert {
offset: *offset,
text: text.clone(),
},
}
}
}
const DEFAULT_MAX_HISTORY_DEPTH: usize = 1000;
#[derive(Clone, Debug)]
struct History {
undo_stack: Vec<Vec<EditOp>>,
redo_stack: Vec<Vec<EditOp>>,
current_group: Vec<EditOp>,
max_depth: usize,
}
impl Default for History {
fn default() -> Self {
Self {
undo_stack: Vec::new(),
redo_stack: Vec::new(),
current_group: Vec::new(),
max_depth: DEFAULT_MAX_HISTORY_DEPTH,
}
}
}
impl History {
fn new() -> Self {
Self::default()
}
fn with_max_depth(max_depth: usize) -> Self {
Self {
max_depth,
..Self::default()
}
}
fn push(&mut self, op: EditOp) {
self.current_group.push(op);
self.redo_stack.clear();
}
fn commit(&mut self) {
if !self.current_group.is_empty() {
self.undo_stack
.push(std::mem::take(&mut self.current_group));
if self.undo_stack.len() > self.max_depth {
let excess = self.undo_stack.len() - self.max_depth;
self.undo_stack.drain(..excess);
}
}
}
fn pop_undo(&mut self) -> Option<Vec<EditOp>> {
self.commit();
self.undo_stack.pop()
}
fn push_redo(&mut self, ops: Vec<EditOp>) {
self.redo_stack.push(ops);
}
fn pop_redo(&mut self) -> Option<Vec<EditOp>> {
self.redo_stack.pop()
}
fn can_undo(&self) -> bool {
!self.undo_stack.is_empty() || !self.current_group.is_empty()
}
fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
self.current_group.clear();
}
}
#[derive(Default)]
pub struct EditBuffer {
buffer: HighlightedBuffer,
cursor: Cursor,
history: History,
}
impl EditBuffer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_text(text: &str) -> Self {
Self {
buffer: HighlightedBuffer::new(TextBuffer::with_text(text)),
cursor: Cursor::start(),
history: History::new(),
}
}
#[must_use]
pub fn with_max_history_depth(max_depth: usize) -> Self {
Self {
buffer: HighlightedBuffer::new(TextBuffer::new()),
cursor: Cursor::start(),
history: History::with_max_depth(max_depth),
}
}
pub fn set_max_history_depth(&mut self, max_depth: usize) {
self.history.max_depth = max_depth;
}
#[must_use]
pub fn max_history_depth(&self) -> usize {
self.history.max_depth
}
#[must_use]
pub fn buffer(&self) -> &TextBuffer {
self.buffer.buffer()
}
#[must_use]
pub fn text(&self) -> String {
self.buffer.buffer().to_string()
}
pub fn set_text(&mut self, text: &str) {
self.buffer.set_text(text);
self.cursor = Cursor::start();
self.history.clear();
self.update_cursor_position();
}
pub fn buffer_mut(&mut self) -> &mut TextBuffer {
self.buffer.buffer_mut()
}
#[must_use]
pub fn highlighted_buffer(&self) -> &HighlightedBuffer {
&self.buffer
}
pub fn highlighted_buffer_mut(&mut self) -> &mut HighlightedBuffer {
&mut self.buffer
}
#[must_use]
pub fn cursor(&self) -> Cursor {
self.cursor
}
pub fn set_cursor(&mut self, cursor: Cursor) {
self.cursor = cursor;
self.clamp_cursor();
}
pub fn set_cursor_by_offset(&mut self, offset: usize) {
self.cursor.offset = offset.min(self.buffer.len_chars());
self.update_cursor_position();
}
#[must_use]
pub fn get_cursor_position(&self) -> CursorPosition {
CursorPosition {
row: self.cursor.row,
col: self.cursor.col,
offset: self.cursor.offset,
}
}
pub fn move_left(&mut self) {
if self.cursor.offset > 0 {
self.cursor.offset -= 1;
self.update_cursor_position();
}
}
pub fn move_right(&mut self) {
if self.cursor.offset < self.buffer.len_chars() {
self.cursor.offset += 1;
self.update_cursor_position();
}
}
pub fn move_up(&mut self) {
if self.cursor.row > 0 {
self.cursor.row -= 1;
self.update_cursor_from_row_col();
}
}
pub fn move_down(&mut self) {
if self.cursor.row + 1 < self.buffer.len_lines() {
self.cursor.row += 1;
self.update_cursor_from_row_col();
}
}
pub fn move_to_line_start(&mut self) {
self.cursor.col = 0;
self.update_cursor_from_row_col();
}
pub fn move_to_line_end(&mut self) {
if let Some(line) = self.buffer.line(self.cursor.row) {
self.cursor.col = line.trim_end_matches('\n').chars().count();
self.update_cursor_from_row_col();
}
}
pub fn move_to(&mut self, row: usize, col: usize) {
self.cursor.row = row.min(self.buffer.len_lines().saturating_sub(1));
self.cursor.col = col;
self.update_cursor_from_row_col();
}
pub fn goto_line(&mut self, row: usize) {
let row = row.min(self.buffer.len_lines().saturating_sub(1));
self.cursor.row = row;
self.cursor.col = 0;
self.update_cursor_from_row_col();
}
pub fn insert(&mut self, text: &str) {
let offset = self.cursor.offset;
self.buffer.rope_mut().insert(offset, text);
let line_delta = text.chars().filter(|&ch| ch == '\n').count();
let start_row = self.cursor.row;
let end_row = start_row.saturating_add(line_delta + 1);
self.buffer.mark_dirty(start_row, end_row);
self.history.push(EditOp::Insert {
offset,
text: text.to_string(),
});
self.cursor.offset += text.chars().count();
self.update_cursor_position();
}
pub fn delete_backward(&mut self) {
if self.cursor.offset == 0 {
return;
}
let start = self.cursor.offset - 1;
let deleted = self
.buffer
.rope()
.slice(start..self.cursor.offset)
.to_string();
self.buffer.rope_mut().remove(start..self.cursor.offset);
self.buffer
.mark_dirty(self.cursor.row.saturating_sub(1), self.cursor.row + 1); self.history.push(EditOp::Delete {
offset: start,
text: deleted,
});
self.cursor.offset = start;
self.update_cursor_position();
}
pub fn delete_forward(&mut self) {
if self.cursor.offset >= self.buffer.len_chars() {
return;
}
let end = self.cursor.offset + 1;
let deleted = self
.buffer
.rope()
.slice(self.cursor.offset..end)
.to_string();
self.buffer.rope_mut().remove(self.cursor.offset..end);
let start_row = self.cursor.row;
let end_row = if deleted.contains('\n') {
start_row.saturating_add(2)
} else {
start_row.saturating_add(1)
};
self.buffer.mark_dirty(start_row, end_row);
self.history.push(EditOp::Delete {
offset: self.cursor.offset,
text: deleted,
});
self.update_cursor_position();
}
pub fn delete_range(&mut self, start: Cursor, end: Cursor) {
let start_offset = start.offset.min(end.offset);
let end_offset = start.offset.max(end.offset);
self.delete_range_offsets(start_offset, end_offset);
}
pub fn delete_range_offsets(&mut self, start: usize, end: usize) {
if start >= end || start >= self.buffer.len_chars() {
return;
}
let end = end.min(self.buffer.len_chars());
let (start_row, end_row, deleted) = {
let rope = self.buffer.rope();
let start_row = rope.char_to_line(start);
let end_row = rope.char_to_line(end.saturating_sub(1));
let deleted = rope.slice(start..end).to_string();
(start_row, end_row, deleted)
};
self.buffer.rope_mut().remove(start..end);
self.buffer.mark_dirty(start_row, end_row.saturating_add(1));
self.history.push(EditOp::Delete {
offset: start,
text: deleted,
});
self.cursor.offset = start;
self.update_cursor_position();
}
pub fn delete_line(&mut self) {
let line_start = self.buffer.rope().line_to_char(self.cursor.row);
if let Some(line) = self.buffer.rope().line(self.cursor.row) {
let line_chars = line.len_chars();
let line_end = line_start + line_chars;
self.delete_range_offsets(line_start, line_end);
}
}
pub fn duplicate_line(&mut self) {
let line_start = self.buffer.rope().line_to_char(self.cursor.row);
if let Some(line) = self.buffer.rope().line(self.cursor.row) {
let line_text = line.to_string();
let insert_pos = line_start + line.len_chars();
let text_to_insert = if line_text.ends_with('\n') {
line_text.clone()
} else {
format!("\n{line_text}")
};
self.buffer.rope_mut().insert(insert_pos, &text_to_insert);
self.buffer.mark_dirty(self.cursor.row, self.cursor.row + 2);
self.history.push(EditOp::Insert {
offset: insert_pos,
text: text_to_insert,
});
self.cursor.row += 1;
self.update_cursor_from_row_col();
}
}
pub fn move_line_up(&mut self) {
if self.cursor.row == 0 {
return;
}
let target_row = self.cursor.row - 1;
let target_col = self.cursor.col;
let current_line_start = self.buffer.rope().line_to_char(self.cursor.row);
let prev_line_start = self.buffer.rope().line_to_char(target_row);
if let (Some(current_line), Some(prev_line)) = (
self.buffer.rope().line(self.cursor.row),
self.buffer.rope().line(target_row),
) {
let current_text = current_line.to_string();
let prev_text = prev_line.to_string();
let end_pos = current_line_start + current_line.len_chars();
self.delete_range_offsets(prev_line_start, end_pos);
let new_text = if current_text.ends_with('\n') {
format!("{current_text}{prev_text}")
} else if prev_text.ends_with('\n') {
format!("{current_text}\n{}", prev_text.trim_end_matches('\n'))
} else {
format!("{current_text}\n{prev_text}")
};
self.buffer.rope_mut().insert(prev_line_start, &new_text);
self.buffer.mark_dirty(target_row, target_row + 2);
self.history.push(EditOp::Insert {
offset: prev_line_start,
text: new_text,
});
self.cursor.row = target_row;
self.cursor.col = target_col;
self.update_cursor_from_row_col();
}
}
pub fn move_line_down(&mut self) {
let total_lines = self.buffer.len_lines();
if self.cursor.row >= total_lines.saturating_sub(1) {
return;
}
let target_row = self.cursor.row + 1;
let target_col = self.cursor.col;
let current_line_start = self.buffer.rope().line_to_char(self.cursor.row);
let next_line_start = self.buffer.rope().line_to_char(target_row);
if let (Some(current_line), Some(next_line)) = (
self.buffer.rope().line(self.cursor.row),
self.buffer.rope().line(target_row),
) {
let current_text = current_line.to_string();
let next_text = next_line.to_string();
let end_pos = next_line_start + next_line.len_chars();
self.delete_range_offsets(current_line_start, end_pos);
let new_text = if next_text.ends_with('\n') {
format!("{next_text}{current_text}")
} else if current_text.ends_with('\n') {
format!("{next_text}\n{}", current_text.trim_end_matches('\n'))
} else {
format!("{next_text}\n{current_text}")
};
self.buffer.rope_mut().insert(current_line_start, &new_text);
self.buffer.mark_dirty(self.cursor.row, self.cursor.row + 2);
self.history.push(EditOp::Insert {
offset: current_line_start,
text: new_text,
});
self.cursor.row = target_row;
self.cursor.col = target_col;
self.update_cursor_from_row_col();
}
}
pub fn replace_text(&mut self, text: &str) {
self.set_text(text);
}
#[must_use]
pub fn get_next_word_boundary(&self) -> usize {
let text = self.buffer.to_string();
let mut chars = text.chars().enumerate().skip(self.cursor.offset);
let mut in_word = false;
let mut last_idx = self.cursor.offset;
while let Some((idx, ch)) = chars.next() {
let word_char = ch.is_alphanumeric() || ch == '_';
if in_word && !word_char {
return idx;
}
if !in_word && word_char {
in_word = true;
}
last_idx = idx + 1;
}
last_idx
}
#[must_use]
pub fn get_prev_word_boundary(&self) -> usize {
let text = self.buffer.to_string();
let chars: Vec<char> = text.chars().collect();
if self.cursor.offset == 0 {
return 0;
}
let mut idx = self.cursor.offset.min(chars.len());
while idx > 0 {
let ch = chars[idx - 1];
if ch.is_alphanumeric() || ch == '_' {
break;
}
idx -= 1;
}
while idx > 0 {
let ch = chars[idx - 1];
if !(ch.is_alphanumeric() || ch == '_') {
break;
}
idx -= 1;
}
idx
}
pub fn move_word_right(&mut self) {
let boundary = self.get_next_word_boundary();
self.set_cursor_by_offset(boundary);
}
pub fn move_word_left(&mut self) {
let boundary = self.get_prev_word_boundary();
self.set_cursor_by_offset(boundary);
}
pub fn delete_word_forward(&mut self) {
let end = self.get_next_word_boundary();
if end > self.cursor.offset {
self.delete_range_offsets(self.cursor.offset, end);
}
}
pub fn delete_word_backward(&mut self) {
let start = self.get_prev_word_boundary();
if start < self.cursor.offset {
self.delete_range_offsets(start, self.cursor.offset);
}
}
#[must_use]
pub fn get_eol(&self) -> usize {
if let Some(line) = self.buffer.rope().line(self.cursor.row) {
let line_chars = line.len_chars();
let has_newline = line_chars > 0 && line.char(line_chars - 1) == '\n';
let line_len = if has_newline {
line_chars - 1
} else {
line_chars
};
let line_start = self.buffer.rope().line_to_char(self.cursor.row);
line_start + line_len
} else {
self.cursor.offset
}
}
pub fn undo(&mut self) -> bool {
let Some(ops) = self.history.pop_undo() else {
return false;
};
let mut redo_ops = Vec::new();
for op in ops.into_iter().rev() {
self.apply_op(&op.invert());
redo_ops.push(op);
}
redo_ops.reverse();
self.history.push_redo(redo_ops);
true
}
pub fn redo(&mut self) -> bool {
let Some(ops) = self.history.pop_redo() else {
return false;
};
for op in &ops {
self.apply_op(op);
}
self.history.undo_stack.push(ops);
true
}
#[must_use]
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
#[must_use]
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn commit(&mut self) {
self.history.commit();
}
pub fn clear_history(&mut self) {
self.history.clear();
}
fn apply_op(&mut self, op: &EditOp) {
match op {
EditOp::Insert { offset, text } => {
self.buffer.rope_mut().insert(*offset, text);
let row = self.buffer.rope().char_to_line(*offset);
let line_delta = text.chars().filter(|&ch| ch == '\n').count();
self.buffer
.mark_dirty(row, row.saturating_add(line_delta + 1));
self.cursor.offset = offset + text.chars().count();
}
EditOp::Delete { offset, text } => {
let end = offset + text.chars().count();
let (start_row, end_row) = {
let rope = self.buffer.rope();
let start_row = rope.char_to_line(*offset);
let end_row = rope.char_to_line(end.saturating_sub(1));
(start_row, end_row)
};
self.buffer.rope_mut().remove(*offset..end);
self.buffer.mark_dirty(start_row, end_row.saturating_add(1));
self.cursor.offset = *offset;
}
}
self.update_cursor_position();
}
fn update_cursor_position(&mut self) {
let rope = self.buffer.rope();
self.cursor.row = rope
.inner()
.char_to_line(self.cursor.offset.min(rope.len_chars()));
let line_start = rope.line_to_char(self.cursor.row);
self.cursor.col = self.cursor.offset.saturating_sub(line_start);
}
fn update_cursor_from_row_col(&mut self) {
let rope = self.buffer.rope();
let line_start = rope.line_to_char(self.cursor.row);
if let Some(line) = rope.line(self.cursor.row) {
let line_chars = line.len_chars();
let has_newline = line_chars > 0 && line.char(line_chars - 1) == '\n';
let line_len = if has_newline {
line_chars - 1
} else {
line_chars
};
self.cursor.col = self.cursor.col.min(line_len);
}
self.cursor.offset = line_start + self.cursor.col;
}
fn clamp_cursor(&mut self) {
self.cursor.offset = self.cursor.offset.min(self.buffer.len_chars());
self.update_cursor_position();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fmt::Write;
#[test]
fn test_edit_basic() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
assert_eq!(edit.text(), "Hello");
assert_eq!(edit.cursor().offset, 5);
}
#[test]
fn test_edit_delete() {
let mut edit = EditBuffer::with_text("Hello");
edit.move_to(0, 5);
edit.delete_backward();
assert_eq!(edit.text(), "Hell");
}
#[test]
fn test_edit_undo() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.insert(" World");
edit.commit();
assert_eq!(edit.text(), "Hello World");
edit.undo();
assert_eq!(edit.text(), "Hello");
edit.undo();
assert_eq!(edit.text(), "");
}
#[test]
fn test_edit_redo() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.undo();
assert_eq!(edit.text(), "");
edit.redo();
assert_eq!(edit.text(), "Hello");
}
#[test]
fn test_cursor_movement() {
let mut edit = EditBuffer::with_text("Line 1\nLine 2");
edit.move_to(0, 0);
assert_eq!(edit.cursor().row, 0);
edit.move_down();
assert_eq!(edit.cursor().row, 1);
edit.move_up();
assert_eq!(edit.cursor().row, 0);
}
#[test]
fn test_history_depth_limit() {
let mut edit = EditBuffer::with_max_history_depth(3);
assert_eq!(edit.max_history_depth(), 3);
for i in 0..5 {
edit.insert(&format!("{i}"));
edit.commit();
}
assert_eq!(edit.text(), "01234");
assert!(edit.undo()); assert!(edit.undo()); assert!(edit.undo()); assert!(!edit.undo());
assert_eq!(edit.text(), "01");
}
#[test]
fn test_set_max_history_depth() {
let mut edit = EditBuffer::new();
assert_eq!(edit.max_history_depth(), 1000);
edit.set_max_history_depth(50);
assert_eq!(edit.max_history_depth(), 50);
}
#[test]
fn test_delete_range_offsets() {
let mut edit = EditBuffer::with_text("Hello, world!");
edit.delete_range_offsets(5, 6);
assert_eq!(edit.text(), "Hello world!");
}
#[test]
fn test_delete_line() {
let mut edit = EditBuffer::with_text("Line 1\nLine 2\nLine 3");
edit.move_to(1, 0);
edit.delete_line();
assert_eq!(edit.text(), "Line 1\nLine 3");
}
#[test]
fn test_word_boundaries() {
let mut edit = EditBuffer::with_text("hello world");
edit.set_cursor_by_offset(0);
assert_eq!(edit.get_next_word_boundary(), 5);
edit.set_cursor_by_offset(6);
assert_eq!(edit.get_prev_word_boundary(), 0);
}
#[test]
fn test_move_word_right() {
let mut edit = EditBuffer::with_text("hello world test");
edit.set_cursor_by_offset(0);
edit.move_word_right();
assert_eq!(edit.cursor().offset, 5);
edit.move_word_right();
assert_eq!(edit.cursor().offset, 11);
}
#[test]
fn test_move_word_left() {
let mut edit = EditBuffer::with_text("hello world test");
edit.set_cursor_by_offset(16);
edit.move_word_left();
assert_eq!(edit.cursor().offset, 12);
edit.move_word_left();
assert_eq!(edit.cursor().offset, 6);
}
#[test]
fn test_delete_word_forward() {
let mut edit = EditBuffer::with_text("hello world");
edit.set_cursor_by_offset(0);
edit.delete_word_forward();
assert_eq!(edit.text(), " world");
}
#[test]
fn test_delete_word_backward() {
let mut edit = EditBuffer::with_text("hello world");
edit.set_cursor_by_offset(11);
edit.delete_word_backward();
assert_eq!(edit.text(), "hello ");
}
#[test]
fn test_goto_line() {
let mut edit = EditBuffer::with_text("Line 1\nLine 2\nLine 3");
edit.goto_line(2);
assert_eq!(edit.cursor().row, 2);
assert_eq!(edit.cursor().col, 0);
}
#[test]
fn test_duplicate_line() {
eprintln!("[TEST] test_duplicate_line: Testing line duplication");
let mut edit = EditBuffer::with_text("Line 1\nLine 2\nLine 3");
edit.goto_line(1);
eprintln!("[TEST] Before duplicate: {:?}", edit.text());
eprintln!("[TEST] Cursor at row: {}", edit.cursor().row);
edit.duplicate_line();
eprintln!("[TEST] After duplicate: {:?}", edit.text());
eprintln!("[TEST] Cursor at row: {}", edit.cursor().row);
assert_eq!(edit.text(), "Line 1\nLine 2\nLine 2\nLine 3");
assert_eq!(
edit.cursor().row,
2,
"Cursor should move to duplicated line"
);
eprintln!("[TEST] SUCCESS: Line duplication works correctly");
}
#[test]
fn test_duplicate_last_line() {
eprintln!(
"[TEST] test_duplicate_last_line: Testing last line duplication (no trailing newline)"
);
let mut edit = EditBuffer::with_text("Line 1\nLine 2");
edit.goto_line(1);
eprintln!("[TEST] Before duplicate: {:?}", edit.text());
edit.duplicate_line();
eprintln!("[TEST] After duplicate: {:?}", edit.text());
assert_eq!(edit.text(), "Line 1\nLine 2\nLine 2");
eprintln!("[TEST] SUCCESS: Last line duplication works correctly");
}
#[test]
fn test_move_line_up() {
eprintln!("[TEST] test_move_line_up: Testing moving line up");
let mut edit = EditBuffer::with_text("Line 1\nLine 2\nLine 3");
edit.goto_line(1);
eprintln!("[TEST] Before move: {:?}", edit.text());
eprintln!("[TEST] Cursor at row: {}", edit.cursor().row);
edit.move_line_up();
eprintln!("[TEST] After move: {:?}", edit.text());
eprintln!("[TEST] Cursor at row: {}", edit.cursor().row);
assert_eq!(edit.text(), "Line 2\nLine 1\nLine 3");
assert_eq!(edit.cursor().row, 0, "Cursor should follow the moved line");
eprintln!("[TEST] SUCCESS: Move line up works correctly");
}
#[test]
fn test_move_line_up_at_top() {
eprintln!("[TEST] test_move_line_up_at_top: Testing move line up at first line (no-op)");
let mut edit = EditBuffer::with_text("Line 1\nLine 2");
edit.goto_line(0);
edit.move_line_up();
assert_eq!(edit.text(), "Line 1\nLine 2", "Should be unchanged");
assert_eq!(edit.cursor().row, 0);
eprintln!("[TEST] SUCCESS: Move line up at top is a no-op");
}
#[test]
fn test_move_line_down() {
eprintln!("[TEST] test_move_line_down: Testing moving line down");
let mut edit = EditBuffer::with_text("Line 1\nLine 2\nLine 3");
edit.goto_line(1);
eprintln!("[TEST] Before move: {:?}", edit.text());
eprintln!("[TEST] Cursor at row: {}", edit.cursor().row);
edit.move_line_down();
eprintln!("[TEST] After move: {:?}", edit.text());
eprintln!("[TEST] Cursor at row: {}", edit.cursor().row);
assert_eq!(edit.text(), "Line 1\nLine 3\nLine 2");
assert_eq!(edit.cursor().row, 2, "Cursor should follow the moved line");
eprintln!("[TEST] SUCCESS: Move line down works correctly");
}
#[test]
fn test_move_line_down_at_bottom() {
eprintln!(
"[TEST] test_move_line_down_at_bottom: Testing move line down at last line (no-op)"
);
let mut edit = EditBuffer::with_text("Line 1\nLine 2");
edit.goto_line(1);
edit.move_line_down();
assert_eq!(edit.text(), "Line 1\nLine 2", "Should be unchanged");
assert_eq!(edit.cursor().row, 1);
eprintln!("[TEST] SUCCESS: Move line down at bottom is a no-op");
}
#[test]
fn test_line_operations_with_undo() {
eprintln!("[TEST] test_line_operations_with_undo: Testing undo for line operations");
let mut edit = EditBuffer::with_text("Line 1\nLine 2\nLine 3");
let original = edit.text().clone();
edit.goto_line(1);
edit.duplicate_line();
assert_ne!(edit.text(), original);
edit.undo();
assert_eq!(
edit.text(),
original,
"Undo should restore original after duplicate"
);
edit.goto_line(1);
edit.move_line_up();
assert_ne!(edit.text(), original);
edit.undo();
edit.undo(); assert_eq!(
edit.text(),
original,
"Undo should restore original after move up"
);
eprintln!("[TEST] SUCCESS: Undo works for line operations");
}
#[test]
fn test_delete_char_forward() {
let mut edit = EditBuffer::with_text("Hello");
edit.set_cursor_by_offset(0); edit.delete_forward();
assert_eq!(edit.text(), "ello");
assert_eq!(edit.cursor().offset, 0);
}
#[test]
fn test_delete_forward_at_end() {
let mut edit = EditBuffer::with_text("Hello");
edit.set_cursor_by_offset(5); edit.delete_forward(); assert_eq!(edit.text(), "Hello");
}
#[test]
fn test_newline_insert_splits_line() {
let mut edit = EditBuffer::with_text("HelloWorld");
edit.set_cursor_by_offset(5); edit.insert("\n");
assert_eq!(edit.text(), "Hello\nWorld");
assert_eq!(edit.cursor().row, 1);
assert_eq!(edit.cursor().col, 0);
}
#[test]
fn test_join_lines_backspace_at_start() {
let mut edit = EditBuffer::with_text("Hello\nWorld");
edit.goto_line(1);
edit.move_to_line_start();
edit.delete_backward(); assert_eq!(edit.text(), "HelloWorld");
assert_eq!(edit.cursor().offset, 5);
}
#[test]
fn test_insert_utf8_chars() {
let mut edit = EditBuffer::new();
edit.insert("日本語");
assert_eq!(edit.text(), "日本語");
assert_eq!(edit.cursor().offset, 3); }
#[test]
fn test_delete_utf8_chars() {
let mut edit = EditBuffer::with_text("日本語");
edit.set_cursor_by_offset(3); edit.delete_backward();
assert_eq!(edit.text(), "日本");
edit.delete_backward();
assert_eq!(edit.text(), "日");
edit.delete_backward();
assert_eq!(edit.text(), "");
}
#[test]
fn test_insert_emoji() {
let mut edit = EditBuffer::new();
edit.insert("🎉🚀🔥");
assert_eq!(edit.text(), "🎉🚀🔥");
assert_eq!(edit.cursor().offset, 3);
}
#[test]
fn test_cursor_at_line_start() {
let mut edit = EditBuffer::with_text("Line 1\nLine 2");
edit.move_to(1, 0);
assert_eq!(edit.cursor().col, 0);
edit.move_to_line_start();
assert_eq!(edit.cursor().col, 0);
}
#[test]
fn test_cursor_at_line_end() {
let mut edit = EditBuffer::with_text("Hello\nWorld");
edit.move_to(0, 0);
edit.move_to_line_end();
assert_eq!(edit.cursor().col, 5); }
#[test]
fn test_undo_delete_restores_text() {
let mut edit = EditBuffer::with_text("Hello World");
edit.set_cursor_by_offset(11);
edit.delete_backward(); edit.commit();
assert_eq!(edit.text(), "Hello Worl");
edit.undo();
assert_eq!(edit.text(), "Hello World");
}
#[test]
fn test_insert_at_middle() {
let mut edit = EditBuffer::with_text("HelloWorld");
edit.set_cursor_by_offset(5);
edit.insert(" ");
assert_eq!(edit.text(), "Hello World");
assert_eq!(edit.cursor().offset, 6);
}
#[test]
fn test_multiple_inserts_single_undo() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.insert(" ");
edit.insert("World");
edit.undo();
assert_eq!(edit.text(), "");
}
#[test]
fn test_clear_history() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.insert(" World");
edit.commit();
assert!(edit.can_undo());
edit.clear_history();
assert!(!edit.can_undo());
assert!(!edit.can_redo());
assert_eq!(edit.text(), "Hello World");
}
#[test]
fn test_get_eol() {
let mut edit = EditBuffer::with_text("Hello\nWorld");
edit.goto_line(0);
assert_eq!(edit.get_eol(), 5);
edit.goto_line(1);
assert_eq!(edit.get_eol(), 11); }
#[test]
fn test_move_left_right_boundaries() {
let mut edit = EditBuffer::with_text("AB");
edit.set_cursor_by_offset(0);
edit.move_left();
assert_eq!(edit.cursor().offset, 0);
edit.move_right();
assert_eq!(edit.cursor().offset, 1);
edit.move_right();
assert_eq!(edit.cursor().offset, 2);
edit.move_right();
assert_eq!(edit.cursor().offset, 2);
}
#[test]
fn test_empty_buffer_operations() {
let mut edit = EditBuffer::new();
assert_eq!(edit.text(), "");
edit.delete_backward();
edit.delete_forward();
assert_eq!(edit.text(), "");
edit.move_left();
edit.move_right();
edit.move_up();
edit.move_down();
assert_eq!(edit.cursor().offset, 0);
}
#[test]
fn test_set_text_resets_cursor() {
let mut edit = EditBuffer::with_text("Hello World");
edit.set_cursor_by_offset(6);
assert_eq!(edit.cursor().offset, 6);
edit.set_text("New");
assert_eq!(edit.text(), "New");
assert_eq!(edit.cursor().offset, 0); }
#[test]
fn test_undo_single_insert_restores_empty() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
assert_eq!(edit.text(), "Hello");
assert!(edit.undo());
assert_eq!(edit.text(), "");
}
#[test]
fn test_undo_single_delete_restores_text() {
let mut edit = EditBuffer::with_text("Hello");
edit.move_to(0, 5);
edit.delete_backward();
edit.commit();
assert_eq!(edit.text(), "Hell");
assert!(edit.undo());
assert_eq!(edit.text(), "Hello");
}
#[test]
fn test_undo_redo_roundtrip() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.undo();
assert_eq!(edit.text(), "");
edit.redo();
assert_eq!(edit.text(), "Hello");
}
#[test]
fn test_multiple_undos() {
let mut edit = EditBuffer::new();
edit.insert("A");
edit.commit();
edit.insert("B");
edit.commit();
edit.insert("C");
edit.commit();
assert_eq!(edit.text(), "ABC");
assert!(edit.undo());
assert_eq!(edit.text(), "AB");
assert!(edit.undo());
assert_eq!(edit.text(), "A");
assert!(edit.undo());
assert_eq!(edit.text(), "");
}
#[test]
fn test_multiple_redos() {
let mut edit = EditBuffer::new();
edit.insert("A");
edit.commit();
edit.insert("B");
edit.commit();
edit.insert("C");
edit.commit();
edit.undo();
edit.undo();
edit.undo();
assert_eq!(edit.text(), "");
assert!(edit.redo());
assert_eq!(edit.text(), "A");
assert!(edit.redo());
assert_eq!(edit.text(), "AB");
assert!(edit.redo());
assert_eq!(edit.text(), "ABC");
}
#[test]
fn test_commit_groups_edits() {
let mut edit = EditBuffer::new();
edit.insert("H");
edit.insert("e");
edit.insert("l");
edit.insert("l");
edit.insert("o");
edit.commit();
assert_eq!(edit.text(), "Hello");
assert!(edit.undo());
assert_eq!(edit.text(), "");
}
#[test]
fn test_empty_commit_is_harmless() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.commit();
edit.commit();
assert!(edit.undo());
assert_eq!(edit.text(), "");
}
#[test]
fn test_multiple_commit_groups() {
let mut edit = EditBuffer::new();
edit.insert("First");
edit.commit();
edit.insert(" Second");
edit.commit();
edit.insert(" Third");
edit.commit();
assert_eq!(edit.text(), "First Second Third");
edit.undo(); assert_eq!(edit.text(), "First Second");
edit.undo(); assert_eq!(edit.text(), "First");
edit.undo(); assert_eq!(edit.text(), "");
}
#[test]
fn test_can_undo_empty_history() {
let edit = EditBuffer::new();
assert!(!edit.can_undo());
}
#[test]
fn test_can_undo_after_commit() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
assert!(edit.can_undo());
}
#[test]
fn test_can_redo_empty() {
let edit = EditBuffer::new();
assert!(!edit.can_redo());
}
#[test]
fn test_can_redo_after_undo() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.undo();
assert!(edit.can_redo());
}
#[test]
fn test_cannot_redo_after_new_edit() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.undo();
assert!(edit.can_redo());
edit.insert("World");
edit.commit();
assert!(!edit.can_redo());
}
#[test]
fn test_undo_returns_false_on_empty_history() {
let mut edit = EditBuffer::new();
assert!(!edit.undo());
assert_eq!(edit.text(), "");
}
#[test]
fn test_redo_returns_false_on_empty_redo_stack() {
let mut edit = EditBuffer::new();
assert!(!edit.redo());
}
#[test]
fn test_new_edit_clears_redo_stack() {
let mut edit = EditBuffer::new();
edit.insert("A");
edit.commit();
edit.insert("B");
edit.commit();
edit.undo(); assert!(edit.can_redo());
edit.insert("C");
edit.commit();
assert!(!edit.can_redo());
assert_eq!(edit.text(), "AC");
}
#[test]
fn test_undo_multiline_delete() {
let mut edit = EditBuffer::with_text("Line1\nLine2\nLine3");
edit.set_cursor_by_offset(6); edit.delete_forward(); edit.delete_forward(); edit.delete_forward(); edit.delete_forward(); edit.delete_forward(); edit.delete_forward(); edit.commit();
assert_eq!(edit.text(), "Line1\nLine3");
edit.undo();
assert_eq!(edit.text(), "Line1\nLine2\nLine3");
}
#[test]
fn test_history_depth_limit_extended() {
let mut edit = EditBuffer::with_max_history_depth(3);
edit.insert("A");
edit.commit();
edit.insert("B");
edit.commit();
edit.insert("C");
edit.commit();
edit.insert("D");
edit.commit();
assert_eq!(edit.text(), "ABCD");
assert!(edit.undo()); assert!(edit.undo()); assert!(edit.undo()); assert!(!edit.undo());
assert_eq!(edit.text(), "A");
}
#[test]
fn test_set_max_history_depth_and_verify() {
let mut edit = EditBuffer::new();
edit.set_max_history_depth(2);
assert_eq!(edit.max_history_depth(), 2);
edit.insert("A");
edit.commit();
edit.insert("B");
edit.commit();
edit.insert("C");
edit.commit();
assert!(edit.undo());
assert!(edit.undo());
assert!(!edit.undo());
}
#[test]
fn test_clear_history_preserves_text() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
assert!(edit.can_undo());
edit.clear_history();
assert!(!edit.can_undo());
assert!(!edit.can_redo());
assert_eq!(edit.text(), "Hello"); }
#[test]
fn test_clear_history_clears_redo() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.undo();
assert!(edit.can_redo());
edit.clear_history();
assert!(!edit.can_redo());
}
#[test]
fn test_undo_delete_backward() {
let mut edit = EditBuffer::with_text("ABC");
edit.set_cursor_by_offset(3); edit.delete_backward();
edit.commit();
assert_eq!(edit.text(), "AB");
edit.undo();
assert_eq!(edit.text(), "ABC");
}
#[test]
fn test_undo_delete_forward() {
let mut edit = EditBuffer::with_text("ABC");
edit.set_cursor_by_offset(0); edit.delete_forward();
edit.commit();
assert_eq!(edit.text(), "BC");
edit.undo();
assert_eq!(edit.text(), "ABC");
}
#[test]
fn test_undo_redo_complex_sequence() {
let mut edit = EditBuffer::new();
edit.insert("Hello");
edit.commit();
edit.insert(" World");
edit.commit();
assert_eq!(edit.text(), "Hello World");
edit.undo();
assert_eq!(edit.text(), "Hello");
edit.insert(" Rust");
edit.commit();
assert_eq!(edit.text(), "Hello Rust");
assert!(!edit.can_redo());
edit.undo();
assert_eq!(edit.text(), "Hello");
edit.undo();
assert_eq!(edit.text(), "");
}
#[test]
fn test_undo_redo_preserves_text_integrity() {
let mut edit = EditBuffer::new();
let text = "The quick brown fox";
edit.insert(text);
edit.commit();
for _ in 0..5 {
edit.undo();
assert_eq!(edit.text(), "");
edit.redo();
assert_eq!(edit.text(), text);
}
}
#[test]
fn test_large_document_line_count() {
let mut lines = String::new();
for i in 0..1000 {
writeln!(&mut lines, "Line {i}").expect("write to String should not fail");
}
let mut edit = EditBuffer::with_text(&lines);
assert_eq!(edit.buffer().len_lines(), 1001);
edit.goto_line(500);
assert_eq!(edit.cursor().row, 500);
edit.insert("INSERTED");
assert!(edit.text().contains("INSERTED"));
}
#[test]
fn test_long_line_handling() {
let long_line: String = "X".repeat(1000);
let mut edit = EditBuffer::with_text(&long_line);
assert_eq!(edit.text().len(), 1000);
edit.move_to(0, 500);
assert_eq!(edit.cursor().col, 500);
edit.insert("Y");
assert_eq!(edit.text().len(), 1001);
}
#[test]
fn test_rapid_insert_delete_cycles() {
let mut edit = EditBuffer::new();
for _ in 0..100 {
edit.insert("test");
edit.delete_backward();
edit.delete_backward();
edit.delete_backward();
edit.delete_backward();
}
assert_eq!(edit.text(), "");
}
#[test]
fn test_many_undo_operations() {
let mut edit = EditBuffer::with_max_history_depth(100);
for i in 0..50 {
edit.insert(&format!("{i} "));
edit.commit();
}
let mut undo_count = 0;
while edit.undo() {
undo_count += 1;
}
assert_eq!(undo_count, 50);
assert_eq!(edit.text(), "");
}
#[test]
fn test_undo_after_clear() {
let mut edit = EditBuffer::with_text("Hello World");
edit.commit();
let len = edit.text().len();
edit.delete_range_offsets(0, len);
edit.commit();
assert_eq!(edit.text(), "");
edit.undo();
assert_eq!(edit.text(), "Hello World");
}
#[test]
fn test_redo_invalidated_by_new_edit() {
let mut edit = EditBuffer::new();
edit.insert("A");
edit.commit();
edit.insert("B");
edit.commit();
edit.undo(); assert!(edit.can_redo());
edit.insert("C"); assert!(!edit.can_redo());
assert_eq!(edit.text(), "AC");
}
#[test]
fn test_cursor_through_emoji() {
let mut edit = EditBuffer::with_text("A👨👩👧B");
edit.set_cursor_by_offset(0);
assert_eq!(edit.cursor().offset, 0);
edit.move_right(); let offset_after_a = edit.cursor().offset;
assert_eq!(offset_after_a, 1);
edit.move_right(); let offset_after_emoji = edit.cursor().offset;
assert!(
offset_after_emoji > offset_after_a,
"Should have moved past emoji"
);
edit.move_right(); }
#[test]
fn test_delete_in_grapheme_cluster() {
let mut edit = EditBuffer::with_text("👨👩👧");
let len = edit.text().len();
edit.set_cursor_by_offset(len); edit.delete_backward();
assert!(
edit.text().is_empty() || !edit.text().contains("👨👩👧"),
"Grapheme cluster should be deleted as unit"
);
}
#[test]
fn test_unicode_combining_characters() {
let mut edit = EditBuffer::with_text("e\u{0301}");
let end_offset = edit.text().len();
edit.set_cursor_by_offset(end_offset);
edit.delete_backward();
assert!(
edit.text().len() < end_offset,
"Should have deleted combining sequence"
);
}
#[test]
fn test_cursor_at_document_end() {
let mut edit = EditBuffer::with_text("Hello");
edit.set_cursor_by_offset(edit.text().len()); assert_eq!(edit.cursor().offset, 5);
edit.move_right();
assert_eq!(edit.cursor().offset, 5);
edit.move_right();
assert_eq!(edit.cursor().offset, 5);
}
#[test]
fn test_cursor_at_document_start() {
let mut edit = EditBuffer::with_text("Hello");
edit.set_cursor_by_offset(0);
assert_eq!(edit.cursor().offset, 0);
edit.move_left();
assert_eq!(edit.cursor().offset, 0);
edit.move_left();
assert_eq!(edit.cursor().offset, 0);
}
#[test]
fn test_cursor_through_empty_lines() {
let mut edit = EditBuffer::with_text("Line1\n\n\nLine4");
edit.goto_line(0);
edit.move_down(); assert_eq!(edit.cursor().row, 1);
assert_eq!(edit.cursor().col, 0);
edit.move_down(); assert_eq!(edit.cursor().row, 2);
assert_eq!(edit.cursor().col, 0);
edit.move_down(); assert_eq!(edit.cursor().row, 3);
}
#[test]
fn test_move_up_at_first_line() {
let mut edit = EditBuffer::with_text("First line\nSecond line");
edit.goto_line(0);
edit.move_up();
assert_eq!(edit.cursor().row, 0);
}
#[test]
fn test_move_down_at_last_line() {
let mut edit = EditBuffer::with_text("First line\nLast line");
edit.goto_line(1);
edit.move_down();
assert_eq!(edit.cursor().row, 1);
}
#[test]
fn test_delete_empty_selection() {
let mut edit = EditBuffer::with_text("Hello");
edit.delete_range_offsets(2, 2);
assert_eq!(edit.text(), "Hello");
}
#[test]
fn test_selection_across_line_boundaries() {
let mut edit = EditBuffer::with_text("Line 1\nLine 2\nLine 3");
edit.delete_range_offsets(3, 17);
assert_eq!(edit.text(), "Line 3");
}
#[test]
fn test_operations_on_empty_buffer() {
let mut edit = EditBuffer::new();
edit.delete_backward();
edit.delete_forward();
edit.move_right();
edit.move_left();
edit.move_up();
edit.move_down();
edit.move_word_left();
edit.move_word_right();
assert_eq!(edit.text(), "");
assert_eq!(edit.cursor().offset, 0);
}
#[test]
fn test_undo_on_empty_buffer() {
let mut edit = EditBuffer::new();
assert!(!edit.undo());
assert!(!edit.redo());
}
#[test]
fn test_delete_at_line_start() {
let mut edit = EditBuffer::with_text("Line 1\nLine 2");
edit.goto_line(1);
edit.move_to_line_start();
edit.delete_backward();
assert_eq!(edit.text(), "Line 1Line 2");
}
#[test]
fn test_delete_at_line_end() {
let mut edit = EditBuffer::with_text("Line 1\nLine 2");
edit.goto_line(0);
edit.move_to_line_end();
edit.delete_forward();
assert_eq!(edit.text(), "Line 1Line 2");
}
}