@@ -4967,19 +4967,27 @@ fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
}
#[gpui::test]
-fn test_following(cx: &mut gpui::MutableAppContext) {
- let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
-
- cx.set_global(Settings::test(cx));
+async fn test_following(cx: &mut gpui::TestAppContext) {
+ Settings::test_async(cx);
+ let fs = FakeFs::new(cx.background());
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
- let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
- let (_, follower) = cx.add_window(
- WindowOptions {
- bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
- ..Default::default()
- },
- |cx| build_editor(buffer.clone(), cx),
- );
+ let buffer = project.update(cx, |project, cx| {
+ let buffer = project
+ .create_buffer(&sample_text(16, 8, 'a'), None, cx)
+ .unwrap();
+ cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
+ });
+ let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
+ let (_, follower) = cx.update(|cx| {
+ cx.add_window(
+ WindowOptions {
+ bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
+ ..Default::default()
+ },
+ |cx| build_editor(buffer.clone(), cx),
+ )
+ });
let pending_update = Rc::new(RefCell::new(None));
follower.update(cx, {
@@ -5000,10 +5008,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
});
follower.update(cx, |follower, cx| {
follower
- .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+ .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
});
- assert_eq!(follower.read(cx).selections.ranges(cx), vec![1..1]);
+ follower.read_with(cx, |follower, cx| {
+ assert_eq!(follower.selections.ranges(cx), vec![1..1]);
+ });
// Update the scroll position only
leader.update(cx, |leader, cx| {
@@ -5011,7 +5021,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
});
follower.update(cx, |follower, cx| {
follower
- .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+ .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
});
assert_eq!(
@@ -5028,12 +5038,14 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
follower.update(cx, |follower, cx| {
let initial_scroll_position = follower.scroll_position(cx);
follower
- .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+ .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
assert_eq!(follower.scroll_position(cx), initial_scroll_position);
assert!(follower.autoscroll_request.is_some());
});
- assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]);
+ follower.read_with(cx, |follower, cx| {
+ assert_eq!(follower.selections.ranges(cx), vec![0..0]);
+ });
// Creating a pending selection that precedes another selection
leader.update(cx, |leader, cx| {
@@ -5042,10 +5054,12 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
});
follower.update(cx, |follower, cx| {
follower
- .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+ .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
});
- assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0, 1..1]);
+ follower.read_with(cx, |follower, cx| {
+ assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
+ });
// Extend the pending selection so that it surrounds another selection
leader.update(cx, |leader, cx| {
@@ -5053,10 +5067,143 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
});
follower.update(cx, |follower, cx| {
follower
- .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+ .apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
.unwrap();
});
- assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..2]);
+ follower.read_with(cx, |follower, cx| {
+ assert_eq!(follower.selections.ranges(cx), vec![0..2]);
+ });
+}
+
+#[gpui::test]
+async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
+ Settings::test_async(cx);
+ let fs = FakeFs::new(cx.background());
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
+
+ let leader = pane.update(cx, |_, cx| {
+ let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+ cx.add_view(|cx| build_editor(multibuffer.clone(), cx))
+ });
+
+ // Start following the editor when it has no excerpts.
+ let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+ let follower_1 = cx
+ .update(|cx| {
+ Editor::from_state_proto(pane.clone(), project.clone(), &mut state_message, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+
+ let follower_1_update = Rc::new(RefCell::new(None));
+ follower_1.update(cx, {
+ let update = follower_1_update.clone();
+ |_, cx| {
+ cx.subscribe(&leader, move |_, leader, event, cx| {
+ leader
+ .read(cx)
+ .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+ })
+ .detach();
+ }
+ });
+
+ let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
+ (
+ project
+ .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
+ .unwrap(),
+ project
+ .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
+ .unwrap(),
+ )
+ });
+
+ // Insert some excerpts.
+ leader.update(cx, |leader, cx| {
+ leader.buffer.update(cx, |multibuffer, cx| {
+ let excerpt_ids = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ ExcerptRange {
+ context: 1..6,
+ primary: None,
+ },
+ ExcerptRange {
+ context: 12..15,
+ primary: None,
+ },
+ ExcerptRange {
+ context: 0..3,
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer.insert_excerpts_after(
+ excerpt_ids[0],
+ buffer_2.clone(),
+ [
+ ExcerptRange {
+ context: 8..12,
+ primary: None,
+ },
+ ExcerptRange {
+ context: 0..6,
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ });
+ });
+
+ // Start following separately after it already has excerpts.
+ let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+ let follower_2 = cx
+ .update(|cx| {
+ Editor::from_state_proto(pane.clone(), project.clone(), &mut state_message, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ follower_2.read_with(cx, Editor::text),
+ leader.read_with(cx, Editor::text)
+ );
+
+ // Apply the update of adding the excerpts.
+ follower_1.update(cx, |follower, cx| {
+ follower
+ .apply_update_proto(&project, follower_1_update.borrow_mut().take().unwrap(), cx)
+ .unwrap()
+ });
+ assert_eq!(
+ follower_1.read_with(cx, Editor::text),
+ leader.read_with(cx, Editor::text)
+ );
+
+ // Remove some excerpts.
+ leader.update(cx, |leader, cx| {
+ leader.buffer.update(cx, |multibuffer, cx| {
+ let excerpt_ids = multibuffer.excerpt_ids();
+ multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
+ multibuffer.remove_excerpts([excerpt_ids[0]], cx);
+ });
+ });
+
+ // Apply the update of removing the excerpts.
+ follower_1.update(cx, |follower, cx| {
+ follower
+ .apply_update_proto(&project, follower_1_update.borrow_mut().take().unwrap(), cx)
+ .unwrap()
+ });
+ assert_eq!(
+ follower_1.read_with(cx, Editor::text),
+ leader.read_with(cx, Editor::text)
+ );
}
#[test]
@@ -72,10 +72,6 @@ impl FollowableItem for Editor {
editors.find(|editor| {
editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0])
})
- } else if let Some(title) = &state.title {
- editors.find(|editor| {
- editor.read(cx).buffer().read(cx).title(cx).as_ref() == title
- })
} else {
None
}
@@ -231,16 +227,17 @@ impl FollowableItem for Editor {
predecessor,
excerpts,
} => {
+ let buffer_id = buffer.read(cx).remote_id();
let mut excerpts = excerpts.iter();
if let Some((id, range)) = excerpts.next() {
update.inserted_excerpts.push(proto::ExcerptInsertion {
previous_excerpt_id: Some(predecessor.to_proto()),
- excerpt: serialize_excerpt(buffer, id, range, cx),
+ excerpt: serialize_excerpt(buffer_id, id, range),
});
update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
proto::ExcerptInsertion {
previous_excerpt_id: None,
- excerpt: serialize_excerpt(buffer, id, range, cx),
+ excerpt: serialize_excerpt(buffer_id, id, range),
}
}))
}
@@ -275,22 +272,69 @@ impl FollowableItem for Editor {
fn apply_update_proto(
&mut self,
+ project: &ModelHandle<Project>,
message: update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Result<()> {
match message {
update_view::Variant::Editor(message) => {
- let buffer = self.buffer.read(cx);
- let buffer = buffer.read(cx);
+ let multibuffer = self.buffer.read(cx);
+ let multibuffer = multibuffer.read(cx);
+ let mut removals = message
+ .deleted_excerpts
+ .into_iter()
+ .map(ExcerptId::from_proto)
+ .collect::<Vec<_>>();
+ removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
+
let selections = message
.selections
.into_iter()
- .filter_map(|selection| deserialize_selection(&buffer, selection))
+ .filter_map(|selection| deserialize_selection(&multibuffer, selection))
.collect::<Vec<_>>();
let scroll_top_anchor = message
.scroll_top_anchor
- .and_then(|anchor| deserialize_anchor(&buffer, anchor));
- drop(buffer);
+ .and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
+ drop(multibuffer);
+
+ self.buffer.update(cx, |multibuffer, cx| {
+ let mut insertions = message.inserted_excerpts.into_iter().peekable();
+ while let Some(insertion) = insertions.next() {
+ let Some(excerpt) = insertion.excerpt else { continue };
+ let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
+ let buffer_id = excerpt.buffer_id;
+ let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
+
+ let adjacent_excerpts = iter::from_fn(|| {
+ let insertion = insertions.peek()?;
+ if insertion.previous_excerpt_id.is_none()
+ && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+ {
+ insertions.next()?.excerpt
+ } else {
+ None
+ }
+ });
+
+ multibuffer.insert_excerpts_with_ids_after(
+ ExcerptId::from_proto(previous_excerpt_id),
+ buffer,
+ [excerpt]
+ .into_iter()
+ .chain(adjacent_excerpts)
+ .filter_map(|excerpt| {
+ Some((
+ ExcerptId::from_proto(excerpt.id),
+ deserialize_excerpt_range(excerpt)?,
+ ))
+ }),
+ cx,
+ );
+ }
+
+ multibuffer.remove_excerpts(removals, cx);
+ });
+
if !selections.is_empty() {
self.set_selections_from_remote(selections, cx);
@@ -318,14 +362,13 @@ impl FollowableItem for Editor {
}
fn serialize_excerpt(
- buffer: &ModelHandle<Buffer>,
+ buffer_id: u64,
id: &ExcerptId,
range: &ExcerptRange<language::Anchor>,
- cx: &AppContext,
) -> Option<proto::Excerpt> {
Some(proto::Excerpt {
id: id.to_proto(),
- buffer_id: buffer.read(cx).remote_id(),
+ buffer_id,
context_start: Some(serialize_text_anchor(&range.context.start)),
context_end: Some(serialize_text_anchor(&range.context.end)),
primary_start: range
@@ -390,7 +433,7 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
Some(Anchor {
excerpt_id,
text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
- buffer_id: Some(buffer.buffer_id_for_excerpt(excerpt_id)?),
+ buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
})
}
@@ -463,6 +463,7 @@ pub trait FollowableItem: Item {
) -> bool;
fn apply_update_proto(
&mut self,
+ project: &ModelHandle<Project>,
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Result<()>;
@@ -482,6 +483,7 @@ pub trait FollowableItemHandle: ItemHandle {
) -> bool;
fn apply_update_proto(
&self,
+ project: &ModelHandle<Project>,
message: proto::update_view::Variant,
cx: &mut MutableAppContext,
) -> Result<()>;
@@ -514,10 +516,11 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
fn apply_update_proto(
&self,
+ project: &ModelHandle<Project>,
message: proto::update_view::Variant,
cx: &mut MutableAppContext,
) -> Result<()> {
- self.update(cx, |this, cx| this.apply_update_proto(message, cx))
+ self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
}
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
@@ -2477,6 +2480,7 @@ impl Workspace {
let variant = update_view
.variant
.ok_or_else(|| anyhow!("missing update view variant"))?;
+ let project = this.project.clone();
this.update_leader_state(leader_id, cx, |state, cx| {
let variant = variant.clone();
match state
@@ -2485,7 +2489,7 @@ impl Workspace {
.or_insert(FollowerItem::Loading(Vec::new()))
{
FollowerItem::Loaded(item) => {
- item.apply_update_proto(variant, cx).log_err();
+ item.apply_update_proto(&project, variant, cx).log_err();
}
FollowerItem::Loading(updates) => updates.push(variant),
}
@@ -2576,7 +2580,7 @@ impl Workspace {
let e = e.into_mut();
if let FollowerItem::Loading(updates) = e {
for update in updates.drain(..) {
- item.apply_update_proto(update, cx)
+ item.apply_update_proto(&this.project, update, cx)
.context("failed to apply view update")
.log_err();
}