@@ -5862,10 +5862,17 @@ async fn test_basic_following(
// Client A updates their selections in those editors
editor_a1.update(cx_a, |editor, cx| {
- editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
+ editor.handle_input("a", cx);
+ editor.handle_input("b", cx);
+ editor.handle_input("c", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![3..2]);
});
editor_a2.update(cx_a, |editor, cx| {
- editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
+ editor.handle_input("d", cx);
+ editor.handle_input("e", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// When client B starts following client A, all visible view states are replicated to client B.
@@ -5878,6 +5885,27 @@ async fn test_basic_following(
.await
.unwrap();
+ cx_c.foreground().run_until_parked();
+ let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ cx_b.read(|cx| editor_b2.project_path(cx)),
+ Some((worktree_id, "2.txt").into())
+ );
+ assert_eq!(
+ editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![2..1]
+ );
+ assert_eq!(
+ editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![3..2]
+ );
+
cx_c.foreground().run_until_parked();
let active_call_c = cx_c.read(ActiveCall::global);
let project_c = client_c.build_remote_project(project_id, cx_c).await;
@@ -6033,26 +6061,6 @@ async fn test_basic_following(
});
}
- let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
- workspace
- .active_item(cx)
- .unwrap()
- .downcast::<Editor>()
- .unwrap()
- });
- assert_eq!(
- cx_b.read(|cx| editor_b2.project_path(cx)),
- Some((worktree_id, "2.txt").into())
- );
- assert_eq!(
- editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
- vec![2..3]
- );
- assert_eq!(
- editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
- vec![0..1]
- );
-
// When client A activates a different editor, client B does so as well.
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a1, cx)
@@ -3,12 +3,12 @@ use crate::{
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
- elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext,
- Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+ elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
+ RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
@@ -72,11 +72,11 @@ impl FollowableItem for Editor {
let editor = pane.read_with(&cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
- editor.remote_id(&client, cx) == Some(remote_id)
- || state.singleton
- && buffers.len() == 1
- && editor.read(cx).buffer.read(cx).as_singleton().as_ref()
- == Some(&buffers[0])
+ let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
+ let singleton_buffer_matches = state.singleton
+ && buffers.first()
+ == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
+ ids_match || singleton_buffer_matches
})
});
@@ -115,46 +115,29 @@ impl FollowableItem for Editor {
multibuffer
});
- cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
+ cx.add_view(|cx| {
+ let mut editor =
+ Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
+ editor.remote_id = Some(remote_id);
+ editor
+ })
})
});
- editor.update(&mut cx, |editor, cx| {
- editor.remote_id = Some(remote_id);
- let buffer = editor.buffer.read(cx).read(cx);
- let selections = state
- .selections
- .into_iter()
- .map(|selection| {
- deserialize_selection(&buffer, selection)
- .ok_or_else(|| anyhow!("invalid selection"))
- })
- .collect::<Result<Vec<_>>>()?;
- let pending_selection = state
- .pending_selection
- .map(|selection| deserialize_selection(&buffer, selection))
- .flatten();
- let scroll_top_anchor = state
- .scroll_top_anchor
- .and_then(|anchor| deserialize_anchor(&buffer, anchor));
- drop(buffer);
-
- if !selections.is_empty() || pending_selection.is_some() {
- editor.set_selections_from_remote(selections, pending_selection, cx);
- }
-
- if let Some(scroll_top_anchor) = scroll_top_anchor {
- editor.set_scroll_anchor_remote(
- ScrollAnchor {
- top_anchor: scroll_top_anchor,
- offset: vec2f(state.scroll_x, state.scroll_y),
- },
- cx,
- );
- }
-
- anyhow::Ok(())
- })?;
+ update_editor_from_message(
+ editor.clone(),
+ project,
+ proto::update_view::Editor {
+ selections: state.selections,
+ pending_selection: state.pending_selection,
+ scroll_top_anchor: state.scroll_top_anchor,
+ scroll_x: state.scroll_x,
+ scroll_y: state.scroll_y,
+ ..Default::default()
+ },
+ &mut cx,
+ )
+ .await?;
Ok(editor)
}))
@@ -299,107 +282,142 @@ impl FollowableItem for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let update_view::Variant::Editor(message) = message;
- let multibuffer = self.buffer.read(cx);
- let multibuffer = multibuffer.read(cx);
+ let project = project.clone();
+ cx.spawn(|this, mut cx| async move {
+ update_editor_from_message(this, project, message, &mut cx).await
+ })
+ }
- let buffer_ids = message
- .inserted_excerpts
- .iter()
- .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
- .collect::<HashSet<_>>();
+ fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
+ match event {
+ Event::Edited => true,
+ Event::SelectionsChanged { local } => *local,
+ Event::ScrollPositionChanged { local } => *local,
+ _ => false,
+ }
+ }
+}
- let mut removals = message
- .deleted_excerpts
+async fn update_editor_from_message(
+ this: ViewHandle<Editor>,
+ project: ModelHandle<Project>,
+ message: proto::update_view::Editor,
+ cx: &mut AsyncAppContext,
+) -> Result<()> {
+ // Open all of the buffers of which excerpts were added to the editor.
+ let inserted_excerpt_buffer_ids = message
+ .inserted_excerpts
+ .iter()
+ .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+ .collect::<HashSet<_>>();
+ let inserted_excerpt_buffers = project.update(cx, |project, cx| {
+ inserted_excerpt_buffer_ids
.into_iter()
- .map(ExcerptId::from_proto)
- .collect::<Vec<_>>();
- removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
+ .map(|id| project.open_buffer_by_id(id, cx))
+ .collect::<Vec<_>>()
+ });
+ let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
+
+ // Update the editor's excerpts.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |multibuffer, cx| {
+ let mut removed_excerpt_ids = message
+ .deleted_excerpts
+ .into_iter()
+ .map(ExcerptId::from_proto)
+ .collect::<Vec<_>>();
+ removed_excerpt_ids.sort_by({
+ let multibuffer = multibuffer.read(cx);
+ move |a, b| a.cmp(&b, &multibuffer)
+ });
+
+ 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(removed_excerpt_ids, cx);
+ });
+ });
+
+ // Deserialize the editor state.
+ let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
+ let buffer = editor.buffer.read(cx).read(cx);
let selections = message
.selections
.into_iter()
- .filter_map(|selection| deserialize_selection(&multibuffer, selection))
+ .filter_map(|selection| deserialize_selection(&buffer, selection))
.collect::<Vec<_>>();
let pending_selection = message
.pending_selection
- .and_then(|selection| deserialize_selection(&multibuffer, selection));
-
+ .and_then(|selection| deserialize_selection(&buffer, selection));
let scroll_top_anchor = message
.scroll_top_anchor
- .and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
- drop(multibuffer);
-
- let buffers = project.update(cx, |project, cx| {
- buffer_ids
- .into_iter()
- .map(|id| project.open_buffer_by_id(id, cx))
- .collect::<Vec<_>>()
- });
-
- let project = project.clone();
- cx.spawn(|this, mut cx| async move {
- let _buffers = try_join_all(buffers).await?;
- this.update(&mut cx, |this, cx| {
- this.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() || pending_selection.is_some() {
- this.set_selections_from_remote(selections, pending_selection, cx);
- this.request_autoscroll_remotely(Autoscroll::newest(), cx);
- } else if let Some(anchor) = scroll_top_anchor {
- this.set_scroll_anchor_remote(ScrollAnchor {
- top_anchor: anchor,
- offset: vec2f(message.scroll_x, message.scroll_y)
- }, cx);
- }
- });
- Ok(())
+ .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+ anyhow::Ok((selections, pending_selection, scroll_top_anchor))
+ })?;
+
+ // Wait until the buffer has received all of the operations referenced by
+ // the editor's new state.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.wait_for_anchors(
+ selections
+ .iter()
+ .chain(pending_selection.as_ref())
+ .flat_map(|selection| [selection.start, selection.end])
+ .chain(scroll_top_anchor),
+ cx,
+ )
})
- }
-
- fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
- match event {
- Event::Edited => true,
- Event::SelectionsChanged { local } => *local,
- Event::ScrollPositionChanged { local } => *local,
- _ => false,
+ })
+ .await?;
+
+ // Update the editor's state.
+ this.update(cx, |editor, cx| {
+ if !selections.is_empty() || pending_selection.is_some() {
+ editor.set_selections_from_remote(selections, pending_selection, cx);
+ editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
+ } else if let Some(scroll_top_anchor) = scroll_top_anchor {
+ editor.set_scroll_anchor_remote(
+ ScrollAnchor {
+ top_anchor: scroll_top_anchor,
+ offset: vec2f(message.scroll_x, message.scroll_y),
+ },
+ cx,
+ );
}
- }
+ });
+ Ok(())
}
fn serialize_excerpt(
@@ -1,6 +1,7 @@
mod anchor;
pub use anchor::{Anchor, AnchorRangeExt};
+use anyhow::{anyhow, Result};
use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
@@ -16,7 +17,9 @@ use language::{
use std::{
borrow::Cow,
cell::{Ref, RefCell},
- cmp, fmt, io,
+ cmp, fmt,
+ future::Future,
+ io,
iter::{self, FromIterator},
mem,
ops::{Range, RangeBounds, Sub},
@@ -1238,6 +1241,39 @@ impl MultiBuffer {
cx.notify();
}
+ pub fn wait_for_anchors<'a>(
+ &self,
+ anchors: impl 'a + Iterator<Item = Anchor>,
+ cx: &mut ModelContext<Self>,
+ ) -> impl 'static + Future<Output = Result<()>> {
+ let borrow = self.buffers.borrow();
+ let mut error = None;
+ let mut futures = Vec::new();
+ for anchor in anchors {
+ if let Some(buffer_id) = anchor.buffer_id {
+ if let Some(buffer) = borrow.get(&buffer_id) {
+ buffer.buffer.update(cx, |buffer, _| {
+ futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
+ });
+ } else {
+ error = Some(anyhow!(
+ "buffer {buffer_id} is not part of this multi-buffer"
+ ));
+ break;
+ }
+ }
+ }
+ async move {
+ if let Some(error) = error {
+ Err(error)?;
+ }
+ for future in futures {
+ future.await?;
+ }
+ Ok(())
+ }
+ }
+
pub fn text_anchor_for_position<T: ToOffset>(
&self,
position: T,
@@ -572,7 +572,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing origin end"))?;
buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
Some(Location {
buffer,
@@ -597,7 +597,7 @@ async fn location_links_from_proto(
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
let target = Location {
buffer,
@@ -868,7 +868,7 @@ impl LspCommand for GetReferences {
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
target_buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
locations.push(Location {
buffer: target_buffer,
@@ -1012,7 +1012,7 @@ impl LspCommand for GetDocumentHighlights {
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
buffer
- .update(&mut cx, |buffer, _| buffer.wait_for_anchors([&start, &end]))
+ .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))
.await?;
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,