Always mirror the leader's selections when following

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs  |   2 
crates/editor/src/element.rs |  11 +++
crates/editor/src/items.rs   | 117 ++++++++++++++++++++++++++-----------
crates/rpc/proto/zed.proto   |   6 +
crates/server/src/rpc.rs     |  19 +----
5 files changed, 103 insertions(+), 52 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -1568,7 +1568,7 @@ impl Editor {
     #[cfg(any(test, feature = "test-support"))]
     pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
-        cx: &mut MutableAppContext,
+        cx: &AppContext,
     ) -> Vec<Range<D>> {
         self.local_selections::<D>(cx)
             .iter()

crates/editor/src/element.rs 🔗

@@ -939,8 +939,12 @@ impl Element for EditorElement {
                         *contains_non_empty_selection |= !is_empty;
                     }
                 }
+
+                // Render the local selections in the leader's color when following.
+                let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
+
                 selections.insert(
-                    view.replica_id(cx),
+                    local_replica_id,
                     local_selections
                         .into_iter()
                         .map(|selection| crate::Selection {
@@ -958,6 +962,11 @@ impl Element for EditorElement {
                 .buffer_snapshot
                 .remote_selections_in_range(&(start_anchor..end_anchor))
             {
+                // The local selections match the leader's selections.
+                if Some(replica_id) == view.leader_replica_id {
+                    continue;
+                }
+
                 selections
                     .entry(replica_id)
                     .or_insert(Vec::new())

crates/editor/src/items.rs 🔗

@@ -1,10 +1,10 @@
-use crate::{Anchor, Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _};
+use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _};
 use anyhow::{anyhow, Result};
 use gpui::{
     elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription,
     Task, View, ViewContext, ViewHandle,
 };
-use language::{Bias, Buffer, Diagnostic, File as _};
+use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
 use project::{File, Project, ProjectEntryId, ProjectPath};
 use rpc::proto::{self, update_view};
 use std::{fmt::Write, path::PathBuf};
@@ -44,7 +44,23 @@ impl FollowableItem for Editor {
                 })
                 .unwrap_or_else(|| {
                     cx.add_view(pane.window_id(), |cx| {
-                        Editor::for_buffer(buffer, Some(project), cx)
+                        let mut editor = Editor::for_buffer(buffer, Some(project), cx);
+                        let selections = {
+                            let buffer = editor.buffer.read(cx);
+                            let buffer = buffer.read(cx);
+                            let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
+                            state
+                                .selections
+                                .into_iter()
+                                .filter_map(|selection| {
+                                    deserialize_selection(&excerpt_id, buffer_id, selection)
+                                })
+                                .collect::<Vec<_>>()
+                        };
+                        if !selections.is_empty() {
+                            editor.set_selections(selections.into(), None, cx);
+                        }
+                        editor
                     })
                 }))
         }))
@@ -55,33 +71,12 @@ impl FollowableItem for Editor {
         leader_replica_id: Option<u16>,
         cx: &mut ViewContext<Self>,
     ) {
-        let prev_leader_replica_id = self.leader_replica_id;
         self.leader_replica_id = leader_replica_id;
         if self.leader_replica_id.is_some() {
-            self.show_local_selections = false;
             self.buffer.update(cx, |buffer, cx| {
                 buffer.remove_active_selections(cx);
             });
         } else {
-            self.show_local_selections = true;
-            if let Some(leader_replica_id) = prev_leader_replica_id {
-                let selections = self
-                    .buffer
-                    .read(cx)
-                    .snapshot(cx)
-                    .remote_selections_in_range(&(Anchor::min()..Anchor::max()))
-                    .filter_map(|(replica_id, selections)| {
-                        if replica_id == leader_replica_id {
-                            Some(selections)
-                        } else {
-                            None
-                        }
-                    })
-                    .collect::<Vec<_>>();
-                if !selections.is_empty() {
-                    self.set_selections(selections.into(), None, cx);
-                }
-            }
             self.buffer.update(cx, |buffer, cx| {
                 if self.focused {
                     buffer.set_active_selections(&self.selections, cx);
@@ -99,6 +94,7 @@ impl FollowableItem for Editor {
                 .scroll_top_anchor
                 .as_ref()
                 .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)),
+            selections: self.selections.iter().map(serialize_selection).collect(),
         }))
     }
 
