diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index b565bcaac841e0482e99d57b756234213ba145ef..3937f39920d4b8a119c1aeb1758153f838149ab0 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -7,6 +7,8 @@ mod point; pub mod random_char_iter; pub mod rope; mod selection; +#[cfg(test)] +mod tests; pub use anchor::*; use anyhow::{anyhow, Result}; @@ -3700,1035 +3702,3 @@ fn contiguous_ranges( } }) } - -#[cfg(test)] -mod tests { - use crate::random_char_iter::RandomCharIter; - - use super::*; - use gpui::ModelHandle; - use rand::prelude::*; - use std::{cell::RefCell, cmp::Ordering, env, mem, rc::Rc}; - - #[gpui::test] - fn test_edit(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "abc", cx); - assert_eq!(buffer.text(), "abc"); - buffer.edit(vec![3..3], "def", cx); - assert_eq!(buffer.text(), "abcdef"); - buffer.edit(vec![0..0], "ghi", cx); - assert_eq!(buffer.text(), "ghiabcdef"); - buffer.edit(vec![5..5], "jkl", cx); - assert_eq!(buffer.text(), "ghiabjklcdef"); - buffer.edit(vec![6..7], "", cx); - assert_eq!(buffer.text(), "ghiabjlcdef"); - buffer.edit(vec![4..9], "mno", cx); - assert_eq!(buffer.text(), "ghiamnoef"); - buffer - }); - } - - #[gpui::test] - fn test_edit_events(cx: &mut gpui::MutableAppContext) { - let mut now = Instant::now(); - let buffer_1_events = Rc::new(RefCell::new(Vec::new())); - let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - - let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); - let buffer_ops = buffer1.update(cx, |buffer, cx| { - let buffer_1_events = buffer_1_events.clone(); - cx.subscribe(&buffer1, move |_, _, event, _| { - buffer_1_events.borrow_mut().push(event.clone()) - }) - .detach(); - let buffer_2_events = buffer_2_events.clone(); - cx.subscribe(&buffer2, move |_, _, event, _| { - buffer_2_events.borrow_mut().push(event.clone()) - }) - .detach(); - - // An edit emits an edited event, followed by a dirtied event, - // since the buffer was previously in a clean state. - buffer.edit(Some(2..4), "XYZ", cx); - - // An empty transaction does not emit any events. - buffer.start_transaction(None).unwrap(); - buffer.end_transaction(None, cx).unwrap(); - - // A transaction containing two edits emits one edited event. - now += Duration::from_secs(1); - buffer.start_transaction_at(None, now).unwrap(); - buffer.edit(Some(5..5), "u", cx); - buffer.edit(Some(6..6), "w", cx); - buffer.end_transaction_at(None, now, cx).unwrap(); - - // Undoing a transaction emits one edited event. - buffer.undo(cx); - - buffer.operations.clone() - }); - - // Incorporating a set of remote ops emits a single edited event, - // followed by a dirtied event. - buffer2.update(cx, |buffer, cx| { - buffer.apply_ops(buffer_ops, cx).unwrap(); - }); - - let buffer_1_events = buffer_1_events.borrow(); - assert_eq!( - *buffer_1_events, - vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] - ); - - let buffer_2_events = buffer_2_events.borrow(); - assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); - } - - #[gpui::test(iterations = 100)] - fn test_random_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let reference_string_len = rng.gen_range(0..3); - let mut reference_string = RandomCharIter::new(&mut rng) - .take(reference_string_len) - .collect::(); - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, reference_string.as_str(), cx); - buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); - let mut buffer_versions = Vec::new(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - for _i in 0..operations { - let (old_ranges, new_text) = buffer.randomly_mutate(&mut rng, cx); - for old_range in old_ranges.iter().rev() { - reference_string.replace_range(old_range.clone(), &new_text); - } - assert_eq!(buffer.text(), reference_string); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - if rng.gen_bool(0.25) { - buffer.randomly_undo_redo(&mut rng, cx); - reference_string = buffer.text(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - } - - let range = buffer.random_byte_range(0, &mut rng); - assert_eq!( - buffer.text_summary_for_range(range.clone()), - TextSummary::from(&reference_string[range]) - ); - - if rng.gen_bool(0.3) { - buffer_versions.push(buffer.clone()); - } - } - - for mut old_buffer in buffer_versions { - let edits = buffer - .edits_since(old_buffer.version.clone()) - .collect::>(); - - log::info!( - "mutating old buffer version {:?}, text: {:?}, edits since: {:?}", - old_buffer.version(), - old_buffer.text(), - edits, - ); - - let mut delta = 0_isize; - for edit in edits { - let old_start = (edit.old_bytes.start as isize + delta) as usize; - let new_text: String = buffer.text_for_range(edit.new_bytes.clone()).collect(); - old_buffer.edit( - Some(old_start..old_start + edit.deleted_bytes()), - new_text, - cx, - ); - delta += edit.delta(); - } - assert_eq!(old_buffer.text(), buffer.text()); - } - - buffer - }); - } - - #[gpui::test] - fn test_line_len(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "abcd\nefg\nhij", cx); - buffer.edit(vec![12..12], "kl\nmno", cx); - buffer.edit(vec![18..18], "\npqrs\n", cx); - buffer.edit(vec![18..21], "\nPQ", cx); - - assert_eq!(buffer.line_len(0), 4); - assert_eq!(buffer.line_len(1), 3); - assert_eq!(buffer.line_len(2), 5); - assert_eq!(buffer.line_len(3), 3); - assert_eq!(buffer.line_len(4), 4); - assert_eq!(buffer.line_len(5), 0); - buffer - }); - } - - #[gpui::test] - fn test_text_summary_for_range(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", cx); - assert_eq!( - buffer.text_summary_for_range(1..3), - TextSummary { - bytes: 2, - lines: Point::new(1, 0), - first_line_chars: 1, - last_line_chars: 0, - longest_row: 0, - longest_row_chars: 1, - } - ); - assert_eq!( - buffer.text_summary_for_range(1..12), - TextSummary { - bytes: 11, - lines: Point::new(3, 0), - first_line_chars: 1, - last_line_chars: 0, - longest_row: 2, - longest_row_chars: 4, - } - ); - assert_eq!( - buffer.text_summary_for_range(0..20), - TextSummary { - bytes: 20, - lines: Point::new(4, 1), - first_line_chars: 2, - last_line_chars: 1, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range(0..22), - TextSummary { - bytes: 22, - lines: Point::new(4, 3), - first_line_chars: 2, - last_line_chars: 3, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range(7..22), - TextSummary { - bytes: 15, - lines: Point::new(2, 3), - first_line_chars: 4, - last_line_chars: 3, - longest_row: 1, - longest_row_chars: 6, - } - ); - buffer - }); - } - - #[gpui::test] - fn test_chars_at(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "abcd\nefgh\nij", cx); - buffer.edit(vec![12..12], "kl\nmno", cx); - buffer.edit(vec![18..18], "\npqrs", cx); - buffer.edit(vec![18..21], "\nPQ", cx); - - let chars = buffer.chars_at(Point::new(0, 0)); - assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(1, 0)); - assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(2, 0)); - assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(3, 0)); - assert_eq!(chars.collect::(), "mno\nPQrs"); - - let chars = buffer.chars_at(Point::new(4, 0)); - assert_eq!(chars.collect::(), "PQrs"); - - // Regression test: - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", cx); - buffer.edit(vec![60..60], "\n", cx); - - let chars = buffer.chars_at(Point::new(6, 0)); - assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); - - buffer - }); - } - - #[gpui::test] - fn test_anchors(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - buffer.edit(vec![0..0], "abc", cx); - let left_anchor = buffer.anchor_before(2); - let right_anchor = buffer.anchor_after(2); - - buffer.edit(vec![1..1], "def\n", cx); - assert_eq!(buffer.text(), "adef\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 6); - assert_eq!(right_anchor.to_offset(&buffer), 6); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit(vec![2..3], "", cx); - assert_eq!(buffer.text(), "adf\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 5); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit(vec![5..5], "ghi\n", cx); - assert_eq!(buffer.text(), "adf\nbghi\nc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 9); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 }); - - buffer.edit(vec![7..9], "", cx); - assert_eq!(buffer.text(), "adf\nbghc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 7); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 }); - - // Ensure anchoring to a point is equivalent to anchoring to an offset. - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 0 }), - buffer.anchor_before(0) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 1 }), - buffer.anchor_before(1) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 2 }), - buffer.anchor_before(2) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 3 }), - buffer.anchor_before(3) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 0 }), - buffer.anchor_before(4) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 1 }), - buffer.anchor_before(5) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 2 }), - buffer.anchor_before(6) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 3 }), - buffer.anchor_before(7) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 4 }), - buffer.anchor_before(8) - ); - - // Comparison between anchors. - let anchor_at_offset_0 = buffer.anchor_before(0); - let anchor_at_offset_1 = buffer.anchor_before(1); - let anchor_at_offset_2 = buffer.anchor_before(2); - - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Equal - ); - - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), - Ordering::Less - ); - - assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), - Ordering::Greater - ); - buffer - }); - } - - #[gpui::test] - fn test_anchors_at_start_and_end(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); - let before_start_anchor = buffer.anchor_before(0); - let after_end_anchor = buffer.anchor_after(0); - - buffer.edit(vec![0..0], "abc", cx); - assert_eq!(buffer.text(), "abc"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_end_anchor.to_offset(&buffer), 3); - - let after_start_anchor = buffer.anchor_after(0); - let before_end_anchor = buffer.anchor_before(3); - - buffer.edit(vec![3..3], "def", cx); - buffer.edit(vec![0..0], "ghi", cx); - assert_eq!(buffer.text(), "ghiabcdef"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_start_anchor.to_offset(&buffer), 3); - assert_eq!(before_end_anchor.to_offset(&buffer), 6); - assert_eq!(after_end_anchor.to_offset(&buffer), 9); - buffer - }); - } - - #[gpui::test] - async fn test_apply_diff(mut cx: gpui::TestAppContext) { - let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); - - let text = "a\nccc\ndddd\nffffff\n"; - let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); - cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); - - let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; - let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; - buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); - cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); - } - - #[gpui::test] - fn test_undo_redo(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "1234", cx); - // Set group interval to zero so as to not group edits in the undo stack. - buffer.history.group_interval = Duration::from_secs(0); - - buffer.edit(vec![1..1], "abx", cx); - buffer.edit(vec![3..4], "yzef", cx); - buffer.edit(vec![3..5], "cd", cx); - assert_eq!(buffer.text(), "1abcdef234"); - - let transactions = buffer.history.undo_stack.clone(); - assert_eq!(transactions.len(), 3); - - buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1cdef234"); - buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abcdx234"); - buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abx234"); - buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1yzef234"); - buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); - assert_eq!(buffer.text(), "1234"); - - buffer - }); - } - - #[gpui::test] - fn test_history(cx: &mut gpui::MutableAppContext) { - cx.add_model(|cx| { - let mut now = Instant::now(); - let mut buffer = Buffer::new(0, "123456", cx); - - let set_id = - buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), cx); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer.edit(vec![2..4], "cd", cx); - buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); - assert_eq!(buffer.text(), "12cd56"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![1..3]).unwrap(), - cx, - ) - .unwrap(); - buffer.edit(vec![4..5], "e", cx); - buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - now += buffer.history.group_interval + Duration::from_millis(1); - buffer.start_transaction_at(Some(set_id), now).unwrap(); - buffer - .update_selection_set( - set_id, - buffer.selections_from_ranges(vec![2..2]).unwrap(), - cx, - ) - .unwrap(); - buffer.edit(vec![0..1], "a", cx); - buffer.edit(vec![1..1], "b", cx); - buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - - // Last transaction happened past the group interval, undo it on its - // own. - buffer.undo(cx); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - // First two transactions happened within the group interval, undo them - // together. - buffer.undo(cx); - assert_eq!(buffer.text(), "123456"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); - - // Redo the first two transactions together. - buffer.redo(cx); - assert_eq!(buffer.text(), "12cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); - - // Redo the last transaction on its own. - buffer.redo(cx); - assert_eq!(buffer.text(), "ab2cde6"); - assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); - - buffer.start_transaction_at(None, now).unwrap(); - buffer.end_transaction_at(None, now, cx).unwrap(); - buffer.undo(cx); - assert_eq!(buffer.text(), "12cde6"); - - buffer - }); - } - - #[gpui::test] - fn test_concurrent_edits(cx: &mut gpui::MutableAppContext) { - let text = "abcdef"; - - let buffer1 = cx.add_model(|cx| Buffer::new(1, text, cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(2, text, cx)); - let buffer3 = cx.add_model(|cx| Buffer::new(3, text, cx)); - - let buf1_op = buffer1.update(cx, |buffer, cx| { - buffer.edit(vec![1..2], "12", cx); - assert_eq!(buffer.text(), "a12cdef"); - buffer.operations.last().unwrap().clone() - }); - let buf2_op = buffer2.update(cx, |buffer, cx| { - buffer.edit(vec![3..4], "34", cx); - assert_eq!(buffer.text(), "abc34ef"); - buffer.operations.last().unwrap().clone() - }); - let buf3_op = buffer3.update(cx, |buffer, cx| { - buffer.edit(vec![5..6], "56", cx); - assert_eq!(buffer.text(), "abcde56"); - buffer.operations.last().unwrap().clone() - }); - - buffer1.update(cx, |buffer, _| { - buffer.apply_op(buf2_op.clone()).unwrap(); - buffer.apply_op(buf3_op.clone()).unwrap(); - }); - buffer2.update(cx, |buffer, _| { - buffer.apply_op(buf1_op.clone()).unwrap(); - buffer.apply_op(buf3_op.clone()).unwrap(); - }); - buffer3.update(cx, |buffer, _| { - buffer.apply_op(buf1_op.clone()).unwrap(); - buffer.apply_op(buf2_op.clone()).unwrap(); - }); - - assert_eq!(buffer1.read(cx).text(), "a12c34e56"); - assert_eq!(buffer2.read(cx).text(), "a12c34e56"); - assert_eq!(buffer3.read(cx).text(), "a12c34e56"); - } - - #[gpui::test(iterations = 100)] - fn test_random_concurrent_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { - let peers = env::var("PEERS") - .map(|i| i.parse().expect("invalid `PEERS` variable")) - .unwrap_or(5); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let base_text_len = rng.gen_range(0..10); - let base_text = RandomCharIter::new(&mut rng) - .take(base_text_len) - .collect::(); - let mut replica_ids = Vec::new(); - let mut buffers = Vec::new(); - let mut network = Network::new(rng.clone()); - - for i in 0..peers { - let buffer = cx.add_model(|cx| { - let mut buf = Buffer::new(i as ReplicaId, base_text.as_str(), cx); - buf.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); - buf - }); - buffers.push(buffer); - replica_ids.push(i as u16); - network.add_peer(i as u16); - } - - log::info!("initial text: {:?}", base_text); - - let mut mutation_count = operations; - loop { - let replica_index = rng.gen_range(0..peers); - let replica_id = replica_ids[replica_index]; - buffers[replica_index].update(cx, |buffer, cx| match rng.gen_range(0..=100) { - 0..=50 if mutation_count != 0 => { - buffer.randomly_mutate(&mut rng, cx); - network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); - log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text()); - mutation_count -= 1; - } - 51..=70 if mutation_count != 0 => { - buffer.randomly_undo_redo(&mut rng, cx); - network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); - mutation_count -= 1; - } - 71..=100 if network.has_unreceived(replica_id) => { - let ops = network.receive(replica_id); - if !ops.is_empty() { - log::info!( - "peer {} applying {} ops from the network.", - replica_id, - ops.len() - ); - buffer.apply_ops(ops, cx).unwrap(); - } - } - _ => {} - }); - - if mutation_count == 0 && network.is_idle() { - break; - } - } - - let first_buffer = buffers[0].read(cx); - for buffer in &buffers[1..] { - let buffer = buffer.read(cx); - assert_eq!( - buffer.text(), - first_buffer.text(), - "Replica {} text != Replica 0 text", - buffer.replica_id - ); - assert_eq!( - buffer.selection_sets().collect::>(), - first_buffer.selection_sets().collect::>() - ); - assert_eq!( - buffer.all_selection_ranges().collect::>(), - first_buffer - .all_selection_ranges() - .collect::>() - ); - } - } - - #[gpui::test] - async fn test_reparse(mut cx: gpui::TestAppContext) { - let rust_lang = rust_lang(); - let buffer = cx.add_model(|cx| { - let text = "fn a() {}".into(); - Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx) - }); - - // Wait for the initial text to parse - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters) ", - "body: (block)))" - ) - ); - - buffer.update(&mut cx, |buffer, _| { - buffer.set_sync_parse_timeout(Duration::ZERO) - }); - - // Perform some edits (add parameter and variable reference) - // Parsing doesn't begin until the transaction is complete - buffer.update(&mut cx, |buf, cx| { - buf.start_transaction(None).unwrap(); - - let offset = buf.text().find(")").unwrap(); - buf.edit(vec![offset..offset], "b: C", cx); - assert!(!buf.is_parsing()); - - let offset = buf.text().find("}").unwrap(); - buf.edit(vec![offset..offset], " d; ", cx); - assert!(!buf.is_parsing()); - - buf.end_transaction(None, cx).unwrap(); - assert_eq!(buf.text(), "fn a(b: C) { d; }"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (identifier))))" - ) - ); - - // Perform a series of edits without waiting for the current parse to complete: - // * turn identifier into a field expression - // * turn field expression into a method call - // * add a turbofish to the method call - buffer.update(&mut cx, |buf, cx| { - let offset = buf.text().find(";").unwrap(); - buf.edit(vec![offset..offset], ".e", cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); - assert!(buf.is_parsing()); - }); - buffer.update(&mut cx, |buf, cx| { - let offset = buf.text().find(";").unwrap(); - buf.edit(vec![offset..offset], "(f)", cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); - assert!(buf.is_parsing()); - }); - buffer.update(&mut cx, |buf, cx| { - let offset = buf.text().find("(f)").unwrap(); - buf.edit(vec![offset..offset], "::", cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (call_expression ", - "function: (generic_function ", - "function: (field_expression value: (identifier) field: (field_identifier)) ", - "type_arguments: (type_arguments (type_identifier))) ", - "arguments: (arguments (identifier))))))", - ) - ); - - buffer.update(&mut cx, |buf, cx| { - buf.undo(cx); - assert_eq!(buf.text(), "fn a() {}"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters) ", - "body: (block)))" - ) - ); - - buffer.update(&mut cx, |buf, cx| { - buf.redo(cx); - assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); - assert!(buf.is_parsing()); - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - assert_eq!( - get_tree_sexp(&buffer, &cx), - concat!( - "(source_file (function_item name: (identifier) ", - "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", - "body: (block (call_expression ", - "function: (generic_function ", - "function: (field_expression value: (identifier) field: (field_identifier)) ", - "type_arguments: (type_arguments (type_identifier))) ", - "arguments: (arguments (identifier))))))", - ) - ); - - fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { - buffer.read_with(cx, |buffer, _| { - buffer.syntax_tree().unwrap().root_node().to_sexp() - }) - } - } - - #[gpui::test] - async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) { - use unindent::Unindent as _; - - let rust_lang = rust_lang(); - let buffer = cx.add_model(|cx| { - let text = " - mod x { - mod y { - - } - } - " - .unindent() - .into(); - Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx) - }); - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - buffer.read_with(&cx, |buf, _| { - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)), - Some(( - Point::new(0, 6)..Point::new(0, 7), - Point::new(4, 0)..Point::new(4, 1) - )) - ); - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) - ); - assert_eq!( - buf.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)), - Some(( - Point::new(1, 10)..Point::new(1, 11), - Point::new(3, 4)..Point::new(3, 5) - )) - ); - }); - } - - #[gpui::test] - async fn test_edit_with_autoindent(mut cx: gpui::TestAppContext) { - let rust_lang = rust_lang(); - let buffer = cx.add_model(|cx| { - let text = "fn a() {}".into(); - Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx) - }); - - buffer - .condition(&cx, |buffer, _| !buffer.is_parsing()) - .await; - - buffer.update(&mut cx, |buffer, cx| { - buffer.edit_with_autoindent([8..8], "\n\n", cx); - assert_eq!(buffer.text(), "fn a() {\n \n}"); - - buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx); - assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); - - buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx); - assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); - }); - } - - #[test] - fn test_contiguous_ranges() { - assert_eq!( - contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::>(), - &[1..4, 5..7, 9..13] - ); - - // Respects the `max_len` parameter - assert_eq!( - contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3) - .collect::>(), - &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32], - ); - } - - #[derive(Clone)] - struct Envelope { - message: T, - sender: ReplicaId, - } - - struct Network { - inboxes: std::collections::BTreeMap>>, - all_messages: Vec, - rng: R, - } - - impl Network { - fn new(rng: R) -> Self { - Network { - inboxes: Default::default(), - all_messages: Vec::new(), - rng, - } - } - - fn add_peer(&mut self, id: ReplicaId) { - self.inboxes.insert(id, Vec::new()); - } - - fn is_idle(&self) -> bool { - self.inboxes.values().all(|i| i.is_empty()) - } - - fn broadcast(&mut self, sender: ReplicaId, messages: Vec) { - for (replica, inbox) in self.inboxes.iter_mut() { - if *replica != sender { - for message in &messages { - let min_index = inbox - .iter() - .enumerate() - .rev() - .find_map(|(index, envelope)| { - if sender == envelope.sender { - Some(index + 1) - } else { - None - } - }) - .unwrap_or(0); - - // Insert one or more duplicates of this message *after* the previous - // message delivered by this replica. - for _ in 0..self.rng.gen_range(1..4) { - let insertion_index = self.rng.gen_range(min_index..inbox.len() + 1); - inbox.insert( - insertion_index, - Envelope { - message: message.clone(), - sender, - }, - ); - } - } - } - } - self.all_messages.extend(messages); - } - - fn has_unreceived(&self, receiver: ReplicaId) -> bool { - !self.inboxes[&receiver].is_empty() - } - - fn receive(&mut self, receiver: ReplicaId) -> Vec { - let inbox = self.inboxes.get_mut(&receiver).unwrap(); - let count = self.rng.gen_range(0..inbox.len() + 1); - inbox - .drain(0..count) - .map(|envelope| envelope.message) - .collect() - } - } - - fn rust_lang() -> Arc { - Arc::new( - Language::new( - LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - tree_sitter_rust::language(), - ) - .with_indents_query( - r#" - (call_expression) @indent - (field_expression) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap() - .with_brackets_query(r#" ("{" @open "}" @close) "#) - .unwrap(), - ) - } -} diff --git a/crates/buffer/src/tests.rs b/crates/buffer/src/tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1b6050de376886d87bb5bb447f1f25bdfde3e22 --- /dev/null +++ b/crates/buffer/src/tests.rs @@ -0,0 +1,2 @@ +mod buffer; +mod syntax; diff --git a/crates/buffer/src/tests/buffer.rs b/crates/buffer/src/tests/buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..7c627a45eddd544444ba9ee5973abb2cc4c63e4e --- /dev/null +++ b/crates/buffer/src/tests/buffer.rs @@ -0,0 +1,790 @@ +use crate::*; +use clock::ReplicaId; +use rand::prelude::*; +use std::{ + cell::RefCell, + cmp::Ordering, + env, + iter::Iterator, + mem, + rc::Rc, + time::{Duration, Instant}, +}; + +#[gpui::test] +fn test_edit(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "abc", cx); + assert_eq!(buffer.text(), "abc"); + buffer.edit(vec![3..3], "def", cx); + assert_eq!(buffer.text(), "abcdef"); + buffer.edit(vec![0..0], "ghi", cx); + assert_eq!(buffer.text(), "ghiabcdef"); + buffer.edit(vec![5..5], "jkl", cx); + assert_eq!(buffer.text(), "ghiabjklcdef"); + buffer.edit(vec![6..7], "", cx); + assert_eq!(buffer.text(), "ghiabjlcdef"); + buffer.edit(vec![4..9], "mno", cx); + assert_eq!(buffer.text(), "ghiamnoef"); + buffer + }); +} + +#[gpui::test] +fn test_edit_events(cx: &mut gpui::MutableAppContext) { + let mut now = Instant::now(); + let buffer_1_events = Rc::new(RefCell::new(Vec::new())); + let buffer_2_events = Rc::new(RefCell::new(Vec::new())); + + let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); + let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); + let buffer_ops = buffer1.update(cx, |buffer, cx| { + let buffer_1_events = buffer_1_events.clone(); + cx.subscribe(&buffer1, move |_, _, event, _| { + buffer_1_events.borrow_mut().push(event.clone()) + }) + .detach(); + let buffer_2_events = buffer_2_events.clone(); + cx.subscribe(&buffer2, move |_, _, event, _| { + buffer_2_events.borrow_mut().push(event.clone()) + }) + .detach(); + + // An edit emits an edited event, followed by a dirtied event, + // since the buffer was previously in a clean state. + buffer.edit(Some(2..4), "XYZ", cx); + + // An empty transaction does not emit any events. + buffer.start_transaction(None).unwrap(); + buffer.end_transaction(None, cx).unwrap(); + + // A transaction containing two edits emits one edited event. + now += Duration::from_secs(1); + buffer.start_transaction_at(None, now).unwrap(); + buffer.edit(Some(5..5), "u", cx); + buffer.edit(Some(6..6), "w", cx); + buffer.end_transaction_at(None, now, cx).unwrap(); + + // Undoing a transaction emits one edited event. + buffer.undo(cx); + + buffer.operations.clone() + }); + + // Incorporating a set of remote ops emits a single edited event, + // followed by a dirtied event. + buffer2.update(cx, |buffer, cx| { + buffer.apply_ops(buffer_ops, cx).unwrap(); + }); + + let buffer_1_events = buffer_1_events.borrow(); + assert_eq!( + *buffer_1_events, + vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] + ); + + let buffer_2_events = buffer_2_events.borrow(); + assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); +} + +#[gpui::test(iterations = 100)] +fn test_random_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let reference_string_len = rng.gen_range(0..3); + let mut reference_string = RandomCharIter::new(&mut rng) + .take(reference_string_len) + .collect::(); + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, reference_string.as_str(), cx); + buffer.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); + let mut buffer_versions = Vec::new(); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + + for _i in 0..operations { + let (old_ranges, new_text) = buffer.randomly_mutate(&mut rng, cx); + for old_range in old_ranges.iter().rev() { + reference_string.replace_range(old_range.clone(), &new_text); + } + assert_eq!(buffer.text(), reference_string); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + + if rng.gen_bool(0.25) { + buffer.randomly_undo_redo(&mut rng, cx); + reference_string = buffer.text(); + log::info!( + "buffer text {:?}, version: {:?}", + buffer.text(), + buffer.version() + ); + } + + let range = buffer.random_byte_range(0, &mut rng); + assert_eq!( + buffer.text_summary_for_range(range.clone()), + TextSummary::from(&reference_string[range]) + ); + + if rng.gen_bool(0.3) { + buffer_versions.push(buffer.clone()); + } + } + + for mut old_buffer in buffer_versions { + let edits = buffer + .edits_since(old_buffer.version.clone()) + .collect::>(); + + log::info!( + "mutating old buffer version {:?}, text: {:?}, edits since: {:?}", + old_buffer.version(), + old_buffer.text(), + edits, + ); + + let mut delta = 0_isize; + for edit in edits { + let old_start = (edit.old_bytes.start as isize + delta) as usize; + let new_text: String = buffer.text_for_range(edit.new_bytes.clone()).collect(); + old_buffer.edit( + Some(old_start..old_start + edit.deleted_bytes()), + new_text, + cx, + ); + delta += edit.delta(); + } + assert_eq!(old_buffer.text(), buffer.text()); + } + + buffer + }); +} + +#[gpui::test] +fn test_line_len(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "abcd\nefg\nhij", cx); + buffer.edit(vec![12..12], "kl\nmno", cx); + buffer.edit(vec![18..18], "\npqrs\n", cx); + buffer.edit(vec![18..21], "\nPQ", cx); + + assert_eq!(buffer.line_len(0), 4); + assert_eq!(buffer.line_len(1), 3); + assert_eq!(buffer.line_len(2), 5); + assert_eq!(buffer.line_len(3), 3); + assert_eq!(buffer.line_len(4), 4); + assert_eq!(buffer.line_len(5), 0); + buffer + }); +} + +#[gpui::test] +fn test_text_summary_for_range(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", cx); + assert_eq!( + buffer.text_summary_for_range(1..3), + TextSummary { + bytes: 2, + lines: Point::new(1, 0), + first_line_chars: 1, + last_line_chars: 0, + longest_row: 0, + longest_row_chars: 1, + } + ); + assert_eq!( + buffer.text_summary_for_range(1..12), + TextSummary { + bytes: 11, + lines: Point::new(3, 0), + first_line_chars: 1, + last_line_chars: 0, + longest_row: 2, + longest_row_chars: 4, + } + ); + assert_eq!( + buffer.text_summary_for_range(0..20), + TextSummary { + bytes: 20, + lines: Point::new(4, 1), + first_line_chars: 2, + last_line_chars: 1, + longest_row: 3, + longest_row_chars: 6, + } + ); + assert_eq!( + buffer.text_summary_for_range(0..22), + TextSummary { + bytes: 22, + lines: Point::new(4, 3), + first_line_chars: 2, + last_line_chars: 3, + longest_row: 3, + longest_row_chars: 6, + } + ); + assert_eq!( + buffer.text_summary_for_range(7..22), + TextSummary { + bytes: 15, + lines: Point::new(2, 3), + first_line_chars: 4, + last_line_chars: 3, + longest_row: 1, + longest_row_chars: 6, + } + ); + buffer + }); +} + +#[gpui::test] +fn test_chars_at(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "abcd\nefgh\nij", cx); + buffer.edit(vec![12..12], "kl\nmno", cx); + buffer.edit(vec![18..18], "\npqrs", cx); + buffer.edit(vec![18..21], "\nPQ", cx); + + let chars = buffer.chars_at(Point::new(0, 0)); + assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(1, 0)); + assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(2, 0)); + assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); + + let chars = buffer.chars_at(Point::new(3, 0)); + assert_eq!(chars.collect::(), "mno\nPQrs"); + + let chars = buffer.chars_at(Point::new(4, 0)); + assert_eq!(chars.collect::(), "PQrs"); + + // Regression test: + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", cx); + buffer.edit(vec![60..60], "\n", cx); + + let chars = buffer.chars_at(Point::new(6, 0)); + assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); + + buffer + }); +} + +#[gpui::test] +fn test_anchors(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + buffer.edit(vec![0..0], "abc", cx); + let left_anchor = buffer.anchor_before(2); + let right_anchor = buffer.anchor_after(2); + + buffer.edit(vec![1..1], "def\n", cx); + assert_eq!(buffer.text(), "adef\nbc"); + assert_eq!(left_anchor.to_offset(&buffer), 6); + assert_eq!(right_anchor.to_offset(&buffer), 6); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + + buffer.edit(vec![2..3], "", cx); + assert_eq!(buffer.text(), "adf\nbc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 5); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + + buffer.edit(vec![5..5], "ghi\n", cx); + assert_eq!(buffer.text(), "adf\nbghi\nc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 9); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 }); + + buffer.edit(vec![7..9], "", cx); + assert_eq!(buffer.text(), "adf\nbghc"); + assert_eq!(left_anchor.to_offset(&buffer), 5); + assert_eq!(right_anchor.to_offset(&buffer), 7); + assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },); + assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 }); + + // Ensure anchoring to a point is equivalent to anchoring to an offset. + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 0 }), + buffer.anchor_before(0) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 1 }), + buffer.anchor_before(1) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 2 }), + buffer.anchor_before(2) + ); + assert_eq!( + buffer.anchor_before(Point { row: 0, column: 3 }), + buffer.anchor_before(3) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 0 }), + buffer.anchor_before(4) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 1 }), + buffer.anchor_before(5) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 2 }), + buffer.anchor_before(6) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 3 }), + buffer.anchor_before(7) + ); + assert_eq!( + buffer.anchor_before(Point { row: 1, column: 4 }), + buffer.anchor_before(8) + ); + + // Comparison between anchors. + let anchor_at_offset_0 = buffer.anchor_before(0); + let anchor_at_offset_1 = buffer.anchor_before(1); + let anchor_at_offset_2 = buffer.anchor_before(2); + + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Equal + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Equal + ); + + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); + assert_eq!( + anchor_at_offset_0 + .cmp(&anchor_at_offset_2, &buffer) + .unwrap(), + Ordering::Less + ); + + assert_eq!( + anchor_at_offset_1 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_1, &buffer) + .unwrap(), + Ordering::Greater + ); + assert_eq!( + anchor_at_offset_2 + .cmp(&anchor_at_offset_0, &buffer) + .unwrap(), + Ordering::Greater + ); + buffer + }); +} + +#[gpui::test] +fn test_anchors_at_start_and_end(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx); + let before_start_anchor = buffer.anchor_before(0); + let after_end_anchor = buffer.anchor_after(0); + + buffer.edit(vec![0..0], "abc", cx); + assert_eq!(buffer.text(), "abc"); + assert_eq!(before_start_anchor.to_offset(&buffer), 0); + assert_eq!(after_end_anchor.to_offset(&buffer), 3); + + let after_start_anchor = buffer.anchor_after(0); + let before_end_anchor = buffer.anchor_before(3); + + buffer.edit(vec![3..3], "def", cx); + buffer.edit(vec![0..0], "ghi", cx); + assert_eq!(buffer.text(), "ghiabcdef"); + assert_eq!(before_start_anchor.to_offset(&buffer), 0); + assert_eq!(after_start_anchor.to_offset(&buffer), 3); + assert_eq!(before_end_anchor.to_offset(&buffer), 6); + assert_eq!(after_end_anchor.to_offset(&buffer), 9); + buffer + }); +} + +#[gpui::test] +async fn test_apply_diff(mut cx: gpui::TestAppContext) { + let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + + let text = "a\nccc\ndddd\nffffff\n"; + let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; + buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); + cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); + + let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; + let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await; + buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx)); + cx.read(|cx| assert_eq!(buffer.read(cx).text(), text)); +} + +#[gpui::test] +fn test_undo_redo(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "1234", cx); + // Set group interval to zero so as to not group edits in the undo stack. + buffer.history.group_interval = Duration::from_secs(0); + + buffer.edit(vec![1..1], "abx", cx); + buffer.edit(vec![3..4], "yzef", cx); + buffer.edit(vec![3..5], "cd", cx); + assert_eq!(buffer.text(), "1abcdef234"); + + let transactions = buffer.history.undo_stack.clone(); + assert_eq!(transactions.len(), 3); + + buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1cdef234"); + buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abcdx234"); + buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abx234"); + buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abcdef234"); + + buffer.undo_or_redo(transactions[2].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1abyzef234"); + buffer.undo_or_redo(transactions[0].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1yzef234"); + buffer.undo_or_redo(transactions[1].clone(), cx).unwrap(); + assert_eq!(buffer.text(), "1234"); + + buffer + }); +} + +#[gpui::test] +fn test_history(cx: &mut gpui::MutableAppContext) { + cx.add_model(|cx| { + let mut now = Instant::now(); + let mut buffer = Buffer::new(0, "123456", cx); + + let set_id = + buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), cx); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer.edit(vec![2..4], "cd", cx); + buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); + assert_eq!(buffer.text(), "12cd56"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![1..3]).unwrap(), + cx, + ) + .unwrap(); + buffer.edit(vec![4..5], "e", cx); + buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + now += buffer.history.group_interval + Duration::from_millis(1); + buffer.start_transaction_at(Some(set_id), now).unwrap(); + buffer + .update_selection_set( + set_id, + buffer.selections_from_ranges(vec![2..2]).unwrap(), + cx, + ) + .unwrap(); + buffer.edit(vec![0..1], "a", cx); + buffer.edit(vec![1..1], "b", cx); + buffer.end_transaction_at(Some(set_id), now, cx).unwrap(); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + + // Last transaction happened past the group interval, undo it on its + // own. + buffer.undo(cx); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + // First two transactions happened within the group interval, undo them + // together. + buffer.undo(cx); + assert_eq!(buffer.text(), "123456"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![4..4]); + + // Redo the first two transactions together. + buffer.redo(cx); + assert_eq!(buffer.text(), "12cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![1..3]); + + // Redo the last transaction on its own. + buffer.redo(cx); + assert_eq!(buffer.text(), "ab2cde6"); + assert_eq!(buffer.selection_ranges(set_id).unwrap(), vec![3..3]); + + buffer.start_transaction_at(None, now).unwrap(); + buffer.end_transaction_at(None, now, cx).unwrap(); + buffer.undo(cx); + assert_eq!(buffer.text(), "12cde6"); + + buffer + }); +} + +#[gpui::test] +fn test_concurrent_edits(cx: &mut gpui::MutableAppContext) { + let text = "abcdef"; + + let buffer1 = cx.add_model(|cx| Buffer::new(1, text, cx)); + let buffer2 = cx.add_model(|cx| Buffer::new(2, text, cx)); + let buffer3 = cx.add_model(|cx| Buffer::new(3, text, cx)); + + let buf1_op = buffer1.update(cx, |buffer, cx| { + buffer.edit(vec![1..2], "12", cx); + assert_eq!(buffer.text(), "a12cdef"); + buffer.operations.last().unwrap().clone() + }); + let buf2_op = buffer2.update(cx, |buffer, cx| { + buffer.edit(vec![3..4], "34", cx); + assert_eq!(buffer.text(), "abc34ef"); + buffer.operations.last().unwrap().clone() + }); + let buf3_op = buffer3.update(cx, |buffer, cx| { + buffer.edit(vec![5..6], "56", cx); + assert_eq!(buffer.text(), "abcde56"); + buffer.operations.last().unwrap().clone() + }); + + buffer1.update(cx, |buffer, _| { + buffer.apply_op(buf2_op.clone()).unwrap(); + buffer.apply_op(buf3_op.clone()).unwrap(); + }); + buffer2.update(cx, |buffer, _| { + buffer.apply_op(buf1_op.clone()).unwrap(); + buffer.apply_op(buf3_op.clone()).unwrap(); + }); + buffer3.update(cx, |buffer, _| { + buffer.apply_op(buf1_op.clone()).unwrap(); + buffer.apply_op(buf2_op.clone()).unwrap(); + }); + + assert_eq!(buffer1.read(cx).text(), "a12c34e56"); + assert_eq!(buffer2.read(cx).text(), "a12c34e56"); + assert_eq!(buffer3.read(cx).text(), "a12c34e56"); +} + +#[gpui::test(iterations = 100)] +fn test_random_concurrent_edits(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + let peers = env::var("PEERS") + .map(|i| i.parse().expect("invalid `PEERS` variable")) + .unwrap_or(5); + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let base_text_len = rng.gen_range(0..10); + let base_text = RandomCharIter::new(&mut rng) + .take(base_text_len) + .collect::(); + let mut replica_ids = Vec::new(); + let mut buffers = Vec::new(); + let mut network = Network::new(rng.clone()); + + for i in 0..peers { + let buffer = cx.add_model(|cx| { + let mut buf = Buffer::new(i as ReplicaId, base_text.as_str(), cx); + buf.history.group_interval = Duration::from_millis(rng.gen_range(0..=200)); + buf + }); + buffers.push(buffer); + replica_ids.push(i as u16); + network.add_peer(i as u16); + } + + log::info!("initial text: {:?}", base_text); + + let mut mutation_count = operations; + loop { + let replica_index = rng.gen_range(0..peers); + let replica_id = replica_ids[replica_index]; + buffers[replica_index].update(cx, |buffer, cx| match rng.gen_range(0..=100) { + 0..=50 if mutation_count != 0 => { + buffer.randomly_mutate(&mut rng, cx); + network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); + log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text()); + mutation_count -= 1; + } + 51..=70 if mutation_count != 0 => { + buffer.randomly_undo_redo(&mut rng, cx); + network.broadcast(buffer.replica_id, mem::take(&mut buffer.operations)); + mutation_count -= 1; + } + 71..=100 if network.has_unreceived(replica_id) => { + let ops = network.receive(replica_id); + if !ops.is_empty() { + log::info!( + "peer {} applying {} ops from the network.", + replica_id, + ops.len() + ); + buffer.apply_ops(ops, cx).unwrap(); + } + } + _ => {} + }); + + if mutation_count == 0 && network.is_idle() { + break; + } + } + + let first_buffer = buffers[0].read(cx); + for buffer in &buffers[1..] { + let buffer = buffer.read(cx); + assert_eq!( + buffer.text(), + first_buffer.text(), + "Replica {} text != Replica 0 text", + buffer.replica_id + ); + assert_eq!( + buffer.selection_sets().collect::>(), + first_buffer.selection_sets().collect::>() + ); + assert_eq!( + buffer.all_selection_ranges().collect::>(), + first_buffer + .all_selection_ranges() + .collect::>() + ); + } +} + +#[derive(Clone)] +struct Envelope { + message: T, + sender: ReplicaId, +} + +struct Network { + inboxes: std::collections::BTreeMap>>, + all_messages: Vec, + rng: R, +} + +impl Network { + fn new(rng: R) -> Self { + Network { + inboxes: Default::default(), + all_messages: Vec::new(), + rng, + } + } + + fn add_peer(&mut self, id: ReplicaId) { + self.inboxes.insert(id, Vec::new()); + } + + fn is_idle(&self) -> bool { + self.inboxes.values().all(|i| i.is_empty()) + } + + fn broadcast(&mut self, sender: ReplicaId, messages: Vec) { + for (replica, inbox) in self.inboxes.iter_mut() { + if *replica != sender { + for message in &messages { + let min_index = inbox + .iter() + .enumerate() + .rev() + .find_map(|(index, envelope)| { + if sender == envelope.sender { + Some(index + 1) + } else { + None + } + }) + .unwrap_or(0); + + // Insert one or more duplicates of this message *after* the previous + // message delivered by this replica. + for _ in 0..self.rng.gen_range(1..4) { + let insertion_index = self.rng.gen_range(min_index..inbox.len() + 1); + inbox.insert( + insertion_index, + Envelope { + message: message.clone(), + sender, + }, + ); + } + } + } + } + self.all_messages.extend(messages); + } + + fn has_unreceived(&self, receiver: ReplicaId) -> bool { + !self.inboxes[&receiver].is_empty() + } + + fn receive(&mut self, receiver: ReplicaId) -> Vec { + let inbox = self.inboxes.get_mut(&receiver).unwrap(); + let count = self.rng.gen_range(0..inbox.len() + 1); + inbox + .drain(0..count) + .map(|envelope| envelope.message) + .collect() + } +} diff --git a/crates/buffer/src/tests/syntax.rs b/crates/buffer/src/tests/syntax.rs new file mode 100644 index 0000000000000000000000000000000000000000..8c05e10316f8cb28c75f7aaf6bd6758ae141c4a1 --- /dev/null +++ b/crates/buffer/src/tests/syntax.rs @@ -0,0 +1,237 @@ +use crate::*; +use gpui::ModelHandle; +use unindent::Unindent as _; + +#[gpui::test] +async fn test_reparse(mut cx: gpui::TestAppContext) { + let buffer = cx.add_model(|cx| { + let text = "fn a() {}".into(); + Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) + }); + + // Wait for the initial text to parse + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters) ", + "body: (block)))" + ) + ); + + buffer.update(&mut cx, |buffer, _| { + buffer.set_sync_parse_timeout(Duration::ZERO) + }); + + // Perform some edits (add parameter and variable reference) + // Parsing doesn't begin until the transaction is complete + buffer.update(&mut cx, |buf, cx| { + buf.start_transaction(None).unwrap(); + + let offset = buf.text().find(")").unwrap(); + buf.edit(vec![offset..offset], "b: C", cx); + assert!(!buf.is_parsing()); + + let offset = buf.text().find("}").unwrap(); + buf.edit(vec![offset..offset], " d; ", cx); + assert!(!buf.is_parsing()); + + buf.end_transaction(None, cx).unwrap(); + assert_eq!(buf.text(), "fn a(b: C) { d; }"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", + "body: (block (identifier))))" + ) + ); + + // Perform a series of edits without waiting for the current parse to complete: + // * turn identifier into a field expression + // * turn field expression into a method call + // * add a turbofish to the method call + buffer.update(&mut cx, |buf, cx| { + let offset = buf.text().find(";").unwrap(); + buf.edit(vec![offset..offset], ".e", cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e; }"); + assert!(buf.is_parsing()); + }); + buffer.update(&mut cx, |buf, cx| { + let offset = buf.text().find(";").unwrap(); + buf.edit(vec![offset..offset], "(f)", cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }"); + assert!(buf.is_parsing()); + }); + buffer.update(&mut cx, |buf, cx| { + let offset = buf.text().find("(f)").unwrap(); + buf.edit(vec![offset..offset], "::", cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", + "body: (block (call_expression ", + "function: (generic_function ", + "function: (field_expression value: (identifier) field: (field_identifier)) ", + "type_arguments: (type_arguments (type_identifier))) ", + "arguments: (arguments (identifier))))))", + ) + ); + + buffer.update(&mut cx, |buf, cx| { + buf.undo(cx); + assert_eq!(buf.text(), "fn a() {}"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters) ", + "body: (block)))" + ) + ); + + buffer.update(&mut cx, |buf, cx| { + buf.redo(cx); + assert_eq!(buf.text(), "fn a(b: C) { d.e::(f); }"); + assert!(buf.is_parsing()); + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + assert_eq!( + get_tree_sexp(&buffer, &cx), + concat!( + "(source_file (function_item name: (identifier) ", + "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ", + "body: (block (call_expression ", + "function: (generic_function ", + "function: (field_expression value: (identifier) field: (field_identifier)) ", + "type_arguments: (type_arguments (type_identifier))) ", + "arguments: (arguments (identifier))))))", + ) + ); + + fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> String { + buffer.read_with(cx, |buffer, _| { + buffer.syntax_tree().unwrap().root_node().to_sexp() + }) + } +} + +#[gpui::test] +async fn test_enclosing_bracket_ranges(mut cx: gpui::TestAppContext) { + let buffer = cx.add_model(|cx| { + let text = " + mod x { + mod y { + + } + } + " + .unindent() + .into(); + Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) + }); + buffer + .condition(&cx, |buffer, _| !buffer.is_parsing()) + .await; + buffer.read_with(&cx, |buf, _| { + assert_eq!( + buf.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)), + Some(( + Point::new(0, 6)..Point::new(0, 7), + Point::new(4, 0)..Point::new(4, 1) + )) + ); + assert_eq!( + buf.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)), + Some(( + Point::new(1, 10)..Point::new(1, 11), + Point::new(3, 4)..Point::new(3, 5) + )) + ); + assert_eq!( + buf.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)), + Some(( + Point::new(1, 10)..Point::new(1, 11), + Point::new(3, 4)..Point::new(3, 5) + )) + ); + }); +} + +#[gpui::test] +async fn test_edit_with_autoindent(mut cx: gpui::TestAppContext) { + let buffer = cx.add_model(|cx| { + let text = "fn a() {}".into(); + Buffer::from_history(0, History::new(text), None, Some(rust_lang()), cx) + }); + + buffer.update(&mut cx, |buffer, cx| { + buffer.edit_with_autoindent([8..8], "\n\n", cx); + assert_eq!(buffer.text(), "fn a() {\n \n}"); + + buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx); + assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); + + buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx); + assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); + }); +} + +#[test] +fn test_contiguous_ranges() { + assert_eq!( + contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12], 100).collect::>(), + &[1..4, 5..7, 9..13] + ); + + // Respects the `max_len` parameter + assert_eq!( + contiguous_ranges([2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31], 3).collect::>(), + &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32], + ); +} + +fn rust_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + tree_sitter_rust::language(), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_brackets_query(r#" ("{" @open "}" @close) "#) + .unwrap(), + ) +}