use crate::buffer::TextBuffer;
use crate::coordinate::{CoordinateMapper, LspPosition};
use crate::edit::{EditHistory, EditTransaction, TextEdit};
use crate::line_index::{LineCol, LineIndex};
#[test]
fn empty_buffer() {
let buf = TextBuffer::new();
assert_eq!(buf.len_bytes(), 0);
assert_eq!(buf.len_chars(), 0);
assert_eq!(buf.len_lines(), 1); assert_eq!(buf.revision(), 0);
assert!(buf.is_empty());
assert_eq!(buf.to_string(), "");
}
#[test]
fn from_str_basic() {
let buf = TextBuffer::from_str("hello\nworld\n");
assert_eq!(buf.len_bytes(), 12);
assert_eq!(buf.len_chars(), 12);
assert_eq!(buf.len_lines(), 3); assert_eq!(buf.revision(), 0);
assert!(!buf.is_empty());
}
#[test]
fn insert_at_start() {
let mut buf = TextBuffer::from_str("world");
buf.insert(0, "hello ");
assert_eq!(buf.to_string(), "hello world");
assert_eq!(buf.revision(), 1);
}
#[test]
fn insert_at_end() {
let mut buf = TextBuffer::from_str("hello");
buf.insert(5, " world");
assert_eq!(buf.to_string(), "hello world");
assert_eq!(buf.revision(), 1);
}
#[test]
fn insert_in_middle() {
let mut buf = TextBuffer::from_str("helo");
buf.insert(3, "l");
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn delete_range() {
let mut buf = TextBuffer::from_str("hello world");
buf.delete(5..11); assert_eq!(buf.to_string(), "hello");
assert_eq!(buf.revision(), 1);
}
#[test]
fn delete_all() {
let mut buf = TextBuffer::from_str("abc");
buf.delete(0..3);
assert_eq!(buf.to_string(), "");
assert!(buf.is_empty());
}
#[test]
fn replace_same_length() {
let mut buf = TextBuffer::from_str("hello world");
buf.replace(6..11, "earth");
assert_eq!(buf.to_string(), "hello earth");
assert_eq!(buf.revision(), 1);
}
#[test]
fn replace_shorter() {
let mut buf = TextBuffer::from_str("hello world");
buf.replace(5..11, "!");
assert_eq!(buf.to_string(), "hello!");
}
#[test]
fn replace_longer() {
let mut buf = TextBuffer::from_str("hello!");
buf.replace(5..6, " beautiful world!");
assert_eq!(buf.to_string(), "hello beautiful world!");
}
#[test]
fn line_contents() {
let buf = TextBuffer::from_str("aaa\nbbb\nccc");
assert_eq!(buf.line(0).to_string(), "aaa\n");
assert_eq!(buf.line(1).to_string(), "bbb\n");
assert_eq!(buf.line(2).to_string(), "ccc");
}
#[test]
fn slice_range() {
let buf = TextBuffer::from_str("hello world");
let s = buf.slice(6..11);
assert_eq!(s.to_string(), "world");
}
#[test]
fn revision_increments() {
let mut buf = TextBuffer::new();
assert_eq!(buf.revision(), 0);
buf.insert(0, "a");
assert_eq!(buf.revision(), 1);
buf.insert(1, "b");
assert_eq!(buf.revision(), 2);
buf.delete(0..1);
assert_eq!(buf.revision(), 3);
buf.replace(0..1, "x");
assert_eq!(buf.revision(), 4);
}
#[test]
fn utf8_multibyte_insert() {
let mut buf = TextBuffer::from_str("cafe");
buf.replace(3..4, "\u{00E9}");
assert_eq!(buf.to_string(), "caf\u{00E9}");
assert_eq!(buf.len_bytes(), 5); assert_eq!(buf.len_chars(), 4);
}
#[test]
fn utf8_emoji() {
let buf = TextBuffer::from_str("hi \u{1F600}!"); assert_eq!(buf.len_bytes(), 8); assert_eq!(buf.len_chars(), 5);
}
#[test]
fn utf8_cjk() {
let buf = TextBuffer::from_str("\u{4F60}\u{597D}"); assert_eq!(buf.len_bytes(), 6);
assert_eq!(buf.len_chars(), 2);
}
#[test]
fn line_index_single_line() {
let idx = LineIndex::build_from_str("hello");
assert_eq!(idx.line_count(), 1);
assert_eq!(idx.line_start_byte(0), Some(0));
assert_eq!(idx.line_end_byte(0), Some(5));
}
#[test]
fn line_index_multiple_lines() {
let idx = LineIndex::build_from_str("aaa\nbbb\nccc\n");
assert_eq!(idx.line_count(), 4); assert_eq!(idx.line_start_byte(0), Some(0));
assert_eq!(idx.line_start_byte(1), Some(4));
assert_eq!(idx.line_start_byte(2), Some(8));
assert_eq!(idx.line_start_byte(3), Some(12));
}
#[test]
fn line_index_round_trip() {
let text = "fn main() {\n println!(\"hi\");\n}\n";
let idx = LineIndex::build_from_str(text);
for byte_offset in 0..text.len() {
if !text.is_char_boundary(byte_offset) {
continue;
}
let lc = idx.byte_to_line_col(byte_offset).unwrap();
let back = idx.line_col_to_byte(lc).unwrap();
assert_eq!(back, byte_offset, "round-trip failed at byte {byte_offset}");
}
}
#[test]
fn line_index_empty() {
let idx = LineIndex::build_from_str("");
assert_eq!(idx.line_count(), 1);
assert_eq!(idx.line_start_byte(0), Some(0));
assert_eq!(idx.line_end_byte(0), Some(0));
assert_eq!(idx.byte_to_line_col(0), Some(LineCol { line: 0, col: 0 }));
}
#[test]
fn line_index_out_of_bounds() {
let idx = LineIndex::build_from_str("abc");
assert_eq!(idx.line_start_byte(1), None);
assert_eq!(idx.line_col_to_byte(LineCol { line: 5, col: 0 }), None);
assert_eq!(idx.byte_to_line_col(100), None);
}
#[test]
fn utf16_ascii_identity() {
let idx = LineIndex::build_from_str("hello\nworld");
assert_eq!(idx.utf16_col_to_byte(0, 3), Some(3));
assert_eq!(idx.utf16_col_to_byte(1, 2), Some(8)); }
#[test]
fn utf16_bmp_char() {
let idx = LineIndex::build_from_str("caf\u{00E9}!");
assert_eq!(idx.utf16_col_to_byte(0, 4), Some(5));
}
#[test]
fn utf16_supplementary_char() {
let idx = LineIndex::build_from_str("a\u{1F600}b");
assert_eq!(idx.utf16_col_to_byte(0, 0), Some(0)); assert_eq!(idx.utf16_col_to_byte(0, 1), Some(1)); assert_eq!(idx.utf16_col_to_byte(0, 3), Some(5)); }
#[test]
fn utf16_round_trip() {
let text = "a\u{00E9}\u{1F600}z";
let idx = LineIndex::build_from_str(text);
for (byte_idx, _ch) in text.char_indices() {
let (line, u16col) = idx.byte_to_utf16_col(byte_idx).unwrap();
let back = idx.utf16_col_to_byte(line, u16col).unwrap();
assert_eq!(back, byte_idx, "utf16 round-trip failed at byte {byte_idx}");
}
}
#[test]
fn coord_mapper_byte_to_lsp() {
let text = "a\u{1F600}b\ncd";
let idx = LineIndex::build_from_str(text);
let m = CoordinateMapper::new(&idx);
assert_eq!(m.byte_to_lsp(0), Some(LspPosition::new(0, 0)));
assert_eq!(m.byte_to_lsp(1), Some(LspPosition::new(0, 1)));
assert_eq!(m.byte_to_lsp(5), Some(LspPosition::new(0, 3)));
assert_eq!(m.byte_to_lsp(7), Some(LspPosition::new(1, 0)));
assert_eq!(m.byte_to_lsp(8), Some(LspPosition::new(1, 1)));
}
#[test]
fn coord_mapper_lsp_to_byte() {
let text = "a\u{1F600}b\ncd";
let idx = LineIndex::build_from_str(text);
let m = CoordinateMapper::new(&idx);
assert_eq!(m.lsp_to_byte(LspPosition::new(0, 0)), Some(0));
assert_eq!(m.lsp_to_byte(LspPosition::new(0, 3)), Some(5));
assert_eq!(m.lsp_to_byte(LspPosition::new(1, 0)), Some(7));
assert_eq!(m.lsp_to_byte(LspPosition::new(1, 1)), Some(8));
}
#[test]
fn coord_mapper_line_col_to_lsp_and_back() {
let text = "hi\u{00E9}!\nok";
let idx = LineIndex::build_from_str(text);
let m = CoordinateMapper::new(&idx);
let lc = LineCol { line: 0, col: 4 }; let lsp = m.line_col_to_lsp(lc).unwrap();
assert_eq!(lsp, LspPosition::new(0, 3)); let back = m.lsp_to_line_col(lsp).unwrap();
assert_eq!(back, lc);
}
#[test]
fn single_undo() {
let mut buf = TextBuffer::from_str("hello");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 5..5, " world");
assert_eq!(buf.to_string(), "hello world");
assert!(hist.undo(&mut buf));
assert_eq!(buf.to_string(), "hello");
}
#[test]
fn single_redo() {
let mut buf = TextBuffer::from_str("hello");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 5..5, " world");
hist.undo(&mut buf);
assert!(hist.redo(&mut buf));
assert_eq!(buf.to_string(), "hello world");
}
#[test]
fn undo_redo_nothing() {
let mut buf = TextBuffer::from_str("hello");
let mut hist = EditHistory::new();
assert!(!hist.undo(&mut buf));
assert!(!hist.redo(&mut buf));
}
#[test]
fn multiple_undo_redo() {
let mut buf = TextBuffer::from_str("");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 0..0, "a");
hist.apply_edit(&mut buf, 1..1, "b");
hist.apply_edit(&mut buf, 2..2, "c");
assert_eq!(buf.to_string(), "abc");
hist.undo(&mut buf);
assert_eq!(buf.to_string(), "ab");
hist.undo(&mut buf);
assert_eq!(buf.to_string(), "a");
hist.undo(&mut buf);
assert_eq!(buf.to_string(), "");
hist.redo(&mut buf);
assert_eq!(buf.to_string(), "a");
hist.redo(&mut buf);
assert_eq!(buf.to_string(), "ab");
hist.redo(&mut buf);
assert_eq!(buf.to_string(), "abc");
}
#[test]
fn redo_cleared_on_new_edit() {
let mut buf = TextBuffer::from_str("ab");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 2..2, "c");
hist.undo(&mut buf);
assert_eq!(hist.redo_depth(), 1);
hist.apply_edit(&mut buf, 2..2, "d");
assert_eq!(hist.redo_depth(), 0);
assert_eq!(buf.to_string(), "abd");
}
#[test]
fn undo_replace() {
let mut buf = TextBuffer::from_str("hello world");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 6..11, "earth");
assert_eq!(buf.to_string(), "hello earth");
hist.undo(&mut buf);
assert_eq!(buf.to_string(), "hello world");
}
#[test]
fn undo_delete() {
let mut buf = TextBuffer::from_str("abcdef");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 2..4, ""); assert_eq!(buf.to_string(), "abef");
hist.undo(&mut buf);
assert_eq!(buf.to_string(), "abcdef");
}
#[test]
fn transaction_group() {
let mut buf = TextBuffer::from_str("aaabbbccc");
let mut hist = EditHistory::new();
let mut txn = EditTransaction::new();
txn.push(TextEdit::new(6..9, "C", "ccc"));
txn.push(TextEdit::new(0..3, "A", "aaa"));
hist.apply(&txn, &mut buf);
assert_eq!(buf.to_string(), "AbbbC");
hist.undo(&mut buf);
assert_eq!(buf.to_string(), "aaabbbccc");
}
#[test]
fn history_depth() {
let mut buf = TextBuffer::from_str("");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 0..0, "a");
hist.apply_edit(&mut buf, 1..1, "b");
assert_eq!(hist.undo_depth(), 2);
assert_eq!(hist.redo_depth(), 0);
hist.undo(&mut buf);
assert_eq!(hist.undo_depth(), 1);
assert_eq!(hist.redo_depth(), 1);
}
#[test]
fn history_cap() {
let mut buf = TextBuffer::from_str("");
let mut hist = EditHistory::with_max(3);
for i in 0..5 {
let s = format!("{}", i);
let len = buf.len_bytes();
hist.apply_edit(&mut buf, len..len, &s);
}
assert_eq!(buf.to_string(), "01234");
assert_eq!(hist.undo_depth(), 3);
hist.undo(&mut buf);
hist.undo(&mut buf);
hist.undo(&mut buf);
assert_eq!(buf.to_string(), "01");
assert!(!hist.undo(&mut buf));
}
#[test]
fn history_clear() {
let mut buf = TextBuffer::from_str("");
let mut hist = EditHistory::new();
hist.apply_edit(&mut buf, 0..0, "a");
hist.undo(&mut buf);
assert_eq!(hist.undo_depth(), 0);
assert_eq!(hist.redo_depth(), 1);
hist.clear();
assert_eq!(hist.undo_depth(), 0);
assert_eq!(hist.redo_depth(), 0);
}
#[test]
fn large_file_insert_and_query() {
let line = "the quick brown fox jumps over the lazy dog\n";
let line_count = 100_000;
let text: String = line.repeat(line_count);
let buf = TextBuffer::from_str(&text);
assert_eq!(buf.len_lines(), line_count + 1);
let mid = line_count / 2;
let l = buf.line(mid);
assert_eq!(l.to_string(), line);
let idx = LineIndex::build(buf.text());
assert_eq!(idx.line_count(), line_count + 1);
let start = idx.line_start_byte(mid).unwrap();
let end = idx.line_end_byte(mid).unwrap();
assert_eq!(end - start, line.len());
}
#[test]
fn large_file_edit_in_middle() {
let line = "abcdefghij\n";
let n = 10_000;
let text: String = line.repeat(n);
let mut buf = TextBuffer::from_str(&text);
let mid_byte = line.len() * (n / 2);
buf.replace(mid_byte..mid_byte + line.len(), "REPLACED\n");
let replaced_line = buf.line(n / 2);
assert_eq!(replaced_line.to_string(), "REPLACED\n");
assert_eq!(buf.len_lines(), n + 1);
}
#[test]
fn buffer_default_is_empty() {
let buf = TextBuffer::default();
assert!(buf.is_empty());
}
#[test]
fn insert_into_empty() {
let mut buf = TextBuffer::new();
buf.insert(0, "hi");
assert_eq!(buf.to_string(), "hi");
}
#[test]
fn delete_empty_range_is_noop_content() {
let mut buf = TextBuffer::from_str("hello");
buf.delete(2..2);
assert_eq!(buf.to_string(), "hello");
assert_eq!(buf.revision(), 1);
}
#[test]
fn only_newlines() {
let buf = TextBuffer::from_str("\n\n\n");
assert_eq!(buf.len_lines(), 4);
assert_eq!(buf.line(0).to_string(), "\n");
assert_eq!(buf.line(3).to_string(), "");
}
#[test]
fn windows_line_endings() {
let buf = TextBuffer::from_str("a\r\nb\r\n");
assert_eq!(buf.len_lines(), 3);
}
#[test]
fn line_index_windows_crlf() {
let idx = LineIndex::build_from_str("ab\r\ncd\r\n");
assert_eq!(idx.line_count(), 3);
assert_eq!(idx.line_start_byte(0), Some(0));
assert_eq!(idx.line_start_byte(1), Some(4));
assert_eq!(idx.line_start_byte(2), Some(8));
}