Stop following when leader disconnects

Antonio Scandurra created

Change summary

crates/project/src/project.rs     |  2 +
crates/server/src/rpc.rs          | 44 +++++++++++++++++++++++++++------
crates/workspace/src/workspace.rs | 24 ++++++++++++++++-
3 files changed, 60 insertions(+), 10 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -125,6 +125,7 @@ pub enum Event {
     DiskBasedDiagnosticsFinished,
     DiagnosticsUpdated(ProjectPath),
     RemoteIdChanged(Option<u64>),
+    CollaboratorLeft(PeerId),
 }
 
 enum LanguageServerEvent {
@@ -3368,6 +3369,7 @@ impl Project {
                     buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
                 }
             }
+            cx.emit(Event::CollaboratorLeft(peer_id));
             cx.notify();
             Ok(())
         })

crates/server/src/rpc.rs 🔗

@@ -4259,6 +4259,7 @@ mod tests {
 
         // Client A opens some editors.
         let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
         let editor_a1 = workspace_a
             .update(cx_a, |workspace, cx| {
                 workspace.open_path((worktree_id, "1.txt"), cx)
@@ -4287,19 +4288,19 @@ mod tests {
             .downcast::<Editor>()
             .unwrap();
 
+        let client_a_id = project_b.read_with(cx_b, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+        let client_b_id = project_a.read_with(cx_a, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+
         // When client B starts following client A, all visible view states are replicated to client B.
         editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx));
         editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
         workspace_b
             .update(cx_b, |workspace, cx| {
-                let leader_id = project_b
-                    .read(cx)
-                    .collaborators()
-                    .values()
-                    .next()
-                    .unwrap()
-                    .peer_id;
-                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+                workspace.toggle_follow(&client_a_id.into(), cx).unwrap()
             })
             .await
             .unwrap();
@@ -4370,6 +4371,33 @@ mod tests {
                 .id()),
             editor_b1.id()
         );
+
+        // Client A starts following client B.
+        workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.toggle_follow(&client_b_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+            Some(client_b_id)
+        );
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .id()),
+            editor_a1.id()
+        );
+
+        // Following interrupts when client B disconnects.
+        client_b.disconnect(&cx_b.to_async()).unwrap();
+        cx_a.foreground().run_until_parked();
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+            None
+        );
     }
 
     #[gpui::test(iterations = 10)]

crates/workspace/src/workspace.rs 🔗

@@ -667,8 +667,14 @@ impl Workspace {
         .detach();
 
         cx.subscribe(&params.project, move |this, project, event, cx| {
-            if let project::Event::RemoteIdChanged(remote_id) = event {
-                this.project_remote_id_changed(*remote_id, cx);
+            match event {
+                project::Event::RemoteIdChanged(remote_id) => {
+                    this.project_remote_id_changed(*remote_id, cx);
+                }
+                project::Event::CollaboratorLeft(peer_id) => {
+                    this.collaborator_left(*peer_id, cx);
+                }
+                _ => {}
             }
             if project.read(cx).is_read_only() {
                 cx.blur();
@@ -1241,6 +1247,20 @@ impl Workspace {
         }
     }
 
+    fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+        self.leader_state.followers.remove(&peer_id);
+        if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
+            for state in states_by_pane.into_values() {
+                for item in state.items_by_leader_view_id.into_values() {
+                    if let FollowerItem::Loaded(item) = item {
+                        item.set_leader_replica_id(None, cx);
+                    }
+                }
+            }
+        }
+        cx.notify();
+    }
+
     pub fn toggle_follow(
         &mut self,
         ToggleFollow(leader_id): &ToggleFollow,