Detailed changes
@@ -1182,19 +1182,10 @@ impl Room {
) -> Task<Result<Model<Project>>> {
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| {
@@ -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;
@@ -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<Client>, user_store: Model<UserStore>, 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,
@@ -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);
@@ -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");
@@ -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;
@@ -670,6 +670,8 @@ pub struct RefreshedChannelBuffer {
}
pub struct Project {
+ pub id: ProjectId,
+ pub role: ChannelRole,
pub collaborators: Vec<ProjectCollaborator>,
pub worktrees: BTreeMap<u64, Worktree>,
pub language_servers: Vec<proto::LanguageServer>,
@@ -695,7 +697,7 @@ impl ProjectCollaborator {
#[derive(Debug)]
pub struct LeftProject {
pub id: ProjectId,
- pub host_user_id: UserId,
+ pub host_user_id: Option<UserId>,
pub host_connection_id: Option<ConnectionId>,
pub connection_ids: Vec<ConnectionId>,
}
@@ -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<bool> {
+ 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
+ }
}
@@ -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
- .find_related(project_collaborator::Entity)
- .all(&*tx)
+ 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::<HashSet<_>>();
+ 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::<BTreeMap<_, _>>();
+
+ // 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?;
- let replica_ids = collaborators
- .iter()
- .map(|c| c.replica_id)
- .collect::<HashSet<_>>();
- 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()
+ 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),
+ });
+ }
}
- .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,
+ // 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,
},
- )
- })
- .collect::<BTreeMap<_, _>>();
-
- // 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 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 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<LeftProject> {
+ 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"))?;
}
- // Populate language servers.
- let language_servers = project
- .find_related(language_server::Entity)
+ 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 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))
+ let connection_ids = collaborators
+ .into_iter()
+ .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
}
@@ -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);
}
}
@@ -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<RoomId>,
+ pub host_user_id: Option<UserId>,
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
+ pub hosted_project_id: Option<HostedProjectId>,
}
impl Model {
@@ -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<proto::JoinProject> {
+ fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
+ Response::<proto::JoinProject>::send(self, result)
+ }
+}
+impl JoinProjectInternalResponse for Response<proto::JoinHostedProject> {
+ fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
+ Response::<proto::JoinHostedProject>::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::<Vec<_>>();
+ 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<proto::JoinHostedProject>,
+ 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(
@@ -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(),
@@ -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,
)
})
@@ -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,
@@ -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,
@@ -98,7 +98,7 @@ impl ProjectSharedNotification {
fn join(&mut self, cx: &mut ViewContext<Self>) {
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);
}
}
@@ -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<WorktreeId, HashSet<Option<PathBuf>>>,
prettier_instances: HashMap<PathBuf, PrettierInstance>,
tasks: Model<Inventory>,
+ hosted_project_id: Option<HostedProjectId>,
}
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<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
- role: proto::ChannelRole,
- mut cx: AsyncAppContext,
+ cx: AsyncAppContext,
) -> Result<Model<Self>> {
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<proto::JoinProjectResponse>,
+ hosted_project_id: Option<HostedProjectId>,
+ client: Arc<Client>,
+ user_store: Model<UserStore>,
+ languages: Arc<LanguageRegistry>,
+ fs: Arc<dyn Fs>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Model<Self>> {
+ 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<UserStore>,
+ client: Arc<Client>,
+ languages: Arc<LanguageRegistry>,
+ fs: Arc<dyn Fs>,
+ cx: AsyncAppContext,
+ ) -> Result<Model<Self>> {
+ 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<HostedProjectId> {
+ self.hosted_project_id
+ }
+
pub fn replica_id(&self) -> ReplicaId {
match self.client_state {
ProjectClientState::Remote { replica_id, .. } => replica_id,
@@ -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 {
@@ -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),
@@ -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(),
@@ -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<AppState>,
+ cx: &mut AppContext,
+) -> Task<Result<()>> {
+ cx.spawn(|mut cx| async move {
+ let existing_window = cx.update(|cx| {
+ cx.windows().into_iter().find_map(|window| {
+ let workspace = window.downcast::<Workspace>()?;
+ 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<AppState>,