From 581bfa341ad9dfb771deb6d8b7dd7c76700847a5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 2 Apr 2024 10:29:20 -0600 Subject: [PATCH] Remote projects can be created and show up Co-Authored-By: Bennet --- crates/channel/src/channel.rs | 2 +- crates/channel/src/channel_store.rs | 175 +++++++++++++++++- crates/client/src/user.rs | 6 + .../20221109000000_test_schema.sql | 14 +- ...20240402155003_add_dev_server_projects.sql | 9 + crates/collab/src/db.rs | 2 + crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries.rs | 1 + crates/collab/src/db/queries/channels.rs | 5 + crates/collab/src/db/queries/dev_servers.rs | 25 ++- crates/collab/src/db/queries/projects.rs | 1 + .../collab/src/db/queries/remote_projects.rs | 73 ++++++++ crates/collab/src/db/tables.rs | 1 + crates/collab/src/db/tables/project.rs | 15 +- crates/collab/src/db/tables/remote_project.rs | 41 ++++ crates/collab/src/rpc.rs | 46 ++++- crates/collab_ui/src/collab_panel.rs | 104 ++++++++++- .../src/collab_panel/dev_server_modal.rs | 97 ++++++++++ crates/rpc/proto/zed.proto | 37 +++- crates/rpc/src/proto.rs | 2 + 20 files changed, 635 insertions(+), 22 deletions(-) create mode 100644 crates/collab/migrations/20240402155003_add_dev_server_projects.sql create mode 100644 crates/collab/src/db/queries/remote_projects.rs create mode 100644 crates/collab/src/db/tables/remote_project.rs create mode 100644 crates/collab_ui/src/collab_panel/dev_server_modal.rs diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index aee92d0f6c5c..13303060e16e 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}; +pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore, RemoteProject}; #[cfg(test)] mod channel_store_tests; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a6a94865b441..c1165863659a 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -3,7 +3,10 @@ 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, ProjectId, Subscription, User, UserId, UserStore}; +use client::{ + ChannelId, Client, ClientSettings, DevServerId, ProjectId, RemoteProjectId, Subscription, User, + UserId, UserStore, +}; use collections::{hash_map, HashMap, HashSet}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{ @@ -40,7 +43,6 @@ pub struct HostedProject { name: SharedString, _visibility: proto::ChannelVisibility, } - impl From for HostedProject { fn from(project: proto::HostedProject) -> Self { Self { @@ -52,12 +54,52 @@ impl From for HostedProject { } } +#[derive(Debug, Clone)] +pub struct RemoteProject { + pub id: RemoteProjectId, + pub project_id: Option, + pub channel_id: ChannelId, + pub name: SharedString, + pub dev_server_id: DevServerId, +} + +impl From for RemoteProject { + fn from(project: proto::RemoteProject) -> Self { + Self { + id: RemoteProjectId(project.id), + project_id: project.project_id.map(|id| ProjectId(id)), + channel_id: ChannelId(project.channel_id), + name: project.name.into(), + dev_server_id: DevServerId(project.dev_server_id), + } + } +} + +#[derive(Debug, Clone)] +pub struct DevServer { + pub id: DevServerId, + pub channel_id: ChannelId, + pub name: SharedString, +} + +impl From for DevServer { + fn from(dev_server: proto::DevServer) -> Self { + Self { + id: DevServerId(dev_server.dev_server_id), + channel_id: ChannelId(dev_server.channel_id), + name: dev_server.name.into(), + } + } +} + pub struct ChannelStore { pub channel_index: ChannelIndex, channel_invitations: Vec>, channel_participants: HashMap>>, channel_states: HashMap, hosted_projects: HashMap, + remote_projects: HashMap, + dev_servers: HashMap, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, @@ -87,6 +129,8 @@ pub struct ChannelState { observed_chat_message: Option, role: Option, projects: HashSet, + dev_servers: HashSet, + remote_projects: HashSet, } impl Channel { @@ -217,6 +261,8 @@ impl ChannelStore { channel_index: ChannelIndex::default(), channel_participants: Default::default(), hosted_projects: Default::default(), + remote_projects: Default::default(), + dev_servers: Default::default(), outgoing_invites: Default::default(), opened_buffers: Default::default(), opened_chats: Default::default(), @@ -316,6 +362,32 @@ impl ChannelStore { projects } + pub fn dev_servers_for_id(&self, channel_id: ChannelId) -> Vec { + let mut dev_servers: Vec = self + .channel_states + .get(&channel_id) + .map(|state| state.dev_servers.clone()) + .unwrap_or_default() + .into_iter() + .flat_map(|id| self.dev_servers.get(&id).cloned()) + .collect(); + dev_servers.sort_by_key(|s| (s.name.clone(), s.id)); + dev_servers + } + + pub fn remote_projects_for_id(&self, channel_id: ChannelId) -> Vec { + let mut remote_projects: Vec = self + .channel_states + .get(&channel_id) + .map(|state| state.remote_projects.clone()) + .unwrap_or_default() + .into_iter() + .flat_map(|id| self.remote_projects.get(&id).cloned()) + .collect(); + remote_projects.sort_by_key(|p| (p.name.clone(), p.id)); + remote_projects + } + pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool { if let Some(buffer) = self.opened_buffers.get(&channel_id) { if let OpenedModelHandle::Open(buffer) = buffer { @@ -812,6 +884,28 @@ impl ChannelStore { }) } + pub fn create_remote_project( + &mut self, + channel_id: ChannelId, + dev_server_id: DevServerId, + name: String, + path: String, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::CreateRemoteProject { + channel_id: channel_id.0, + dev_server_id: dev_server_id.0, + name, + path, + }) + .await?; + Ok(()) + }) + } + pub fn get_channel_member_details( &self, channel_id: ChannelId, @@ -1059,6 +1153,7 @@ impl ChannelStore { payload: proto::UpdateChannels, cx: &mut ModelContext, ) -> Option>> { + dbg!(&payload); if !payload.remove_channel_invitations.is_empty() { self.channel_invitations .retain(|channel| !payload.remove_channel_invitations.contains(&channel.id.0)); @@ -1092,7 +1187,11 @@ impl ChannelStore { || !payload.latest_channel_message_ids.is_empty() || !payload.latest_channel_buffer_versions.is_empty() || !payload.hosted_projects.is_empty() - || !payload.deleted_hosted_projects.is_empty(); + || !payload.deleted_hosted_projects.is_empty() + || !payload.dev_servers.is_empty() + || !payload.deleted_dev_servers.is_empty() + || !payload.remote_projects.is_empty() + || !payload.deleted_remote_projects.is_empty(); if channels_changed { if !payload.delete_channels.is_empty() { @@ -1180,6 +1279,60 @@ impl ChannelStore { .remove_hosted_project(old_project.project_id); } } + + for remote_project in payload.remote_projects { + let remote_project: RemoteProject = remote_project.into(); + if let Some(old_remote_project) = self + .remote_projects + .insert(remote_project.id, remote_project.clone()) + { + self.channel_states + .entry(old_remote_project.channel_id) + .or_default() + .remove_remote_project(old_remote_project.id); + } + self.channel_states + .entry(remote_project.channel_id) + .or_default() + .add_remote_project(remote_project.id); + } + + for remote_project_id in payload.deleted_remote_projects { + let remote_project_id = RemoteProjectId(remote_project_id); + + if let Some(old_project) = self.remote_projects.remove(&remote_project_id) { + self.channel_states + .entry(old_project.channel_id) + .or_default() + .remove_remote_project(old_project.id); + } + } + + for dev_server in payload.dev_servers { + let dev_server: DevServer = dev_server.into(); + if let Some(old_server) = self.dev_servers.insert(dev_server.id, dev_server.clone()) + { + self.channel_states + .entry(old_server.channel_id) + .or_default() + .remove_dev_server(old_server.id); + } + self.channel_states + .entry(dev_server.channel_id) + .or_default() + .add_dev_server(dev_server.id); + } + + for dev_server_id in payload.deleted_dev_servers { + let dev_server_id = DevServerId(dev_server_id); + + if let Some(old_server) = self.dev_servers.remove(&dev_server_id) { + self.channel_states + .entry(old_server.channel_id) + .or_default() + .remove_dev_server(old_server.id); + } + } } cx.notify(); @@ -1294,4 +1447,20 @@ impl ChannelState { fn remove_hosted_project(&mut self, project_id: ProjectId) { self.projects.remove(&project_id); } + + fn add_remote_project(&mut self, remote_project_id: RemoteProjectId) { + self.remote_projects.insert(remote_project_id); + } + + fn remove_remote_project(&mut self, remote_project_id: RemoteProjectId) { + self.remote_projects.remove(&remote_project_id); + } + + fn add_dev_server(&mut self, dev_server_id: DevServerId) { + self.dev_servers.insert(dev_server_id); + } + + fn remove_dev_server(&mut self, dev_server_id: DevServerId) { + self.dev_servers.remove(&dev_server_id); + } } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index e8be09dd642e..2c5632593d54 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -27,6 +27,12 @@ impl std::fmt::Display for ChannelId { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub struct ProjectId(pub u64); +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct DevServerId(pub u64); + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub struct RemoteProjectId(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 9ad045e56dd1..f03e11e3dc23 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -397,7 +397,9 @@ CREATE TABLE hosted_projects ( channel_id INTEGER NOT NULL REFERENCES channels(id), name TEXT NOT NULL, visibility TEXT NOT NULL, - deleted_at TIMESTAMP NULL + deleted_at TIMESTAMP NULL, + dev_server_id INTEGER REFERENCES dev_servers(id), + dev_server_path TEXT, ); CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id); CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL); @@ -409,3 +411,13 @@ CREATE TABLE dev_servers ( hashed_token TEXT NOT NULL ); CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id); + +CREATE TABLE remote_projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_id INTEGER NOT NULL REFERENCES channels(id), + dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id), + name TEXT NOT NULL, + path TEXT NOT NULL +); + +ALTER TABLE hosted_projects ADD COLUMN remote_project_id INTEGER REFERENCES remote_projects(id); diff --git a/crates/collab/migrations/20240402155003_add_dev_server_projects.sql b/crates/collab/migrations/20240402155003_add_dev_server_projects.sql new file mode 100644 index 000000000000..003c43f4e27f --- /dev/null +++ b/crates/collab/migrations/20240402155003_add_dev_server_projects.sql @@ -0,0 +1,9 @@ +CREATE TABLE remote_projects ( + id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + channel_id INT NOT NULL REFERENCES channels(id), + dev_server_id INT NOT NULL REFERENCES dev_servers(id), + name TEXT NOT NULL, + path TEXT NOT NULL +); + +ALTER TABLE projects ADD COLUMN remote_project_id INTEGER REFERENCES remote_projects(id); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 0527e070ea09..daeaa1fa502f 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -592,6 +592,8 @@ pub struct ChannelsForUser { pub channel_memberships: Vec, pub channel_participants: HashMap>, pub hosted_projects: Vec, + pub dev_servers: Vec, + pub remote_projects: Vec, pub observed_buffer_versions: Vec, pub observed_channel_messages: Vec, diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 91c0c440a549..14a780f48595 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -84,6 +84,7 @@ id_type!(NotificationId); id_type!(NotificationKindId); id_type!(ProjectCollaboratorId); id_type!(ProjectId); +id_type!(RemoteProjectId); id_type!(ReplicaId); id_type!(RoomId); id_type!(RoomParticipantId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index 0582b8f256e3..2cbbc6796936 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -12,6 +12,7 @@ pub mod messages; pub mod notifications; pub mod projects; pub mod rate_buckets; +pub mod remote_projects; pub mod rooms; pub mod servers; pub mod users; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 3f168e08544c..279f767df888 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -640,10 +640,15 @@ impl Database { .get_hosted_projects(&channel_ids, &roles_by_channel_id, tx) .await?; + let dev_servers = self.get_dev_servers(&channel_ids, tx).await?; + let remote_projects = self.get_remote_projects(&channel_ids, tx).await?; + Ok(ChannelsForUser { channel_memberships, channels, hosted_projects, + dev_servers, + remote_projects, channel_participants, latest_buffer_versions, latest_channel_messages, diff --git a/crates/collab/src/db/queries/dev_servers.rs b/crates/collab/src/db/queries/dev_servers.rs index d95897b51e05..7ffd8fe3e168 100644 --- a/crates/collab/src/db/queries/dev_servers.rs +++ b/crates/collab/src/db/queries/dev_servers.rs @@ -1,6 +1,7 @@ -use sea_orm::EntityTrait; +use rpc::proto; +use sea_orm::{ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter}; -use super::{dev_server, Database, DevServerId}; +use super::{dev_server, ChannelId, Database, DevServerId}; impl Database { pub async fn get_dev_server( @@ -15,4 +16,24 @@ impl Database { }) .await } + + pub async fn get_dev_servers( + &self, + channel_ids: &Vec, + tx: &DatabaseTransaction, + ) -> crate::Result> { + let servers = dev_server::Entity::find() + .filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0))) + .all(tx) + .await?; + Ok(servers + .into_iter() + .map(|s| proto::DevServer { + channel_id: s.channel_id.to_proto(), + name: s.name, + dev_server_id: s.id.to_proto(), + status: proto::DevServerStatus::Online.into(), + }) + .collect()) + } } diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 6bd7022a799c..9ae26f00d600 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -65,6 +65,7 @@ impl Database { ))), id: ActiveValue::NotSet, hosted_project_id: ActiveValue::Set(None), + remote_project_id: ActiveValue::Set(None), } .insert(&*tx) .await?; diff --git a/crates/collab/src/db/queries/remote_projects.rs b/crates/collab/src/db/queries/remote_projects.rs new file mode 100644 index 000000000000..1a2a353c9de6 --- /dev/null +++ b/crates/collab/src/db/queries/remote_projects.rs @@ -0,0 +1,73 @@ +use rpc::proto; +use sea_orm::{ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter}; + +use super::{ + channel, project, remote_project, ChannelId, Database, DevServerId, RemoteProjectId, UserId, +}; + +impl Database { + pub async fn get_remote_project( + &self, + remote_project_id: RemoteProjectId, + ) -> crate::Result { + self.transaction(|tx| async move { + Ok(remote_project::Entity::find_by_id(remote_project_id) + .one(&*tx) + .await? + .ok_or_else(|| { + anyhow::anyhow!("no remote project with id {}", remote_project_id) + })?) + }) + .await + } + + pub async fn get_remote_projects( + &self, + channel_ids: &Vec, + tx: &DatabaseTransaction, + ) -> crate::Result> { + let servers = remote_project::Entity::find() + .filter(remote_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0))) + .find_also_related(project::Entity) + .all(tx) + .await?; + Ok(servers + .into_iter() + .map(|(remote_project, project)| proto::RemoteProject { + id: remote_project.id.to_proto(), + project_id: project.map(|p| p.id.to_proto()), + channel_id: remote_project.channel_id.to_proto(), + name: remote_project.name, + dev_server_id: remote_project.dev_server_id.to_proto(), + }) + .collect()) + } + + pub async fn create_remote_project( + &self, + channel_id: ChannelId, + dev_server_id: DevServerId, + name: &str, + path: &str, + user_id: UserId, + ) -> crate::Result<(channel::Model, remote_project::Model)> { + self.transaction(|tx| async move { + let channel = self.get_channel_internal(channel_id, &*tx).await?; + self.check_user_is_channel_admin(&channel, user_id, &*tx) + .await?; + + let project = remote_project::Entity::insert(remote_project::ActiveModel { + name: ActiveValue::Set(name.to_string()), + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_id), + dev_server_id: ActiveValue::Set(dev_server_id), + path: ActiveValue::Set(path.to_string()), + }) + .exec_with_returning(&*tx) + .await?; + + Ok((channel, project)) + }) + .await + } +} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index b6793379437b..4a284682b2ac 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -24,6 +24,7 @@ pub mod observed_channel_messages; pub mod project; pub mod project_collaborator; pub mod rate_buckets; +pub mod remote_project; pub mod room; pub mod room_participant; pub mod server; diff --git a/crates/collab/src/db/tables/project.rs b/crates/collab/src/db/tables/project.rs index a357634aff61..bfb0b17c9ae6 100644 --- a/crates/collab/src/db/tables/project.rs +++ b/crates/collab/src/db/tables/project.rs @@ -1,4 +1,4 @@ -use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId}; +use crate::db::{HostedProjectId, ProjectId, RemoteProjectId, Result, RoomId, ServerId, UserId}; use anyhow::anyhow; use rpc::ConnectionId; use sea_orm::entity::prelude::*; @@ -13,6 +13,7 @@ pub struct Model { pub host_connection_id: Option, pub host_connection_server_id: Option, pub hosted_project_id: Option, + pub remote_project_id: Option, } impl Model { @@ -56,6 +57,12 @@ pub enum Relation { to = "super::hosted_project::Column::Id" )] HostedProject, + #[sea_orm( + belongs_to = "super::remote_project::Entity", + from = "Column::RemoteProjectId", + to = "super::remote_project::Column::Id" + )] + RemoteProject, } impl Related for Entity { @@ -94,4 +101,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::RemoteProject.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/remote_project.rs b/crates/collab/src/db/tables/remote_project.rs new file mode 100644 index 000000000000..2d73d5ef9012 --- /dev/null +++ b/crates/collab/src/db/tables/remote_project.rs @@ -0,0 +1,41 @@ +use super::project; +use crate::db::{ChannelId, DevServerId, RemoteProjectId}; +use rpc::proto; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "remote_projects")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: RemoteProjectId, + pub channel_id: ChannelId, + pub dev_server_id: DevServerId, + pub name: String, + pub path: String, +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_one = "super::project::Entity")] + Project, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl Model { + pub fn to_proto(&self, project: Option) -> proto::RemoteProject { + proto::RemoteProject { + id: self.id.to_proto(), + project_id: project.map(|p| p.id.to_proto()), + channel_id: self.channel_id.to_proto(), + dev_server_id: self.dev_server_id.to_proto(), + name: self.name.clone(), + } + } +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7251f095cd30..9f26a5526947 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,11 +1,11 @@ mod connection_pool; use crate::{ - auth::{self}, + auth, db::{ self, dev_server, BufferId, Channel, ChannelId, ChannelRole, ChannelsForUser, - CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, - NotificationId, Project, ProjectId, RemoveChannelMemberResult, ReplicaId, + CreatedChannelMessage, Database, DevServerId, InviteMemberResult, MembershipUpdated, + MessageId, NotificationId, Project, ProjectId, RemoveChannelMemberResult, ReplicaId, RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId, }, executor::Executor, @@ -328,6 +328,7 @@ impl Server { .add_message_handler(unshare_project) .add_request_handler(user_handler(join_project)) .add_request_handler(user_handler(join_hosted_project)) + .add_request_handler(user_handler(create_remote_project)) .add_message_handler(user_message_handler(leave_project)) .add_request_handler(update_project) .add_request_handler(update_worktree) @@ -1979,6 +1980,38 @@ async fn join_hosted_project( join_project_internal(response, session, &mut project, &replica_id) } +async fn create_remote_project( + request: proto::CreateRemoteProject, + response: Response, + session: UserSession, +) -> Result<()> { + let (channel, remote_project) = session + .db() + .await + .create_remote_project( + ChannelId(request.channel_id as i32), + DevServerId(request.dev_server_id as i32), + &request.name, + &request.path, + session.user_id(), + ) + .await?; + + let update = proto::UpdateChannels { + remote_projects: vec![remote_project.to_proto(None)], + ..Default::default() + }; + let connection_pool = session.connection_pool().await; + for (connection_id, role) in connection_pool.channel_connection_ids(channel.root_id()) { + if role.can_see_channel(channel.visibility) { + session.peer.send(connection_id, update.clone())?; + } + } + + response.send(proto::Ack {})?; + Ok(()) +} + /// Updates other participants with changes to the project async fn update_project( request: proto::UpdateProject, @@ -4063,9 +4096,10 @@ fn build_channels_update( for channel in channel_invites { update.channel_invitations.push(channel.to_proto()); } - for project in channels.hosted_projects { - update.hosted_projects.push(project); - } + + update.hosted_projects = channels.hosted_projects; + update.dev_servers = channels.dev_servers; + update.remote_projects = channels.remote_projects; update } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 20943922e850..d21c938d5574 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1,13 +1,15 @@ mod channel_modal; mod contact_finder; +mod dev_server_modal; use self::channel_modal::ChannelModal; +use self::dev_server_modal::DevServerModal; use crate::{ channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile, CollaborationPanelSettings, }; use call::ActiveCall; -use channel::{Channel, ChannelEvent, ChannelStore}; +use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject}; use client::{ChannelId, Client, Contact, ProjectId, User, UserStore}; use contact_finder::ContactFinder; use db::kvp::KEY_VALUE_STORE; @@ -188,6 +190,7 @@ enum ListEntry { id: ProjectId, name: SharedString, }, + RemoteProject(channel::RemoteProject), Contact { contact: Arc, calling: bool, @@ -569,6 +572,7 @@ impl CollabPanel { } let hosted_projects = channel_store.projects_for_id(channel.id); + let remote_projects = channel_store.remote_projects_for_id(channel.id); let has_children = channel_store .channel_at_index(mat.candidate_id + 1) .map_or(false, |next_channel| { @@ -604,7 +608,12 @@ impl CollabPanel { } for (name, id) in hosted_projects { - self.entries.push(ListEntry::HostedProject { id, name }) + self.entries.push(ListEntry::HostedProject { id, name }); + } + + for remote_project in remote_projects { + dbg!("Pushing remote project entry", remote_project.id); + self.entries.push(ListEntry::RemoteProject(remote_project)); } } } @@ -1065,6 +1074,39 @@ impl CollabPanel { .tooltip(move |cx| Tooltip::text("Open Project", cx)) } + fn render_remote_project( + &self, + remote_project: &RemoteProject, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let id = remote_project.id; + let name = remote_project.name.clone(); + let maybe_project_id = remote_project.project_id.clone(); + //TODO grey out if project is not hosted yet + + ListItem::new(ElementId::NamedInteger( + "remote-project".into(), + id.0 as usize, + )) + .indent_level(2) + .indent_step_size(px(20.)) + .selected(is_selected) + .on_click(cx.listener(move |this, _, cx| { + if let Some(project_id) = maybe_project_id { + this.join_remote_project(project_id, cx); + } + })) + .start_slot( + h_flex() + .relative() + .gap_1() + .child(IconButton::new(0, IconName::FileTree)), + ) + .child(Label::new(name.clone())) + .tooltip(move |cx| Tooltip::text("Open Remote Project", cx)) + } + fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).map_or(false, |entry| { if let ListEntry::Channel { has_children, .. } = entry { @@ -1266,11 +1308,22 @@ impl CollabPanel { } if self.channel_store.read(cx).is_root_channel(channel_id) { - context_menu = context_menu.separator().entry( - "Manage Members", - None, - cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)), - ) + context_menu = context_menu + .separator() + .entry( + "Manage Members", + None, + cx.handler_for(&this, move |this, cx| { + this.manage_members(channel_id, cx) + }), + ) + .entry( + "Manage Dev Servers", + None, + cx.handler_for(&this, move |this, cx| { + this.manage_dev_servers(channel_id, cx) + }), + ) } else { context_menu = context_menu.entry( "Move this channel", @@ -1534,6 +1587,11 @@ impl CollabPanel { } => { // todo() } + ListEntry::RemoteProject(project) => { + if let Some(project_id) = project.project_id { + self.join_remote_project(project_id, cx) + } + } ListEntry::OutgoingRequest(_) => {} ListEntry::ChannelEditor { .. } => {} @@ -1706,6 +1764,18 @@ impl CollabPanel { self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx); } + fn manage_dev_servers(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + let channel_store = self.channel_store.clone(); + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| { + DevServerModal::new(channel_store.clone(), channel_id, cx) + }); + }); + } + fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext) { if let Some(channel) = self.selected_channel() { self.remove_channel(channel.id, cx) @@ -2006,6 +2076,18 @@ impl CollabPanel { .detach_and_prompt_err("Failed to join channel", cx, |_, _| None) } + fn join_remote_project(&mut self, project_id: ProjectId, cx: &mut ViewContext) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_hosted_project(project_id, app_state, cx).detach_and_prompt_err( + "Failed to join project", + cx, + |_, _| None, + ) + } + fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { let Some(workspace) = self.workspace.upgrade() else { return; @@ -2141,6 +2223,9 @@ impl CollabPanel { ListEntry::HostedProject { id, name } => self .render_channel_project(*id, name, is_selected, cx) .into_any_element(), + ListEntry::RemoteProject(remote_project) => self + .render_remote_project(remote_project, is_selected, cx) + .into_any_element(), } } @@ -2883,6 +2968,11 @@ impl PartialEq for ListEntry { return id == other_id; } } + ListEntry::RemoteProject(project) => { + if let ListEntry::RemoteProject(other) = other { + return project.id == other.id; + } + } ListEntry::ChannelNotes { channel_id } => { if let ListEntry::ChannelNotes { channel_id: other_id, diff --git a/crates/collab_ui/src/collab_panel/dev_server_modal.rs b/crates/collab_ui/src/collab_panel/dev_server_modal.rs new file mode 100644 index 000000000000..8fc6bdcc6d7a --- /dev/null +++ b/crates/collab_ui/src/collab_panel/dev_server_modal.rs @@ -0,0 +1,97 @@ +use channel::ChannelStore; +use client::{ChannelId, DevServerId}; +use editor::Editor; +use gpui::{ + AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, View, ViewContext, +}; +use ui::prelude::*; +use workspace::{notifications::DetachAndPromptErr, ModalView}; + +pub struct DevServerModal { + focus_handle: FocusHandle, + channel_store: Model, + channel_id: ChannelId, + name_editor: View, + path_editor: View, +} + +impl DevServerModal { + pub fn new( + channel_store: Model, + channel_id: ChannelId, + cx: &mut ViewContext, + ) -> Self { + let name_editor = cx.new_view(|cx| Editor::single_line(cx)); + let path_editor = cx.new_view(|cx| Editor::single_line(cx)); + + Self { + focus_handle: cx.focus_handle(), + channel_store, + channel_id, + name_editor, + path_editor, + } + } + + pub fn on_create(&self, cx: &mut ViewContext) { + let channel_id = self.channel_id; + let name = self.name_editor.read(cx).text(cx).trim().to_string(); + let path = self.path_editor.read(cx).text(cx).trim().to_string(); + + if name == "" { + return; + } + if path == "" { + return; + } + + let task = self.channel_store.update(cx, |store, cx| { + store.create_remote_project(channel_id, DevServerId(1), name, path, cx) + }); + + task.detach_and_prompt_err("Failed to create remote project", cx, |_, _| None); + } +} +impl ModalView for DevServerModal {} + +impl FocusableView for DevServerModal { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for DevServerModal {} + +impl Render for DevServerModal { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let channel_store = self.channel_store.read(cx); + let dev_servers = channel_store.dev_servers_for_id(self.channel_id); + div() + .track_focus(&self.focus_handle) + .elevation_2(cx) + .key_context("DevServerModal") + // .on_action(cx.listener(Self::cancel)) + // .on_action(cx.listener(Self::confirm)) + .w_96() + .child( + v_flex() + .px_1() + .pt_0p5() + .gap_px() + .child( + v_flex() + .py_0p5() + .px_1() + .child(div().px_1().py_0p5().child("Add Remote Project:")), + ) + .child(h_flex().child("Name:").child(self.name_editor.clone())) + .child("Dev Server:") + .children(dev_servers.iter().map(|dev_server| dev_server.name.clone())) + .child(h_flex().child("Path:").child(self.path_editor.clone())) + .child( + Button::new("create-button", "Create") + .on_click(cx.listener(|this, _event, cx| this.on_create(cx))), + ), + ) + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index a610e2b85032..3bc9d2c1b164 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -208,7 +208,9 @@ message Envelope { ChannelMessageUpdate channel_message_update = 171; BlameBuffer blame_buffer = 172; - BlameBufferResponse blame_buffer_response = 173; // Current max + BlameBufferResponse blame_buffer_response = 173; + + CreateRemoteProject create_remote_project = 174; // Current max } reserved 158 to 161; @@ -424,6 +426,13 @@ message JoinHostedProject { uint64 project_id = 1; } +message CreateRemoteProject { + uint64 channel_id = 1; + string name = 2; + uint64 dev_server_id = 3; + string path = 4; +} + message JoinProjectResponse { uint64 project_id = 5; uint32 replica_id = 1; @@ -1052,6 +1061,12 @@ message UpdateChannels { repeated HostedProject hosted_projects = 10; repeated uint64 deleted_hosted_projects = 11; + + repeated DevServer dev_servers = 12; + repeated uint64 deleted_dev_servers = 13; + + repeated RemoteProject remote_projects = 14; + repeated uint64 deleted_remote_projects = 15; } message UpdateUserChannels { @@ -1087,6 +1102,26 @@ message HostedProject { ChannelVisibility visibility = 4; } +message RemoteProject { + uint64 id = 1; + optional uint64 project_id = 2; + uint64 channel_id = 3; + string name = 4; + uint64 dev_server_id = 5; +} + +message DevServer { + uint64 channel_id = 1; + uint64 dev_server_id = 2; + string name = 3; + DevServerStatus status = 4; +} + +enum DevServerStatus { + Offline = 0; + Online = 1; +} + message JoinChannel { uint64 channel_id = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index bc2b44046f35..a1025fd1c49a 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -298,6 +298,7 @@ messages!( (SetRoomParticipantRole, Foreground), (BlameBuffer, Foreground), (BlameBufferResponse, Foreground), + (CreateRemoteProject, Foreground), ); request_messages!( @@ -389,6 +390,7 @@ request_messages!( (LspExtExpandMacro, LspExtExpandMacroResponse), (SetRoomParticipantRole, Ack), (BlameBuffer, BlameBufferResponse), + (CreateRemoteProject, Ack), ); entity_messages!(