use piece_tree::{PieceTree, ReverseTreeWalker, TreeWalker};
#[cfg(test)]
mod tests_char_api {
use super::*;
#[test]
fn test_lines_iterator() {
let mut tree = PieceTree::new();
tree.insert(0, "First line\nSecond line\nThird line");
assert_eq!(tree.len_lines(), 3);
let mut lines = tree.lines();
assert_eq!(lines.next().unwrap(), "First line\n");
assert_eq!(lines.next().unwrap(), "Second line\n");
assert_eq!(lines.next().unwrap(), "Third line");
assert!(lines.next().is_none());
}
#[test]
fn test_chars_iterator() {
let mut tree = PieceTree::new();
tree.insert(0, "abc 🦀");
let chars: Vec<char> = tree.chars().collect();
assert_eq!(chars, vec!['a', 'b', 'c', ' ', '🦀']);
}
#[test]
fn test_tree_walker_utf8() {
let mut tree = PieceTree::new();
tree.insert(0, "Rust 🦀");
tree.insert(9, " is fast.");
let mut walker = TreeWalker::new(&tree);
assert_eq!(walker.next(), Some('R'));
walker.seek(5);
assert_eq!(walker.next(), Some('🦀'));
assert_eq!(walker.offset, 9); assert_eq!(walker.next(), Some(' '));
let full_text: String = TreeWalker::new(&tree).collect();
assert_eq!(full_text, "Rust 🦀 is fast.");
}
#[test]
fn test_line_col_resolution() {
let mut tree = PieceTree::new();
tree.insert(0, "Line 0\nLine 1 🦀\nLine 2");
assert_eq!(tree.line_to_offset(0), Some(0));
assert_eq!(tree.line_to_offset(1), Some(7));
assert_eq!(tree.line_to_offset(2), Some(19));
assert_eq!(tree.offset_to_line_col(0), Some((0, 0)));
assert_eq!(tree.offset_to_line_col(3), Some((0, 3)));
assert_eq!(tree.offset_to_line_col(14), Some((1, 7)));
assert_eq!(tree.offset_to_line_col(18), Some((1, 8)));
assert_eq!(tree.offset_to_line_col(25), Some((2, 6)));
}
#[test]
fn test_garbage_collection() {
let mut tree = PieceTree::new();
tree.insert(0, "A");
let initial_node_count = tree.pieces.nodes.len();
for _ in 0..100 {
tree.insert(1, "X");
tree.remove_at(1, 1);
}
let bloated_node_count = tree.pieces.nodes.len();
assert!(bloated_node_count > initial_node_count + 100);
tree.compact();
let compacted_node_count = tree.pieces.nodes.len();
assert!(compacted_node_count < bloated_node_count);
assert_eq!(tree.to_string(), "A");
tree.try_undo(0);
assert_eq!(tree.to_string(), "AX"); }
#[test]
fn test_reverse_walker() {
let mut tree = PieceTree::new();
tree.insert(0, "Hello");
tree.insert(5, " World");
let mut walker = ReverseTreeWalker::new(&tree);
assert_eq!(walker.next(), Some('d'));
assert_eq!(walker.next(), Some('l'));
walker.seek(5);
assert_eq!(walker.next(), Some('o'));
assert_eq!(walker.next(), Some('l'));
}
#[test]
fn test_line_extractors() {
let mut tree = PieceTree::new();
tree.insert(0, "Line 0\nLine 1\nLine 2");
assert_eq!(tree.get_line_range(1), Some((7, 14)));
assert_eq!(tree.get_line_content_allocating(0).unwrap(), "Line 0\n");
assert_eq!(tree.get_line_content_allocating(1).unwrap(), "Line 1\n");
assert_eq!(tree.get_line_content_allocating(2).unwrap(), "Line 2");
}
#[test]
fn test_range_removal() {
let mut tree = PieceTree::new();
tree.insert(0, "The quick brown fox");
tree.remove(4..10);
assert_eq!(tree.to_string(), "The brown fox");
tree.remove(4..=9);
assert_eq!(tree.to_string(), "The fox");
tree.remove(3..);
assert_eq!(tree.to_string(), "The");
}
#[test]
fn test_char_and_byte_conversion() {
let mut tree = PieceTree::new();
tree.insert(0, "a🦀cé");
assert_eq!(tree.len_chars(), 4);
assert_eq!(tree.len_bytes(), 8);
assert_eq!(tree.byte_to_char(0), Some(0)); assert_eq!(tree.byte_to_char(1), Some(1)); assert_eq!(tree.byte_to_char(5), Some(2)); assert_eq!(tree.byte_to_char(6), Some(3)); assert_eq!(tree.byte_to_char(8), Some(4));
assert_eq!(tree.char_to_byte(0), Some(0));
assert_eq!(tree.char_to_byte(1), Some(1));
assert_eq!(tree.char_to_byte(2), Some(5));
assert_eq!(tree.char_to_byte(3), Some(6));
assert_eq!(tree.char_to_byte(4), Some(8));
}
#[test]
fn test_optimized_offset_col() {
let mut tree = PieceTree::new();
tree.insert(0, "L0\nL1 🦀\nL2");
let pos = tree.offset_to_line_col(10).unwrap();
assert_eq!(pos, (1, 4)); }
}
#[cfg(test)]
mod tests_fredbuf {
use super::*;
#[test]
fn test_fredbuf_test3_fragments_and_lines() {
let mut tree = PieceTree::new();
tree.insert_no_commit(tree.total_length(), "Hello");
tree.insert_no_commit(tree.total_length(), ",");
tree.insert_no_commit(tree.total_length(), " ");
tree.insert_no_commit(tree.total_length(), "World");
tree.insert_no_commit(tree.total_length(), "!");
tree.insert_no_commit(tree.total_length(), "\nThis is a second line.");
tree.insert_no_commit(tree.total_length(), " Continue...\nANOTHER!");
assert_eq!(tree.get_line_content_allocating(0).unwrap(), "Hello, World!\n");
assert_eq!(tree.get_line_content_allocating(1).unwrap(), "This is a second line. Continue...\n");
assert_eq!(tree.get_line_content_allocating(2).unwrap(), "ANOTHER!");
tree.insert(37, "Hello");
tree.remove_at(13, 5); tree.remove_at(37, 5);
tree.insert(tree.total_length(), "a");
tree.insert(tree.total_length(), "a");
tree.insert(tree.total_length(), "a");
tree.insert(tree.total_length(), "a");
tree.insert(tree.total_length(), "END!!");
tree.remove_at(52, 4);
tree.insert(tree.total_length(), "\nfoobar\nnext\nnextnext\nnextnextnext");
tree.insert(tree.total_length(), "\nfoobar2\nnext\nnextnext\nnextnextnext");
assert_eq!(tree.get_line_content_allocating(99), None);
}
#[test]
fn test_fredbuf_test4_boundary_removal() {
let mut tree = PieceTree::new();
tree.insert_no_commit(0, "ABCD");
tree.insert(4, "a");
assert_eq!(tree.to_string(), "ABCDa");
tree.remove_at(3, 2);
assert_eq!(tree.to_string(), "ABC");
}
#[test]
fn test_fredbuf_test5_empty_buffer_lifecycle() {
let mut tree = PieceTree::new();
tree.insert(0, "a");
assert_eq!(tree.to_string(), "a");
tree.remove_at(0, 1);
assert_eq!(tree.to_string(), "");
}
#[test]
fn test_fredbuf_test8_suppress_history() {
let mut tree = PieceTree::new();
tree.insert_no_commit(0, "Hello, World!");
tree.insert_no_commit(0, "a");
assert_eq!(tree.to_string(), "aHello, World!");
assert!(tree.try_undo(0).is_none());
tree.remove_no_commit(0, 1); assert_eq!(tree.to_string(), "Hello, World!");
assert!(tree.try_undo(0).is_none());
tree.commit_head(0);
tree.insert_no_commit(0, "a");
tree.insert_no_commit(1, "b");
tree.insert_no_commit(2, "c");
assert_eq!(tree.to_string(), "abcHello, World!");
assert!(tree.try_undo(0).is_some());
assert_eq!(tree.to_string(), "Hello, World!");
tree.commit_head(0);
tree.remove_no_commit(0, 7);
assert_eq!(tree.to_string(), "World!");
tree.remove_no_commit(5, 1);
assert_eq!(tree.to_string(), "World");
assert!(tree.try_undo(0).is_some());
assert_eq!(tree.to_string(), "Hello, World!");
assert!(tree.try_redo(0).is_some());
assert_eq!(tree.to_string(), "World");
}
#[test]
fn test_fredbuf_test9_branching_and_snap_to() {
let mut tree = PieceTree::new();
tree.insert_no_commit(0, "Hello, World!");
let initial_commit = tree.root;
tree.insert_no_commit(0, "a");
assert_eq!(tree.to_string(), "aHello, World!");
assert!(tree.try_undo(0).is_none());
let commit = tree.root;
tree.root = initial_commit;
assert_eq!(tree.to_string(), "Hello, World!");
tree.root = commit;
assert_eq!(tree.to_string(), "aHello, World!");
tree.remove_no_commit(0, 8);
assert_eq!(tree.to_string(), "World!");
tree.root = commit;
assert_eq!(tree.to_string(), "aHello, World!");
tree.root = initial_commit;
assert_eq!(tree.to_string(), "Hello, World!");
tree.insert_no_commit(13, " My name is fredbuf.");
assert_eq!(tree.to_string(), "Hello, World! My name is fredbuf.");
let branch = tree.root;
tree.root = commit;
assert_eq!(tree.to_string(), "aHello, World!");
tree.root = branch;
assert_eq!(tree.to_string(), "Hello, World! My name is fredbuf.");
}
}
#[cfg(test)]
mod tests_stress {
use piece_tree::{Edit, NIL, NodeRef};
use super::*;
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self { Self { state: seed } }
fn next(&mut self) -> u64 {
self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
self.state
}
fn next_range(&mut self, max: u32) -> u32 {
(self.next() % (max as u64)) as u32
}
}
fn get_max_depth(tree: &PieceTree, node: NodeRef) -> usize {
if node == NIL {
return 0;
}
let n = tree.pieces.get(node);
1 + core::cmp::max(
get_max_depth(tree, n.left),
get_max_depth(tree, n.right),
)
}
#[test]
fn test_massive_append_and_metrics() {
let mut tree = PieceTree::new();
let iters = 50_000;
for _ in 0..iters {
let len = tree.len_bytes();
tree.insert(len, "A");
}
assert_eq!(tree.len_bytes(), iters);
assert_eq!(tree.len_chars(), iters);
assert_eq!(tree.len_lines(), 1);
for _ in 0..(iters / 2) {
tree.remove_at(0, 1);
}
assert_eq!(tree.len_bytes(), iters / 2);
}
#[test]
fn test_fuzz_random_edits() {
let mut tree = PieceTree::new();
tree.insert(0, "INITIAL");
let mut rng = Lcg::new(42);
let mut expected_len = 7;
for _ in 0..10_000 {
let op = rng.next() % 3;
let current_len = tree.len_bytes();
if op < 2 || current_len == 0 {
let offset = rng.next_range(current_len + 1);
tree.insert(offset, "X");
expected_len += 1;
} else {
let offset = rng.next_range(current_len);
let remove_len = core::cmp::min(3, current_len - offset);
tree.remove_at(offset, remove_len);
expected_len -= remove_len;
}
if tree.undo_stack.len() > 100 {
tree.undo_stack.drain(0..50);
}
if tree.pieces.nodes.len() > 20_000 {
tree.compact();
}
}
assert_eq!(tree.len_bytes(), expected_len);
assert!(tree.pieces.nodes.len() < 30_000);
}
#[test]
fn test_deep_history_yoyo() {
let mut tree = PieceTree::new();
let commits = 10_000;
for _ in 0..commits {
tree.insert(tree.len_bytes(), "A");
}
assert_eq!(tree.len_bytes(), commits);
assert_eq!(tree.undo_stack.len() as u32, commits);
assert!(tree.redo_stack.is_empty());
for _ in 0..(commits / 2) {
tree.try_undo(0).unwrap();
}
assert_eq!(tree.len_bytes(), commits / 2);
assert_eq!(tree.undo_stack.len() as u32, commits / 2);
assert_eq!(tree.redo_stack.len() as u32, commits / 2);
for _ in 0..(commits / 4) {
tree.try_redo(0).unwrap();
}
assert_eq!(tree.len_bytes(), (commits / 2) + (commits / 4));
assert_eq!(tree.undo_stack.len() as u32, (commits / 2) + (commits / 4));
assert_eq!(tree.redo_stack.len() as u32, commits / 4);
}
#[test]
fn test_history_branching_timeline_burn() {
let mut tree = PieceTree::new();
tree.insert(0, "A");
tree.insert(1, "B");
tree.insert(2, "C");
tree.insert(3, "D");
tree.insert(4, "E");
assert_eq!(tree.to_string(), "ABCDE");
assert_eq!(tree.undo_stack.len(), 5);
tree.try_undo(0);
tree.try_undo(0);
tree.try_undo(0);
assert_eq!(tree.to_string(), "AB");
assert_eq!(tree.undo_stack.len(), 2);
assert_eq!(tree.redo_stack.len(), 3);
tree.insert(2, "X");
assert_eq!(tree.to_string(), "ABX");
assert_eq!(tree.undo_stack.len(), 3);
assert!(tree.redo_stack.is_empty(), "Redo stack was not cleared on diverging edit!");
assert!(tree.try_redo(0).is_none());
tree.try_undo(0);
assert_eq!(tree.to_string(), "AB");
tree.try_undo(0);
assert_eq!(tree.to_string(), "A");
tree.try_undo(0);
assert_eq!(tree.to_string(), "");
}
#[test]
fn test_transaction_batching_undo() {
let mut tree = PieceTree::new();
tree.insert(0, "Line 1\nLine 2\nLine 3");
let pre_tx_undos = tree.undo_stack.len();
let mut edits = vec![
Edit::Insert { offset: 0, text: "> " },
Edit::Insert { offset: 7, text: "> " },
Edit::Insert { offset: 14, text: "> " },
];
tree.apply_edits(0, &mut edits);
assert_eq!(tree.to_string(), "> Line 1\n> Line 2\n> Line 3");
assert_eq!(tree.undo_stack.len(), pre_tx_undos + 1);
tree.try_undo(0);
assert_eq!(tree.to_string(), "Line 1\nLine 2\nLine 3");
}
#[test]
fn test_compact_preserves_history() {
let mut tree = PieceTree::new();
tree.insert(0, "Phase 1");
tree.insert(7, " -> Phase 2");
tree.remove_at(0, 5);
tree.insert(0, "Step");
assert_eq!(tree.to_string(), "Step 1 -> Phase 2");
let active_nodes_before = tree.pieces.nodes.len();
tree.compact();
let active_nodes_after = tree.pieces.nodes.len();
assert!(active_nodes_after < active_nodes_before, "Compaction didn't reclaim nodes");
tree.try_undo(0);
assert_eq!(tree.to_string(), " 1 -> Phase 2");
tree.try_undo(0);
assert_eq!(tree.to_string(), "Phase 1 -> Phase 2");
tree.try_redo(0);
tree.try_redo(0);
assert_eq!(tree.to_string(), "Step 1 -> Phase 2");
}
#[test]
fn test_diabolical_out_of_bounds_and_empty() {
let mut tree = PieceTree::new();
tree.remove_at(0, 100);
assert_eq!(tree.to_string(), "");
tree.insert(0, "");
assert_eq!(tree.len_bytes(), 0);
tree.insert(0, "A");
tree.remove_at(0, 1000);
assert_eq!(tree.to_string(), "");
tree.insert(0, "123456789");
let mut edits = vec![
Edit::Remove { offset: 2, length: 4 }, Edit::Remove { offset: 4, length: 4 }, ];
tree.apply_edits(0, &mut edits);
}
#[test]
fn test_diabolical_history_yoyo() {
let mut tree = PieceTree::new();
tree.insert(0, "A"); tree.insert(1, "B"); tree.insert(2, "C");
assert_eq!(tree.to_string(), "ABC");
tree.try_undo(0);
assert_eq!(tree.to_string(), "AB");
tree.try_undo(0);
assert_eq!(tree.to_string(), "A");
tree.try_undo(0);
assert_eq!(tree.to_string(), "");
tree.try_redo(0);
assert_eq!(tree.to_string(), "A");
tree.try_redo(0);
assert_eq!(tree.to_string(), "AB");
tree.try_redo(0);
assert_eq!(tree.to_string(), "ABC");
tree.try_undo(0); tree.insert(2, "X");
assert!(tree.try_redo(0).is_none());
assert_eq!(tree.to_string(), "ABX");
}
#[test]
fn test_reverse_iterator_utf8_fracture() {
let mut tree = PieceTree::new();
tree.insert(0, "Hello 🦀");
tree.insert(4, "🌎");
let rev_chars: String = tree.chars_rev().collect();
assert_eq!(rev_chars, "🦀 o🌎lleH");
}
#[test]
fn test_red_black_tree_depth_stress() {
let mut tree = PieceTree::new();
for _ in 0..10_000 {
let len = tree.len_bytes();
tree.insert(len, "x");
}
assert_eq!(tree.len_bytes(), 10_000);
let depth = get_max_depth(&tree, tree.root);
assert!(depth < 40, "Tree is unbalanced! Depth is {}, expected < 40", depth);
}
#[test]
fn test_basic_bounds() {
let mut tree = PieceTree::new();
tree.insert(0, "ABC");
tree.insert(3, "GHI");
tree.insert(3, "DEF");
assert_eq!(tree.to_string(), "ABCDEFGHI");
tree.remove_at(0, 3);
assert_eq!(tree.to_string(), "DEFGHI");
tree.remove_at(3, 3);
assert_eq!(tree.to_string(), "DEF");
}
#[test]
fn test_delete_exact_piece() {
let mut tree = PieceTree::new();
tree.insert(0, "Left");
tree.insert(4, "Center");
tree.insert(10, "Right");
tree.remove_at(4, 6);
assert_eq!(tree.to_string(), "LeftRight");
assert_eq!(tree.pieces().count(), 2); }
#[test]
fn test_clear_entire_document() {
let mut tree = PieceTree::new();
tree.insert(0, "Data Oriented Design");
tree.remove(..); assert_eq!(tree.to_string(), "");
assert_eq!(tree.len_bytes(), 0);
assert_eq!(tree.len_chars(), 0);
assert_eq!(tree.len_lines(), 1); }
#[test]
fn test_multicursor_typing() {
let mut tree = PieceTree::new();
tree.insert(0, "Apple\nBanana\nCherry");
let mut edits = vec![
Edit::Insert { offset: 0, text: "- " },
Edit::Insert { offset: 6, text: "- " },
Edit::Insert { offset: 13, text: "- " },
];
tree.apply_edits(0, &mut edits);
assert_eq!(tree.to_string(), "- Apple\n- Banana\n- Cherry");
}
#[test]
fn test_multicursor_wrapping_html() {
let mut tree = PieceTree::new();
tree.insert(0, "Word1 Word2");
let mut edits = vec![
Edit::Insert { offset: 0, text: "<b>" },
Edit::Insert { offset: 5, text: "</b>" },
Edit::Insert { offset: 6, text: "<b>" },
Edit::Insert { offset: 11, text: "</b>" },
];
tree.apply_edits(0, &mut edits);
assert_eq!(tree.to_string(), "<b>Word1</b> <b>Word2</b>");
}
#[test]
fn test_multicursor_transaction_undo() {
let mut tree = PieceTree::new();
tree.insert(0, "Line 1\nLine 2\nLine 3");
let mut edits = vec![
Edit::Remove { offset: 5, length: 1 },
Edit::Remove { offset: 12, length: 1 },
Edit::Remove { offset: 19, length: 1 },
];
tree.apply_edits(19, &mut edits);
assert_eq!(tree.to_string(), "Line \nLine \nLine ");
let undo_jump_offset = tree.try_undo(0).unwrap();
assert_eq!(tree.to_string(), "Line 1\nLine 2\nLine 3");
assert_eq!(undo_jump_offset, 19); }
#[test]
fn test_mixed_edits_transaction() {
let mut tree = PieceTree::new();
tree.insert(0, "Hello World!");
let mut edits = vec![
Edit::Remove { offset: 0, length: 5 },
Edit::Insert { offset: 0, text: "Goodbye" },
];
tree.apply_edits(0, &mut edits);
assert_eq!(tree.to_string(), "Goodbye World!");
}
}
#[cfg(test)]
mod tests_proptest {
use super::*;
use proptest::prelude::*;
#[derive(Debug, Clone)]
enum Op {
Insert { pos_ratio: f64, text: String },
Remove { pos_ratio: f64, len_ratio: f64 },
Undo,
Redo,
}
fn op_strategy() -> impl Strategy<Value = Op> {
prop_oneof![
5 => (0.0..=1.0, "[a-zA-Z0-9 \n\t🦀🌎]{1, 20}").prop_map(|(r, t)| Op::Insert { pos_ratio: r, text: t }),
3 => (0.0..=1.0, 0.0..=1.0).prop_map(|(pr, lr)| Op::Remove { pos_ratio: pr, len_ratio: lr }),
1 => Just(Op::Undo),
1 => Just(Op::Redo),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10_000))]
#[test]
fn piece_tree_matches_string_oracle(ops in prop::collection::vec(op_strategy(), 1..500)) {
let mut tree = PieceTree::new();
let mut oracle = String::new();
let mut oracle_undo = Vec::new();
let mut oracle_redo = Vec::new();
for op in ops {
match op {
Op::Insert { pos_ratio, text } => {
let offset = (tree.len_bytes() as f64 * pos_ratio) as u32;
let offset = clamp_to_char_boundary(&oracle, offset);
oracle_undo.push(oracle.clone());
oracle_redo.clear();
tree.insert(offset, &text);
oracle.insert_str(offset as usize, &text);
}
Op::Remove { pos_ratio, len_ratio } => {
let current_len = tree.len_bytes();
if current_len == 0 { continue; }
let offset = (current_len as f64 * pos_ratio) as u32;
let offset = clamp_to_char_boundary(&oracle, offset);
let max_len = current_len - offset;
let len = (max_len as f64 * len_ratio) as u32;
let end = clamp_to_char_boundary(&oracle, offset + len);
let final_len = end - offset;
if final_len > 0 {
oracle_undo.push(oracle.clone());
oracle_redo.clear();
tree.remove_at(offset, final_len);
oracle.drain(offset as usize..end as usize);
}
}
Op::Undo => {
if let Some(prev) = oracle_undo.pop() {
oracle_redo.push(oracle.clone());
oracle = prev;
tree.try_undo(0);
}
}
Op::Redo => {
if let Some(next) = oracle_redo.pop() {
oracle_undo.push(oracle.clone());
oracle = next;
tree.try_redo(0);
}
}
}
assert_eq!(tree.to_string(), oracle);
assert_eq!(tree.len_bytes() as usize, oracle.len());
assert_eq!(tree.len_chars() as usize, oracle.chars().count());
}
}
}
fn clamp_to_char_boundary(s: &str, mut index: u32) -> u32 {
while index > 0 && !s.is_char_boundary(index as usize) {
index -= 1;
}
index
}
}
#[cfg(test)]
mod tests_edit_merging {
use super::*;
#[test]
fn test_sequential_typing_merges_pieces() {
let mut tree = PieceTree::new();
tree.insert(0, "H");
tree.insert(1, "e");
tree.insert(2, "l");
tree.insert(3, "l");
tree.insert(4, "o");
assert_eq!(tree.to_string(), "Hello");
assert_eq!(tree.pieces().count(), 1);
}
#[test]
fn test_cursor_jumps_break_merging() {
let mut tree = PieceTree::new();
tree.insert(0, "H");
tree.insert(1, "o");
tree.insert(1, "e");
tree.insert(2, "l");
tree.insert(3, "l");
assert_eq!(tree.to_string(), "Hello");
assert_eq!(tree.pieces().count(), 3);
}
#[test]
fn test_merging_preserves_history() {
let mut tree = PieceTree::new();
tree.insert(0, "A");
tree.insert(1, "B");
tree.insert(2, "C");
assert_eq!(tree.to_string(), "ABC");
assert_eq!(tree.pieces().count(), 1, "Should be perfectly merged in memory");
tree.try_undo(0);
assert_eq!(tree.to_string(), "AB");
tree.try_undo(0);
assert_eq!(tree.to_string(), "A");
tree.try_undo(0);
assert_eq!(tree.to_string(), "");
tree.try_redo(0);
assert_eq!(tree.to_string(), "A");
tree.try_redo(0);
assert_eq!(tree.to_string(), "AB");
tree.try_redo(0);
assert_eq!(tree.to_string(), "ABC");
}
#[test]
fn test_merging_transaction_batch() {
let mut tree = PieceTree::new();
tree.insert(0, "Base");
tree.commit_head(0);
tree.insert_no_commit(4, "X");
tree.insert_no_commit(5, "Y");
tree.insert_no_commit(6, "Z");
assert_eq!(tree.to_string(), "BaseXYZ");
assert_eq!(tree.pieces().count(), 1, "Tree successfully merged across a transaction boundary");
tree.try_undo(0);
assert_eq!(tree.to_string(), "Base", "Undo successfully truncated the view of the merged piece");
assert_eq!(tree.pieces().count(), 1);
}
}
#[cfg(test)]
mod tests_coordinates {
use super::*;
#[test]
fn test_exact_coordinate_mapping() {
let mut tree = PieceTree::new();
tree.insert(0, "Hello\n🦀world\r\nПривет");
assert_eq!(tree.char_to_byte(0), Some(0));
assert_eq!(tree.byte_to_char(0), Some(0));
assert_eq!(tree.char_to_byte(6), Some(6));
assert_eq!(tree.byte_to_char(6), Some(6));
assert_eq!(tree.char_to_byte(7), Some(10));
assert_eq!(tree.byte_to_char(10), Some(7));
assert_eq!(tree.char_to_byte(20), Some(29));
assert_eq!(tree.byte_to_char(29), Some(20));
assert_eq!(tree.char_to_byte(21), None);
assert_eq!(tree.byte_to_char(30), None);
assert_eq!(tree.line_to_offset(0), Some(0));
assert_eq!(tree.line_to_offset(1), Some(6));
assert_eq!(tree.line_to_offset(2), Some(17));
assert_eq!(tree.line_to_offset(3), None);
assert_eq!(tree.offset_to_line_col(0), Some((0, 0)));
assert_eq!(tree.offset_to_line_col(5), Some((0, 5)));
assert_eq!(tree.offset_to_line_col(6), Some((1, 0)));
assert_eq!(tree.offset_to_line_col(10), Some((1, 1)));
assert_eq!(tree.offset_to_line_col(17), Some((2, 0)));
assert_eq!(tree.offset_to_line_col(29), Some((2, 6)));
assert_eq!(tree.offset_to_line_col(30), None);
}
#[test]
fn test_crlf_vs_lf_line_endings() {
let mut tree = PieceTree::new();
tree.insert(0, "A\r\nB\nC\r\n");
assert_eq!(tree.line_to_offset(0), Some(0));
assert_eq!(tree.line_to_offset(1), Some(3));
assert_eq!(tree.line_to_offset(2), Some(5));
assert_eq!(tree.line_to_offset(3), Some(8));
assert_eq!(tree.offset_to_line_col(3), Some((1, 0)));
assert_eq!(tree.offset_to_line_col(5), Some((2, 0)));
}
}
#[cfg(test)]
mod tests_proptest_coordinates {
use super::*;
use proptest::prelude::*;
fn string_offset_to_line_col(s: &str, byte_offset: usize) -> (usize, usize) {
let slice = &s[..byte_offset];
let line = slice.chars().filter(|&c| c == '\n').count();
let last_newline_byte = slice.rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = s[last_newline_byte..byte_offset].chars().count();
(line, col)
}
fn string_line_to_byte_offset(s: &str, target_line: usize) -> usize {
if target_line == 0 { return 0; }
let mut current_line = 0;
for (i, c) in s.char_indices() {
if c == '\n' {
current_line += 1;
if current_line == target_line {
return i + 1; }
}
}
s.len()
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(5_000))]
#[test]
fn piece_tree_coordinates_match_string_oracle(
chunks in prop::collection::vec("[a-zA-Z0-9 \n\r\t🦀🌎Привет]{1,15}", 1..50)
) {
let mut tree = PieceTree::new();
let mut oracle = String::new();
for chunk in chunks {
let insert_pos = if tree.len_bytes() == 0 { 0 } else { tree.char_to_byte(tree.len_chars() / 2).unwrap() };
let mut safe_insert_byte = insert_pos;
while safe_insert_byte > 0 && !oracle.is_char_boundary(safe_insert_byte as usize) {
safe_insert_byte -= 1;
}
tree.insert(safe_insert_byte, &chunk);
oracle.insert_str(safe_insert_byte as usize, &chunk);
}
for (byte_idx, _) in oracle.char_indices() {
let char_idx = oracle[..byte_idx].chars().count() as u32;
let byte_idx_u32 = byte_idx as u32;
prop_assert_eq!(tree.char_to_byte(char_idx), Some(byte_idx_u32));
prop_assert_eq!(tree.byte_to_char(byte_idx_u32), Some(char_idx));
let expected_line_col = string_offset_to_line_col(&oracle, byte_idx);
let actual_line_col = tree.offset_to_line_col(byte_idx_u32).unwrap();
prop_assert_eq!(
(actual_line_col.0 as usize, actual_line_col.1 as usize),
expected_line_col
);
}
let total_lines = oracle.chars().filter(|&c| c == '\n').count() + 1;
for line_idx in 0..total_lines {
let expected_byte_offset = string_line_to_byte_offset(&oracle, line_idx) as u32;
prop_assert_eq!(
tree.line_to_offset(line_idx as u32),
Some(expected_byte_offset)
);
}
prop_assert_eq!(tree.char_to_byte(oracle.chars().count() as u32 + 1), None);
prop_assert_eq!(tree.byte_to_char(oracle.len() as u32 + 1), None);
prop_assert_eq!(tree.line_to_offset(total_lines as u32), None);
prop_assert_eq!(tree.offset_to_line_col(oracle.len() as u32 + 1), None);
}
}
}