@@ -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::<Vec<_>>(),
- (0..buffer.read(cx).row_count())
- .map(Some)
- .collect::<Vec<_>>()
- );
-
- 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::<Vec<_>>(),
- (0..buffer.read(cx).row_count())
- .map(Some)
- .collect::<Vec<_>>()
- );
- }
-
- #[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::<Event>::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::<Vec<_>>(),
- [Some(1), Some(2), Some(3), Some(4), Some(3)]
- );
- assert_eq!(
- snapshot.buffer_rows(MultiBufferRow(2)).collect::<Vec<_>>(),
- [Some(3), Some(4), Some(3)]
- );
- assert_eq!(
- snapshot.buffer_rows(MultiBufferRow(4)).collect::<Vec<_>>(),
- [Some(3)]
- );
- assert_eq!(
- snapshot.buffer_rows(MultiBufferRow(5)).collect::<Vec<_>>(),
- []
- );
-
- 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<Point>,
- 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::<String>(),
- starts_new_buffer,
- )
- })
- })
- .collect::<Vec<_>>()
- }
- }
-
- #[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::<usize>(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<_>>(),
- 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<_>>(),
- 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::<Vec<_>>(),
- &[Some(0)]
- );
- assert_eq!(
- snapshot.buffer_rows(MultiBufferRow(1)).collect::<Vec<_>>(),
- &[]
- );
- }
-
- #[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::<usize>(&snapshot_1.anchor_before(2)),
- 0
- );
- assert_eq!(
- snapshot_2.summaries_for_anchors::<usize, _>(&[
- 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::<usize, _>(&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::<Vec<_>>(),
- &[(0, true), (1, true), (2, true), (3, true)]
- );
- assert_eq!(
- snapshot_3.summaries_for_anchors::<usize, _>(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<Model<Buffer>> = Vec::new();
- let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
- let mut excerpt_ids = Vec::<ExcerptId>::new();
- let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::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::<Vec<_>>();
- 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::<String>(),
- );
- }
- 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::<String>();
-
- 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::<Vec<_>>(),
- 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::<Vec<_>>(),
- &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::<PointUtf16, _>(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::<String>();
- 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::<String>()
- })
- .collect::<Vec<_>>()
- .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::<TextSummary, _>(start_ix..end_ix),
- expected_summary,
- "incorrect summary for range {:?}",
- start_ix..end_ix
- );
- }
-
- // Anchor resolution
- let summaries = snapshot.summaries_for_anchors::<usize, _>(&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::<usize>(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::<String>(),
- expected_text[..end_ix].chars().rev().collect::<String>(),
- );
- }
-
- 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::<Vec<_>>(),
- 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<Anchor>)],
- expected: &Vec<(ExcerptId, BufferId, Range<Anchor>)>,
- ) {
- 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<usize>,
- ) -> Range<Anchor> {
- 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<Buffer>,
- range: Range<usize>,
- ) -> (ExcerptId, BufferId, Range<Anchor>) {
- (
- 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::<Vec<_>>();
-
- 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::<Vec<_>>();
-
- 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);
- }
-}
@@ -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::<Vec<_>>(),
+ (0..buffer.read(cx).row_count())
+ .map(Some)
+ .collect::<Vec<_>>()
+ );
+
+ 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::<Vec<_>>(),
+ (0..buffer.read(cx).row_count())
+ .map(Some)
+ .collect::<Vec<_>>()
+ );
+}
+
+#[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::<Event>::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::<Vec<_>>(),
+ [Some(1), Some(2), Some(3), Some(4), Some(3)]
+ );
+ assert_eq!(
+ snapshot.buffer_rows(MultiBufferRow(2)).collect::<Vec<_>>(),
+ [Some(3), Some(4), Some(3)]
+ );
+ assert_eq!(
+ snapshot.buffer_rows(MultiBufferRow(4)).collect::<Vec<_>>(),
+ [Some(3)]
+ );
+ assert_eq!(
+ snapshot.buffer_rows(MultiBufferRow(5)).collect::<Vec<_>>(),
+ []
+ );
+
+ 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<Point>,
+ 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::<String>(),
+ starts_new_buffer,
+ )
+ })
+ })
+ .collect::<Vec<_>>()
+ }
+}
+
+#[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::<usize>(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<_>>(),
+ 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<_>>(),
+ 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::<Vec<_>>(),
+ &[Some(0)]
+ );
+ assert_eq!(
+ snapshot.buffer_rows(MultiBufferRow(1)).collect::<Vec<_>>(),
+ &[]
+ );
+}
+
+#[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::<usize>(&snapshot_1.anchor_before(2)),
+ 0
+ );
+ assert_eq!(
+ snapshot_2.summaries_for_anchors::<usize, _>(&[
+ 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::<usize, _>(&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::<Vec<_>>(),
+ &[(0, true), (1, true), (2, true), (3, true)]
+ );
+ assert_eq!(
+ snapshot_3.summaries_for_anchors::<usize, _>(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<Model<Buffer>> = Vec::new();
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
+ let mut excerpt_ids = Vec::<ExcerptId>::new();
+ let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::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::<Vec<_>>();
+ 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::<String>(),
+ );
+ }
+ 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::<String>();
+
+ 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::<Vec<_>>(),
+ 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::<Vec<_>>(),
+ &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::<PointUtf16, _>(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::<String>();
+ 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::<String>()
+ })
+ .collect::<Vec<_>>()
+ .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::<TextSummary, _>(start_ix..end_ix),
+ expected_summary,
+ "incorrect summary for range {:?}",
+ start_ix..end_ix
+ );
+ }
+
+ // Anchor resolution
+ let summaries = snapshot.summaries_for_anchors::<usize, _>(&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::<usize>(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::<String>(),
+ expected_text[..end_ix].chars().rev().collect::<String>(),
+ );
+ }
+
+ 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::<Vec<_>>(),
+ 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<Anchor>)],
+ expected: &Vec<(ExcerptId, BufferId, Range<Anchor>)>,
+) {
+ 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<usize>,
+) -> Range<Anchor> {
+ 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<Buffer>,
+ range: Range<usize>,
+) -> (ExcerptId, BufferId, Range<Anchor>) {
+ (
+ 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::<Vec<_>>();
+
+ 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::<Vec<_>>();
+
+ 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);
+}