diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 3e524f5b3e..5599c15b6d 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1182,19 +1182,10 @@ impl Room { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); - let role = self.local_participant.role; cx.emit(Event::RemoteProjectJoined { project_id: id }); cx.spawn(move |this, mut cx| async move { - let project = Project::remote( - id, - client, - user_store, - language_registry, - fs, - role, - cx.clone(), - ) - .await?; + let project = + Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; this.update(&mut cx, |this, cx| { this.joined_projects.retain(|project| { diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 1dbf502252..aee92d0f6c 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -11,7 +11,7 @@ pub use channel_chat::{ mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, MessageParams, }; -pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore, HostedProjectId}; +pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore}; #[cfg(test)] mod channel_store_tests; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 0afd7e41b9..3197bc1bd3 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -3,7 +3,9 @@ mod channel_index; use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage}; use anyhow::{anyhow, Result}; use channel_index::ChannelIndex; -use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore}; +use client::{ + ChannelId, Client, ClientSettings, HostedProjectId, Subscription, User, UserId, UserStore, +}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{ @@ -27,9 +29,6 @@ pub fn init(client: &Arc, user_store: Model, cx: &mut AppCont cx.set_global(GlobalChannelStore(channel_store)); } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub struct HostedProjectId(pub u64); - #[derive(Debug, Clone, Default)] struct NotesVersion { epoch: u64, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 9b7ef1dbb1..0c10e50ac5 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -24,6 +24,9 @@ impl std::fmt::Display for ChannelId { } } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct HostedProjectId(pub u64); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParticipantIndex(pub u32); diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 20658e95cb..b7b427a6b7 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -46,10 +46,11 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id"); CREATE TABLE "projects" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL, - "host_user_id" INTEGER REFERENCES users (id) NOT NULL, + "host_user_id" INTEGER REFERENCES users (id), "host_connection_id" INTEGER, "host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE, - "unregistered" BOOLEAN NOT NULL DEFAULT FALSE + "unregistered" BOOLEAN NOT NULL DEFAULT FALSE, + "hosted_project_id" INTEGER REFERENCES hosted_projects (id) ); CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id"); CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id"); diff --git a/crates/collab/migrations/20240227215556_hosted_projects_in_projects.sql b/crates/collab/migrations/20240227215556_hosted_projects_in_projects.sql new file mode 100644 index 0000000000..69905d12f6 --- /dev/null +++ b/crates/collab/migrations/20240227215556_hosted_projects_in_projects.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE projects ALTER COLUMN host_user_id DROP NOT NULL; +ALTER TABLE projects ADD COLUMN hosted_project_id INTEGER REFERENCES hosted_projects(id) UNIQUE NULL; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 4a3d4f5656..caf8a83d50 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -670,6 +670,8 @@ pub struct RefreshedChannelBuffer { } pub struct Project { + pub id: ProjectId, + pub role: ChannelRole, pub collaborators: Vec, pub worktrees: BTreeMap, pub language_servers: Vec, @@ -695,7 +697,7 @@ impl ProjectCollaborator { #[derive(Debug)] pub struct LeftProject { pub id: ProjectId, - pub host_user_id: UserId, + pub host_user_id: Option, pub host_connection_id: Option, pub connection_ids: Vec, } diff --git a/crates/collab/src/db/queries/hosted_projects.rs b/crates/collab/src/db/queries/hosted_projects.rs index fd9a991906..394f1055c6 100644 --- a/crates/collab/src/db/queries/hosted_projects.rs +++ b/crates/collab/src/db/queries/hosted_projects.rs @@ -1,4 +1,4 @@ -use rpc::proto; +use rpc::{proto, ErrorCode}; use super::*; @@ -39,4 +39,44 @@ impl Database { }) .collect()) } + + pub async fn get_hosted_project( + &self, + hosted_project_id: HostedProjectId, + user_id: UserId, + tx: &DatabaseTransaction, + ) -> Result<(hosted_project::Model, ChannelRole)> { + let project = hosted_project::Entity::find_by_id(hosted_project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?; + let channel = channel::Entity::find_by_id(project.channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?; + + let role = match project.visibility { + ChannelVisibility::Public => { + self.check_user_is_channel_participant(&channel, user_id, tx) + .await? + } + ChannelVisibility::Members => { + self.check_user_is_channel_member(&channel, user_id, tx) + .await? + } + }; + + Ok((project, role)) + } + + pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result { + self.transaction(|tx| async move { + Ok(project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .map(|project| project.hosted_project_id.is_some()) + .ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?) + }) + .await + } } diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index ed489169c9..190f854bfa 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -57,13 +57,14 @@ impl Database { } let project = project::ActiveModel { - room_id: ActiveValue::set(participant.room_id), - host_user_id: ActiveValue::set(participant.user_id), + room_id: ActiveValue::set(Some(participant.room_id)), + host_user_id: ActiveValue::set(Some(participant.user_id)), host_connection_id: ActiveValue::set(Some(connection.id as i32)), host_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), - ..Default::default() + id: ActiveValue::NotSet, + hosted_project_id: ActiveValue::Set(None), } .insert(&*tx) .await?; @@ -153,8 +154,12 @@ impl Database { self.update_project_worktrees(project.id, worktrees, &tx) .await?; + let room_id = project + .room_id + .ok_or_else(|| anyhow!("project not in a room"))?; + let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?; - let room = self.get_room(project.room_id, &tx).await?; + let room = self.get_room(room_id, &tx).await?; Ok((room, guest_connection_ids)) }) .await @@ -504,8 +509,30 @@ impl Database { .await } - /// Adds the given connection to the specified project. - pub async fn join_project( + /// Adds the given connection to the specified hosted project + pub async fn join_hosted_project( + &self, + id: HostedProjectId, + user_id: UserId, + connection: ConnectionId, + ) -> Result<(Project, ReplicaId)> { + self.transaction(|tx| async move { + let (hosted_project, role) = self.get_hosted_project(id, user_id, &tx).await?; + let project = project::Entity::find() + .filter(project::Column::HostedProjectId.eq(hosted_project.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("hosted project is no longer shared"))?; + + self.join_project_internal(project, user_id, connection, role, &tx) + .await + }) + .await + } + + /// Adds the given connection to the specified project + /// in the current room. + pub async fn join_project_in_room( &self, project_id: ProjectId, connection: ConnectionId, @@ -532,180 +559,240 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("no such project"))?; - if project.room_id != participant.room_id { + if project.room_id != Some(participant.room_id) { return Err(anyhow!("no such project"))?; } + self.join_project_internal( + project, + participant.user_id, + connection, + participant.role.unwrap_or(ChannelRole::Member), + &tx, + ) + .await + }) + .await + } - let mut collaborators = project + async fn join_project_internal( + &self, + project: project::Model, + user_id: UserId, + connection: ConnectionId, + role: ChannelRole, + tx: &DatabaseTransaction, + ) -> Result<(Project, ReplicaId)> { + let mut collaborators = project + .find_related(project_collaborator::Entity) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(1); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + let new_collaborator = project_collaborator::ActiveModel { + project_id: ActiveValue::set(project.id), + connection_id: ActiveValue::set(connection.id as i32), + connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::set(user_id), + replica_id: ActiveValue::set(replica_id), + is_host: ActiveValue::set(false), + ..Default::default() + } + .insert(&*tx) + .await?; + collaborators.push(new_collaborator); + + let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; + let mut worktrees = db_worktrees + .into_iter() + .map(|db_worktree| { + ( + db_worktree.id as u64, + Worktree { + id: db_worktree.id as u64, + abs_path: db_worktree.abs_path, + root_name: db_worktree.root_name, + visible: db_worktree.visible, + entries: Default::default(), + repository_entries: Default::default(), + diagnostic_summaries: Default::default(), + settings_files: Default::default(), + scan_id: db_worktree.scan_id as u64, + completed_scan_id: db_worktree.completed_scan_id as u64, + }, + ) + }) + .collect::>(); + + // Populate worktree entries. + { + let mut db_entries = worktree_entry::Entity::find() + .filter( + Condition::all() + .add(worktree_entry::Column::ProjectId.eq(project.id)) + .add(worktree_entry::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_entry) = db_entries.next().await { + let db_entry = db_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { + worktree.entries.push(proto::Entry { + id: db_entry.id as u64, + is_dir: db_entry.is_dir, + path: db_entry.path, + inode: db_entry.inode as u64, + mtime: Some(proto::Timestamp { + seconds: db_entry.mtime_seconds as u64, + nanos: db_entry.mtime_nanos as u32, + }), + is_symlink: db_entry.is_symlink, + is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, + git_status: db_entry.git_status.map(|status| status as i32), + }); + } + } + } + + // Populate repository entries. + { + let mut db_repository_entries = worktree_repository::Entity::find() + .filter( + Condition::all() + .add(worktree_repository::Column::ProjectId.eq(project.id)) + .add(worktree_repository::Column::IsDeleted.eq(false)), + ) + .stream(&*tx) + .await?; + while let Some(db_repository_entry) = db_repository_entries.next().await { + let db_repository_entry = db_repository_entry?; + if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) + { + worktree.repository_entries.insert( + db_repository_entry.work_directory_id as u64, + proto::RepositoryEntry { + work_directory_id: db_repository_entry.work_directory_id as u64, + branch: db_repository_entry.branch, + }, + ); + } + } + } + + // Populate worktree diagnostic summaries. + { + let mut db_summaries = worktree_diagnostic_summary::Entity::find() + .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project.id)) + .stream(&*tx) + .await?; + while let Some(db_summary) = db_summaries.next().await { + let db_summary = db_summary?; + if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { + worktree + .diagnostic_summaries + .push(proto::DiagnosticSummary { + path: db_summary.path, + language_server_id: db_summary.language_server_id as u64, + error_count: db_summary.error_count as u32, + warning_count: db_summary.warning_count as u32, + }); + } + } + } + + // Populate worktree settings files + { + let mut db_settings_files = worktree_settings_file::Entity::find() + .filter(worktree_settings_file::Column::ProjectId.eq(project.id)) + .stream(&*tx) + .await?; + while let Some(db_settings_file) = db_settings_files.next().await { + let db_settings_file = db_settings_file?; + if let Some(worktree) = worktrees.get_mut(&(db_settings_file.worktree_id as u64)) { + worktree.settings_files.push(WorktreeSettingsFile { + path: db_settings_file.path, + content: db_settings_file.content, + }); + } + } + } + + // Populate language servers. + let language_servers = project + .find_related(language_server::Entity) + .all(&*tx) + .await?; + + let project = Project { + id: project.id, + role, + collaborators: collaborators + .into_iter() + .map(|collaborator| ProjectCollaborator { + connection_id: collaborator.connection(), + user_id: collaborator.user_id, + replica_id: collaborator.replica_id, + is_host: collaborator.is_host, + }) + .collect(), + worktrees, + language_servers: language_servers + .into_iter() + .map(|language_server| proto::LanguageServer { + id: language_server.id as u64, + name: language_server.name, + }) + .collect(), + }; + Ok((project, replica_id as ReplicaId)) + } + + pub async fn leave_hosted_project( + &self, + project_id: ProjectId, + connection: ConnectionId, + ) -> Result { + self.transaction(|tx| async move { + let result = project_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(project_collaborator::Column::ProjectId.eq(project_id)) + .add(project_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + return Err(anyhow!("not in the project"))?; + } + + let project = project::Entity::find_by_id(project_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such project"))?; + let collaborators = project .find_related(project_collaborator::Entity) .all(&*tx) .await?; - let replica_ids = collaborators - .iter() - .map(|c| c.replica_id) - .collect::>(); - let mut replica_id = ReplicaId(1); - while replica_ids.contains(&replica_id) { - replica_id.0 += 1; - } - let new_collaborator = project_collaborator::ActiveModel { - project_id: ActiveValue::set(project_id), - connection_id: ActiveValue::set(connection.id as i32), - connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), - user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(replica_id), - is_host: ActiveValue::set(false), - ..Default::default() - } - .insert(&*tx) - .await?; - collaborators.push(new_collaborator); - - let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; - let mut worktrees = db_worktrees + let connection_ids = collaborators .into_iter() - .map(|db_worktree| { - ( - db_worktree.id as u64, - Worktree { - id: db_worktree.id as u64, - abs_path: db_worktree.abs_path, - root_name: db_worktree.root_name, - visible: db_worktree.visible, - entries: Default::default(), - repository_entries: Default::default(), - diagnostic_summaries: Default::default(), - settings_files: Default::default(), - scan_id: db_worktree.scan_id as u64, - completed_scan_id: db_worktree.completed_scan_id as u64, - }, - ) - }) - .collect::>(); - - // Populate worktree entries. - { - let mut db_entries = worktree_entry::Entity::find() - .filter( - Condition::all() - .add(worktree_entry::Column::ProjectId.eq(project_id)) - .add(worktree_entry::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_entry) = db_entries.next().await { - let db_entry = db_entry?; - if let Some(worktree) = worktrees.get_mut(&(db_entry.worktree_id as u64)) { - worktree.entries.push(proto::Entry { - id: db_entry.id as u64, - is_dir: db_entry.is_dir, - path: db_entry.path, - inode: db_entry.inode as u64, - mtime: Some(proto::Timestamp { - seconds: db_entry.mtime_seconds as u64, - nanos: db_entry.mtime_nanos as u32, - }), - is_symlink: db_entry.is_symlink, - is_ignored: db_entry.is_ignored, - is_external: db_entry.is_external, - git_status: db_entry.git_status.map(|status| status as i32), - }); - } - } - } - - // Populate repository entries. - { - let mut db_repository_entries = worktree_repository::Entity::find() - .filter( - Condition::all() - .add(worktree_repository::Column::ProjectId.eq(project_id)) - .add(worktree_repository::Column::IsDeleted.eq(false)), - ) - .stream(&*tx) - .await?; - while let Some(db_repository_entry) = db_repository_entries.next().await { - let db_repository_entry = db_repository_entry?; - if let Some(worktree) = - worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) - { - worktree.repository_entries.insert( - db_repository_entry.work_directory_id as u64, - proto::RepositoryEntry { - work_directory_id: db_repository_entry.work_directory_id as u64, - branch: db_repository_entry.branch, - }, - ); - } - } - } - - // Populate worktree diagnostic summaries. - { - let mut db_summaries = worktree_diagnostic_summary::Entity::find() - .filter(worktree_diagnostic_summary::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_summary) = db_summaries.next().await { - let db_summary = db_summary?; - if let Some(worktree) = worktrees.get_mut(&(db_summary.worktree_id as u64)) { - worktree - .diagnostic_summaries - .push(proto::DiagnosticSummary { - path: db_summary.path, - language_server_id: db_summary.language_server_id as u64, - error_count: db_summary.error_count as u32, - warning_count: db_summary.warning_count as u32, - }); - } - } - } - - // Populate worktree settings files - { - let mut db_settings_files = worktree_settings_file::Entity::find() - .filter(worktree_settings_file::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(db_settings_file) = db_settings_files.next().await { - let db_settings_file = db_settings_file?; - if let Some(worktree) = - worktrees.get_mut(&(db_settings_file.worktree_id as u64)) - { - worktree.settings_files.push(WorktreeSettingsFile { - path: db_settings_file.path, - content: db_settings_file.content, - }); - } - } - } - - // Populate language servers. - let language_servers = project - .find_related(language_server::Entity) - .all(&*tx) - .await?; - - let project = Project { - collaborators: collaborators - .into_iter() - .map(|collaborator| ProjectCollaborator { - connection_id: collaborator.connection(), - user_id: collaborator.user_id, - replica_id: collaborator.replica_id, - is_host: collaborator.is_host, - }) - .collect(), - worktrees, - language_servers: language_servers - .into_iter() - .map(|language_server| proto::LanguageServer { - id: language_server.id as u64, - name: language_server.name, - }) - .collect(), - }; - Ok((project, replica_id as ReplicaId)) + .map(|collaborator| collaborator.connection()) + .collect(); + Ok(LeftProject { + id: project.id, + connection_ids, + host_user_id: None, + host_connection_id: None, + }) }) .await } @@ -772,7 +859,7 @@ impl Database { .exec(&*tx) .await?; - let room = self.get_room(project.room_id, &tx).await?; + let room = self.get_room(room_id, &tx).await?; let left_project = LeftProject { id: project_id, host_user_id: project.host_user_id, @@ -996,7 +1083,9 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("project {} not found", project_id))?; - Ok(project.room_id) + Ok(project + .room_id + .ok_or_else(|| anyhow!("project not in room"))?) }) .await } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index cd960ec5a7..13707d6f47 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -491,7 +491,7 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("project does not exist"))?; - if project.host_user_id != user_id { + if project.host_user_id != Some(user_id) { return Err(anyhow!("no such project"))?; } @@ -851,7 +851,7 @@ impl Database { } if collaborator.is_host { - left_project.host_user_id = collaborator.user_id; + left_project.host_user_id = Some(collaborator.user_id); left_project.host_connection_id = Some(collaborator_connection_id); } } diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs index 8c26836046..550f8415d7 100644 --- a/crates/collab/src/db/tables/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -1,4 +1,4 @@ -use crate::db::{ProjectId, Result, RoomId, ServerId, UserId}; +use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId}; use anyhow::anyhow; use rpc::ConnectionId; use sea_orm::entity::prelude::*; @@ -8,10 +8,11 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: ProjectId, - pub room_id: RoomId, - pub host_user_id: UserId, + pub room_id: Option, + pub host_user_id: Option, pub host_connection_id: Option, pub host_connection_server_id: Option, + pub hosted_project_id: Option, } impl Model { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index d6a0ba8e89..4fda8b9959 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4,8 +4,9 @@ use crate::{ auth::{self, Impersonator}, db::{ self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database, - InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId, - RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId, + HostedProjectId, InviteMemberResult, MembershipUpdated, MessageId, NotificationId, Project, + ProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId, + User, UserId, }, executor::Executor, AppState, Error, Result, @@ -197,6 +198,7 @@ impl Server { .add_request_handler(share_project) .add_message_handler(unshare_project) .add_request_handler(join_project) + .add_request_handler(join_hosted_project) .add_message_handler(leave_project) .add_request_handler(update_project) .add_request_handler(update_worktree) @@ -1584,22 +1586,46 @@ async fn join_project( session: Session, ) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); - let guest_user_id = session.user_id; tracing::info!(%project_id, "join project"); let (project, replica_id) = &mut *session .db() .await - .join_project(project_id, session.connection_id) + .join_project_in_room(project_id, session.connection_id) .await?; + join_project_internal(response, session, project, replica_id) +} + +trait JoinProjectInternalResponse { + fn send(self, result: proto::JoinProjectResponse) -> Result<()>; +} +impl JoinProjectInternalResponse for Response { + fn send(self, result: proto::JoinProjectResponse) -> Result<()> { + Response::::send(self, result) + } +} +impl JoinProjectInternalResponse for Response { + fn send(self, result: proto::JoinProjectResponse) -> Result<()> { + Response::::send(self, result) + } +} + +fn join_project_internal( + response: impl JoinProjectInternalResponse, + session: Session, + project: &mut Project, + replica_id: &ReplicaId, +) -> Result<()> { let collaborators = project .collaborators .iter() .filter(|collaborator| collaborator.connection_id != session.connection_id) .map(|collaborator| collaborator.to_proto()) .collect::>(); + let project_id = project.id; + let guest_user_id = session.user_id; let worktrees = project .worktrees @@ -1631,10 +1657,12 @@ async fn join_project( // First, we send the metadata associated with each worktree. response.send(proto::JoinProjectResponse { + project_id: project.id.0 as u64, worktrees: worktrees.clone(), replica_id: replica_id.0 as u32, collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), + role: project.role.into(), // todo })?; for (worktree_id, worktree) in mem::take(&mut project.worktrees) { @@ -1707,15 +1735,17 @@ async fn join_project( async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { let sender_id = session.connection_id; let project_id = ProjectId::from_proto(request.project_id); + let db = session.db().await; + if db.is_hosted_project(project_id).await? { + let project = db.leave_hosted_project(project_id, sender_id).await?; + project_left(&project, &session); + return Ok(()); + } - let (room, project) = &*session - .db() - .await - .leave_project(project_id, sender_id) - .await?; + let (room, project) = &*db.leave_project(project_id, sender_id).await?; tracing::info!( %project_id, - host_user_id = %project.host_user_id, + host_user_id = ?project.host_user_id, host_connection_id = ?project.host_connection_id, "leave project" ); @@ -1726,6 +1756,24 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result Ok(()) } +async fn join_hosted_project( + request: proto::JoinHostedProject, + response: Response, + session: Session, +) -> Result<()> { + let (mut project, replica_id) = session + .db() + .await + .join_hosted_project( + HostedProjectId(request.id as i32), + session.user_id, + session.connection_id, + ) + .await?; + + join_project_internal(response, session, &mut project, &replica_id) +} + /// Updates other participants with changes to the project async fn update_project( request: proto::UpdateProject, @@ -3624,7 +3672,7 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { fn project_left(project: &db::LeftProject, session: &Session) { for connection_id in &project.connection_ids { - if project.host_user_id == session.user_id { + if project.host_user_id == Some(session.user_id) { session .peer .send( diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index ff53c339e6..57e5388045 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1534,7 +1534,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut executor.run_until_parked(); assert_eq!(visible_push_notifications(cx_a).len(), 1); cx_a.update(|cx| { - workspace::join_remote_project( + workspace::join_in_room_project( project_b_id, client_b.user_id().unwrap(), client_a.app_state.clone(), diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 08e811adcf..443617bbe3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -22,7 +22,6 @@ use project::{ search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, }; use rand::prelude::*; -use rpc::proto::ChannelRole; use serde_json::json; use settings::SettingsStore; use std::{ @@ -3742,7 +3741,6 @@ async fn test_leaving_project( client_b.user_store().clone(), client_b.language_registry().clone(), FakeFs::new(cx.background_executor().clone()), - ChannelRole::Member, cx, ) }) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 500c7affcf..4f242d68bb 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -7,8 +7,8 @@ use crate::{ CollaborationPanelSettings, }; use call::ActiveCall; -use channel::{Channel, ChannelEvent, ChannelStore, HostedProjectId}; -use client::{ChannelId, Client, Contact, User, UserStore}; +use channel::{Channel, ChannelEvent, ChannelStore}; +use client::{ChannelId, Client, Contact, HostedProjectId, User, UserStore}; use contact_finder::ContactFinder; use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorElement, EditorStyle}; @@ -911,7 +911,7 @@ impl CollabPanel { this.workspace .update(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); - workspace::join_remote_project(project_id, host_user_id, app_state, cx) + workspace::join_in_room_project(project_id, host_user_id, app_state, cx) .detach_and_prompt_err("Failed to join project", cx, |_, _| None); }) .ok(); @@ -1047,8 +1047,15 @@ impl CollabPanel { .indent_level(2) .indent_step_size(px(20.)) .selected(is_selected) - .on_click(cx.listener(move |_this, _, _cx| { - // todo() + .on_click(cx.listener(move |this, _, cx| { + if let Some(workspace) = this.workspace.upgrade() { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_hosted_project(id, app_state, cx).detach_and_prompt_err( + "Failed to open project", + cx, + |_, _| None, + ) + } })) .start_slot( h_flex() @@ -1461,7 +1468,7 @@ impl CollabPanel { } => { if let Some(workspace) = self.workspace.upgrade() { let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project( + workspace::join_in_room_project( *project_id, *host_user_id, app_state, diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index b6ba2f9507..a8ba20c1e5 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -82,7 +82,7 @@ impl IncomingCallNotificationState { if let Some(project_id) = initial_project_id { cx.update(|cx| { if let Some(app_state) = app_state.upgrade() { - workspace::join_remote_project( + workspace::join_in_room_project( project_id, caller_user_id, app_state, diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index 08141d0c2e..46c5c8ce8a 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -98,7 +98,7 @@ impl ProjectSharedNotification { fn join(&mut self, cx: &mut ViewContext) { if let Some(app_state) = self.app_state.upgrade() { - workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx) + workspace::join_in_room_project(self.project_id, self.owner.id, app_state, cx) .detach_and_log_err(cx); } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index aee2f5e6c2..8661ceada0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,7 @@ mod project_tests; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; -use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; +use client::{proto, Client, Collaborator, HostedProjectId, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use copilot::Copilot; @@ -167,6 +167,7 @@ pub struct Project { prettiers_per_worktree: HashMap>>, prettier_instances: HashMap, tasks: Model, + hosted_project_id: Option, } pub enum LanguageServerToQuery { @@ -605,6 +606,7 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), tasks, + hosted_project_id: None, } }) } @@ -615,17 +617,30 @@ impl Project { user_store: Model, languages: Arc, fs: Arc, - role: proto::ChannelRole, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> Result> { client.authenticate_and_connect(true, &cx).await?; - let subscription = client.subscribe_to_entity(remote_id)?; let response = client .request_envelope(proto::JoinProject { project_id: remote_id, }) .await?; + Self::from_join_project_response(response, None, client, user_store, languages, fs, cx) + .await + } + async fn from_join_project_response( + response: TypedEnvelope, + hosted_project_id: Option, + client: Arc, + user_store: Model, + languages: Arc, + fs: Arc, + mut cx: AsyncAppContext, + ) -> Result> { + let remote_id = response.payload.project_id; + let role = response.payload.role(); + let subscription = client.subscribe_to_entity(remote_id)?; let this = cx.new_model(|cx| { let replica_id = response.payload.replica_id as ReplicaId; let tasks = Inventory::new(cx); @@ -714,6 +729,7 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), tasks, + hosted_project_id, }; this.set_role(role, cx); for worktree in worktrees { @@ -742,6 +758,31 @@ impl Project { Ok(this) } + pub async fn hosted( + hosted_project_id: HostedProjectId, + user_store: Model, + client: Arc, + languages: Arc, + fs: Arc, + cx: AsyncAppContext, + ) -> Result> { + let response = client + .request_envelope(proto::JoinHostedProject { + id: hosted_project_id.0, + }) + .await?; + Self::from_join_project_response( + response, + Some(hosted_project_id), + client, + user_store, + languages, + fs, + cx, + ) + .await + } + fn release(&mut self, cx: &mut AppContext) { match &self.client_state { ProjectClientState::Local => {} @@ -987,6 +1028,10 @@ impl Project { } } + pub fn hosted_project_id(&self) -> Option { + self.hosted_project_id + } + pub fn replica_id(&self) -> ReplicaId { match self.client_state { ProjectClientState::Remote { replica_id, .. } => replica_id, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6a986ff1f3..a45e7a5e21 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -196,6 +196,8 @@ message Envelope { GetImplementation get_implementation = 162; GetImplementationResponse get_implementation_response = 163; + + JoinHostedProject join_hosted_project = 164; } reserved 158 to 161; @@ -230,6 +232,7 @@ enum ErrorCode { CircularNesting = 10; WrongMoveTarget = 11; UnsharedItem = 12; + NoSuchProject = 13; reserved 6; } @@ -404,11 +407,17 @@ message JoinProject { uint64 project_id = 1; } +message JoinHostedProject { + uint64 id = 1; +} + message JoinProjectResponse { + uint64 project_id = 5; uint32 replica_id = 1; repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; + ChannelRole role = 6; } message LeaveProject { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8e112ab56a..38e80103ca 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -206,6 +206,7 @@ messages!( (JoinChannelChat, Foreground), (JoinChannelChatResponse, Foreground), (JoinProject, Foreground), + (JoinHostedProject, Foreground), (JoinProjectResponse, Foreground), (JoinRoom, Foreground), (JoinRoomResponse, Foreground), @@ -329,6 +330,7 @@ request_messages!( (JoinChannel, JoinRoomResponse), (JoinChannelBuffer, JoinChannelBufferResponse), (JoinChannelChat, JoinChannelChatResponse), + (JoinHostedProject, JoinProjectResponse), (JoinProject, JoinProjectResponse), (JoinRoom, JoinRoomResponse), (LeaveChannelBuffer, Ack), diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 40c636c26f..0c3f9b9dc0 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -268,7 +268,7 @@ impl Member { this.cursor_pointer().on_mouse_down( MouseButton::Left, cx.listener(move |this, _, cx| { - crate::join_remote_project( + crate::join_in_room_project( leader_project_id, leader_user_id, this.app_state().clone(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 390b36a7f0..b4cb71c45a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,7 @@ use anyhow::{anyhow, Context as _, Result}; use call::{call_settings::CallSettings, ActiveCall}; use client::{ proto::{self, ErrorCode, PeerId}, - ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore, + ChannelId, Client, ErrorExt, HostedProjectId, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use derive_more::{Deref, DerefMut}; @@ -2635,7 +2635,7 @@ impl Workspace { // if they are active in another project, follow there. if let Some(project_id) = other_project_id { let app_state = self.app_state.clone(); - crate::join_remote_project(project_id, remote_participant.user.id, app_state, cx) + crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx) .detach_and_log_err(cx); } @@ -4158,7 +4158,7 @@ async fn join_channel_internal( if let Some(room) = open_room { let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_remote_project(project, host, app_state.clone(), cx)); + return Some(join_in_room_project(project, host, app_state.clone(), cx)); } None @@ -4229,7 +4229,7 @@ async fn join_channel_internal( let task = room.update(cx, |room, cx| { if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_remote_project(project, host, app_state.clone(), cx)); + return Some(join_in_room_project(project, host, app_state.clone(), cx)); } // if you are the first to join a channel, share your project @@ -4464,7 +4464,56 @@ pub fn create_and_open_local_file( }) } -pub fn join_remote_project( +pub fn join_hosted_project( + hosted_project_id: HostedProjectId, + app_state: Arc, + cx: &mut AppContext, +) -> Task> { + cx.spawn(|mut cx| async move { + let existing_window = cx.update(|cx| { + cx.windows().into_iter().find_map(|window| { + let workspace = window.downcast::()?; + workspace + .read(cx) + .is_ok_and(|workspace| { + workspace.project().read(cx).hosted_project_id() == Some(hosted_project_id) + }) + .then(|| workspace) + }) + })?; + + let workspace = if let Some(existing_window) = existing_window { + existing_window + } else { + let project = Project::hosted( + hosted_project_id, + app_state.user_store.clone(), + app_state.client.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + cx.clone(), + ) + .await?; + + let window_bounds_override = window_bounds_env_override(&cx); + cx.update(|cx| { + let options = (app_state.build_window_options)(window_bounds_override, None, cx); + cx.open_window(options, |cx| { + cx.new_view(|cx| Workspace::new(0, project, app_state.clone(), cx)) + }) + })? + }; + + workspace.update(&mut cx, |_, cx| { + cx.activate(true); + cx.activate_window(); + })?; + + Ok(()) + }) +} + +pub fn join_in_room_project( project_id: u64, follow_user_id: u64, app_state: Arc,