diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3cc5a91cbe..d7a928e424 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -83,7 +83,8 @@ message Envelope { Follow follow = 72; FollowResponse follow_response = 73; - UpdateFollower update_follower = 74; + UpdateFollowers update_followers = 74; + Unfollow unfollow = 75; } } @@ -537,16 +538,27 @@ message UpdateDiagnostics { repeated Diagnostic diagnostics = 3; } -message Follow {} +message Follow { + uint64 project_id = 1; + uint32 leader_id = 2; +} message FollowResponse { uint64 current_view_id = 1; repeated View views = 2; } -message UpdateFollower { - uint64 current_view_id = 1; - repeated ViewUpdate view_updates = 2; +message UpdateFollowers { + uint64 project_id = 1; + uint64 current_view_id = 2; + repeated View created_views = 3; + repeated ViewUpdate updated_views = 4; + repeated uint32 follower_ids = 5; +} + +message Unfollow { + uint64 project_id = 1; + uint32 leader_id = 2; } // Entities diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 230db3119c..39a0d669d5 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -147,6 +147,8 @@ messages!( (BufferSaved, Foreground), (ChannelMessageSent, Foreground), (Error, Foreground), + (Follow, Foreground), + (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), (GetChannelMessages, Foreground), @@ -196,6 +198,7 @@ messages!( (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), (Test, Foreground), + (Unfollow, Foreground), (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), (UnshareProject, Foreground), @@ -203,6 +206,7 @@ messages!( (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), + (UpdateFollowers, Foreground), (UpdateWorktree, Foreground), ); @@ -212,6 +216,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), @@ -248,6 +253,7 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + Follow, FormatBuffers, GetCodeActions, GetCompletions, @@ -266,11 +272,13 @@ entity_messages!( SaveBuffer, SearchProject, StartLanguageServer, + Unfollow, UnregisterWorktree, UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateFollowers, UpdateLanguageServer, RegisterWorktree, UpdateWorktree, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 57a74b52ef..ec27d2400e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,7 @@ use super::{ItemHandle, SplitDirection}; use crate::{Item, Settings, WeakItemHandle, Workspace}; +use anyhow::{anyhow, Result}; +use client::PeerId; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -105,6 +107,13 @@ pub struct Pane { active_toolbar_visible: bool, } +pub(crate) struct FollowerState { + pub(crate) leader_id: PeerId, + pub(crate) current_view_id: usize, + pub(crate) items_by_leader_view_id: + HashMap, Box)>, +} + pub trait Toolbar: View { fn active_item_changed( &mut self, @@ -313,6 +322,21 @@ impl Pane { cx.notify(); } + pub(crate) fn set_follow_state( + &mut self, + follower_state: FollowerState, + cx: &mut ViewContext, + ) -> Result<()> { + let current_view_id = follower_state.current_view_id as usize; + let (project_entry_id, item) = follower_state + .items_by_leader_view_id + .get(¤t_view_id) + .ok_or_else(|| anyhow!("invalid current view id"))? + .clone(); + self.add_item(project_entry_id, item, cx); + Ok(()) + } + pub fn items(&self) -> impl Iterator> { self.items.iter().map(|(_, view)| view) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index df771a700a..80f5b4e7a1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7,7 +7,7 @@ pub mod sidebar; mod status_bar; use anyhow::{anyhow, Result}; -use client::{Authenticate, ChannelList, Client, User, UserStore}; +use client::{proto, Authenticate, ChannelList, Client, PeerId, User, UserStore}; use clock::ReplicaId; use collections::HashMap; use gpui::{ @@ -42,16 +42,18 @@ use std::{ }; use theme::{Theme, ThemeRegistry}; -type ItemBuilders = HashMap< +type ProjectItemBuilders = HashMap< TypeId, - Arc< - dyn Fn( - usize, - ModelHandle, - AnyModelHandle, - &mut MutableAppContext, - ) -> Box, - >, + fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, +>; + +type FollowedItemBuilders = Vec< + fn( + ViewHandle, + ModelHandle, + &mut Option, + &mut MutableAppContext, + ) -> Option, Box)>>>, >; action!(Open, Arc); @@ -108,18 +110,18 @@ pub fn init(cx: &mut MutableAppContext) { ]); } -pub fn register_project_item(cx: &mut MutableAppContext) -where - V: ProjectItem, -{ - cx.update_default_global(|builders: &mut ItemBuilders, _| { - builders.insert( - TypeId::of::(), - Arc::new(move |window_id, project, model, cx| { - let item = model.downcast::().unwrap(); - Box::new(cx.add_view(window_id, |cx| V::for_project_item(project, item, cx))) - }), - ); +pub fn register_project_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { + builders.insert(TypeId::of::(), |window_id, project, model, cx| { + let item = model.downcast::().unwrap(); + Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx))) + }); + }); +} + +pub fn register_followed_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { + builders.push(I::for_state_message) }); } @@ -214,6 +216,17 @@ pub trait ProjectItem: Item { ) -> Self; } +pub trait FollowedItem: Item { + type UpdateMessage; + + fn for_state_message( + pane: ViewHandle, + project: ModelHandle, + state: &mut Option, + cx: &mut MutableAppContext, + ) -> Option, Box)>>>; +} + pub trait ItemHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; @@ -840,7 +853,7 @@ impl Workspace { cx.as_mut().spawn(|mut cx| async move { let (project_entry_id, project_item) = project_item.await?; let build_item = cx.update(|cx| { - cx.default_global::() + cx.default_global::() .get(&project_item.model_type()) .ok_or_else(|| anyhow!("no item builder for project item")) .cloned() @@ -990,6 +1003,63 @@ impl Workspace { }); } + pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Task> { + if let Some(project_id) = self.project.read(cx).remote_id() { + let request = self.client.request(proto::Follow { + project_id, + leader_id: leader_id.0, + }); + cx.spawn_weak(|this, mut cx| async move { + let mut response = request.await?; + if let Some(this) = this.upgrade(&cx) { + let mut item_tasks = Vec::new(); + let (project, pane) = this.read_with(&cx, |this, _| { + (this.project.clone(), this.active_pane().clone()) + }); + for view in &mut response.views { + let variant = view + .variant + .take() + .ok_or_else(|| anyhow!("missing variant"))?; + cx.update(|cx| { + let mut variant = Some(variant); + for build_item in cx.default_global::().clone() { + if let Some(task) = + build_item(pane.clone(), project.clone(), &mut variant, cx) + { + item_tasks.push(task); + break; + } else { + assert!(variant.is_some()); + } + } + }); + } + + let items = futures::future::try_join_all(item_tasks).await?; + let mut items_by_leader_view_id = HashMap::default(); + for (view, item) in response.views.into_iter().zip(items) { + items_by_leader_view_id.insert(view.id as usize, item); + } + + pane.update(&mut cx, |pane, cx| { + pane.set_follow_state( + FollowerState { + leader_id, + current_view_id: response.current_view_id as usize, + items_by_leader_view_id, + }, + cx, + ) + })?; + } + Ok(()) + }) + } else { + Task::ready(Err(anyhow!("project is not remote"))) + } + } + fn render_connection_status(&self, cx: &mut RenderContext) -> Option { let theme = &cx.global::().theme; match &*self.client.status().borrow() {