diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0fd26f4797..e02b109b52 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8,7 +8,9 @@ use clock::ReplicaId; use collections::HashMap; use futures::Future; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{ + AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, +}; use language::{Buffer, DiagnosticEntry, LanguageRegistry}; use lsp::DiagnosticSeverity; use postage::{prelude::Stream, watch}; @@ -42,6 +44,7 @@ enum ProjectClientState { _maintain_remote_id_task: Task>, }, Remote { + sharing_has_stopped: bool, remote_id: u64, replica_id: ReplicaId, }, @@ -106,59 +109,61 @@ pub struct ProjectEntry { impl Project { pub fn local( - languages: Arc, client: Arc, user_store: ModelHandle, + languages: Arc, fs: Arc, - cx: &mut ModelContext, - ) -> Self { - let (remote_id_tx, remote_id_rx) = watch::channel(); - let _maintain_remote_id_task = cx.spawn_weak({ - let rpc = client.clone(); - move |this, mut cx| { - async move { - let mut status = rpc.status(); - while let Some(status) = status.recv().await { - if let Some(this) = this.upgrade(&cx) { - let remote_id = if let client::Status::Connected { .. } = status { - let response = rpc.request(proto::RegisterProject {}).await?; - Some(response.project_id) - } else { - None - }; - this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx)); + cx: &mut MutableAppContext, + ) -> ModelHandle { + cx.add_model(|cx: &mut ModelContext| { + let (remote_id_tx, remote_id_rx) = watch::channel(); + let _maintain_remote_id_task = cx.spawn_weak({ + let rpc = client.clone(); + move |this, mut cx| { + async move { + let mut status = rpc.status(); + while let Some(status) = status.recv().await { + if let Some(this) = this.upgrade(&cx) { + let remote_id = if let client::Status::Connected { .. } = status { + let response = rpc.request(proto::RegisterProject {}).await?; + Some(response.project_id) + } else { + None + }; + this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx)); + } } + Ok(()) } - Ok(()) + .log_err() } - .log_err() - } - }); + }); - Self { - worktrees: Default::default(), - collaborators: Default::default(), - client_state: ProjectClientState::Local { - is_shared: false, - remote_id_tx, - remote_id_rx, - _maintain_remote_id_task, - }, - subscriptions: Vec::new(), - active_worktree: None, - active_entry: None, - languages, - client, - user_store, - fs, - } + Self { + worktrees: Default::default(), + collaborators: Default::default(), + client_state: ProjectClientState::Local { + is_shared: false, + remote_id_tx, + remote_id_rx, + _maintain_remote_id_task, + }, + subscriptions: Vec::new(), + active_worktree: None, + active_entry: None, + languages, + client, + user_store, + fs, + } + }) } - pub async fn open_remote( + pub async fn remote( remote_id: u64, - languages: Arc, client: Arc, user_store: ModelHandle, + languages: Arc, fs: Arc, cx: &mut AsyncAppContext, ) -> Result> { @@ -211,6 +216,7 @@ impl Project { user_store, fs, subscriptions: vec![ + client.subscribe_to_entity(remote_id, cx, Self::handle_unshare_project), client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator), client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator), client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree), @@ -221,6 +227,7 @@ impl Project { ], client, client_state: ProjectClientState::Remote { + sharing_has_stopped: false, remote_id, replica_id, }, @@ -252,6 +259,27 @@ impl Project { } } + pub fn next_remote_id(&self) -> impl Future { + let mut id = None; + let mut watch = None; + match &self.client_state { + ProjectClientState::Local { remote_id_rx, .. } => watch = Some(remote_id_rx.clone()), + ProjectClientState::Remote { remote_id, .. } => id = Some(*remote_id), + } + + async move { + if let Some(id) = id { + return id; + } + let mut watch = watch.unwrap(); + loop { + if let Some(Some(id)) = watch.recv().await { + return id; + } + } + } + } + pub fn replica_id(&self) -> ReplicaId { match &self.client_state { ProjectClientState::Local { .. } => 0, @@ -277,7 +305,7 @@ impl Project { pub fn share(&self, cx: &mut ModelContext) -> Task> { let rpc = self.client.clone(); cx.spawn(|this, mut cx| async move { - let remote_id = this.update(&mut cx, |this, _| { + let project_id = this.update(&mut cx, |this, _| { if let ProjectClientState::Local { is_shared, remote_id_rx, @@ -285,25 +313,22 @@ impl Project { } = &mut this.client_state { *is_shared = true; - Ok(*remote_id_rx.borrow()) + remote_id_rx + .borrow() + .ok_or_else(|| anyhow!("no project id")) } else { Err(anyhow!("can't share a remote project")) } })?; - let remote_id = remote_id.ok_or_else(|| anyhow!("no project id"))?; - rpc.send(proto::ShareProject { - project_id: remote_id, - }) - .await?; - + rpc.send(proto::ShareProject { project_id }).await?; this.update(&mut cx, |this, cx| { for worktree in &this.worktrees { worktree.update(cx, |worktree, cx| { worktree .as_local_mut() .unwrap() - .share(remote_id, cx) + .share(project_id, cx) .detach(); }); } @@ -312,6 +337,41 @@ impl Project { }) } + pub fn unshare(&self, cx: &mut ModelContext) -> Task> { + let rpc = self.client.clone(); + cx.spawn(|this, mut cx| async move { + let project_id = this.update(&mut cx, |this, _| { + if let ProjectClientState::Local { + is_shared, + remote_id_rx, + .. + } = &mut this.client_state + { + *is_shared = true; + remote_id_rx + .borrow() + .ok_or_else(|| anyhow!("no project id")) + } else { + Err(anyhow!("can't share a remote project")) + } + })?; + + rpc.send(proto::UnshareProject { project_id }).await?; + + Ok(()) + }) + } + + pub fn is_read_only(&self) -> bool { + match &self.client_state { + ProjectClientState::Local { .. } => false, + ProjectClientState::Remote { + sharing_has_stopped, + .. + } => *sharing_has_stopped, + } + } + pub fn open_buffer( &self, path: ProjectPath, @@ -333,14 +393,14 @@ impl Project { pub fn add_local_worktree( &mut self, - abs_path: &Path, + abs_path: impl AsRef, cx: &mut ModelContext, ) -> Task>> { let fs = self.fs.clone(); let client = self.client.clone(); let user_store = self.user_store.clone(); let languages = self.languages.clone(); - let path = Arc::from(abs_path); + let path = Arc::from(abs_path.as_ref()); cx.spawn(|project, mut cx| async move { let worktree = Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx) @@ -352,10 +412,12 @@ impl Project { }); if let Some(project_id) = remote_project_id { + let worktree_id = worktree.id() as u64; let register_message = worktree.update(&mut cx, |worktree, _| { let worktree = worktree.as_local_mut().unwrap(); proto::RegisterWorktree { project_id, + worktree_id, root_name: worktree.root_name().to_string(), authorized_logins: worktree.authorized_logins(), } @@ -432,6 +494,25 @@ impl Project { // RPC message handlers + fn handle_unshare_project( + &mut self, + _: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + if let ProjectClientState::Remote { + sharing_has_stopped, + .. + } = &mut self.client_state + { + *sharing_has_stopped = true; + cx.notify(); + Ok(()) + } else { + unreachable!() + } + } + fn handle_add_collaborator( &mut self, mut envelope: TypedEnvelope, @@ -812,6 +893,6 @@ mod tests { let client = client::Client::new(); let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) }); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - cx.add_model(|cx| Project::local(languages, client, user_store, fs, cx)) + cx.update(|cx| Project::local(client, user_store, languages, fs, cx)) } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c96e478c26..bf8a1c418a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -617,18 +617,18 @@ mod tests { ) .await; - let project = cx.add_model(|cx| { + let project = cx.update(|cx| { Project::local( - params.languages.clone(), params.client.clone(), params.user_store.clone(), + params.languages.clone(), params.fs.clone(), cx, ) }); let root1 = project .update(&mut cx, |project, cx| { - project.add_local_worktree("/root1".as_ref(), cx) + project.add_local_worktree("/root1", cx) }) .await .unwrap(); @@ -637,7 +637,7 @@ mod tests { .await; let root2 = project .update(&mut cx, |project, cx| { - project.add_local_worktree("/root2".as_ref(), cx) + project.add_local_worktree("/root2", cx) }) .await .unwrap(); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index b86a4c1e30..d8fa9bc8e5 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -96,8 +96,9 @@ message LeaveProject { message RegisterWorktree { uint64 project_id = 1; - string root_name = 2; - repeated string authorized_logins = 3; + uint64 worktree_id = 2; + string root_name = 3; + repeated string authorized_logins = 4; } message UnregisterWorktree { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index de338fe43f..5b328c02ec 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -186,6 +186,7 @@ entity_messages!( SaveBuffer, ShareWorktree, UnregisterWorktree, + UnshareProject, UpdateBuffer, UpdateWorktree, ); diff --git a/crates/server/src/db.rs b/crates/server/src/db.rs index ebc861be03..e3267bad0e 100644 --- a/crates/server/src/db.rs +++ b/crates/server/src/db.rs @@ -443,7 +443,9 @@ impl Db { macro_rules! id_type { ($name:ident) => { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, sqlx::Type, Serialize)] + #[derive( + Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Serialize, + )] #[sqlx(transparent)] #[serde(transparent)] pub struct $name(pub i32); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index faed6e5a39..2e60f01436 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -43,6 +43,7 @@ pub struct Server { const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; +const NO_SUCH_PROJECT: &'static str = "no such project"; impl Server { pub fn new( @@ -60,12 +61,15 @@ impl Server { server .add_handler(Server::ping) + .add_handler(Server::register_project) + .add_handler(Server::unregister_project) + .add_handler(Server::share_project) + .add_handler(Server::unshare_project) + .add_handler(Server::join_project) + .add_handler(Server::leave_project) .add_handler(Server::register_worktree) .add_handler(Server::unregister_worktree) .add_handler(Server::share_worktree) - .add_handler(Server::unshare_worktree) - .add_handler(Server::join_worktree) - .add_handler(Server::leave_worktree) .add_handler(Server::update_worktree) .add_handler(Server::open_buffer) .add_handler(Server::close_buffer) @@ -207,162 +211,85 @@ impl Server { Ok(()) } - async fn register_worktree( + async fn register_project( mut self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> tide::Result<()> { - let receipt = request.receipt(); - let host_user_id = self.state().user_id_for_connection(request.sender_id)?; - - let mut contact_user_ids = HashSet::default(); - contact_user_ids.insert(host_user_id); - for github_login in request.payload.authorized_logins { - match self.app_state.db.create_user(&github_login, false).await { - Ok(contact_user_id) => { - contact_user_ids.insert(contact_user_id); - } - Err(err) => { - let message = err.to_string(); - self.peer - .respond_with_error(receipt, proto::Error { message }) - .await?; - return Ok(()); - } - } - } - - let contact_user_ids = contact_user_ids.into_iter().collect::>(); - let ok = self.state_mut().register_worktree( - request.project_id, - request.worktree_id, - Worktree { - authorized_user_ids: contact_user_ids.clone(), - root_name: request.payload.root_name, - }, - ); - - if ok { - self.peer.respond(receipt, proto::Ack {}).await?; - self.update_contacts_for_users(&contact_user_ids).await?; - } else { - self.peer - .respond_with_error( - receipt, - proto::Error { - message: "no such project".to_string(), - }, - ) - .await?; - } - + let mut state = self.state_mut(); + let user_id = state.user_id_for_connection(request.sender_id)?; + state.register_project(request.sender_id, user_id); Ok(()) } - async fn unregister_worktree( + async fn unregister_project( mut self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, + ) -> tide::Result<()> { + self.state_mut() + .unregister_project(request.payload.project_id, request.sender_id); + Ok(()) + } + + async fn share_project( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + self.state_mut() + .share_project(request.payload.project_id, request.sender_id); + Ok(()) + } + + async fn unshare_project( + mut self: Arc, + request: TypedEnvelope, ) -> tide::Result<()> { let project_id = request.payload.project_id; - let worktree_id = request.payload.worktree_id; - let worktree = - self.state_mut() - .unregister_worktree(project_id, worktree_id, request.sender_id)?; - - if let Some(share) = worktree.share { - broadcast( - request.sender_id, - share.guests.keys().copied().collect(), - |conn_id| { - self.peer.send( - conn_id, - proto::UnregisterWorktree { - project_id, - worktree_id, - }, - ) - }, - ) - .await?; - } - self.update_contacts_for_users(&worktree.authorized_user_ids) - .await?; - Ok(()) - } - - async fn share_worktree( - mut self: Arc, - mut request: TypedEnvelope, - ) -> tide::Result<()> { - let worktree = request - .payload - .worktree - .as_mut() - .ok_or_else(|| anyhow!("missing worktree"))?; - let entries = mem::take(&mut worktree.entries) - .into_iter() - .map(|entry| (entry.id, entry)) - .collect(); - - let contact_user_ids = - self.state_mut() - .share_worktree(worktree.id, request.sender_id, entries); - if let Some(contact_user_ids) = contact_user_ids { - self.peer - .respond(request.receipt(), proto::ShareWorktreeResponse {}) - .await?; - self.update_contacts_for_users(&contact_user_ids).await?; - } else { - self.peer - .respond_with_error( - request.receipt(), - proto::Error { - message: "no such worktree".to_string(), - }, - ) - .await?; - } - Ok(()) - } - - async fn unshare_worktree( - mut self: Arc, - request: TypedEnvelope, - ) -> tide::Result<()> { - let worktree_id = request.payload.worktree_id; - let worktree = self + let project = self .state_mut() - .unshare_worktree(worktree_id, request.sender_id)?; + .unshare_project(project_id, request.sender_id)?; - broadcast(request.sender_id, worktree.connection_ids, |conn_id| { + broadcast(request.sender_id, project.connection_ids, |conn_id| { self.peer - .send(conn_id, proto::UnshareWorktree { worktree_id }) + .send(conn_id, proto::UnshareProject { project_id }) }) .await?; - self.update_contacts_for_users(&worktree.authorized_user_ids) + self.update_contacts_for_users(&project.authorized_user_ids) .await?; Ok(()) } - async fn join_worktree( + async fn join_project( mut self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> tide::Result<()> { - let worktree_id = request.payload.worktree_id; + let project_id = request.payload.project_id; let user_id = self.state().user_id_for_connection(request.sender_id)?; let response_data = self .state_mut() - .join_worktree(request.sender_id, user_id, worktree_id) + .join_project(request.sender_id, user_id, project_id) .and_then(|joined| { - let share = joined.worktree.share()?; + let share = joined.project.share()?; let peer_count = share.guests.len(); let mut collaborators = Vec::with_capacity(peer_count); collaborators.push(proto::Collaborator { - peer_id: joined.worktree.host_connection_id.0, + peer_id: joined.project.host_connection_id.0, replica_id: 0, - user_id: joined.worktree.host_user_id.to_proto(), + user_id: joined.project.host_user_id.to_proto(), }); + let worktrees = joined + .project + .worktrees + .values() + .filter_map(|worktree| { + worktree.share.as_ref().map(|share| proto::Worktree { + id: project_id, + root_name: worktree.root_name.clone(), + entries: share.entries.values().cloned().collect(), + }) + }) + .collect(); for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { if *peer_conn_id != request.sender_id { collaborators.push(proto::Collaborator { @@ -372,17 +299,13 @@ impl Server { }); } } - let response = proto::JoinWorktreeResponse { - worktree: Some(proto::Worktree { - id: worktree_id, - root_name: joined.worktree.root_name.clone(), - entries: share.entries.values().cloned().collect(), - }), + let response = proto::JoinProjectResponse { + worktrees, replica_id: joined.replica_id as u32, collaborators, }; - let connection_ids = joined.worktree.connection_ids(); - let contact_user_ids = joined.worktree.authorized_user_ids.clone(); + let connection_ids = joined.project.connection_ids(); + let contact_user_ids = joined.project.authorized_user_ids(); Ok((response, connection_ids, contact_user_ids)) }); @@ -391,8 +314,8 @@ impl Server { broadcast(request.sender_id, connection_ids, |conn_id| { self.peer.send( conn_id, - proto::AddCollaborator { - worktree_id, + proto::AddProjectCollaborator { + project_id: project_id, collaborator: Some(proto::Collaborator { peer_id: request.sender_id.0, replica_id: response.replica_id, @@ -420,19 +343,19 @@ impl Server { Ok(()) } - async fn leave_worktree( + async fn leave_project( mut self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> tide::Result<()> { let sender_id = request.sender_id; - let worktree_id = request.payload.worktree_id; - let worktree = self.state_mut().leave_worktree(sender_id, worktree_id); + let project_id = request.payload.project_id; + let worktree = self.state_mut().leave_project(sender_id, project_id); if let Some(worktree) = worktree { broadcast(sender_id, worktree.connection_ids, |conn_id| { self.peer.send( conn_id, - proto::RemoveCollaborator { - worktree_id, + proto::RemoveProjectCollaborator { + project_id, peer_id: sender_id.0, }, ) @@ -444,16 +367,133 @@ impl Server { Ok(()) } + async fn register_worktree( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receipt = request.receipt(); + let host_user_id = self.state().user_id_for_connection(request.sender_id)?; + + let mut contact_user_ids = HashSet::default(); + contact_user_ids.insert(host_user_id); + for github_login in request.payload.authorized_logins { + match self.app_state.db.create_user(&github_login, false).await { + Ok(contact_user_id) => { + contact_user_ids.insert(contact_user_id); + } + Err(err) => { + let message = err.to_string(); + self.peer + .respond_with_error(receipt, proto::Error { message }) + .await?; + return Ok(()); + } + } + } + + let contact_user_ids = contact_user_ids.into_iter().collect::>(); + let ok = self.state_mut().register_worktree( + request.payload.project_id, + request.payload.worktree_id, + Worktree { + authorized_user_ids: contact_user_ids.clone(), + root_name: request.payload.root_name, + share: None, + }, + ); + + if ok { + self.peer.respond(receipt, proto::Ack {}).await?; + self.update_contacts_for_users(&contact_user_ids).await?; + } else { + self.peer + .respond_with_error( + receipt, + proto::Error { + message: NO_SUCH_PROJECT.to_string(), + }, + ) + .await?; + } + + Ok(()) + } + + async fn unregister_worktree( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let project_id = request.payload.project_id; + let worktree_id = request.payload.worktree_id; + let (worktree, guest_connection_ids) = + self.state_mut() + .unregister_worktree(project_id, worktree_id, request.sender_id)?; + + broadcast(request.sender_id, guest_connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::UnregisterWorktree { + project_id, + worktree_id, + }, + ) + }) + .await?; + self.update_contacts_for_users(&worktree.authorized_user_ids) + .await?; + Ok(()) + } + + async fn share_worktree( + mut self: Arc, + mut request: TypedEnvelope, + ) -> tide::Result<()> { + let worktree = request + .payload + .worktree + .as_mut() + .ok_or_else(|| anyhow!("missing worktree"))?; + let entries = mem::take(&mut worktree.entries) + .into_iter() + .map(|entry| (entry.id, entry)) + .collect(); + + let contact_user_ids = self.state_mut().share_worktree( + request.payload.project_id, + worktree.id, + request.sender_id, + entries, + ); + if let Some(contact_user_ids) = contact_user_ids { + self.peer.respond(request.receipt(), proto::Ack {}).await?; + self.update_contacts_for_users(&contact_user_ids).await?; + } else { + self.peer + .respond_with_error( + request.receipt(), + proto::Error { + message: "no such worktree".to_string(), + }, + ) + .await?; + } + Ok(()) + } + async fn update_worktree( mut self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { - let connection_ids = self.state_mut().update_worktree( - request.sender_id, - request.payload.worktree_id, - &request.payload.removed_entries, - &request.payload.updated_entries, - )?; + let connection_ids = self + .state_mut() + .update_worktree( + request.sender_id, + request.payload.project_id, + request.payload.worktree_id, + &request.payload.removed_entries, + &request.payload.updated_entries, + ) + .ok_or_else(|| anyhow!("no such worktree"))?; broadcast(request.sender_id, connection_ids, |connection_id| { self.peer @@ -471,7 +511,9 @@ impl Server { let receipt = request.receipt(); let host_connection_id = self .state() - .worktree_host_connection_id(request.sender_id, request.payload.worktree_id)?; + .read_project(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))? + .host_connection_id; let response = self .peer .forward_request(request.sender_id, host_connection_id, request.payload) @@ -486,7 +528,9 @@ impl Server { ) -> tide::Result<()> { let host_connection_id = self .state() - .worktree_host_connection_id(request.sender_id, request.payload.worktree_id)?; + .read_project(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))? + .host_connection_id; self.peer .forward_send(request.sender_id, host_connection_id, request.payload) .await?; @@ -501,10 +545,11 @@ impl Server { let guests; { let state = self.state(); - host = state - .worktree_host_connection_id(request.sender_id, request.payload.worktree_id)?; - guests = state - .worktree_guest_connection_ids(request.sender_id, request.payload.worktree_id)?; + let project = state + .read_project(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + host = project.host_connection_id; + guests = project.guest_connection_ids() } let sender = request.sender_id; @@ -536,7 +581,8 @@ impl Server { ) -> tide::Result<()> { let receiver_ids = self .state() - .worktree_connection_ids(request.sender_id, request.payload.worktree_id)?; + .project_connection_ids(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; broadcast(request.sender_id, receiver_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) @@ -552,7 +598,8 @@ impl Server { ) -> tide::Result<()> { let receiver_ids = self .state() - .worktree_connection_ids(request.sender_id, request.payload.worktree_id)?; + .project_connection_ids(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; broadcast(request.sender_id, receiver_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) @@ -959,7 +1006,6 @@ mod tests { self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Credentials, EstablishConnectionError, UserStore, }, - contacts_panel::JoinWorktree, editor::{Editor, EditorSettings, Input, MultiBuffer}, fs::{FakeFs, Fs as _}, language::{ @@ -967,25 +1013,22 @@ mod tests { LanguageRegistry, LanguageServerConfig, Point, }, lsp, - project::{ProjectPath, Worktree}, - test::test_app_state, - workspace::Workspace, + project::Project, }; #[gpui::test] - async fn test_share_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + async fn test_share_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { let (window_b, _) = cx_b.add_window(|_| EmptyView); let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); + cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. let mut server = TestServer::start().await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; - cx_a.foreground().forbid_parking(); - - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); + // Share a project as client A fs.insert_tree( "/a", json!({ @@ -995,47 +1038,56 @@ mod tests { }), ) .await; - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/a".as_ref(), - fs, - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( + // Join that project as client B, and see that a guest has joined as client A. + let project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await .unwrap(); + let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); - let replica_id_b = worktree_b.read_with(&cx_b, |tree, _| { + let replica_id_b = project_b.read_with(&cx_b, |project, _| { assert_eq!( - tree.collaborators() + project + .collaborators() .get(&client_a.peer_id) .unwrap() .user .github_login, "user_a" ); - tree.replica_id() + project.replica_id() }); - worktree_a + project_a .condition(&cx_a, |tree, _| { tree.collaborators() .get(&client_b.peer_id) @@ -1093,30 +1145,24 @@ mod tests { // Dropping the worktree removes client B from client A's collaborators. cx_b.update(move |_| drop(worktree_b)); - worktree_a - .condition(&cx_a, |tree, _| tree.collaborators().is_empty()) + project_a + .condition(&cx_a, |project, _| project.collaborators().is_empty()) .await; } #[gpui::test] - async fn test_unshare_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + async fn test_unshare_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_b.update(zed::contacts_panel::init); - let mut app_state_a = cx_a.update(test_app_state); - let mut app_state_b = cx_b.update(test_app_state); + let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. let mut server = TestServer::start().await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; - Arc::get_mut(&mut app_state_a).unwrap().client = client_a.clone(); - Arc::get_mut(&mut app_state_a).unwrap().user_store = client_a.user_store.clone(); - Arc::get_mut(&mut app_state_b).unwrap().client = client_b.clone(); - Arc::get_mut(&mut app_state_b).unwrap().user_store = client_b.user_store.clone(); - cx_a.foreground().forbid_parking(); - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); + // Share a project as client A fs.insert_tree( "/a", json!({ @@ -1126,71 +1172,55 @@ mod tests { }), ) .await; - let worktree_a = Worktree::open_local( - app_state_a.client.clone(), - app_state_a.user_store.clone(), - "/a".as_ref(), - fs, - app_state_a.languages.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - - let remote_worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - let (window_b, workspace_b) = - cx_b.add_window(|cx| Workspace::new(&app_state_b.as_ref().into(), cx)); - cx_b.update(|cx| { - cx.dispatch_action( - window_b, - vec![workspace_b.id()], - &JoinWorktree(remote_worktree_id), - ); - }); - workspace_b - .condition(&cx_b, |workspace, cx| workspace.worktrees(cx).len() == 1) - .await; + // Join that project as client B + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); - let local_worktree_id_b = workspace_b.read_with(&cx_b, |workspace, cx| { - let active_pane = workspace.active_pane().read(cx); - assert!(active_pane.active_item().is_none()); - workspace.worktrees(cx).first().unwrap().id() - }); - workspace_b - .update(&mut cx_b, |workspace, cx| { - workspace.open_entry( - ProjectPath { - worktree_id: local_worktree_id_b, - path: Path::new("a.txt").into(), - }, - cx, - ) - }) - .unwrap() + let worktree_b = project_b.read_with(&cx_b, |p, _| p.worktrees()[0].clone()); + worktree_b + .update(&mut cx_b, |tree, cx| tree.open_buffer("a.txt", cx)) .await .unwrap(); - workspace_b.read_with(&cx_b, |workspace, cx| { - let active_pane = workspace.active_pane().read(cx); - assert!(active_pane.active_item().is_some()); - }); - worktree_a.update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().unshare(cx); - }); - workspace_b - .condition(&cx_b, |workspace, cx| workspace.worktrees(cx).len() == 0) + project_a + .update(&mut cx_a, |project, cx| project.unshare(cx)) + .await + .unwrap(); + project_b + .condition(&mut cx_b, |project, _| project.is_read_only()) .await; - workspace_b.read_with(&cx_b, |workspace, cx| { - let active_pane = workspace.active_pane().read(cx); - assert!(active_pane.active_item().is_none()); - }); } #[gpui::test] @@ -1201,6 +1231,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); // Connect to a server as 3 clients. let mut server = TestServer::start().await; @@ -1208,8 +1239,6 @@ mod tests { let client_b = server.create_client(&mut cx_b, "user_b").await; let client_c = server.create_client(&mut cx_c, "user_c").await; - let fs = Arc::new(FakeFs::new()); - // Share a worktree as client A. fs.insert_tree( "/a", @@ -1220,46 +1249,55 @@ mod tests { }), ) .await; - - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/a".as_ref(), - fs.clone(), - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); // Join that worktree as clients B and C. - let worktree_b = Worktree::open_remote( + let project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await .unwrap(); - let worktree_c = Worktree::open_remote( + let project_c = Project::remote( + project_id, client_c.clone(), - worktree_id, - lang_registry.clone(), client_c.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_c.to_async(), ) .await .unwrap(); // Open and edit a buffer as both guests B and C. + let worktree_b = project_b.read_with(&cx_b, |p, _| p.worktrees()[0].clone()); + let worktree_c = project_c.read_with(&cx_c, |p, _| p.worktrees()[0].clone()); let buffer_b = worktree_b .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx)) .await @@ -1343,14 +1381,14 @@ mod tests { async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. let mut server = TestServer::start().await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); + // Share a project as client A fs.insert_tree( "/dir", json!({ @@ -1360,35 +1398,44 @@ mod tests { ) .await; - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/dir".as_ref(), - fs, - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( + // Join that project as client B + let project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await .unwrap(); + let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + // Open a buffer as client B let buffer_b = worktree_b .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)) .await @@ -1430,14 +1477,14 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. let mut server = TestServer::start().await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); + // Share a project as client A fs.insert_tree( "/dir", json!({ @@ -1446,44 +1493,56 @@ mod tests { }), ) .await; - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/dir".as_ref(), - fs, - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( + // Join that project as client B + let project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await .unwrap(); + let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + // Open a buffer as client A let buffer_a = worktree_a .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx)) .await .unwrap(); + + // Start opening the same buffer as client B let buffer_b = cx_b .background() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); - task::yield_now().await; + + // Edit the buffer as client A while client B is still opening it. buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx)); let text = buffer_a.read_with(&cx_a, |buf, _| buf.text()); @@ -1498,14 +1557,14 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. let mut server = TestServer::start().await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); + // Share a project as client A fs.insert_tree( "/dir", json!({ @@ -1514,45 +1573,58 @@ mod tests { }), ) .await; - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/dir".as_ref(), - fs, - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - // Join that worktree as client B, and see that a guest has joined as client A. - let worktree_b = Worktree::open_remote( + // Join that project as client B + let project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await .unwrap(); - worktree_a - .condition(&cx_a, |tree, _| tree.collaborators().len() == 1) + let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); + + // See that a guest has joined as client A. + project_a + .condition(&cx_a, |p, _| p.collaborators().len() == 1) .await; + // Begin opening a buffer as client B, but leave the project before the open completes. let buffer_b = cx_b .background() .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); cx_b.update(|_| drop(worktree_b)); drop(buffer_b); - worktree_a - .condition(&cx_a, |tree, _| tree.collaborators().len() == 0) + + // See that the guest has left. + project_a + .condition(&cx_a, |p, _| p.collaborators().len() == 0) .await; } @@ -1560,14 +1632,14 @@ mod tests { async fn test_peer_disconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); // Connect to a server as 2 clients. let mut server = TestServer::start().await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); + // Share a project as client A fs.insert_tree( "/a", json!({ @@ -1577,42 +1649,51 @@ mod tests { }), ) .await; - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/a".as_ref(), - fs, - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - // Join that worktree as client B, and see that a guest has joined as client A. - let _worktree_b = Worktree::open_remote( + // Join that project as client B + let _project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await .unwrap(); - worktree_a - .condition(&cx_a, |tree, _| tree.collaborators().len() == 1) + + // See that a guest has joined as client A. + project_a + .condition(&cx_a, |p, _| p.collaborators().len() == 1) .await; // Drop client B's connection and ensure client A observes client B leaving the worktree. client_b.disconnect(&cx_b.to_async()).await.unwrap(); - worktree_a - .condition(&cx_a, |tree, _| tree.collaborators().len() == 0) + project_a + .condition(&cx_a, |p, _| p.collaborators().len() == 0) .await; } @@ -1622,28 +1703,30 @@ mod tests { mut cx_b: TestAppContext, ) { cx_a.foreground().forbid_parking(); + let mut lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); + + // Set up a fake language server. let (language_server_config, mut fake_language_server) = LanguageServerConfig::fake(cx_a.background()).await; - let mut lang_registry = LanguageRegistry::new(); - lang_registry.add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".to_string(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); - - let lang_registry = Arc::new(lang_registry); + Arc::get_mut(&mut lang_registry) + .unwrap() + .add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); // Connect to a server as 2 clients. let mut server = TestServer::start().await; let client_a = server.create_client(&mut cx_a, "user_a").await; let client_b = server.create_client(&mut cx_b, "user_b").await; - // Share a local worktree as client A - let fs = Arc::new(FakeFs::new()); + // Share a project as client A fs.insert_tree( "/a", json!({ @@ -1653,25 +1736,31 @@ mod tests { }), ) .await; - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/a".as_ref(), - fs, - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + .await + .unwrap(); worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - // Cause language server to start. + // Cause the language server to start. let _ = cx_a .background() .spawn(worktree_a.update(&mut cx_a, |worktree, cx| { @@ -1706,15 +1795,17 @@ mod tests { .await; // Join the worktree as client B. - let worktree_b = Worktree::open_remote( + let project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await .unwrap(); + let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone()); // Open the file with the errors. let buffer_b = cx_b @@ -2175,6 +2266,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); // Connect to a server as 3 clients. let mut server = TestServer::start().await; @@ -2182,8 +2274,6 @@ mod tests { let client_b = server.create_client(&mut cx_b, "user_b").await; let client_c = server.create_client(&mut cx_c, "user_c").await; - let fs = Arc::new(FakeFs::new()); - // Share a worktree as client A. fs.insert_tree( "/a", @@ -2193,16 +2283,22 @@ mod tests { ) .await; - let worktree_a = Worktree::open_local( - client_a.clone(), - client_a.user_store.clone(), - "/a".as_ref(), - fs.clone(), - lang_registry.clone(), - &mut cx_a.to_async(), - ) - .await - .unwrap(); + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let worktree_a = project_a + .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx)) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; client_a .user_store @@ -2223,16 +2319,20 @@ mod tests { }) .await; - let worktree_id = worktree_a - .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + let project_id = project_a + .update(&mut cx_a, |project, _| project.next_remote_id()) + .await; + project_a + .update(&mut cx_a, |project, cx| project.share(cx)) .await .unwrap(); - let _worktree_b = Worktree::open_remote( + let _project_b = Project::remote( + project_id, client_b.clone(), - worktree_id, - lang_registry.clone(), client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), &mut cx_b.to_async(), ) .await @@ -2257,13 +2357,13 @@ mod tests { }) .await; - worktree_a - .condition(&cx_a, |worktree, _| { - worktree.collaborators().contains_key(&client_b.peer_id) + project_a + .condition(&cx_a, |project, _| { + project.collaborators().contains_key(&client_b.peer_id) }) .await; - cx_a.update(move |_| drop(worktree_a)); + cx_a.update(move |_| drop(project_a)); client_a .user_store .condition(&cx_a, |user_store, _| contacts(user_store) == vec![]) @@ -2283,12 +2383,12 @@ mod tests { .iter() .map(|contact| { let worktrees = contact - .worktrees + .projects .iter() - .map(|w| { + .map(|p| { ( - w.root_name.as_str(), - w.guests.iter().map(|p| p.github_login.as_str()).collect(), + p.worktree_root_names[0].as_str(), + p.guests.iter().map(|p| p.github_login.as_str()).collect(), ) }) .collect(); diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index 5b0b6d9554..4ed6454afd 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -11,7 +11,7 @@ pub struct Store { projects: HashMap, visible_projects_by_user_id: HashMap>, channels: HashMap, - next_worktree_id: u64, + next_project_id: u64, } struct ConnectionState { @@ -24,19 +24,19 @@ pub struct Project { pub host_connection_id: ConnectionId, pub host_user_id: UserId, pub share: Option, - worktrees: HashMap, + pub worktrees: HashMap, } pub struct Worktree { pub authorized_user_ids: Vec, pub root_name: String, + pub share: Option, } #[derive(Default)] pub struct ProjectShare { pub guests: HashMap, pub active_replica_ids: HashSet, - pub worktrees: HashMap, } pub struct WorktreeShare { @@ -57,9 +57,9 @@ pub struct RemovedConnectionState { pub contact_ids: HashSet, } -pub struct JoinedWorktree<'a> { +pub struct JoinedProject<'a> { pub replica_id: ReplicaId, - pub worktree: &'a Worktree, + pub project: &'a Project, } pub struct UnsharedWorktree { @@ -67,7 +67,7 @@ pub struct UnsharedWorktree { pub authorized_user_ids: Vec, } -pub struct LeftWorktree { +pub struct LeftProject { pub connection_ids: Vec, pub authorized_user_ids: Vec, } @@ -114,17 +114,17 @@ impl Store { } let mut result = RemovedConnectionState::default(); - for worktree_id in connection.worktrees.clone() { - if let Ok(worktree) = self.unregister_worktree(worktree_id, connection_id) { + for project_id in connection.projects.clone() { + if let Some((project, authorized_user_ids)) = + self.unregister_project(project_id, connection_id) + { + result.contact_ids.extend(authorized_user_ids); + result.hosted_projects.insert(project_id, project); + } else if let Some(project) = self.leave_project(connection_id, project_id) { result - .contact_ids - .extend(worktree.authorized_user_ids.iter().copied()); - result.hosted_worktrees.insert(worktree_id, worktree); - } else if let Some(worktree) = self.leave_worktree(connection_id, worktree_id) { - result - .guest_worktree_ids - .insert(worktree_id, worktree.connection_ids); - result.contact_ids.extend(worktree.authorized_user_ids); + .guest_project_ids + .insert(project_id, project.connection_ids); + result.contact_ids.extend(project.authorized_user_ids); } } @@ -191,7 +191,7 @@ impl Store { let project = &self.projects[project_id]; let mut guests = HashSet::default(); - if let Ok(share) = worktree.share() { + if let Ok(share) = project.share() { for guest_connection_id in share.guests.keys() { if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) { guests.insert(user_id.to_proto()); @@ -200,6 +200,12 @@ impl Store { } if let Ok(host_user_id) = self.user_id_for_connection(project.host_connection_id) { + let mut worktree_root_names = project + .worktrees + .values() + .map(|worktree| worktree.root_name.clone()) + .collect::>(); + worktree_root_names.sort_unstable(); contacts .entry(host_user_id) .or_insert_with(|| proto::Contact { @@ -209,11 +215,7 @@ impl Store { .projects .push(proto::ProjectMetadata { id: *project_id, - worktree_root_names: project - .worktrees - .iter() - .map(|worktree| worktree.root_name.clone()) - .collect(), + worktree_root_names, is_shared: project.share.is_some(), guests: guests.into_iter().collect(), }); @@ -268,7 +270,20 @@ impl Store { } } - pub fn unregister_project(&mut self, project_id: u64) { + pub fn unregister_project( + &mut self, + project_id: u64, + connection_id: ConnectionId, + ) -> Option<(Project, Vec)> { + match self.projects.entry(project_id) { + hash_map::Entry::Occupied(e) => { + if e.get().host_connection_id != connection_id { + return None; + } + } + hash_map::Entry::Vacant(_) => return None, + } + todo!() } @@ -277,7 +292,7 @@ impl Store { project_id: u64, worktree_id: u64, acting_connection_id: ConnectionId, - ) -> tide::Result { + ) -> tide::Result<(Worktree, Vec)> { let project = self .projects .get_mut(&project_id) @@ -291,31 +306,25 @@ impl Store { .remove(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; - if let Some(connection) = self.connections.get_mut(&project.host_connection_id) { - connection.worktrees.remove(&worktree_id); - } - - if let Some(share) = &worktree.share { - for connection_id in share.guests.keys() { - if let Some(connection) = self.connections.get_mut(connection_id) { - connection.worktrees.remove(&worktree_id); - } - } + let mut guest_connection_ids = Vec::new(); + if let Some(share) = &project.share { + guest_connection_ids.extend(share.guests.keys()); } for authorized_user_id in &worktree.authorized_user_ids { - if let Some(visible_worktrees) = self - .visible_worktrees_by_user_id - .get_mut(&authorized_user_id) + if let Some(visible_projects) = + self.visible_projects_by_user_id.get_mut(authorized_user_id) { - visible_worktrees.remove(&worktree_id); + if !project.has_authorized_user_id(*authorized_user_id) { + visible_projects.remove(&project_id); + } } } #[cfg(test)] self.check_invariants(); - Ok(worktree) + Ok((worktree, guest_connection_ids)) } pub fn share_project(&mut self, project_id: u64, connection_id: ConnectionId) -> bool { @@ -328,47 +337,27 @@ impl Store { false } - pub fn share_worktree( + pub fn unshare_project( &mut self, project_id: u64, - worktree_id: u64, - connection_id: ConnectionId, - entries: HashMap, - ) -> Option> { - if let Some(project) = self.projects.get_mut(&project_id) { - if project.host_connection_id == connection_id { - if let Some(share) = project.share.as_mut() { - share - .worktrees - .insert(worktree_id, WorktreeShare { entries }); - return Some(project.authorized_user_ids()); - } - } - } - None - } - - pub fn unshare_worktree( - &mut self, - worktree_id: u64, acting_connection_id: ConnectionId, ) -> tide::Result { - let worktree = if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { - worktree + let project = if let Some(project) = self.projects.get_mut(&project_id) { + project } else { - return Err(anyhow!("no such worktree"))?; + return Err(anyhow!("no such project"))?; }; - if worktree.host_connection_id != acting_connection_id { - return Err(anyhow!("not your worktree"))?; + if project.host_connection_id != acting_connection_id { + return Err(anyhow!("not your project"))?; } - let connection_ids = worktree.connection_ids(); - let authorized_user_ids = worktree.authorized_user_ids.clone(); - if let Some(share) = worktree.share.take() { + let connection_ids = project.connection_ids(); + let authorized_user_ids = project.authorized_user_ids(); + if let Some(share) = project.share.take() { for connection_id in share.guests.into_keys() { if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.worktrees.remove(&worktree_id); + connection.projects.remove(&project_id); } } @@ -380,34 +369,51 @@ impl Store { authorized_user_ids, }) } else { - Err(anyhow!("worktree is not shared"))? + Err(anyhow!("project is not shared"))? } } - pub fn join_worktree( + pub fn share_worktree( + &mut self, + project_id: u64, + worktree_id: u64, + connection_id: ConnectionId, + entries: HashMap, + ) -> Option> { + let project = self.projects.get_mut(&project_id)?; + let worktree = project.worktrees.get_mut(&worktree_id)?; + if project.host_connection_id == connection_id && project.share.is_some() { + worktree.share = Some(WorktreeShare { entries }); + Some(project.authorized_user_ids()) + } else { + None + } + } + + pub fn join_project( &mut self, connection_id: ConnectionId, user_id: UserId, - worktree_id: u64, - ) -> tide::Result { + project_id: u64, + ) -> tide::Result { let connection = self .connections .get_mut(&connection_id) .ok_or_else(|| anyhow!("no such connection"))?; - let worktree = self - .worktrees - .get_mut(&worktree_id) - .and_then(|worktree| { - if worktree.authorized_user_ids.contains(&user_id) { - Some(worktree) + let project = self + .projects + .get_mut(&project_id) + .and_then(|project| { + if project.has_authorized_user_id(user_id) { + Some(project) } else { None } }) - .ok_or_else(|| anyhow!("no such worktree"))?; + .ok_or_else(|| anyhow!("no such project"))?; - let share = worktree.share_mut()?; - connection.worktrees.insert(worktree_id); + let share = project.share_mut()?; + connection.projects.insert(project_id); let mut replica_id = 1; while share.active_replica_ids.contains(&replica_id) { @@ -419,33 +425,33 @@ impl Store { #[cfg(test)] self.check_invariants(); - Ok(JoinedWorktree { + Ok(JoinedProject { replica_id, - worktree: &self.worktrees[&worktree_id], + project: &self.projects[&project_id], }) } - pub fn leave_worktree( + pub fn leave_project( &mut self, connection_id: ConnectionId, - worktree_id: u64, - ) -> Option { - let worktree = self.worktrees.get_mut(&worktree_id)?; - let share = worktree.share.as_mut()?; + project_id: u64, + ) -> Option { + let project = self.projects.get_mut(&project_id)?; + let share = project.share.as_mut()?; let (replica_id, _) = share.guests.remove(&connection_id)?; share.active_replica_ids.remove(&replica_id); if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.worktrees.remove(&worktree_id); + connection.projects.remove(&project_id); } - let connection_ids = worktree.connection_ids(); - let authorized_user_ids = worktree.authorized_user_ids.clone(); + let connection_ids = project.connection_ids(); + let authorized_user_ids = project.authorized_user_ids(); #[cfg(test)] self.check_invariants(); - Some(LeftWorktree { + Some(LeftProject { connection_ids, authorized_user_ids, }) @@ -454,115 +460,75 @@ impl Store { pub fn update_worktree( &mut self, connection_id: ConnectionId, + project_id: u64, worktree_id: u64, removed_entries: &[u64], updated_entries: &[proto::Entry], - ) -> tide::Result> { - let worktree = self.write_worktree(worktree_id, connection_id)?; - let share = worktree.share_mut()?; + ) -> Option> { + let project = self.write_project(project_id, connection_id)?; + let share = project.worktrees.get_mut(&worktree_id)?.share.as_mut()?; for entry_id in removed_entries { share.entries.remove(&entry_id); } for entry in updated_entries { share.entries.insert(entry.id, entry.clone()); } - Ok(worktree.connection_ids()) + Some(project.connection_ids()) } - pub fn worktree_host_connection_id( + pub fn project_connection_ids( &self, - connection_id: ConnectionId, - worktree_id: u64, - ) -> tide::Result { - Ok(self - .read_worktree(worktree_id, connection_id)? - .host_connection_id) - } - - pub fn worktree_guest_connection_ids( - &self, - connection_id: ConnectionId, - worktree_id: u64, - ) -> tide::Result> { - Ok(self - .read_worktree(worktree_id, connection_id)? - .share()? - .guests - .keys() - .copied() - .collect()) - } - - pub fn worktree_connection_ids( - &self, - connection_id: ConnectionId, - worktree_id: u64, - ) -> tide::Result> { - Ok(self - .read_worktree(worktree_id, connection_id)? - .connection_ids()) + project_id: u64, + acting_connection_id: ConnectionId, + ) -> Option> { + Some( + self.read_project(project_id, acting_connection_id)? + .connection_ids(), + ) } pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Option> { Some(self.channels.get(&channel_id)?.connection_ids()) } - fn read_worktree( - &self, - worktree_id: u64, - connection_id: ConnectionId, - ) -> tide::Result<&Worktree> { - let worktree = self - .worktrees - .get(&worktree_id) - .ok_or_else(|| anyhow!("worktree not found"))?; - - if worktree.host_connection_id == connection_id - || worktree.share()?.guests.contains_key(&connection_id) + pub fn read_project(&self, project_id: u64, connection_id: ConnectionId) -> Option<&Project> { + let project = self.projects.get(&project_id)?; + if project.host_connection_id == connection_id + || project.share.as_ref()?.guests.contains_key(&connection_id) { - Ok(worktree) + Some(project) } else { - Err(anyhow!( - "{} is not a member of worktree {}", - connection_id, - worktree_id - ))? + None } } - fn write_worktree( + fn write_project( &mut self, - worktree_id: u64, + project_id: u64, connection_id: ConnectionId, - ) -> tide::Result<&mut Worktree> { - let worktree = self - .worktrees - .get_mut(&worktree_id) - .ok_or_else(|| anyhow!("worktree not found"))?; - - if worktree.host_connection_id == connection_id - || worktree - .share - .as_ref() - .map_or(false, |share| share.guests.contains_key(&connection_id)) + ) -> Option<&mut Project> { + let project = self.projects.get_mut(&project_id)?; + if project.host_connection_id == connection_id + || project.share.as_ref()?.guests.contains_key(&connection_id) { - Ok(worktree) + Some(project) } else { - Err(anyhow!( - "{} is not a member of worktree {}", - connection_id, - worktree_id - ))? + None } } #[cfg(test)] fn check_invariants(&self) { for (connection_id, connection) in &self.connections { - for worktree_id in &connection.worktrees { - let worktree = &self.worktrees.get(&worktree_id).unwrap(); - if worktree.host_connection_id != *connection_id { - assert!(worktree.share().unwrap().guests.contains_key(connection_id)); + for project_id in &connection.projects { + let project = &self.projects.get(&project_id).unwrap(); + if project.host_connection_id != *connection_id { + assert!(project + .share + .as_ref() + .unwrap() + .guests + .contains_key(connection_id)); } } for channel_id in &connection.channels { @@ -585,22 +551,22 @@ impl Store { } } - for (worktree_id, worktree) in &self.worktrees { - let host_connection = self.connections.get(&worktree.host_connection_id).unwrap(); - assert!(host_connection.worktrees.contains(worktree_id)); + for (project_id, project) in &self.projects { + let host_connection = self.connections.get(&project.host_connection_id).unwrap(); + assert!(host_connection.projects.contains(project_id)); - for authorized_user_ids in &worktree.authorized_user_ids { - let visible_worktree_ids = self - .visible_worktrees_by_user_id - .get(authorized_user_ids) + for authorized_user_ids in project.authorized_user_ids() { + let visible_project_ids = self + .visible_projects_by_user_id + .get(&authorized_user_ids) .unwrap(); - assert!(visible_worktree_ids.contains(worktree_id)); + assert!(visible_project_ids.contains(project_id)); } - if let Some(share) = &worktree.share { + if let Some(share) = &project.share { for guest_connection_id in share.guests.keys() { let guest_connection = self.connections.get(guest_connection_id).unwrap(); - assert!(guest_connection.worktrees.contains(worktree_id)); + assert!(guest_connection.projects.contains(project_id)); } assert_eq!(share.active_replica_ids.len(), share.guests.len(),); assert_eq!( @@ -614,10 +580,10 @@ impl Store { } } - for (user_id, visible_worktree_ids) in &self.visible_worktrees_by_user_id { - for worktree_id in visible_worktree_ids { - let worktree = self.worktrees.get(worktree_id).unwrap(); - assert!(worktree.authorized_user_ids.contains(user_id)); + for (user_id, visible_project_ids) in &self.visible_projects_by_user_id { + for project_id in visible_project_ids { + let project = self.projects.get(project_id).unwrap(); + assert!(project.authorized_user_ids().contains(user_id)); } } @@ -630,7 +596,33 @@ impl Store { } } -impl Worktree { +impl Project { + pub fn has_authorized_user_id(&self, user_id: UserId) -> bool { + self.worktrees + .values() + .any(|worktree| worktree.authorized_user_ids.contains(&user_id)) + } + + pub fn authorized_user_ids(&self) -> Vec { + let mut ids = self + .worktrees + .values() + .flat_map(|worktree| worktree.authorized_user_ids.iter()) + .copied() + .collect::>(); + ids.sort_unstable(); + ids.dedup(); + ids + } + + pub fn guest_connection_ids(&self) -> Vec { + if let Some(share) = &self.share { + share.guests.keys().copied().collect() + } else { + Vec::new() + } + } + pub fn connection_ids(&self) -> Vec { if let Some(share) = &self.share { share diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 747a914ffe..8818bbd5d1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -391,15 +391,13 @@ pub struct Workspace { impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { - let project = cx.add_model(|cx| { - Project::local( - params.languages.clone(), - params.client.clone(), - params.user_store.clone(), - params.fs.clone(), - cx, - ) - }); + let project = Project::local( + params.client.clone(), + params.user_store.clone(), + params.languages.clone(), + params.fs.clone(), + cx, + ); cx.observe(&project, |_, _, cx| cx.notify()).detach(); let pane = cx.add_view(|_| Pane::new(params.settings.clone()));