From 3632b36fde4c4e73eadc7e231d7b040a3b7fb55b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 19 Dec 2024 18:02:32 -0800 Subject: [PATCH] Move multibuffer tests to their own source file (#22270) Release Notes: - N/A --- crates/multi_buffer/src/multi_buffer.rs | 1999 +---------------- crates/multi_buffer/src/multi_buffer_tests.rs | 1990 ++++++++++++++++ 2 files changed, 1992 insertions(+), 1997 deletions(-) create mode 100644 crates/multi_buffer/src/multi_buffer_tests.rs diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 0e688e6cdd2bf1..1253e82ebc56a4 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1,4 +1,6 @@ mod anchor; +#[cfg(test)] +mod multi_buffer_tests; pub use anchor::{Anchor, AnchorRangeExt, Offset}; use anyhow::{anyhow, Result}; @@ -5258,2000 +5260,3 @@ where (excerpt_ranges, range_counts) } - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{AppContext, Context, TestAppContext}; - use language::{Buffer, Rope}; - use parking_lot::RwLock; - use rand::prelude::*; - use settings::SettingsStore; - use std::env; - use util::test::sample_text; - - #[ctor::ctor] - fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } - } - - #[gpui::test] - fn test_singleton(cx: &mut AppContext) { - let buffer = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), buffer.read(cx).text()); - - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(snapshot.text(), buffer.read(cx).text()); - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - } - - #[gpui::test] - fn test_remote(cx: &mut AppContext) { - let host_buffer = cx.new_model(|cx| Buffer::local("a", cx)); - let guest_buffer = cx.new_model(|cx| { - let state = host_buffer.read(cx).to_proto(cx); - let ops = cx - .background_executor() - .block(host_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap(); - buffer.apply_ops( - ops.into_iter() - .map(|op| language::proto::deserialize_operation(op).unwrap()), - cx, - ); - buffer - }); - let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "a"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "ab"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "abc"); - } - - #[gpui::test] - fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - - let events = Arc::new(RwLock::new(Vec::::new())); - multibuffer.update(cx, |_, cx| { - let events = events.clone(); - cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited { .. } = event { - events.write().push(event.clone()) - } - }) - .detach(); - }); - - let subscription = multibuffer.update(cx, |multibuffer, cx| { - let subscription = multibuffer.subscribe(); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: Point::new(1, 2)..Point::new(2, 5), - primary: None, - }], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: 0..0, - new: 0..10 - }] - ); - - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: Point::new(3, 3)..Point::new(4, 4), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: Point::new(3, 1)..Point::new(3, 3), - primary: None, - }], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: 10..10, - new: 10..22 - }] - ); - - subscription - }); - - // Adding excerpts emits an edited event. - assert_eq!( - events.read().as_slice(), - &[ - Event::Edited { - singleton_buffer_edited: false, - edited_buffer: None, - }, - Event::Edited { - singleton_buffer_edited: false, - edited_buffer: None, - }, - Event::Edited { - singleton_buffer_edited: false, - edited_buffer: None, - } - ] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "ccccc\n", // - "ddd\n", // - "eeee\n", // - "jj" // - ) - ); - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), - [Some(1), Some(2), Some(3), Some(4), Some(3)] - ); - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(2)).collect::>(), - [Some(3), Some(4), Some(3)] - ); - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(4)).collect::>(), - [Some(3)] - ); - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(5)).collect::>(), - [] - ); - - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot), - &[ - (MultiBufferRow(0), "bbbb\nccccc".to_string(), true), - (MultiBufferRow(2), "ddd\neeee".to_string(), false), - (MultiBufferRow(4), "jj".to_string(), true), - ] - ); - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot), - &[(MultiBufferRow(0), "bbbb\nccccc".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot), - &[(MultiBufferRow(4), "jj".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot), - &[] - ); - - buffer_1.update(cx, |buffer, cx| { - let text = "\n"; - buffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), text), - (Point::new(2, 1)..Point::new(2, 3), text), - ], - None, - cx, - ); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee\n", // - "jj" // - ) - ); - - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: 6..8, - new: 6..7 - }] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Left), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Right), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 1), Bias::Right), - Point::new(5, 1) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 2), Bias::Right), - Point::new(5, 2) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 3), Bias::Right), - Point::new(5, 2) - ); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| { - let (buffer_2_excerpt_id, _) = - multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); - multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); - multibuffer.snapshot(cx) - }); - - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee", // - ) - ); - - fn boundaries_in_range( - range: Range, - snapshot: &MultiBufferSnapshot, - ) -> Vec<(MultiBufferRow, String, bool)> { - snapshot - .excerpt_boundaries_in_range(range) - .filter_map(|boundary| { - let starts_new_buffer = boundary.starts_new_buffer(); - boundary.next.map(|next| { - ( - boundary.row, - next.buffer - .text_for_range(next.range.context) - .collect::(), - starts_new_buffer, - ) - }) - }) - .collect::>() - } - } - - #[gpui::test] - fn test_excerpt_events(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(10, 3, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(10, 3, 'm'), cx)); - - let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let follower_edit_event_count = Arc::new(RwLock::new(0)); - - follower_multibuffer.update(cx, |_, cx| { - let follower_edit_event_count = follower_edit_event_count.clone(); - cx.subscribe( - &leader_multibuffer, - move |follower, _, event, cx| match event.clone() { - Event::ExcerptsAdded { - buffer, - predecessor, - excerpts, - } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), - Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), - Event::Edited { .. } => { - *follower_edit_event_count.write() += 1; - } - _ => {} - }, - ) - .detach(); - }); - - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts( - buffer_1.clone(), - [ - ExcerptRange { - context: 0..8, - primary: None, - }, - ExcerptRange { - context: 12..16, - primary: None, - }, - ], - cx, - ); - leader.insert_excerpts_after( - leader.excerpt_ids()[0], - buffer_2.clone(), - [ - ExcerptRange { - context: 0..5, - primary: None, - }, - ExcerptRange { - context: 10..15, - primary: None, - }, - ], - cx, - ) - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 2); - - leader_multibuffer.update(cx, |leader, cx| { - let excerpt_ids = leader.excerpt_ids(); - leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Removing an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.remove_excerpts([], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Adding an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts::(buffer_2.clone(), [], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - leader_multibuffer.update(cx, |leader, cx| { - leader.clear(cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 4); - } - - #[gpui::test] - fn test_expand_excerpts(cx: &mut AppContext) { - let buffer = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - vec![ - // Note that in this test, this first excerpt - // does not contain a new line - Point::new(3, 2)..Point::new(3, 3), - Point::new(7, 1)..Point::new(7, 3), - Point::new(15, 0)..Point::new(15, 0), - ], - 1, - cx, - ) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!( - snapshot.text(), - concat!( - "ccc\n", // - "ddd\n", // - "eee", // - "\n", // End of excerpt - "ggg\n", // - "hhh\n", // - "iii", // - "\n", // End of excerpt - "ooo\n", // - "ppp\n", // - "qqq", // End of excerpt - ) - ); - drop(snapshot); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_excerpts( - multibuffer.excerpt_ids(), - 1, - ExpandExcerptDirection::UpAndDown, - cx, - ) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - // Expanding context lines causes the line containing 'fff' to appear in two different excerpts. - // We don't attempt to merge them, because removing the excerpt could create inconsistency with other layers - // that are tracking excerpt ids. - assert_eq!( - snapshot.text(), - concat!( - "bbb\n", // - "ccc\n", // - "ddd\n", // - "eee\n", // - "fff\n", // End of excerpt - "fff\n", // - "ggg\n", // - "hhh\n", // - "iii\n", // - "jjj\n", // End of excerpt - "nnn\n", // - "ooo\n", // - "ppp\n", // - "qqq\n", // - "rrr", // End of excerpt - ) - ); - } - - #[gpui::test] - fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { - let buffer = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - vec![ - // Note that in this test, this first excerpt - // does contain a new line - Point::new(3, 2)..Point::new(4, 2), - Point::new(7, 1)..Point::new(7, 3), - Point::new(15, 0)..Point::new(15, 0), - ], - 2, - cx, - ) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - concat!( - "bbb\n", // Preserve newlines - "ccc\n", // - "ddd\n", // - "eee\n", // - "fff\n", // - "ggg\n", // - "hhh\n", // - "iii\n", // - "jjj\n", // - "nnn\n", // - "ooo\n", // - "ppp\n", // - "qqq\n", // - "rrr", // - ) - ); - - assert_eq!( - anchor_ranges - .iter() - .map(|range| range.to_point(&snapshot)) - .collect::>(), - vec![ - Point::new(2, 2)..Point::new(3, 2), - Point::new(6, 1)..Point::new(6, 3), - Point::new(11, 0)..Point::new(11, 0) - ] - ); - } - - #[gpui::test(iterations = 100)] - async fn test_push_multiple_excerpts_with_context_lines(cx: &mut TestAppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(15, 4, 'a'), cx)); - let snapshot_1 = buffer_1.update(cx, |buffer, _| buffer.snapshot()); - let snapshot_2 = buffer_2.update(cx, |buffer, _| buffer.snapshot()); - let ranges_1 = vec![ - snapshot_1.anchor_before(Point::new(3, 2))..snapshot_1.anchor_before(Point::new(4, 2)), - snapshot_1.anchor_before(Point::new(7, 1))..snapshot_1.anchor_before(Point::new(7, 3)), - snapshot_1.anchor_before(Point::new(15, 0)) - ..snapshot_1.anchor_before(Point::new(15, 0)), - ]; - let ranges_2 = vec![ - snapshot_2.anchor_before(Point::new(2, 1))..snapshot_2.anchor_before(Point::new(3, 1)), - snapshot_2.anchor_before(Point::new(10, 0)) - ..snapshot_2.anchor_before(Point::new(10, 2)), - ]; - - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let anchor_ranges = multibuffer - .update(cx, |multibuffer, cx| { - multibuffer.push_multiple_excerpts_with_context_lines( - vec![(buffer_1.clone(), ranges_1), (buffer_2.clone(), ranges_2)], - 2, - cx, - ) - }) - .await; - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - snapshot.text(), - concat!( - "bbb\n", // buffer_1 - "ccc\n", // - "ddd\n", // <-- excerpt 1 - "eee\n", // <-- excerpt 1 - "fff\n", // - "ggg\n", // - "hhh\n", // <-- excerpt 2 - "iii\n", // - "jjj\n", // - // - "nnn\n", // - "ooo\n", // - "ppp\n", // <-- excerpt 3 - "qqq\n", // - "rrr\n", // - // - "aaaa\n", // buffer 2 - "bbbb\n", // - "cccc\n", // <-- excerpt 4 - "dddd\n", // <-- excerpt 4 - "eeee\n", // - "ffff\n", // - // - "iiii\n", // - "jjjj\n", // - "kkkk\n", // <-- excerpt 5 - "llll\n", // - "mmmm", // - ) - ); - - assert_eq!( - anchor_ranges - .iter() - .map(|range| range.to_point(&snapshot)) - .collect::>(), - vec![ - Point::new(2, 2)..Point::new(3, 2), - Point::new(6, 1)..Point::new(6, 3), - Point::new(11, 0)..Point::new(11, 0), - Point::new(16, 1)..Point::new(17, 1), - Point::new(22, 0)..Point::new(22, 2) - ] - ); - } - - #[gpui::test] - fn test_empty_multibuffer(cx: &mut AppContext) { - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), ""); - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), - &[Some(0)] - ); - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(1)).collect::>(), - &[] - ); - } - - #[gpui::test] - fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { - let buffer = cx.new_model(|cx| Buffer::local("abcd", cx)); - let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "X")], None, cx); - buffer.edit([(5..5, "Y")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd"); - assert_eq!(new_snapshot.text(), "XabcdY"); - - assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); - assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); - assert_eq!(old_snapshot.anchor_before(4).to_offset(&new_snapshot), 5); - assert_eq!(old_snapshot.anchor_after(4).to_offset(&new_snapshot), 6); - } - - #[gpui::test] - fn test_multibuffer_anchors(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local("abcd", cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local("efghi", cx)); - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..4, - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..5, - primary: None, - }], - cx, - ); - multibuffer - }); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.anchor_before(0).to_offset(&old_snapshot), 0); - assert_eq!(old_snapshot.anchor_after(0).to_offset(&old_snapshot), 0); - assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); - assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); - assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); - assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); - - buffer_1.update(cx, |buffer, cx| { - buffer.edit([(0..0, "W")], None, cx); - buffer.edit([(5..5, "X")], None, cx); - }); - buffer_2.update(cx, |buffer, cx| { - buffer.edit([(0..0, "Y")], None, cx); - buffer.edit([(6..6, "Z")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd\nefghi"); - assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ"); - - assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); - assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); - assert_eq!(old_snapshot.anchor_before(1).to_offset(&new_snapshot), 2); - assert_eq!(old_snapshot.anchor_after(1).to_offset(&new_snapshot), 2); - assert_eq!(old_snapshot.anchor_before(2).to_offset(&new_snapshot), 3); - assert_eq!(old_snapshot.anchor_after(2).to_offset(&new_snapshot), 3); - assert_eq!(old_snapshot.anchor_before(5).to_offset(&new_snapshot), 7); - assert_eq!(old_snapshot.anchor_after(5).to_offset(&new_snapshot), 8); - assert_eq!(old_snapshot.anchor_before(10).to_offset(&new_snapshot), 13); - assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14); - } - - #[gpui::test] - fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local("abcd", cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local("ABCDEFGHIJKLMNOP", cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - - // Create an insertion id in buffer 1 that doesn't exist in buffer 2. - // Add an excerpt from buffer 1 that spans this new insertion. - buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); - let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..7, - primary: None, - }], - cx, - ) - .pop() - .unwrap() - }); - - let snapshot_1 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_1.text(), "abcd123"); - - // Replace the buffer 1 excerpt with new excerpts from buffer 2. - let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_1], cx); - let mut ids = multibuffer - .push_excerpts( - buffer_2.clone(), - [ - ExcerptRange { - context: 0..4, - primary: None, - }, - ExcerptRange { - context: 6..10, - primary: None, - }, - ExcerptRange { - context: 12..16, - primary: None, - }, - ], - cx, - ) - .into_iter(); - (ids.next().unwrap(), ids.next().unwrap()) - }); - let snapshot_2 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); - - // The old excerpt id doesn't get reused. - assert_ne!(excerpt_id_2, excerpt_id_1); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The current excerpts are from a different buffer, so we don't attempt to - // resolve the old text anchor in the new buffer. - assert_eq!( - snapshot_2.summary_for_anchor::(&snapshot_1.anchor_before(2)), - 0 - ); - assert_eq!( - snapshot_2.summaries_for_anchors::(&[ - snapshot_1.anchor_before(2), - snapshot_1.anchor_after(3) - ]), - vec![0, 0] - ); - - // Refresh anchors from the old snapshot. The return value indicates that both - // anchors lost their original excerpt. - let refresh = - snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]); - assert_eq!( - refresh, - &[ - (0, snapshot_2.anchor_before(0), false), - (1, snapshot_2.anchor_after(0), false), - ] - ); - - // Replace the middle excerpt with a smaller excerpt in buffer 2, - // that intersects the old excerpt. - let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_3], cx); - multibuffer - .insert_excerpts_after( - excerpt_id_2, - buffer_2.clone(), - [ExcerptRange { - context: 5..8, - primary: None, - }], - cx, - ) - .pop() - .unwrap() - }); - - let snapshot_3 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP"); - assert_ne!(excerpt_id_5, excerpt_id_3); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The third anchor can't be resolved, since its excerpt has been removed, - // so it resolves to the same position as its predecessor. - let anchors = [ - snapshot_2.anchor_before(0), - snapshot_2.anchor_after(2), - snapshot_2.anchor_after(6), - snapshot_2.anchor_after(14), - ]; - assert_eq!( - snapshot_3.summaries_for_anchors::(&anchors), - &[0, 2, 9, 13] - ); - - let new_anchors = snapshot_3.refresh_anchors(&anchors); - assert_eq!( - new_anchors.iter().map(|a| (a.0, a.2)).collect::>(), - &[(0, true), (1, true), (2, true), (3, true)] - ); - assert_eq!( - snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.1)), - &[0, 2, 7, 13] - ); - } - - #[gpui::test(iterations = 100)] - fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let mut buffers: Vec> = Vec::new(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_ids = Vec::::new(); - let mut expected_excerpts = Vec::<(Model, Range)>::new(); - let mut anchors = Vec::new(); - let mut old_versions = Vec::new(); - - for _ in 0..operations { - match rng.gen_range(0..100) { - 0..=14 if !buffers.is_empty() => { - let buffer = buffers.choose(&mut rng).unwrap(); - buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 5, cx)); - } - 15..=19 if !expected_excerpts.is_empty() => { - multibuffer.update(cx, |multibuffer, cx| { - let ids = multibuffer.excerpt_ids(); - let mut excerpts = HashSet::default(); - for _ in 0..rng.gen_range(0..ids.len()) { - excerpts.extend(ids.choose(&mut rng).copied()); - } - - let line_count = rng.gen_range(0..5); - - let excerpt_ixs = excerpts - .iter() - .map(|id| excerpt_ids.iter().position(|i| i == id).unwrap()) - .collect::>(); - log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines"); - multibuffer.expand_excerpts( - excerpts.iter().cloned(), - line_count, - ExpandExcerptDirection::UpAndDown, - cx, - ); - - if line_count > 0 { - for id in excerpts { - let excerpt_ix = excerpt_ids.iter().position(|&i| i == id).unwrap(); - let (buffer, range) = &mut expected_excerpts[excerpt_ix]; - let snapshot = buffer.read(cx).snapshot(); - let mut point_range = range.to_point(&snapshot); - point_range.start = - Point::new(point_range.start.row.saturating_sub(line_count), 0); - point_range.end = snapshot.clip_point( - Point::new(point_range.end.row + line_count, 0), - Bias::Left, - ); - point_range.end.column = snapshot.line_len(point_range.end.row); - *range = snapshot.anchor_before(point_range.start) - ..snapshot.anchor_after(point_range.end); - } - } - }); - } - 20..=29 if !expected_excerpts.is_empty() => { - let mut ids_to_remove = vec![]; - for _ in 0..rng.gen_range(1..=3) { - if expected_excerpts.is_empty() { - break; - } - - let ix = rng.gen_range(0..expected_excerpts.len()); - ids_to_remove.push(excerpt_ids.remove(ix)); - let (buffer, range) = expected_excerpts.remove(ix); - let buffer = buffer.read(cx); - log::info!( - "Removing excerpt {}: {:?}", - ix, - buffer - .text_for_range(range.to_offset(buffer)) - .collect::(), - ); - } - let snapshot = multibuffer.read(cx).read(cx); - ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); - drop(snapshot); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts(ids_to_remove, cx) - }); - } - 30..=39 if !expected_excerpts.is_empty() => { - let multibuffer = multibuffer.read(cx).read(cx); - let offset = - multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left); - let bias = if rng.gen() { Bias::Left } else { Bias::Right }; - log::info!("Creating anchor at {} with bias {:?}", offset, bias); - anchors.push(multibuffer.anchor_at(offset, bias)); - anchors.sort_by(|a, b| a.cmp(b, &multibuffer)); - } - 40..=44 if !anchors.is_empty() => { - let multibuffer = multibuffer.read(cx).read(cx); - let prev_len = anchors.len(); - anchors = multibuffer - .refresh_anchors(&anchors) - .into_iter() - .map(|a| a.1) - .collect(); - - // Ensure the newly-refreshed anchors point to a valid excerpt and don't - // overshoot its boundaries. - assert_eq!(anchors.len(), prev_len); - for anchor in &anchors { - if anchor.excerpt_id == ExcerptId::min() - || anchor.excerpt_id == ExcerptId::max() - { - continue; - } - - let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap(); - assert_eq!(excerpt.id, anchor.excerpt_id); - assert!(excerpt.contains(anchor)); - } - } - _ => { - let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { - let base_text = util::RandomCharIter::new(&mut rng) - .take(25) - .collect::(); - - buffers.push(cx.new_model(|cx| Buffer::local(base_text, cx))); - buffers.last().unwrap() - } else { - buffers.choose(&mut rng).unwrap() - }; - - let buffer = buffer_handle.read(cx); - let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); - let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); - let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); - let prev_excerpt_ix = rng.gen_range(0..=expected_excerpts.len()); - let prev_excerpt_id = excerpt_ids - .get(prev_excerpt_ix) - .cloned() - .unwrap_or_else(ExcerptId::max); - let excerpt_ix = (prev_excerpt_ix + 1).min(expected_excerpts.len()); - - log::info!( - "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", - excerpt_ix, - expected_excerpts.len(), - buffer_handle.read(cx).remote_id(), - buffer.text(), - start_ix..end_ix, - &buffer.text()[start_ix..end_ix] - ); - - let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .insert_excerpts_after( - prev_excerpt_id, - buffer_handle.clone(), - [ExcerptRange { - context: start_ix..end_ix, - primary: None, - }], - cx, - ) - .pop() - .unwrap() - }); - - excerpt_ids.insert(excerpt_ix, excerpt_id); - expected_excerpts.insert(excerpt_ix, (buffer_handle.clone(), anchor_range)); - } - } - - if rng.gen_bool(0.3) { - multibuffer.update(cx, |multibuffer, cx| { - old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); - }) - } - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let mut excerpt_starts = Vec::new(); - let mut expected_text = String::new(); - let mut expected_buffer_rows = Vec::new(); - for (buffer, range) in &expected_excerpts { - let buffer = buffer.read(cx); - let buffer_range = range.to_offset(buffer); - - excerpt_starts.push(TextSummary::from(expected_text.as_str())); - expected_text.extend(buffer.text_for_range(buffer_range.clone())); - expected_text.push('\n'); - - let buffer_row_range = buffer.offset_to_point(buffer_range.start).row - ..=buffer.offset_to_point(buffer_range.end).row; - for row in buffer_row_range { - expected_buffer_rows.push(Some(row)); - } - } - // Remove final trailing newline. - if !expected_excerpts.is_empty() { - expected_text.pop(); - } - - // Always report one buffer row - if expected_buffer_rows.is_empty() { - expected_buffer_rows.push(Some(0)); - } - - assert_eq!(snapshot.text(), expected_text); - log::info!("MultiBuffer text: {:?}", expected_text); - - assert_eq!( - snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), - expected_buffer_rows, - ); - - for _ in 0..5 { - let start_row = rng.gen_range(0..=expected_buffer_rows.len()); - assert_eq!( - snapshot - .buffer_rows(MultiBufferRow(start_row as u32)) - .collect::>(), - &expected_buffer_rows[start_row..], - "buffer_rows({})", - start_row - ); - } - - assert_eq!( - snapshot.widest_line_number(), - expected_buffer_rows.into_iter().flatten().max().unwrap() + 1 - ); - - let mut excerpt_starts = excerpt_starts.into_iter(); - for (buffer, range) in &expected_excerpts { - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - let buffer_range = range.to_offset(buffer); - let buffer_start_point = buffer.offset_to_point(buffer_range.start); - let buffer_start_point_utf16 = - buffer.text_summary_for_range::(0..buffer_range.start); - - let excerpt_start = excerpt_starts.next().unwrap(); - let mut offset = excerpt_start.len; - let mut buffer_offset = buffer_range.start; - let mut point = excerpt_start.lines; - let mut buffer_point = buffer_start_point; - let mut point_utf16 = excerpt_start.lines_utf16(); - let mut buffer_point_utf16 = buffer_start_point_utf16; - for ch in buffer - .snapshot() - .chunks(buffer_range.clone(), false) - .flat_map(|c| c.text.chars()) - { - for _ in 0..ch.len_utf8() { - let left_offset = snapshot.clip_offset(offset, Bias::Left); - let right_offset = snapshot.clip_offset(offset, Bias::Right); - let buffer_left_offset = buffer.clip_offset(buffer_offset, Bias::Left); - let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right); - assert_eq!( - left_offset, - excerpt_start.len + (buffer_left_offset - buffer_range.start), - "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}", - offset, - buffer_id, - buffer_offset, - ); - assert_eq!( - right_offset, - excerpt_start.len + (buffer_right_offset - buffer_range.start), - "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}", - offset, - buffer_id, - buffer_offset, - ); - - let left_point = snapshot.clip_point(point, Bias::Left); - let right_point = snapshot.clip_point(point, Bias::Right); - let buffer_left_point = buffer.clip_point(buffer_point, Bias::Left); - let buffer_right_point = buffer.clip_point(buffer_point, Bias::Right); - assert_eq!( - left_point, - excerpt_start.lines + (buffer_left_point - buffer_start_point), - "clip_point({:?}, Left). buffer: {:?}, buffer point: {:?}", - point, - buffer_id, - buffer_point, - ); - assert_eq!( - right_point, - excerpt_start.lines + (buffer_right_point - buffer_start_point), - "clip_point({:?}, Right). buffer: {:?}, buffer point: {:?}", - point, - buffer_id, - buffer_point, - ); - - assert_eq!( - snapshot.point_to_offset(left_point), - left_offset, - "point_to_offset({:?})", - left_point, - ); - assert_eq!( - snapshot.offset_to_point(left_offset), - left_point, - "offset_to_point({:?})", - left_offset, - ); - - offset += 1; - buffer_offset += 1; - if ch == '\n' { - point += Point::new(1, 0); - buffer_point += Point::new(1, 0); - } else { - point += Point::new(0, 1); - buffer_point += Point::new(0, 1); - } - } - - for _ in 0..ch.len_utf16() { - let left_point_utf16 = - snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left); - let right_point_utf16 = - snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right); - let buffer_left_point_utf16 = - buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left); - let buffer_right_point_utf16 = - buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right); - assert_eq!( - left_point_utf16, - excerpt_start.lines_utf16() - + (buffer_left_point_utf16 - buffer_start_point_utf16), - "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}", - point_utf16, - buffer_id, - buffer_point_utf16, - ); - assert_eq!( - right_point_utf16, - excerpt_start.lines_utf16() - + (buffer_right_point_utf16 - buffer_start_point_utf16), - "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}", - point_utf16, - buffer_id, - buffer_point_utf16, - ); - - if ch == '\n' { - point_utf16 += PointUtf16::new(1, 0); - buffer_point_utf16 += PointUtf16::new(1, 0); - } else { - point_utf16 += PointUtf16::new(0, 1); - buffer_point_utf16 += PointUtf16::new(0, 1); - } - } - } - } - - for (row, line) in expected_text.split('\n').enumerate() { - assert_eq!( - snapshot.line_len(MultiBufferRow(row as u32)), - line.len() as u32, - "line_len({}).", - row - ); - } - - let text_rope = Rope::from(expected_text.as_str()); - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); - let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); - - let text_for_range = snapshot - .text_for_range(start_ix..end_ix) - .collect::(); - assert_eq!( - text_for_range, - &expected_text[start_ix..end_ix], - "incorrect text for range {:?}", - start_ix..end_ix - ); - - let excerpted_buffer_ranges = multibuffer - .read(cx) - .range_to_buffer_ranges(start_ix..end_ix, cx); - let excerpted_buffers_text = excerpted_buffer_ranges - .iter() - .map(|(buffer, buffer_range, _)| { - buffer - .read(cx) - .text_for_range(buffer_range.clone()) - .collect::() - }) - .collect::>() - .join("\n"); - assert_eq!(excerpted_buffers_text, text_for_range); - if !expected_excerpts.is_empty() { - assert!(!excerpted_buffer_ranges.is_empty()); - } - - let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]); - assert_eq!( - snapshot.text_summary_for_range::(start_ix..end_ix), - expected_summary, - "incorrect summary for range {:?}", - start_ix..end_ix - ); - } - - // Anchor resolution - let summaries = snapshot.summaries_for_anchors::(&anchors); - assert_eq!(anchors.len(), summaries.len()); - for (anchor, resolved_offset) in anchors.iter().zip(summaries) { - assert!(resolved_offset <= snapshot.len()); - assert_eq!( - snapshot.summary_for_anchor::(anchor), - resolved_offset - ); - } - - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); - assert_eq!( - snapshot.reversed_chars_at(end_ix).collect::(), - expected_text[..end_ix].chars().rev().collect::(), - ); - } - - for _ in 0..10 { - let end_ix = rng.gen_range(0..=text_rope.len()); - let start_ix = rng.gen_range(0..=end_ix); - assert_eq!( - snapshot - .bytes_in_range(start_ix..end_ix) - .flatten() - .copied() - .collect::>(), - expected_text.as_bytes()[start_ix..end_ix].to_vec(), - "bytes_in_range({:?})", - start_ix..end_ix, - ); - } - } - - let snapshot = multibuffer.read(cx).snapshot(cx); - for (old_snapshot, subscription) in old_versions { - let edits = subscription.consume().into_inner(); - - log::info!( - "applying subscription edits to old text: {:?}: {:?}", - old_snapshot.text(), - edits, - ); - - let mut text = old_snapshot.text(); - for edit in edits { - let new_text: String = snapshot.text_for_range(edit.new.clone()).collect(); - text.replace_range(edit.new.start..edit.new.start + edit.old.len(), &new_text); - } - assert_eq!(text.to_string(), snapshot.text()); - } - } - - #[gpui::test] - fn test_history(cx: &mut AppContext) { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - let group_interval: Duration = Duration::from_millis(1); - let buffer_1 = cx.new_model(|cx| { - let mut buf = Buffer::local("1234", cx); - buf.set_group_interval(group_interval); - buf - }); - let buffer_2 = cx.new_model(|cx| { - let mut buf = Buffer::local("5678", cx); - buf.set_group_interval(group_interval); - buf - }); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |this, _| { - this.history.group_interval = group_interval; - }); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let mut now = Instant::now(); - - multibuffer.update(cx, |multibuffer, cx| { - let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); - multibuffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), "A"), - (Point::new(1, 0)..Point::new(1, 0), "A"), - ], - None, - cx, - ); - multibuffer.edit( - [ - (Point::new(0, 1)..Point::new(0, 1), "B"), - (Point::new(1, 1)..Point::new(1, 1), "B"), - ], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - // Verify edited ranges for transaction 1 - assert_eq!( - multibuffer.edited_ranges_for_transaction(transaction_1, cx), - &[ - Point::new(0, 0)..Point::new(0, 2), - Point::new(1, 0)..Point::new(1, 2) - ] - ); - - // Edit buffer 1 through the multibuffer - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(2..2, "C")], None, cx); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); - - // Edit buffer 1 independently - buffer_1.update(cx, |buffer_1, cx| { - buffer_1.start_transaction_at(now); - buffer_1.edit([(3..3, "D")], None, cx); - buffer_1.end_transaction_at(now, cx); - - now += 2 * group_interval; - buffer_1.start_transaction_at(now); - buffer_1.edit([(4..4, "E")], None, cx); - buffer_1.end_transaction_at(now, cx); - }); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // An undo in the multibuffer undoes the multibuffer transaction - // and also any individual buffer edits that have occurred since - // that transaction. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // Undo buffer 2 independently. - buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678"); - - // An undo in the multibuffer undoes the components of the - // the last multibuffer transaction that are not already undone. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - - // Redo stack gets cleared after an edit. - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(0..0, "X")], None, cx); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - // Transactions can be grouped manually. - multibuffer.redo(cx); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.group_until_transaction(transaction_1, cx); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - }); - } - - #[gpui::test] - fn test_excerpts_in_ranges_no_ranges(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - - let mut excerpts = snapshot.excerpts_in_ranges(iter::from_fn(|| None)); - - assert!(excerpts.next().is_none()); - } - - fn validate_excerpts( - actual: &[(ExcerptId, BufferId, Range)], - expected: &Vec<(ExcerptId, BufferId, Range)>, - ) { - assert_eq!(actual.len(), expected.len()); - - actual - .iter() - .zip(expected) - .map(|(actual, expected)| { - assert_eq!(actual.0, expected.0); - assert_eq!(actual.1, expected.1); - assert_eq!(actual.2.start, expected.2.start); - assert_eq!(actual.2.end, expected.2.end); - }) - .collect_vec(); - } - - fn map_range_from_excerpt( - snapshot: &MultiBufferSnapshot, - excerpt_id: ExcerptId, - excerpt_buffer: &BufferSnapshot, - range: Range, - ) -> Range { - snapshot - .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_before(range.start)) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_after(range.end)) - .unwrap() - } - - fn make_expected_excerpt_info( - snapshot: &MultiBufferSnapshot, - cx: &mut AppContext, - excerpt_id: ExcerptId, - buffer: &Model, - range: Range, - ) -> (ExcerptId, BufferId, Range) { - ( - excerpt_id, - buffer.read(cx).remote_id(), - map_range_from_excerpt(snapshot, excerpt_id, &buffer.read(cx).snapshot(), range), - ) - } - - #[gpui::test] - fn test_excerpts_in_ranges_range_inside_the_excerpt(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut expected_excerpt_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - expected_excerpt_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - - let range = snapshot - .anchor_in_excerpt(expected_excerpt_id, buffer_1.read(cx).anchor_before(1)) - .unwrap() - ..snapshot - .anchor_in_excerpt( - expected_excerpt_id, - buffer_1.read(cx).anchor_after(buffer_len / 2), - ) - .unwrap(); - - let expected_excerpts = vec![make_expected_excerpt_info( - &snapshot, - cx, - expected_excerpt_id, - &buffer_1, - 1..(buffer_len / 2), - )]; - - let excerpts = snapshot - .excerpts_in_ranges(vec![range.clone()].into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); - } - - #[gpui::test] - fn test_excerpts_in_ranges_range_crosses_excerpts_boundary(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let expected_range = snapshot - .anchor_in_excerpt( - excerpt_1_id, - buffer_1.read(cx).anchor_before(buffer_len / 2), - ) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt_2_id, buffer_2.read(cx).anchor_after(buffer_len / 2)) - .unwrap(); - - let expected_excerpts = vec![ - make_expected_excerpt_info( - &snapshot, - cx, - excerpt_1_id, - &buffer_1, - (buffer_len / 2)..buffer_len, - ), - make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len / 2), - ]; - - let excerpts = snapshot - .excerpts_in_ranges(vec![expected_range.clone()].into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); - } - - #[gpui::test] - fn test_excerpts_in_ranges_range_encloses_excerpt(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'r'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - let mut excerpt_3_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_3_id = multibuffer.push_excerpts( - buffer_3.clone(), - [ExcerptRange { - context: 0..buffer_3.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let expected_range = snapshot - .anchor_in_excerpt( - excerpt_1_id, - buffer_1.read(cx).anchor_before(buffer_len / 2), - ) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt_3_id, buffer_3.read(cx).anchor_after(buffer_len / 2)) - .unwrap(); - - let expected_excerpts = vec![ - make_expected_excerpt_info( - &snapshot, - cx, - excerpt_1_id, - &buffer_1, - (buffer_len / 2)..buffer_len, - ), - make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len), - make_expected_excerpt_info(&snapshot, cx, excerpt_3_id, &buffer_3, 0..buffer_len / 2), - ]; - - let excerpts = snapshot - .excerpts_in_ranges(vec![expected_range.clone()].into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); - } - - #[gpui::test] - fn test_excerpts_in_ranges_multiple_ranges(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let ranges = vec![ - 1..(buffer_len / 4), - (buffer_len / 3)..(buffer_len / 2), - (buffer_len / 4 * 3)..(buffer_len), - ]; - - let expected_excerpts = ranges - .iter() - .map(|range| { - make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, range.clone()) - }) - .collect_vec(); - - let ranges = ranges.into_iter().map(|range| { - map_range_from_excerpt( - &snapshot, - excerpt_1_id, - &buffer_1.read(cx).snapshot(), - range, - ) - }); - - let excerpts = snapshot - .excerpts_in_ranges(ranges) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); - } - - #[gpui::test] - fn test_excerpts_in_ranges_range_ends_at_excerpt_end(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_len = buffer_1.read(cx).len(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - let mut excerpt_1_id = ExcerptId(0); - let mut excerpt_2_id = ExcerptId(0); - - multibuffer.update(cx, |multibuffer, cx| { - excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - )[0]; - excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - )[0]; - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let ranges = [0..buffer_len, (buffer_len / 3)..(buffer_len / 2)]; - - let expected_excerpts = vec![ - make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, ranges[0].clone()), - make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, ranges[1].clone()), - ]; - - let ranges = [ - map_range_from_excerpt( - &snapshot, - excerpt_1_id, - &buffer_1.read(cx).snapshot(), - ranges[0].clone(), - ), - map_range_from_excerpt( - &snapshot, - excerpt_2_id, - &buffer_2.read(cx).snapshot(), - ranges[1].clone(), - ), - ]; - - let excerpts = snapshot - .excerpts_in_ranges(ranges.into_iter()) - .map(|(excerpt_id, buffer, actual_range)| { - ( - excerpt_id, - buffer.remote_id(), - map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), - ) - }) - .collect_vec(); - - validate_excerpts(&excerpts, &expected_excerpts); - } - - #[gpui::test] - fn test_split_ranges(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let buffer_1_len = buffer_1.read(cx).len(); - let buffer_2_len = buffer_2.read(cx).len(); - let buffer_1_midpoint = buffer_1_len / 2; - let buffer_2_start = buffer_1_len + '\n'.len_utf8(); - let buffer_2_midpoint = buffer_2_start + buffer_2_len / 2; - let total_len = buffer_2_start + buffer_2_len; - - let input_ranges = [ - 0..buffer_1_midpoint, - buffer_1_midpoint..buffer_2_midpoint, - buffer_2_midpoint..total_len, - ] - .map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)); - - let actual_ranges = snapshot - .split_ranges(input_ranges.into_iter()) - .map(|range| range.to_offset(&snapshot)) - .collect::>(); - - let expected_ranges = vec![ - 0..buffer_1_midpoint, - buffer_1_midpoint..buffer_1_len, - buffer_2_start..buffer_2_midpoint, - buffer_2_midpoint..total_len, - ]; - - assert_eq!(actual_ranges, expected_ranges); - } - - #[gpui::test] - fn test_split_ranges_single_range_spanning_three_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'm'), cx)); - let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_3.clone(), - [ExcerptRange { - context: 0..buffer_3.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let buffer_1_len = buffer_1.read(cx).len(); - let buffer_2_len = buffer_2.read(cx).len(); - let buffer_3_len = buffer_3.read(cx).len(); - let buffer_2_start = buffer_1_len + '\n'.len_utf8(); - let buffer_3_start = buffer_2_start + buffer_2_len + '\n'.len_utf8(); - let buffer_1_midpoint = buffer_1_len / 2; - let buffer_3_midpoint = buffer_3_start + buffer_3_len / 2; - - let input_range = - snapshot.anchor_before(buffer_1_midpoint)..snapshot.anchor_after(buffer_3_midpoint); - - let actual_ranges = snapshot - .split_ranges(std::iter::once(input_range)) - .map(|range| range.to_offset(&snapshot)) - .collect::>(); - - let expected_ranges = vec![ - buffer_1_midpoint..buffer_1_len, - buffer_2_start..buffer_2_start + buffer_2_len, - buffer_3_start..buffer_3_midpoint, - ]; - - assert_eq!(actual_ranges, expected_ranges); - } -} diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs new file mode 100644 index 00000000000000..fb2237bb428c33 --- /dev/null +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -0,0 +1,1990 @@ +use super::*; +use gpui::{AppContext, Context, TestAppContext}; +use language::{Buffer, Rope}; +use parking_lot::RwLock; +use rand::prelude::*; +use settings::SettingsStore; +use std::env; +use util::test::sample_text; + +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} + +#[gpui::test] +fn test_singleton(cx: &mut AppContext) { + let buffer = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), buffer.read(cx).text()); + + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + (0..buffer.read(cx).row_count()) + .map(Some) + .collect::>() + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(snapshot.text(), buffer.read(cx).text()); + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + (0..buffer.read(cx).row_count()) + .map(Some) + .collect::>() + ); +} + +#[gpui::test] +fn test_remote(cx: &mut AppContext) { + let host_buffer = cx.new_model(|cx| Buffer::local("a", cx)); + let guest_buffer = cx.new_model(|cx| { + let state = host_buffer.read(cx).to_proto(cx); + let ops = cx + .background_executor() + .block(host_buffer.read(cx).serialize_ops(None, cx)); + let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap(); + buffer.apply_ops( + ops.into_iter() + .map(|op| language::proto::deserialize_operation(op).unwrap()), + cx, + ); + buffer + }); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), "a"); + + guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), "ab"); + + guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), "abc"); +} + +#[gpui::test] +fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + + let events = Arc::new(RwLock::new(Vec::::new())); + multibuffer.update(cx, |_, cx| { + let events = events.clone(); + cx.subscribe(&multibuffer, move |_, _, event, _| { + if let Event::Edited { .. } = event { + events.write().push(event.clone()) + } + }) + .detach(); + }); + + let subscription = multibuffer.update(cx, |multibuffer, cx| { + let subscription = multibuffer.subscribe(); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(1, 2)..Point::new(2, 5), + primary: None, + }], + cx, + ); + assert_eq!( + subscription.consume().into_inner(), + [Edit { + old: 0..0, + new: 0..10 + }] + ); + + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(3, 3)..Point::new(4, 4), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(3, 1)..Point::new(3, 3), + primary: None, + }], + cx, + ); + assert_eq!( + subscription.consume().into_inner(), + [Edit { + old: 10..10, + new: 10..22 + }] + ); + + subscription + }); + + // Adding excerpts emits an edited event. + assert_eq!( + events.read().as_slice(), + &[ + Event::Edited { + singleton_buffer_edited: false, + edited_buffer: None, + }, + Event::Edited { + singleton_buffer_edited: false, + edited_buffer: None, + }, + Event::Edited { + singleton_buffer_edited: false, + edited_buffer: None, + } + ] + ); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.text(), + concat!( + "bbbb\n", // Preserve newlines + "ccccc\n", // + "ddd\n", // + "eeee\n", // + "jj" // + ) + ); + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + [Some(1), Some(2), Some(3), Some(4), Some(3)] + ); + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(2)).collect::>(), + [Some(3), Some(4), Some(3)] + ); + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(4)).collect::>(), + [Some(3)] + ); + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(5)).collect::>(), + [] + ); + + assert_eq!( + boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot), + &[ + (MultiBufferRow(0), "bbbb\nccccc".to_string(), true), + (MultiBufferRow(2), "ddd\neeee".to_string(), false), + (MultiBufferRow(4), "jj".to_string(), true), + ] + ); + assert_eq!( + boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot), + &[(MultiBufferRow(0), "bbbb\nccccc".to_string(), true)] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot), + &[] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot), + &[] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), + &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] + ); + assert_eq!( + boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), + &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] + ); + assert_eq!( + boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot), + &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] + ); + assert_eq!( + boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot), + &[(MultiBufferRow(4), "jj".to_string(), true)] + ); + assert_eq!( + boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot), + &[] + ); + + buffer_1.update(cx, |buffer, cx| { + let text = "\n"; + buffer.edit( + [ + (Point::new(0, 0)..Point::new(0, 0), text), + (Point::new(2, 1)..Point::new(2, 3), text), + ], + None, + cx, + ); + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.text(), + concat!( + "bbbb\n", // Preserve newlines + "c\n", // + "cc\n", // + "ddd\n", // + "eeee\n", // + "jj" // + ) + ); + + assert_eq!( + subscription.consume().into_inner(), + [Edit { + old: 6..8, + new: 6..7 + }] + ); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.clip_point(Point::new(0, 5), Bias::Left), + Point::new(0, 4) + ); + assert_eq!( + snapshot.clip_point(Point::new(0, 5), Bias::Right), + Point::new(0, 4) + ); + assert_eq!( + snapshot.clip_point(Point::new(5, 1), Bias::Right), + Point::new(5, 1) + ); + assert_eq!( + snapshot.clip_point(Point::new(5, 2), Bias::Right), + Point::new(5, 2) + ); + assert_eq!( + snapshot.clip_point(Point::new(5, 3), Bias::Right), + Point::new(5, 2) + ); + + let snapshot = multibuffer.update(cx, |multibuffer, cx| { + let (buffer_2_excerpt_id, _) = multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); + multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); + multibuffer.snapshot(cx) + }); + + assert_eq!( + snapshot.text(), + concat!( + "bbbb\n", // Preserve newlines + "c\n", // + "cc\n", // + "ddd\n", // + "eeee", // + ) + ); + + fn boundaries_in_range( + range: Range, + snapshot: &MultiBufferSnapshot, + ) -> Vec<(MultiBufferRow, String, bool)> { + snapshot + .excerpt_boundaries_in_range(range) + .filter_map(|boundary| { + let starts_new_buffer = boundary.starts_new_buffer(); + boundary.next.map(|next| { + ( + boundary.row, + next.buffer + .text_for_range(next.range.context) + .collect::(), + starts_new_buffer, + ) + }) + }) + .collect::>() + } +} + +#[gpui::test] +fn test_excerpt_events(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(10, 3, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(10, 3, 'm'), cx)); + + let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let follower_edit_event_count = Arc::new(RwLock::new(0)); + + follower_multibuffer.update(cx, |_, cx| { + let follower_edit_event_count = follower_edit_event_count.clone(); + cx.subscribe( + &leader_multibuffer, + move |follower, _, event, cx| match event.clone() { + Event::ExcerptsAdded { + buffer, + predecessor, + excerpts, + } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), + Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), + Event::Edited { .. } => { + *follower_edit_event_count.write() += 1; + } + _ => {} + }, + ) + .detach(); + }); + + leader_multibuffer.update(cx, |leader, cx| { + leader.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: 0..8, + primary: None, + }, + ExcerptRange { + context: 12..16, + primary: None, + }, + ], + cx, + ); + leader.insert_excerpts_after( + leader.excerpt_ids()[0], + buffer_2.clone(), + [ + ExcerptRange { + context: 0..5, + primary: None, + }, + ExcerptRange { + context: 10..15, + primary: None, + }, + ], + cx, + ) + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 2); + + leader_multibuffer.update(cx, |leader, cx| { + let excerpt_ids = leader.excerpt_ids(); + leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 3); + + // Removing an empty set of excerpts is a noop. + leader_multibuffer.update(cx, |leader, cx| { + leader.remove_excerpts([], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 3); + + // Adding an empty set of excerpts is a noop. + leader_multibuffer.update(cx, |leader, cx| { + leader.push_excerpts::(buffer_2.clone(), [], cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 3); + + leader_multibuffer.update(cx, |leader, cx| { + leader.clear(cx); + }); + assert_eq!( + leader_multibuffer.read(cx).snapshot(cx).text(), + follower_multibuffer.read(cx).snapshot(cx).text(), + ); + assert_eq!(*follower_edit_event_count.read(), 4); +} + +#[gpui::test] +fn test_expand_excerpts(cx: &mut AppContext) { + let buffer = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + vec![ + // Note that in this test, this first excerpt + // does not contain a new line + Point::new(3, 2)..Point::new(3, 3), + Point::new(7, 1)..Point::new(7, 3), + Point::new(15, 0)..Point::new(15, 0), + ], + 1, + cx, + ) + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!( + snapshot.text(), + concat!( + "ccc\n", // + "ddd\n", // + "eee", // + "\n", // End of excerpt + "ggg\n", // + "hhh\n", // + "iii", // + "\n", // End of excerpt + "ooo\n", // + "ppp\n", // + "qqq", // End of excerpt + ) + ); + drop(snapshot); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.expand_excerpts( + multibuffer.excerpt_ids(), + 1, + ExpandExcerptDirection::UpAndDown, + cx, + ) + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + // Expanding context lines causes the line containing 'fff' to appear in two different excerpts. + // We don't attempt to merge them, because removing the excerpt could create inconsistency with other layers + // that are tracking excerpt ids. + assert_eq!( + snapshot.text(), + concat!( + "bbb\n", // + "ccc\n", // + "ddd\n", // + "eee\n", // + "fff\n", // End of excerpt + "fff\n", // + "ggg\n", // + "hhh\n", // + "iii\n", // + "jjj\n", // End of excerpt + "nnn\n", // + "ooo\n", // + "ppp\n", // + "qqq\n", // + "rrr", // End of excerpt + ) + ); +} + +#[gpui::test] +fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { + let buffer = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + vec![ + // Note that in this test, this first excerpt + // does contain a new line + Point::new(3, 2)..Point::new(4, 2), + Point::new(7, 1)..Point::new(7, 3), + Point::new(15, 0)..Point::new(15, 0), + ], + 2, + cx, + ) + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!( + snapshot.text(), + concat!( + "bbb\n", // Preserve newlines + "ccc\n", // + "ddd\n", // + "eee\n", // + "fff\n", // + "ggg\n", // + "hhh\n", // + "iii\n", // + "jjj\n", // + "nnn\n", // + "ooo\n", // + "ppp\n", // + "qqq\n", // + "rrr", // + ) + ); + + assert_eq!( + anchor_ranges + .iter() + .map(|range| range.to_point(&snapshot)) + .collect::>(), + vec![ + Point::new(2, 2)..Point::new(3, 2), + Point::new(6, 1)..Point::new(6, 3), + Point::new(11, 0)..Point::new(11, 0) + ] + ); +} + +#[gpui::test(iterations = 100)] +async fn test_push_multiple_excerpts_with_context_lines(cx: &mut TestAppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(15, 4, 'a'), cx)); + let snapshot_1 = buffer_1.update(cx, |buffer, _| buffer.snapshot()); + let snapshot_2 = buffer_2.update(cx, |buffer, _| buffer.snapshot()); + let ranges_1 = vec![ + snapshot_1.anchor_before(Point::new(3, 2))..snapshot_1.anchor_before(Point::new(4, 2)), + snapshot_1.anchor_before(Point::new(7, 1))..snapshot_1.anchor_before(Point::new(7, 3)), + snapshot_1.anchor_before(Point::new(15, 0))..snapshot_1.anchor_before(Point::new(15, 0)), + ]; + let ranges_2 = vec![ + snapshot_2.anchor_before(Point::new(2, 1))..snapshot_2.anchor_before(Point::new(3, 1)), + snapshot_2.anchor_before(Point::new(10, 0))..snapshot_2.anchor_before(Point::new(10, 2)), + ]; + + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let anchor_ranges = multibuffer + .update(cx, |multibuffer, cx| { + multibuffer.push_multiple_excerpts_with_context_lines( + vec![(buffer_1.clone(), ranges_1), (buffer_2.clone(), ranges_2)], + 2, + cx, + ) + }) + .await; + + let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + assert_eq!( + snapshot.text(), + concat!( + "bbb\n", // buffer_1 + "ccc\n", // + "ddd\n", // <-- excerpt 1 + "eee\n", // <-- excerpt 1 + "fff\n", // + "ggg\n", // + "hhh\n", // <-- excerpt 2 + "iii\n", // + "jjj\n", // + // + "nnn\n", // + "ooo\n", // + "ppp\n", // <-- excerpt 3 + "qqq\n", // + "rrr\n", // + // + "aaaa\n", // buffer 2 + "bbbb\n", // + "cccc\n", // <-- excerpt 4 + "dddd\n", // <-- excerpt 4 + "eeee\n", // + "ffff\n", // + // + "iiii\n", // + "jjjj\n", // + "kkkk\n", // <-- excerpt 5 + "llll\n", // + "mmmm", // + ) + ); + + assert_eq!( + anchor_ranges + .iter() + .map(|range| range.to_point(&snapshot)) + .collect::>(), + vec![ + Point::new(2, 2)..Point::new(3, 2), + Point::new(6, 1)..Point::new(6, 3), + Point::new(11, 0)..Point::new(11, 0), + Point::new(16, 1)..Point::new(17, 1), + Point::new(22, 0)..Point::new(22, 2) + ] + ); +} + +#[gpui::test] +fn test_empty_multibuffer(cx: &mut AppContext) { + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + + let snapshot = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot.text(), ""); + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + &[Some(0)] + ); + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(1)).collect::>(), + &[] + ); +} + +#[gpui::test] +fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { + let buffer = cx.new_model(|cx| Buffer::local("abcd", cx)); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + let old_snapshot = multibuffer.read(cx).snapshot(cx); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "X")], None, cx); + buffer.edit([(5..5, "Y")], None, cx); + }); + let new_snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(old_snapshot.text(), "abcd"); + assert_eq!(new_snapshot.text(), "XabcdY"); + + assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); + assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); + assert_eq!(old_snapshot.anchor_before(4).to_offset(&new_snapshot), 5); + assert_eq!(old_snapshot.anchor_after(4).to_offset(&new_snapshot), 6); +} + +#[gpui::test] +fn test_multibuffer_anchors(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local("abcd", cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local("efghi", cx)); + let multibuffer = cx.new_model(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..4, + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..5, + primary: None, + }], + cx, + ); + multibuffer + }); + let old_snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(old_snapshot.anchor_before(0).to_offset(&old_snapshot), 0); + assert_eq!(old_snapshot.anchor_after(0).to_offset(&old_snapshot), 0); + assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); + assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); + assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); + assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); + + buffer_1.update(cx, |buffer, cx| { + buffer.edit([(0..0, "W")], None, cx); + buffer.edit([(5..5, "X")], None, cx); + }); + buffer_2.update(cx, |buffer, cx| { + buffer.edit([(0..0, "Y")], None, cx); + buffer.edit([(6..6, "Z")], None, cx); + }); + let new_snapshot = multibuffer.read(cx).snapshot(cx); + + assert_eq!(old_snapshot.text(), "abcd\nefghi"); + assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ"); + + assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); + assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); + assert_eq!(old_snapshot.anchor_before(1).to_offset(&new_snapshot), 2); + assert_eq!(old_snapshot.anchor_after(1).to_offset(&new_snapshot), 2); + assert_eq!(old_snapshot.anchor_before(2).to_offset(&new_snapshot), 3); + assert_eq!(old_snapshot.anchor_after(2).to_offset(&new_snapshot), 3); + assert_eq!(old_snapshot.anchor_before(5).to_offset(&new_snapshot), 7); + assert_eq!(old_snapshot.anchor_after(5).to_offset(&new_snapshot), 8); + assert_eq!(old_snapshot.anchor_before(10).to_offset(&new_snapshot), 13); + assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14); +} + +#[gpui::test] +fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local("abcd", cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local("ABCDEFGHIJKLMNOP", cx)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + + // Create an insertion id in buffer 1 that doesn't exist in buffer 2. + // Add an excerpt from buffer 1 that spans this new insertion. + buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); + let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..7, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + + let snapshot_1 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_1.text(), "abcd123"); + + // Replace the buffer 1 excerpt with new excerpts from buffer 2. + let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt_id_1], cx); + let mut ids = multibuffer + .push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: 0..4, + primary: None, + }, + ExcerptRange { + context: 6..10, + primary: None, + }, + ExcerptRange { + context: 12..16, + primary: None, + }, + ], + cx, + ) + .into_iter(); + (ids.next().unwrap(), ids.next().unwrap()) + }); + let snapshot_2 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); + + // The old excerpt id doesn't get reused. + assert_ne!(excerpt_id_2, excerpt_id_1); + + // Resolve some anchors from the previous snapshot in the new snapshot. + // The current excerpts are from a different buffer, so we don't attempt to + // resolve the old text anchor in the new buffer. + assert_eq!( + snapshot_2.summary_for_anchor::(&snapshot_1.anchor_before(2)), + 0 + ); + assert_eq!( + snapshot_2.summaries_for_anchors::(&[ + snapshot_1.anchor_before(2), + snapshot_1.anchor_after(3) + ]), + vec![0, 0] + ); + + // Refresh anchors from the old snapshot. The return value indicates that both + // anchors lost their original excerpt. + let refresh = + snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]); + assert_eq!( + refresh, + &[ + (0, snapshot_2.anchor_before(0), false), + (1, snapshot_2.anchor_after(0), false), + ] + ); + + // Replace the middle excerpt with a smaller excerpt in buffer 2, + // that intersects the old excerpt. + let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([excerpt_id_3], cx); + multibuffer + .insert_excerpts_after( + excerpt_id_2, + buffer_2.clone(), + [ExcerptRange { + context: 5..8, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + + let snapshot_3 = multibuffer.read(cx).snapshot(cx); + assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP"); + assert_ne!(excerpt_id_5, excerpt_id_3); + + // Resolve some anchors from the previous snapshot in the new snapshot. + // The third anchor can't be resolved, since its excerpt has been removed, + // so it resolves to the same position as its predecessor. + let anchors = [ + snapshot_2.anchor_before(0), + snapshot_2.anchor_after(2), + snapshot_2.anchor_after(6), + snapshot_2.anchor_after(14), + ]; + assert_eq!( + snapshot_3.summaries_for_anchors::(&anchors), + &[0, 2, 9, 13] + ); + + let new_anchors = snapshot_3.refresh_anchors(&anchors); + assert_eq!( + new_anchors.iter().map(|a| (a.0, a.2)).collect::>(), + &[(0, true), (1, true), (2, true), (3, true)] + ); + assert_eq!( + snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.1)), + &[0, 2, 7, 13] + ); +} + +#[gpui::test(iterations = 100)] +fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let mut buffers: Vec> = Vec::new(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let mut excerpt_ids = Vec::::new(); + let mut expected_excerpts = Vec::<(Model, Range)>::new(); + let mut anchors = Vec::new(); + let mut old_versions = Vec::new(); + + for _ in 0..operations { + match rng.gen_range(0..100) { + 0..=14 if !buffers.is_empty() => { + let buffer = buffers.choose(&mut rng).unwrap(); + buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 5, cx)); + } + 15..=19 if !expected_excerpts.is_empty() => { + multibuffer.update(cx, |multibuffer, cx| { + let ids = multibuffer.excerpt_ids(); + let mut excerpts = HashSet::default(); + for _ in 0..rng.gen_range(0..ids.len()) { + excerpts.extend(ids.choose(&mut rng).copied()); + } + + let line_count = rng.gen_range(0..5); + + let excerpt_ixs = excerpts + .iter() + .map(|id| excerpt_ids.iter().position(|i| i == id).unwrap()) + .collect::>(); + log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines"); + multibuffer.expand_excerpts( + excerpts.iter().cloned(), + line_count, + ExpandExcerptDirection::UpAndDown, + cx, + ); + + if line_count > 0 { + for id in excerpts { + let excerpt_ix = excerpt_ids.iter().position(|&i| i == id).unwrap(); + let (buffer, range) = &mut expected_excerpts[excerpt_ix]; + let snapshot = buffer.read(cx).snapshot(); + let mut point_range = range.to_point(&snapshot); + point_range.start = + Point::new(point_range.start.row.saturating_sub(line_count), 0); + point_range.end = snapshot.clip_point( + Point::new(point_range.end.row + line_count, 0), + Bias::Left, + ); + point_range.end.column = snapshot.line_len(point_range.end.row); + *range = snapshot.anchor_before(point_range.start) + ..snapshot.anchor_after(point_range.end); + } + } + }); + } + 20..=29 if !expected_excerpts.is_empty() => { + let mut ids_to_remove = vec![]; + for _ in 0..rng.gen_range(1..=3) { + if expected_excerpts.is_empty() { + break; + } + + let ix = rng.gen_range(0..expected_excerpts.len()); + ids_to_remove.push(excerpt_ids.remove(ix)); + let (buffer, range) = expected_excerpts.remove(ix); + let buffer = buffer.read(cx); + log::info!( + "Removing excerpt {}: {:?}", + ix, + buffer + .text_for_range(range.to_offset(buffer)) + .collect::(), + ); + } + let snapshot = multibuffer.read(cx).read(cx); + ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); + drop(snapshot); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(ids_to_remove, cx) + }); + } + 30..=39 if !expected_excerpts.is_empty() => { + let multibuffer = multibuffer.read(cx).read(cx); + let offset = + multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left); + let bias = if rng.gen() { Bias::Left } else { Bias::Right }; + log::info!("Creating anchor at {} with bias {:?}", offset, bias); + anchors.push(multibuffer.anchor_at(offset, bias)); + anchors.sort_by(|a, b| a.cmp(b, &multibuffer)); + } + 40..=44 if !anchors.is_empty() => { + let multibuffer = multibuffer.read(cx).read(cx); + let prev_len = anchors.len(); + anchors = multibuffer + .refresh_anchors(&anchors) + .into_iter() + .map(|a| a.1) + .collect(); + + // Ensure the newly-refreshed anchors point to a valid excerpt and don't + // overshoot its boundaries. + assert_eq!(anchors.len(), prev_len); + for anchor in &anchors { + if anchor.excerpt_id == ExcerptId::min() + || anchor.excerpt_id == ExcerptId::max() + { + continue; + } + + let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap(); + assert_eq!(excerpt.id, anchor.excerpt_id); + assert!(excerpt.contains(anchor)); + } + } + _ => { + let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { + let base_text = util::RandomCharIter::new(&mut rng) + .take(25) + .collect::(); + + buffers.push(cx.new_model(|cx| Buffer::local(base_text, cx))); + buffers.last().unwrap() + } else { + buffers.choose(&mut rng).unwrap() + }; + + let buffer = buffer_handle.read(cx); + let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); + let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); + let prev_excerpt_ix = rng.gen_range(0..=expected_excerpts.len()); + let prev_excerpt_id = excerpt_ids + .get(prev_excerpt_ix) + .cloned() + .unwrap_or_else(ExcerptId::max); + let excerpt_ix = (prev_excerpt_ix + 1).min(expected_excerpts.len()); + + log::info!( + "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", + excerpt_ix, + expected_excerpts.len(), + buffer_handle.read(cx).remote_id(), + buffer.text(), + start_ix..end_ix, + &buffer.text()[start_ix..end_ix] + ); + + let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .insert_excerpts_after( + prev_excerpt_id, + buffer_handle.clone(), + [ExcerptRange { + context: start_ix..end_ix, + primary: None, + }], + cx, + ) + .pop() + .unwrap() + }); + + excerpt_ids.insert(excerpt_ix, excerpt_id); + expected_excerpts.insert(excerpt_ix, (buffer_handle.clone(), anchor_range)); + } + } + + if rng.gen_bool(0.3) { + multibuffer.update(cx, |multibuffer, cx| { + old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); + }) + } + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let mut excerpt_starts = Vec::new(); + let mut expected_text = String::new(); + let mut expected_buffer_rows = Vec::new(); + for (buffer, range) in &expected_excerpts { + let buffer = buffer.read(cx); + let buffer_range = range.to_offset(buffer); + + excerpt_starts.push(TextSummary::from(expected_text.as_str())); + expected_text.extend(buffer.text_for_range(buffer_range.clone())); + expected_text.push('\n'); + + let buffer_row_range = buffer.offset_to_point(buffer_range.start).row + ..=buffer.offset_to_point(buffer_range.end).row; + for row in buffer_row_range { + expected_buffer_rows.push(Some(row)); + } + } + // Remove final trailing newline. + if !expected_excerpts.is_empty() { + expected_text.pop(); + } + + // Always report one buffer row + if expected_buffer_rows.is_empty() { + expected_buffer_rows.push(Some(0)); + } + + assert_eq!(snapshot.text(), expected_text); + log::info!("MultiBuffer text: {:?}", expected_text); + + assert_eq!( + snapshot.buffer_rows(MultiBufferRow(0)).collect::>(), + expected_buffer_rows, + ); + + for _ in 0..5 { + let start_row = rng.gen_range(0..=expected_buffer_rows.len()); + assert_eq!( + snapshot + .buffer_rows(MultiBufferRow(start_row as u32)) + .collect::>(), + &expected_buffer_rows[start_row..], + "buffer_rows({})", + start_row + ); + } + + assert_eq!( + snapshot.widest_line_number(), + expected_buffer_rows.into_iter().flatten().max().unwrap() + 1 + ); + + let mut excerpt_starts = excerpt_starts.into_iter(); + for (buffer, range) in &expected_excerpts { + let buffer = buffer.read(cx); + let buffer_id = buffer.remote_id(); + let buffer_range = range.to_offset(buffer); + let buffer_start_point = buffer.offset_to_point(buffer_range.start); + let buffer_start_point_utf16 = + buffer.text_summary_for_range::(0..buffer_range.start); + + let excerpt_start = excerpt_starts.next().unwrap(); + let mut offset = excerpt_start.len; + let mut buffer_offset = buffer_range.start; + let mut point = excerpt_start.lines; + let mut buffer_point = buffer_start_point; + let mut point_utf16 = excerpt_start.lines_utf16(); + let mut buffer_point_utf16 = buffer_start_point_utf16; + for ch in buffer + .snapshot() + .chunks(buffer_range.clone(), false) + .flat_map(|c| c.text.chars()) + { + for _ in 0..ch.len_utf8() { + let left_offset = snapshot.clip_offset(offset, Bias::Left); + let right_offset = snapshot.clip_offset(offset, Bias::Right); + let buffer_left_offset = buffer.clip_offset(buffer_offset, Bias::Left); + let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right); + assert_eq!( + left_offset, + excerpt_start.len + (buffer_left_offset - buffer_range.start), + "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}", + offset, + buffer_id, + buffer_offset, + ); + assert_eq!( + right_offset, + excerpt_start.len + (buffer_right_offset - buffer_range.start), + "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}", + offset, + buffer_id, + buffer_offset, + ); + + let left_point = snapshot.clip_point(point, Bias::Left); + let right_point = snapshot.clip_point(point, Bias::Right); + let buffer_left_point = buffer.clip_point(buffer_point, Bias::Left); + let buffer_right_point = buffer.clip_point(buffer_point, Bias::Right); + assert_eq!( + left_point, + excerpt_start.lines + (buffer_left_point - buffer_start_point), + "clip_point({:?}, Left). buffer: {:?}, buffer point: {:?}", + point, + buffer_id, + buffer_point, + ); + assert_eq!( + right_point, + excerpt_start.lines + (buffer_right_point - buffer_start_point), + "clip_point({:?}, Right). buffer: {:?}, buffer point: {:?}", + point, + buffer_id, + buffer_point, + ); + + assert_eq!( + snapshot.point_to_offset(left_point), + left_offset, + "point_to_offset({:?})", + left_point, + ); + assert_eq!( + snapshot.offset_to_point(left_offset), + left_point, + "offset_to_point({:?})", + left_offset, + ); + + offset += 1; + buffer_offset += 1; + if ch == '\n' { + point += Point::new(1, 0); + buffer_point += Point::new(1, 0); + } else { + point += Point::new(0, 1); + buffer_point += Point::new(0, 1); + } + } + + for _ in 0..ch.len_utf16() { + let left_point_utf16 = + snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left); + let right_point_utf16 = + snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right); + let buffer_left_point_utf16 = + buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left); + let buffer_right_point_utf16 = + buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right); + assert_eq!( + left_point_utf16, + excerpt_start.lines_utf16() + + (buffer_left_point_utf16 - buffer_start_point_utf16), + "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}", + point_utf16, + buffer_id, + buffer_point_utf16, + ); + assert_eq!( + right_point_utf16, + excerpt_start.lines_utf16() + + (buffer_right_point_utf16 - buffer_start_point_utf16), + "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}", + point_utf16, + buffer_id, + buffer_point_utf16, + ); + + if ch == '\n' { + point_utf16 += PointUtf16::new(1, 0); + buffer_point_utf16 += PointUtf16::new(1, 0); + } else { + point_utf16 += PointUtf16::new(0, 1); + buffer_point_utf16 += PointUtf16::new(0, 1); + } + } + } + } + + for (row, line) in expected_text.split('\n').enumerate() { + assert_eq!( + snapshot.line_len(MultiBufferRow(row as u32)), + line.len() as u32, + "line_len({}).", + row + ); + } + + let text_rope = Rope::from(expected_text.as_str()); + for _ in 0..10 { + let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); + let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + + let text_for_range = snapshot + .text_for_range(start_ix..end_ix) + .collect::(); + assert_eq!( + text_for_range, + &expected_text[start_ix..end_ix], + "incorrect text for range {:?}", + start_ix..end_ix + ); + + let excerpted_buffer_ranges = multibuffer + .read(cx) + .range_to_buffer_ranges(start_ix..end_ix, cx); + let excerpted_buffers_text = excerpted_buffer_ranges + .iter() + .map(|(buffer, buffer_range, _)| { + buffer + .read(cx) + .text_for_range(buffer_range.clone()) + .collect::() + }) + .collect::>() + .join("\n"); + assert_eq!(excerpted_buffers_text, text_for_range); + if !expected_excerpts.is_empty() { + assert!(!excerpted_buffer_ranges.is_empty()); + } + + let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]); + assert_eq!( + snapshot.text_summary_for_range::(start_ix..end_ix), + expected_summary, + "incorrect summary for range {:?}", + start_ix..end_ix + ); + } + + // Anchor resolution + let summaries = snapshot.summaries_for_anchors::(&anchors); + assert_eq!(anchors.len(), summaries.len()); + for (anchor, resolved_offset) in anchors.iter().zip(summaries) { + assert!(resolved_offset <= snapshot.len()); + assert_eq!( + snapshot.summary_for_anchor::(anchor), + resolved_offset + ); + } + + for _ in 0..10 { + let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); + assert_eq!( + snapshot.reversed_chars_at(end_ix).collect::(), + expected_text[..end_ix].chars().rev().collect::(), + ); + } + + for _ in 0..10 { + let end_ix = rng.gen_range(0..=text_rope.len()); + let start_ix = rng.gen_range(0..=end_ix); + assert_eq!( + snapshot + .bytes_in_range(start_ix..end_ix) + .flatten() + .copied() + .collect::>(), + expected_text.as_bytes()[start_ix..end_ix].to_vec(), + "bytes_in_range({:?})", + start_ix..end_ix, + ); + } + } + + let snapshot = multibuffer.read(cx).snapshot(cx); + for (old_snapshot, subscription) in old_versions { + let edits = subscription.consume().into_inner(); + + log::info!( + "applying subscription edits to old text: {:?}: {:?}", + old_snapshot.text(), + edits, + ); + + let mut text = old_snapshot.text(); + for edit in edits { + let new_text: String = snapshot.text_for_range(edit.new.clone()).collect(); + text.replace_range(edit.new.start..edit.new.start + edit.old.len(), &new_text); + } + assert_eq!(text.to_string(), snapshot.text()); + } +} + +#[gpui::test] +fn test_history(cx: &mut AppContext) { + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); + let group_interval: Duration = Duration::from_millis(1); + let buffer_1 = cx.new_model(|cx| { + let mut buf = Buffer::local("1234", cx); + buf.set_group_interval(group_interval); + buf + }); + let buffer_2 = cx.new_model(|cx| { + let mut buf = Buffer::local("5678", cx); + buf.set_group_interval(group_interval); + buf + }); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + multibuffer.update(cx, |this, _| { + this.history.group_interval = group_interval; + }); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + ); + }); + + let mut now = Instant::now(); + + multibuffer.update(cx, |multibuffer, cx| { + let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); + multibuffer.edit( + [ + (Point::new(0, 0)..Point::new(0, 0), "A"), + (Point::new(1, 0)..Point::new(1, 0), "A"), + ], + None, + cx, + ); + multibuffer.edit( + [ + (Point::new(0, 1)..Point::new(0, 1), "B"), + (Point::new(1, 1)..Point::new(1, 1), "B"), + ], + None, + cx, + ); + multibuffer.end_transaction_at(now, cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + // Verify edited ranges for transaction 1 + assert_eq!( + multibuffer.edited_ranges_for_transaction(transaction_1, cx), + &[ + Point::new(0, 0)..Point::new(0, 2), + Point::new(1, 0)..Point::new(1, 2) + ] + ); + + // Edit buffer 1 through the multibuffer + now += 2 * group_interval; + multibuffer.start_transaction_at(now, cx); + multibuffer.edit([(2..2, "C")], None, cx); + multibuffer.end_transaction_at(now, cx); + assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); + + // Edit buffer 1 independently + buffer_1.update(cx, |buffer_1, cx| { + buffer_1.start_transaction_at(now); + buffer_1.edit([(3..3, "D")], None, cx); + buffer_1.end_transaction_at(now, cx); + + now += 2 * group_interval; + buffer_1.start_transaction_at(now); + buffer_1.edit([(4..4, "E")], None, cx); + buffer_1.end_transaction_at(now, cx); + }); + assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); + + // An undo in the multibuffer undoes the multibuffer transaction + // and also any individual buffer edits that have occurred since + // that transaction. + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); + + // Undo buffer 2 independently. + buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx)); + assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678"); + + // An undo in the multibuffer undoes the components of the + // the last multibuffer transaction that are not already undone. + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678"); + + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); + + buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx)); + assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); + + // Redo stack gets cleared after an edit. + now += 2 * group_interval; + multibuffer.start_transaction_at(now, cx); + multibuffer.edit([(0..0, "X")], None, cx); + multibuffer.end_transaction_at(now, cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + + // Transactions can be grouped manually. + multibuffer.redo(cx); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + multibuffer.group_until_transaction(transaction_1, cx); + multibuffer.undo(cx); + assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); + multibuffer.redo(cx); + assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); + }); +} + +#[gpui::test] +fn test_excerpts_in_ranges_no_ranges(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + ); + }); + + let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + + let mut excerpts = snapshot.excerpts_in_ranges(iter::from_fn(|| None)); + + assert!(excerpts.next().is_none()); +} + +fn validate_excerpts( + actual: &[(ExcerptId, BufferId, Range)], + expected: &Vec<(ExcerptId, BufferId, Range)>, +) { + assert_eq!(actual.len(), expected.len()); + + actual + .iter() + .zip(expected) + .map(|(actual, expected)| { + assert_eq!(actual.0, expected.0); + assert_eq!(actual.1, expected.1); + assert_eq!(actual.2.start, expected.2.start); + assert_eq!(actual.2.end, expected.2.end); + }) + .collect_vec(); +} + +fn map_range_from_excerpt( + snapshot: &MultiBufferSnapshot, + excerpt_id: ExcerptId, + excerpt_buffer: &BufferSnapshot, + range: Range, +) -> Range { + snapshot + .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_before(range.start)) + .unwrap() + ..snapshot + .anchor_in_excerpt(excerpt_id, excerpt_buffer.anchor_after(range.end)) + .unwrap() +} + +fn make_expected_excerpt_info( + snapshot: &MultiBufferSnapshot, + cx: &mut AppContext, + excerpt_id: ExcerptId, + buffer: &Model, + range: Range, +) -> (ExcerptId, BufferId, Range) { + ( + excerpt_id, + buffer.read(cx).remote_id(), + map_range_from_excerpt(snapshot, excerpt_id, &buffer.read(cx).snapshot(), range), + ) +} + +#[gpui::test] +fn test_excerpts_in_ranges_range_inside_the_excerpt(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let buffer_len = buffer_1.read(cx).len(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let mut expected_excerpt_id = ExcerptId(0); + + multibuffer.update(cx, |multibuffer, cx| { + expected_excerpt_id = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + )[0]; + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + ); + }); + + let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + + let range = snapshot + .anchor_in_excerpt(expected_excerpt_id, buffer_1.read(cx).anchor_before(1)) + .unwrap() + ..snapshot + .anchor_in_excerpt( + expected_excerpt_id, + buffer_1.read(cx).anchor_after(buffer_len / 2), + ) + .unwrap(); + + let expected_excerpts = vec![make_expected_excerpt_info( + &snapshot, + cx, + expected_excerpt_id, + &buffer_1, + 1..(buffer_len / 2), + )]; + + let excerpts = snapshot + .excerpts_in_ranges(vec![range.clone()].into_iter()) + .map(|(excerpt_id, buffer, actual_range)| { + ( + excerpt_id, + buffer.remote_id(), + map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), + ) + }) + .collect_vec(); + + validate_excerpts(&excerpts, &expected_excerpts); +} + +#[gpui::test] +fn test_excerpts_in_ranges_range_crosses_excerpts_boundary(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let buffer_len = buffer_1.read(cx).len(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let mut excerpt_1_id = ExcerptId(0); + let mut excerpt_2_id = ExcerptId(0); + + multibuffer.update(cx, |multibuffer, cx| { + excerpt_1_id = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + )[0]; + excerpt_2_id = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + )[0]; + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let expected_range = snapshot + .anchor_in_excerpt( + excerpt_1_id, + buffer_1.read(cx).anchor_before(buffer_len / 2), + ) + .unwrap() + ..snapshot + .anchor_in_excerpt(excerpt_2_id, buffer_2.read(cx).anchor_after(buffer_len / 2)) + .unwrap(); + + let expected_excerpts = vec![ + make_expected_excerpt_info( + &snapshot, + cx, + excerpt_1_id, + &buffer_1, + (buffer_len / 2)..buffer_len, + ), + make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len / 2), + ]; + + let excerpts = snapshot + .excerpts_in_ranges(vec![expected_range.clone()].into_iter()) + .map(|(excerpt_id, buffer, actual_range)| { + ( + excerpt_id, + buffer.remote_id(), + map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), + ) + }) + .collect_vec(); + + validate_excerpts(&excerpts, &expected_excerpts); +} + +#[gpui::test] +fn test_excerpts_in_ranges_range_encloses_excerpt(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'r'), cx)); + let buffer_len = buffer_1.read(cx).len(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let mut excerpt_1_id = ExcerptId(0); + let mut excerpt_2_id = ExcerptId(0); + let mut excerpt_3_id = ExcerptId(0); + + multibuffer.update(cx, |multibuffer, cx| { + excerpt_1_id = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + )[0]; + excerpt_2_id = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + )[0]; + excerpt_3_id = multibuffer.push_excerpts( + buffer_3.clone(), + [ExcerptRange { + context: 0..buffer_3.read(cx).len(), + primary: None, + }], + cx, + )[0]; + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let expected_range = snapshot + .anchor_in_excerpt( + excerpt_1_id, + buffer_1.read(cx).anchor_before(buffer_len / 2), + ) + .unwrap() + ..snapshot + .anchor_in_excerpt(excerpt_3_id, buffer_3.read(cx).anchor_after(buffer_len / 2)) + .unwrap(); + + let expected_excerpts = vec![ + make_expected_excerpt_info( + &snapshot, + cx, + excerpt_1_id, + &buffer_1, + (buffer_len / 2)..buffer_len, + ), + make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, 0..buffer_len), + make_expected_excerpt_info(&snapshot, cx, excerpt_3_id, &buffer_3, 0..buffer_len / 2), + ]; + + let excerpts = snapshot + .excerpts_in_ranges(vec![expected_range.clone()].into_iter()) + .map(|(excerpt_id, buffer, actual_range)| { + ( + excerpt_id, + buffer.remote_id(), + map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), + ) + }) + .collect_vec(); + + validate_excerpts(&excerpts, &expected_excerpts); +} + +#[gpui::test] +fn test_excerpts_in_ranges_multiple_ranges(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let buffer_len = buffer_1.read(cx).len(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let mut excerpt_1_id = ExcerptId(0); + let mut excerpt_2_id = ExcerptId(0); + + multibuffer.update(cx, |multibuffer, cx| { + excerpt_1_id = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + )[0]; + excerpt_2_id = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + )[0]; + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let ranges = vec![ + 1..(buffer_len / 4), + (buffer_len / 3)..(buffer_len / 2), + (buffer_len / 4 * 3)..(buffer_len), + ]; + + let expected_excerpts = ranges + .iter() + .map(|range| { + make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, range.clone()) + }) + .collect_vec(); + + let ranges = ranges.into_iter().map(|range| { + map_range_from_excerpt( + &snapshot, + excerpt_1_id, + &buffer_1.read(cx).snapshot(), + range, + ) + }); + + let excerpts = snapshot + .excerpts_in_ranges(ranges) + .map(|(excerpt_id, buffer, actual_range)| { + ( + excerpt_id, + buffer.remote_id(), + map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), + ) + }) + .collect_vec(); + + validate_excerpts(&excerpts, &expected_excerpts); +} + +#[gpui::test] +fn test_excerpts_in_ranges_range_ends_at_excerpt_end(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let buffer_len = buffer_1.read(cx).len(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + let mut excerpt_1_id = ExcerptId(0); + let mut excerpt_2_id = ExcerptId(0); + + multibuffer.update(cx, |multibuffer, cx| { + excerpt_1_id = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + )[0]; + excerpt_2_id = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + )[0]; + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let ranges = [0..buffer_len, (buffer_len / 3)..(buffer_len / 2)]; + + let expected_excerpts = vec![ + make_expected_excerpt_info(&snapshot, cx, excerpt_1_id, &buffer_1, ranges[0].clone()), + make_expected_excerpt_info(&snapshot, cx, excerpt_2_id, &buffer_2, ranges[1].clone()), + ]; + + let ranges = [ + map_range_from_excerpt( + &snapshot, + excerpt_1_id, + &buffer_1.read(cx).snapshot(), + ranges[0].clone(), + ), + map_range_from_excerpt( + &snapshot, + excerpt_2_id, + &buffer_2.read(cx).snapshot(), + ranges[1].clone(), + ), + ]; + + let excerpts = snapshot + .excerpts_in_ranges(ranges.into_iter()) + .map(|(excerpt_id, buffer, actual_range)| { + ( + excerpt_id, + buffer.remote_id(), + map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range), + ) + }) + .collect_vec(); + + validate_excerpts(&excerpts, &expected_excerpts); +} + +#[gpui::test] +fn test_split_ranges(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + ); + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let buffer_1_len = buffer_1.read(cx).len(); + let buffer_2_len = buffer_2.read(cx).len(); + let buffer_1_midpoint = buffer_1_len / 2; + let buffer_2_start = buffer_1_len + '\n'.len_utf8(); + let buffer_2_midpoint = buffer_2_start + buffer_2_len / 2; + let total_len = buffer_2_start + buffer_2_len; + + let input_ranges = [ + 0..buffer_1_midpoint, + buffer_1_midpoint..buffer_2_midpoint, + buffer_2_midpoint..total_len, + ] + .map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)); + + let actual_ranges = snapshot + .split_ranges(input_ranges.into_iter()) + .map(|range| range.to_offset(&snapshot)) + .collect::>(); + + let expected_ranges = vec![ + 0..buffer_1_midpoint, + buffer_1_midpoint..buffer_1_len, + buffer_2_start..buffer_2_midpoint, + buffer_2_midpoint..total_len, + ]; + + assert_eq!(actual_ranges, expected_ranges); +} + +#[gpui::test] +fn test_split_ranges_single_range_spanning_three_excerpts(cx: &mut AppContext) { + let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); + let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text(6, 6, 'm'), cx)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite)); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: 0..buffer_1.read(cx).len(), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: 0..buffer_2.read(cx).len(), + primary: None, + }], + cx, + ); + multibuffer.push_excerpts( + buffer_3.clone(), + [ExcerptRange { + context: 0..buffer_3.read(cx).len(), + primary: None, + }], + cx, + ); + }); + + let snapshot = multibuffer.read(cx).snapshot(cx); + + let buffer_1_len = buffer_1.read(cx).len(); + let buffer_2_len = buffer_2.read(cx).len(); + let buffer_3_len = buffer_3.read(cx).len(); + let buffer_2_start = buffer_1_len + '\n'.len_utf8(); + let buffer_3_start = buffer_2_start + buffer_2_len + '\n'.len_utf8(); + let buffer_1_midpoint = buffer_1_len / 2; + let buffer_3_midpoint = buffer_3_start + buffer_3_len / 2; + + let input_range = + snapshot.anchor_before(buffer_1_midpoint)..snapshot.anchor_after(buffer_3_midpoint); + + let actual_ranges = snapshot + .split_ranges(std::iter::once(input_range)) + .map(|range| range.to_offset(&snapshot)) + .collect::>(); + + let expected_ranges = vec![ + buffer_1_midpoint..buffer_1_len, + buffer_2_start..buffer_2_start + buffer_2_len, + buffer_3_start..buffer_3_midpoint, + ]; + + assert_eq!(actual_ranges, expected_ranges); +}