@@ -108,12 +104,13 @@ impl FollowableItem for Editor {
         _: &AppContext,
     ) -> Option<update_view::Variant> {
         match event {
-            Event::ScrollPositionChanged => {
+            Event::ScrollPositionChanged | Event::SelectionsChanged => {
                 Some(update_view::Variant::Editor(update_view::Editor {
                     scroll_top: self
                         .scroll_top_anchor
                         .as_ref()
                         .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)),
+                    selections: self.selections.iter().map(serialize_selection).collect(),
                 }))
             }
             _ => None,
@@ -127,25 +124,77 @@ impl FollowableItem for Editor {
     ) -> Result<()> {
         match message {
             update_view::Variant::Editor(message) => {
+                let buffer = self.buffer.read(cx);
+                let buffer = buffer.read(cx);
+                let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
+                let excerpt_id = excerpt_id.clone();
+                drop(buffer);
+
                 if let Some(anchor) = message.scroll_top {
-                    let anchor = language::proto::deserialize_anchor(anchor)
-                        .ok_or_else(|| anyhow!("invalid scroll top"))?;
-                    let anchor = {
-                        let buffer = self.buffer.read(cx);
-                        let buffer = buffer.read(cx);
-                        let (excerpt_id, _, _) = buffer.as_singleton().unwrap();
-                        buffer.anchor_in_excerpt(excerpt_id.clone(), anchor)
-                    };
-                    self.set_scroll_top_anchor(Some(anchor), cx);
+                    self.set_scroll_top_anchor(
+                        Some(Anchor {
+                            buffer_id: Some(buffer_id),
+                            excerpt_id: excerpt_id.clone(),
+                            text_anchor: language::proto::deserialize_anchor(anchor)
+                                .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                        }),
+                        cx,
+                    );
                 } else {
                     self.set_scroll_top_anchor(None, cx);
                 }
+
+                let selections = message
+                    .selections
+                    .into_iter()
+                    .filter_map(|selection| {
+                        deserialize_selection(&excerpt_id, buffer_id, selection)
+                    })
+                    .collect::<Vec<_>>();
+                if !selections.is_empty() {
+                    self.set_selections(selections.into(), None, cx);
+                }
             }
         }
         Ok(())
     }
 }
 
+fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(language::proto::serialize_anchor(
+            &selection.start.text_anchor,
+        )),
+        end: Some(language::proto::serialize_anchor(
+            &selection.end.text_anchor,
+        )),
+        reversed: selection.reversed,
+    }
+}
+
+fn deserialize_selection(
+    excerpt_id: &ExcerptId,
+    buffer_id: usize,
+    selection: proto::Selection,
+) -> Option<Selection<Anchor>> {
+    Some(Selection {
+        id: selection.id as usize,
+        start: Anchor {
+            buffer_id: Some(buffer_id),
+            excerpt_id: excerpt_id.clone(),
+            text_anchor: language::proto::deserialize_anchor(selection.start?)?,
+        },
+        end: Anchor {
+            buffer_id: Some(buffer_id),
+            excerpt_id: excerpt_id.clone(),
+            text_anchor: language::proto::deserialize_anchor(selection.end?)?,
+        },
+        reversed: selection.reversed,
+        goal: SelectionGoal::None,
+    })
+}
+
 impl Item for Editor {
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
         if let Some(data) = data.downcast_ref::<NavigationData>() {

crates/rpc/proto/zed.proto 🔗

@@ -579,7 +579,8 @@ message UpdateView {
     }
 
     message Editor {
-        Anchor scroll_top = 1;
+        repeated Selection selections = 1;
+        Anchor scroll_top = 2;
     }
 }
 
@@ -593,7 +594,8 @@ message View {
 
     message Editor {
         uint64 buffer_id = 1;
-        Anchor scroll_top = 2;
+        repeated Selection selections = 2;
+        Anchor scroll_top = 3;
     }
 }
 

crates/server/src/rpc.rs 🔗

@@ -1083,8 +1083,8 @@ mod tests {
     };
     use collections::BTreeMap;
     use editor::{
-        self, Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo,
-        Rename, ToOffset, ToggleCodeActions, Undo,
+        self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
+        ToOffset, ToggleCodeActions, Undo,
     };
     use gpui::{executor, ModelHandle, TestAppContext, ViewHandle};
     use language::{
@@ -4324,18 +4324,13 @@ mod tests {
             })
             .await;
 
+        // When client A selects something, client B does as well.
         editor_a1.update(cx_a, |editor, cx| {
-            editor.select_ranges([2..2], None, cx);
+            editor.select_ranges([1..1, 2..2], None, cx);
         });
         editor_b1
             .condition(cx_b, |editor, cx| {
-                let snapshot = editor.buffer().read(cx).snapshot(cx);
-                let selection = snapshot
-                    .remote_selections_in_range(&(Anchor::min()..Anchor::max()))
-                    .next();
-                selection.map_or(false, |selection| {
-                    selection.1.start.to_offset(&snapshot) == 2
-                })
+                editor.selected_ranges(cx) == vec![1..1, 2..2]
             })
             .await;
 
@@ -4343,10 +4338,6 @@ mod tests {
         workspace_b.update(cx_b, |workspace, cx| {
             workspace.unfollow(&workspace.active_pane().clone(), cx)
         });
-        editor_b1.update(cx_b, |editor, cx| {
-            assert_eq!(editor.selected_ranges::<usize>(cx), &[2..2]);
-        });
-
         workspace_a.update(cx_a, |workspace, cx| {
             workspace.activate_item(&editor_a2, cx);
             editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx));