Start work on following in zed2

Max Brunsfeld and Nathan created

Co-authored-by: Nathan <nathan@zed.dev>

Change summary

crates/collab_ui2/src/collab_panel.rs |   7 
crates/gpui2/src/window.rs            |   7 
crates/workspace2/src/pane_group.rs   | 103 ++++++++-
crates/workspace2/src/workspace2.rs   | 303 ++++++++++++++--------------
4 files changed, 249 insertions(+), 171 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1165,12 +1165,11 @@ impl CollabPanel {
                         div().into_any_element()
                     }),
             )
-            .when(!is_current_user, |this| {
+            .when_some(peer_id, |this, peer_id| {
                 this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
                     .on_click(cx.listener(move |this, _, cx| {
-                        this.workspace.update(cx, |workspace, cx| {
-                            // workspace.follow(peer_id, cx)
-                        });
+                        this.workspace
+                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
                     }))
             })
     }

crates/gpui2/src/window.rs 🔗

@@ -2708,6 +2708,7 @@ pub enum ElementId {
     Integer(usize),
     Name(SharedString),
     FocusHandle(FocusId),
+    NamedInteger(SharedString, usize),
 }
 
 impl ElementId {
@@ -2757,3 +2758,9 @@ impl<'a> From<&'a FocusHandle> for ElementId {
         ElementId::FocusHandle(handle.id)
     }
 }
+
+impl From<(&'static str, EntityId)> for ElementId {
+    fn from((name, id): (&'static str, EntityId)) -> Self {
+        ElementId::NamedInteger(name.into(), id.as_u64() as usize)
+    }
+}

crates/workspace2/src/pane_group.rs 🔗

@@ -1,19 +1,20 @@
 use crate::{AppState, FollowerState, Pane, Workspace};
 use anyhow::{anyhow, bail, Result};
-use call::ActiveCall;
+use call::{ActiveCall, ParticipantLocation};
 use collections::HashMap;
 use db::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
     statement::Statement,
 };
 use gpui::{
-    point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext,
+    point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View,
+    ViewContext,
 };
 use parking_lot::Mutex;
 use project::Project;
 use serde::Deserialize;
 use std::sync::Arc;
-use ui::prelude::*;
+use ui::{prelude::*, Button};
 
 const HANDLE_HITBOX_SIZE: f32 = 4.0;
 const HORIZONTAL_MIN_SIZE: f32 = 80.;
@@ -207,19 +208,89 @@ impl Member {
     ) -> impl IntoElement {
         match self {
             Member::Pane(pane) => {
-                // todo!()
-                // let pane_element = if Some(pane.into()) == zoomed {
-                //     None
-                // } else {
-                //     Some(pane)
-                // };
-
-                div().size_full().child(pane.clone()).into_any()
-
-                //         Stack::new()
-                //             .with_child(pane_element.contained().with_border(leader_border))
-                //             .with_children(leader_status_box)
-                //             .into_any()
+                let leader = follower_states.get(pane).and_then(|state| {
+                    let room = active_call?.read(cx).room()?.read(cx);
+                    room.remote_participant_for_peer_id(state.leader_id)
+                });
+
+                let mut leader_border = None;
+                let mut leader_status_box = None;
+                if let Some(leader) = &leader {
+                    let mut leader_color = cx
+                        .theme()
+                        .players()
+                        .color_for_participant(leader.participant_index.0)
+                        .cursor;
+                    leader_color.fade_out(0.3);
+                    leader_border = Some(leader_color);
+
+                    leader_status_box = match leader.location {
+                        ParticipantLocation::SharedProject {
+                            project_id: leader_project_id,
+                        } => {
+                            if Some(leader_project_id) == project.read(cx).remote_id() {
+                                None
+                            } else {
+                                let leader_user = leader.user.clone();
+                                let leader_user_id = leader.user.id;
+                                Some(
+                                    Button::new(
+                                        ("leader-status", pane.entity_id()),
+                                        format!(
+                                            "Follow {} to their active project",
+                                            leader_user.github_login,
+                                        ),
+                                    )
+                                    .on_click(cx.listener(
+                                        move |this, _, cx| {
+                                            crate::join_remote_project(
+                                                leader_project_id,
+                                                leader_user_id,
+                                                this.app_state().clone(),
+                                                cx,
+                                            )
+                                            .detach_and_log_err(cx);
+                                        },
+                                    )),
+                                )
+                            }
+                        }
+                        ParticipantLocation::UnsharedProject => Some(Button::new(
+                            ("leader-status", pane.entity_id()),
+                            format!(
+                                "{} is viewing an unshared Zed project",
+                                leader.user.github_login
+                            ),
+                        )),
+                        ParticipantLocation::External => Some(Button::new(
+                            ("leader-status", pane.entity_id()),
+                            format!(
+                                "{} is viewing a window outside of Zed",
+                                leader.user.github_login
+                            ),
+                        )),
+                    };
+                }
+
+                div()
+                    .relative()
+                    .size_full()
+                    .child(pane.clone())
+                    .when_some(leader_border, |this, color| {
+                        this.border_2().border_color(color)
+                    })
+                    .when_some(leader_status_box, |this, status_box| {
+                        this.child(
+                            div()
+                                .absolute()
+                                .w_96()
+                                .bottom_3()
+                                .right_3()
+                                .z_index(1)
+                                .child(status_box),
+                        )
+                    })
+                    .into_any()
 
                 // let el = div()
                 //     .flex()

crates/workspace2/src/workspace2.rs 🔗

@@ -2270,60 +2270,60 @@ impl Workspace {
         cx.notify();
     }
 
-    //     fn start_following(
-    //         &mut self,
-    //         leader_id: PeerId,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let pane = self.active_pane().clone();
-
-    //         self.last_leaders_by_pane
-    //             .insert(pane.downgrade(), leader_id);
-    //         self.unfollow(&pane, cx);
-    //         self.follower_states.insert(
-    //             pane.clone(),
-    //             FollowerState {
-    //                 leader_id,
-    //                 active_view_id: None,
-    //                 items_by_leader_view_id: Default::default(),
-    //             },
-    //         );
-    //         cx.notify();
-
-    //         let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
-    //         let project_id = self.project.read(cx).remote_id();
-    //         let request = self.app_state.client.request(proto::Follow {
-    //             room_id,
-    //             project_id,
-    //             leader_id: Some(leader_id),
-    //         });
+    fn start_following(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let pane = self.active_pane().clone();
+
+        self.last_leaders_by_pane
+            .insert(pane.downgrade(), leader_id);
+        self.unfollow(&pane, cx);
+        self.follower_states.insert(
+            pane.clone(),
+            FollowerState {
+                leader_id,
+                active_view_id: None,
+                items_by_leader_view_id: Default::default(),
+            },
+        );
+        cx.notify();
 
-    //         Some(cx.spawn(|this, mut cx| async move {
-    //             let response = request.await?;
-    //             this.update(&mut cx, |this, _| {
-    //                 let state = this
-    //                     .follower_states
-    //                     .get_mut(&pane)
-    //                     .ok_or_else(|| anyhow!("following interrupted"))?;
-    //                 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
-    //                     Some(ViewId::from_proto(active_view_id)?)
-    //                 } else {
-    //                     None
-    //                 };
-    //                 Ok::<_, anyhow::Error>(())
-    //             })??;
-    //             Self::add_views_from_leader(
-    //                 this.clone(),
-    //                 leader_id,
-    //                 vec![pane],
-    //                 response.views,
-    //                 &mut cx,
-    //             )
-    //             .await?;
-    //             this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
-    //             Ok(())
-    //         }))
-    //     }
+        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+        let project_id = self.project.read(cx).remote_id();
+        let request = self.app_state.client.request(proto::Follow {
+            room_id,
+            project_id,
+            leader_id: Some(leader_id),
+        });
+
+        Some(cx.spawn(|this, mut cx| async move {
+            let response = request.await?;
+            this.update(&mut cx, |this, _| {
+                let state = this
+                    .follower_states
+                    .get_mut(&pane)
+                    .ok_or_else(|| anyhow!("following interrupted"))?;
+                state.active_view_id = if let Some(active_view_id) = response.active_view_id {
+                    Some(ViewId::from_proto(active_view_id)?)
+                } else {
+                    None
+                };
+                Ok::<_, anyhow::Error>(())
+            })??;
+            Self::add_views_from_leader(
+                this.clone(),
+                leader_id,
+                vec![pane],
+                response.views,
+                &mut cx,
+            )
+            .await?;
+            this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
+            Ok(())
+        }))
+    }
 
     //     pub fn follow_next_collaborator(
     //         &mut self,
@@ -2362,52 +2362,52 @@ impl Workspace {
     //         self.follow(leader_id, cx)
     //     }
 
-    //     pub fn follow(
-    //         &mut self,
-    //         leader_id: PeerId,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
-    //         let project = self.project.read(cx);
+    pub fn follow(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+        let project = self.project.read(cx);
 
-    //         let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
-    //             return None;
-    //         };
+        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+            return None;
+        };
 
-    //         let other_project_id = match remote_participant.location {
-    //             call::ParticipantLocation::External => None,
-    //             call::ParticipantLocation::UnsharedProject => None,
-    //             call::ParticipantLocation::SharedProject { project_id } => {
-    //                 if Some(project_id) == project.remote_id() {
-    //                     None
-    //                 } else {
-    //                     Some(project_id)
-    //                 }
-    //             }
-    //         };
+        let other_project_id = match remote_participant.location {
+            call::ParticipantLocation::External => None,
+            call::ParticipantLocation::UnsharedProject => None,
+            call::ParticipantLocation::SharedProject { project_id } => {
+                if Some(project_id) == project.remote_id() {
+                    None
+                } else {
+                    Some(project_id)
+                }
+            }
+        };
 
-    //         // if they are active in another project, follow there.
-    //         if let Some(project_id) = other_project_id {
-    //             let app_state = self.app_state.clone();
-    //             return Some(crate::join_remote_project(
-    //                 project_id,
-    //                 remote_participant.user.id,
-    //                 app_state,
-    //                 cx,
-    //             ));
-    //         }
+        // if they are active in another project, follow there.
+        if let Some(project_id) = other_project_id {
+            let app_state = self.app_state.clone();
+            return Some(crate::join_remote_project(
+                project_id,
+                remote_participant.user.id,
+                app_state,
+                cx,
+            ));
+        }
 
-    //         // if you're already following, find the right pane and focus it.
-    //         for (pane, state) in &self.follower_states {
-    //             if leader_id == state.leader_id {
-    //                 cx.focus(pane);
-    //                 return None;
-    //             }
-    //         }
+        // if you're already following, find the right pane and focus it.
+        for (pane, state) in &self.follower_states {
+            if leader_id == state.leader_id {
+                cx.focus_view(pane);
+                return None;
+            }
+        }
 
-    //         // Otherwise, follow.
-    //         self.start_following(leader_id, cx)
-    //     }
+        // Otherwise, follow.
+        self.start_following(leader_id, cx)
+    }
 
     pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
         let state = self.follower_states.remove(pane)?;
@@ -2557,57 +2557,55 @@ impl Workspace {
         }
     }
 
-    //     // RPC handlers
+    // RPC handlers
 
     fn handle_follow(
         &mut self,
-        _follower_project_id: Option<u64>,
-        _cx: &mut ViewContext<Self>,
+        follower_project_id: Option<u64>,
+        cx: &mut ViewContext<Self>,
     ) -> proto::FollowResponse {
-        todo!()
+        let client = &self.app_state.client;
+        let project_id = self.project.read(cx).remote_id();
 
-        //     let client = &self.app_state.client;
-        //     let project_id = self.project.read(cx).remote_id();
+        let active_view_id = self.active_item(cx).and_then(|i| {
+            Some(
+                i.to_followable_item_handle(cx)?
+                    .remote_id(client, cx)?
+                    .to_proto(),
+            )
+        });
 
-        //     let active_view_id = self.active_item(cx).and_then(|i| {
-        //         Some(
-        //             i.to_followable_item_handle(cx)?
-        //                 .remote_id(client, cx)?
-        //                 .to_proto(),
-        //         )
-        //     });
+        cx.notify();
 
-        //     cx.notify();
-
-        //     self.last_active_view_id = active_view_id.clone();
-        //     proto::FollowResponse {
-        //         active_view_id,
-        //         views: self
-        //             .panes()
-        //             .iter()
-        //             .flat_map(|pane| {
-        //                 let leader_id = self.leader_for_pane(pane);
-        //                 pane.read(cx).items().filter_map({
-        //                     let cx = &cx;
-        //                     move |item| {
-        //                         let item = item.to_followable_item_handle(cx)?;
-        //                         if (project_id.is_none() || project_id != follower_project_id)
-        //                             && item.is_project_item(cx)
-        //                         {
-        //                             return None;
-        //                         }
-        //                         let id = item.remote_id(client, cx)?.to_proto();
-        //                         let variant = item.to_state_proto(cx)?;
-        //                         Some(proto::View {
-        //                             id: Some(id),
-        //                             leader_id,
-        //                             variant: Some(variant),
-        //                         })
-        //                     }
-        //                 })
-        //             })
-        //             .collect(),
-        //     }
+        self.last_active_view_id = active_view_id.clone();
+        proto::FollowResponse {
+            active_view_id,
+            views: self
+                .panes()
+                .iter()
+                .flat_map(|pane| {
+                    let leader_id = self.leader_for_pane(pane);
+                    pane.read(cx).items().filter_map({
+                        let cx = &cx;
+                        move |item| {
+                            let item = item.to_followable_item_handle(cx)?;
+                            if (project_id.is_none() || project_id != follower_project_id)
+                                && item.is_project_item(cx)
+                            {
+                                return None;
+                            }
+                            let id = item.remote_id(client, cx)?.to_proto();
+                            let variant = item.to_state_proto(cx)?;
+                            Some(proto::View {
+                                id: Some(id),
+                                leader_id,
+                                variant: Some(variant),
+                            })
+                        }
+                    })
+                })
+                .collect(),
+        }
     }
 
     fn handle_update_followers(
@@ -2627,6 +2625,8 @@ impl Workspace {
         update: proto::UpdateFollowers,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
+        dbg!("process_leader_update", &update);
+
         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
                 this.update(cx, |this, _| {
@@ -3762,15 +3762,15 @@ impl Render for Workspace {
 // }
 
 impl WorkspaceStore {
-    pub fn new(client: Arc<Client>, _cx: &mut ModelContext<Self>) -> Self {
+    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
         Self {
             workspaces: Default::default(),
             followers: Default::default(),
-            _subscriptions: vec![],
-            //     client.add_request_handler(cx.weak_model(), Self::handle_follow),
-            //     client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
-            //     client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
-            // ],
+            _subscriptions: vec![
+                client.add_request_handler(cx.weak_model(), Self::handle_follow),
+                client.add_message_handler(cx.weak_model(), Self::handle_unfollow),
+                client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
+            ],
             client,
         }
     }
@@ -3875,11 +3875,13 @@ impl WorkspaceStore {
         this: Model<Self>,
         envelope: TypedEnvelope<proto::UpdateFollowers>,
         _: Arc<Client>,
-        mut cx: AsyncWindowContext,
+        mut cx: AsyncAppContext,
     ) -> Result<()> {
         let leader_id = envelope.original_sender_id()?;
         let update = envelope.payload;
 
+        dbg!("handle_upate_followers");
+
         this.update(&mut cx, |this, cx| {
             for workspace in &this.workspaces {
                 workspace.update(cx, |workspace, cx| {
@@ -4310,12 +4312,11 @@ pub fn join_remote_project(
                         Some(collaborator.peer_id)
                     });
 
-                // todo!("uncomment following")
-                // if let Some(follow_peer_id) = follow_peer_id {
-                //     workspace
-                //         .follow(follow_peer_id, cx)
-                //         .map(|follow| follow.detach_and_log_err(cx));
-                // }
+                if let Some(follow_peer_id) = follow_peer_id {
+                    workspace
+                        .follow(follow_peer_id, cx)
+                        .map(|follow| follow.detach_and_log_err(cx));
+                }
             }
         })?;