@@ -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::<String>();
- 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::<Vec<_>>();
-
- 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::<String>(), "abcd\nefgh\nijkl\nmno\nPQrs");
-
- let chars = buffer.chars_at(Point::new(1, 0));
- assert_eq!(chars.collect::<String>(), "efgh\nijkl\nmno\nPQrs");
-
- let chars = buffer.chars_at(Point::new(2, 0));
- assert_eq!(chars.collect::<String>(), "ijkl\nmno\nPQrs");
-
- let chars = buffer.chars_at(Point::new(3, 0));
- assert_eq!(chars.collect::<String>(), "mno\nPQrs");
-
- let chars = buffer.chars_at(Point::new(4, 0));
- assert_eq!(chars.collect::<String>(), "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::<String>(), " \"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::<String>();
- 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::<HashMap<_, _>>(),
- first_buffer.selection_sets().collect::<HashMap<_, _>>()
- );
- assert_eq!(
- buffer.all_selection_ranges().collect::<HashMap<_, _>>(),
- first_buffer
- .all_selection_ranges()
- .collect::<HashMap<_, _>>()
- );
- }
- }
-
- #[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], "::<G>", cx);
- assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(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::<G>(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<Buffer>, 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::<Vec<_>>(),
- &[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::<Vec<_>>(),
- &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
- );
- }
-
- #[derive(Clone)]
- struct Envelope<T: Clone> {
- message: T,
- sender: ReplicaId,
- }
-
- struct Network<T: Clone, R: rand::Rng> {
- inboxes: std::collections::BTreeMap<ReplicaId, Vec<Envelope<T>>>,
- all_messages: Vec<T>,
- rng: R,
- }
-
- impl<T: Clone, R: rand::Rng> Network<T, R> {
- 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<T>) {
- 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<T> {
- 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<Language> {
- 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(),
- )
- }
-}
@@ -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::<String>();
+ 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::<Vec<_>>();
+
+ 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::<String>(), "abcd\nefgh\nijkl\nmno\nPQrs");
+
+ let chars = buffer.chars_at(Point::new(1, 0));
+ assert_eq!(chars.collect::<String>(), "efgh\nijkl\nmno\nPQrs");
+
+ let chars = buffer.chars_at(Point::new(2, 0));
+ assert_eq!(chars.collect::<String>(), "ijkl\nmno\nPQrs");
+
+ let chars = buffer.chars_at(Point::new(3, 0));
+ assert_eq!(chars.collect::<String>(), "mno\nPQrs");
+
+ let chars = buffer.chars_at(Point::new(4, 0));
+ assert_eq!(chars.collect::<String>(), "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::<String>(), " \"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::<String>();
+ 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::<HashMap<_, _>>(),
+ first_buffer.selection_sets().collect::<HashMap<_, _>>()
+ );
+ assert_eq!(
+ buffer.all_selection_ranges().collect::<HashMap<_, _>>(),
+ first_buffer
+ .all_selection_ranges()
+ .collect::<HashMap<_, _>>()
+ );
+ }
+}
+
+#[derive(Clone)]
+struct Envelope<T: Clone> {
+ message: T,
+ sender: ReplicaId,
+}
+
+struct Network<T: Clone, R: rand::Rng> {
+ inboxes: std::collections::BTreeMap<ReplicaId, Vec<Envelope<T>>>,
+ all_messages: Vec<T>,
+ rng: R,
+}
+
+impl<T: Clone, R: rand::Rng> Network<T, R> {
+ 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<T>) {
+ 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<T> {
+ 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()
+ }
+}
@@ -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], "::<G>", cx);
+ assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(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::<G>(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<Buffer>, 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::<Vec<_>>(),
+ &[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::<Vec<_>>(),
+ &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
+ );
+}
+
+fn rust_lang() -> Arc<Language> {
+ 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(),
+ )
+}