Slicker remote project creation (#11309)

Conrad Irwin and Bennet created

Inline the editor into the modal

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennetbo@gmx.de>

Change summary

crates/collab/migrations/20240502180204_remove_old_remote_projects.sql |   2 
crates/collab/src/db/queries/dev_server_projects.rs                    |  75 
crates/collab/src/db/queries/projects.rs                               |  11 
crates/collab/src/rpc.rs                                               |  63 
crates/collab/src/tests/dev_server_tests.rs                            |  52 
crates/dev_server_projects/src/dev_server_projects.rs                  |  16 
crates/recent_projects/Cargo.toml                                      |   1 
crates/recent_projects/src/dev_servers.rs                              | 450 
crates/rpc/proto/zed.proto                                             |   7 
crates/rpc/src/proto.rs                                                |   2 
crates/workspace/src/persistence.rs                                    |  16 
11 files changed, 503 insertions(+), 192 deletions(-)

Detailed changes

crates/collab/src/db/queries/dev_server_projects.rs 🔗

@@ -1,5 +1,8 @@
 use anyhow::anyhow;
-use rpc::{proto, ConnectionId};
+use rpc::{
+    proto::{self},
+    ConnectionId,
+};
 use sea_orm::{
     ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
     ModelTrait, QueryFilter,
@@ -35,24 +38,33 @@ impl Database {
         dev_server_id: DevServerId,
     ) -> crate::Result<Vec<proto::DevServerProject>> {
         self.transaction(|tx| async move {
-            let servers = dev_server_project::Entity::find()
-                .filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
-                .find_also_related(project::Entity)
-                .all(&*tx)
-                .await?;
-            Ok(servers
-                .into_iter()
-                .map(|(dev_server_project, project)| proto::DevServerProject {
-                    id: dev_server_project.id.to_proto(),
-                    project_id: project.map(|p| p.id.to_proto()),
-                    dev_server_id: dev_server_project.dev_server_id.to_proto(),
-                    path: dev_server_project.path,
-                })
-                .collect())
+            self.get_projects_for_dev_server_internal(dev_server_id, &tx)
+                .await
         })
         .await
     }
 
+    pub async fn get_projects_for_dev_server_internal(
+        &self,
+        dev_server_id: DevServerId,
+        tx: &DatabaseTransaction,
+    ) -> crate::Result<Vec<proto::DevServerProject>> {
+        let servers = dev_server_project::Entity::find()
+            .filter(dev_server_project::Column::DevServerId.eq(dev_server_id))
+            .find_also_related(project::Entity)
+            .all(tx)
+            .await?;
+        Ok(servers
+            .into_iter()
+            .map(|(dev_server_project, project)| proto::DevServerProject {
+                id: dev_server_project.id.to_proto(),
+                project_id: project.map(|p| p.id.to_proto()),
+                dev_server_id: dev_server_project.dev_server_id.to_proto(),
+                path: dev_server_project.path,
+            })
+            .collect())
+    }
+
     pub async fn dev_server_project_ids_for_user(
         &self,
         user_id: UserId,
@@ -136,6 +148,39 @@ impl Database {
         .await
     }
 
+    pub async fn delete_dev_server_project(
+        &self,
+        dev_server_project_id: DevServerProjectId,
+        dev_server_id: DevServerId,
+        user_id: UserId,
+    ) -> crate::Result<(Vec<proto::DevServerProject>, proto::DevServerProjectsUpdate)> {
+        self.transaction(|tx| async move {
+            project::Entity::delete_many()
+                .filter(project::Column::DevServerProjectId.eq(dev_server_project_id))
+                .exec(&*tx)
+                .await?;
+            let result = dev_server_project::Entity::delete_by_id(dev_server_project_id)
+                .exec(&*tx)
+                .await?;
+            if result.rows_affected != 1 {
+                return Err(anyhow!(
+                    "no dev server project with id {}",
+                    dev_server_project_id
+                ))?;
+            }
+
+            let status = self
+                .dev_server_projects_update_internal(user_id, &tx)
+                .await?;
+
+            let projects = self
+                .get_projects_for_dev_server_internal(dev_server_id, &tx)
+                .await?;
+            Ok((projects, status))
+        })
+        .await
+    }
+
     pub async fn share_dev_server_project(
         &self,
         dev_server_project_id: DevServerProjectId,

crates/collab/src/db/queries/projects.rs 🔗

@@ -598,6 +598,17 @@ impl Database {
         .await
     }
 
+    pub async fn find_dev_server_project(&self, id: DevServerProjectId) -> Result<project::Model> {
+        self.transaction(|tx| async move {
+            Ok(project::Entity::find()
+                .filter(project::Column::DevServerProjectId.eq(id))
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?)
+        })
+        .await
+    }
+
     /// Adds the given connection to the specified project
     /// in the current room.
     pub async fn join_project(

crates/collab/src/rpc.rs 🔗

@@ -413,6 +413,7 @@ impl Server {
             .add_request_handler(user_handler(join_hosted_project))
             .add_request_handler(user_handler(rejoin_dev_server_projects))
             .add_request_handler(user_handler(create_dev_server_project))
+            .add_request_handler(user_handler(delete_dev_server_project))
             .add_request_handler(user_handler(create_dev_server))
             .add_request_handler(user_handler(delete_dev_server))
             .add_request_handler(dev_server_handler(share_dev_server_project))
@@ -2363,6 +2364,68 @@ async fn delete_dev_server(
     Ok(())
 }
 
+async fn delete_dev_server_project(
+    request: proto::DeleteDevServerProject,
+    response: Response<proto::DeleteDevServerProject>,
+    session: UserSession,
+) -> Result<()> {
+    let dev_server_project_id = DevServerProjectId(request.dev_server_project_id as i32);
+    let dev_server_project = session
+        .db()
+        .await
+        .get_dev_server_project(dev_server_project_id)
+        .await?;
+
+    let dev_server = session
+        .db()
+        .await
+        .get_dev_server(dev_server_project.dev_server_id)
+        .await?;
+    if dev_server.user_id != session.user_id() {
+        return Err(anyhow!(ErrorCode::Forbidden))?;
+    }
+
+    let dev_server_connection_id = session
+        .connection_pool()
+        .await
+        .dev_server_connection_id(dev_server.id);
+
+    if let Some(dev_server_connection_id) = dev_server_connection_id {
+        let project = session
+            .db()
+            .await
+            .find_dev_server_project(dev_server_project_id)
+            .await;
+        if let Ok(project) = project {
+            unshare_project_internal(
+                project.id,
+                dev_server_connection_id,
+                Some(session.user_id()),
+                &session,
+            )
+            .await?;
+        }
+    }
+
+    let (projects, status) = session
+        .db()
+        .await
+        .delete_dev_server_project(dev_server_project_id, dev_server.id, session.user_id())
+        .await?;
+
+    if let Some(dev_server_connection_id) = dev_server_connection_id {
+        session.peer.send(
+            dev_server_connection_id,
+            proto::DevServerInstructions { projects },
+        )?;
+    }
+
+    send_dev_server_projects_update(session.user_id(), status, &session).await;
+
+    response.send(proto::Ack {})?;
+    Ok(())
+}
+
 async fn rejoin_dev_server_projects(
     request: proto::RejoinRemoteProjects,
     response: Response<proto::RejoinRemoteProjects>,

crates/collab/src/tests/dev_server_tests.rs 🔗

@@ -263,6 +263,58 @@ async fn test_dev_server_leave_room(
     cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
 }
 
+#[gpui::test]
+async fn test_dev_server_delete(
+    cx1: &mut gpui::TestAppContext,
+    cx2: &mut gpui::TestAppContext,
+    cx3: &mut gpui::TestAppContext,
+) {
+    let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await;
+
+    let (_dev_server, remote_workspace) =
+        create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await;
+
+    cx1.update(|cx| {
+        workspace::join_channel(
+            channel_id,
+            client1.app_state.clone(),
+            Some(remote_workspace),
+            cx,
+        )
+    })
+    .await
+    .unwrap();
+    cx1.executor().run_until_parked();
+
+    remote_workspace
+        .update(cx1, |ws, cx| {
+            assert!(ws.project().read(cx).is_shared());
+        })
+        .unwrap();
+
+    join_channel(channel_id, &client2, cx2).await.unwrap();
+    cx2.executor().run_until_parked();
+
+    cx1.update(|cx| {
+        dev_server_projects::Store::global(cx).update(cx, |store, cx| {
+            store.delete_dev_server_project(store.dev_server_projects().first().unwrap().id, cx)
+        })
+    })
+    .await
+    .unwrap();
+
+    cx1.executor().run_until_parked();
+
+    let (workspace, cx2) = client2.active_workspace(cx2);
+    cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
+
+    cx1.update(|cx| {
+        dev_server_projects::Store::global(cx).update(cx, |store, _| {
+            assert_eq!(store.dev_server_projects().len(), 0);
+        })
+    })
+}
+
 #[gpui::test]
 async fn test_dev_server_reconnect(
     cx1: &mut gpui::TestAppContext,

crates/dev_server_projects/src/dev_server_projects.rs 🔗

@@ -188,4 +188,20 @@ impl Store {
             Ok(())
         })
     }
+
+    pub fn delete_dev_server_project(
+        &mut self,
+        id: DevServerProjectId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.clone();
+        cx.background_executor().spawn(async move {
+            client
+                .request(proto::DeleteDevServerProject {
+                    dev_server_project_id: id.0,
+                })
+                .await?;
+            Ok(())
+        })
+    }
 }

crates/recent_projects/Cargo.toml 🔗

@@ -14,6 +14,7 @@ doctest = false
 
 [dependencies]
 anyhow.workspace = true
+editor.workspace = true
 feature_flags.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true

crates/recent_projects/src/dev_servers.rs 🔗

@@ -1,14 +1,15 @@
 use std::time::Duration;
 
 use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
+use editor::Editor;
 use feature_flags::FeatureFlagViewExt;
 use gpui::{
-    percentage, Action, Animation, AnimationExt, AppContext, ClipboardItem, DismissEvent,
-    EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View,
-    ViewContext,
+    percentage, Action, Animation, AnimationExt, AnyElement, AppContext, ClipboardItem,
+    DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation,
+    View, ViewContext,
 };
 use rpc::{
-    proto::{self, CreateDevServerResponse, DevServerStatus},
+    proto::{CreateDevServerResponse, DevServerStatus},
     ErrorCode, ErrorExt,
 };
 use settings::Settings;
@@ -16,7 +17,7 @@ use theme::ThemeSettings;
 use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip};
 use ui_text_field::{FieldLabelLayout, TextField};
 use util::ResultExt;
-use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace};
+use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
 
 use crate::OpenRemote;
 
@@ -25,7 +26,7 @@ pub struct DevServerProjects {
     focus_handle: FocusHandle,
     scroll_handle: ScrollHandle,
     dev_server_store: Model<dev_server_projects::Store>,
-    project_path_input: View<TextField>,
+    project_path_input: View<Editor>,
     dev_server_name_input: View<TextField>,
     _subscription: gpui::Subscription,
 }
@@ -36,15 +37,14 @@ struct CreateDevServer {
     dev_server: Option<CreateDevServerResponse>,
 }
 
+#[derive(Clone)]
 struct CreateDevServerProject {
     dev_server_id: DevServerId,
     creating: bool,
-    dev_server_project: Option<proto::DevServerProject>,
 }
 
 enum Mode {
-    Default,
-    CreateDevServerProject(CreateDevServerProject),
+    Default(Option<CreateDevServerProject>),
     CreateDevServer(CreateDevServer),
 }
 
@@ -67,7 +67,11 @@ impl DevServerProjects {
     }
 
     pub fn new(cx: &mut ViewContext<Self>) -> Self {
-        let project_path_input = cx.new_view(|cx| TextField::new(cx, "", "Project path"));
+        let project_path_input = cx.new_view(|cx| {
+            let mut editor = Editor::single_line(cx);
+            editor.set_placeholder_text("Project path", cx);
+            editor
+        });
         let dev_server_name_input =
             cx.new_view(|cx| TextField::new(cx, "Name", "").with_label(FieldLabelLayout::Stacked));
 
@@ -79,7 +83,7 @@ impl DevServerProjects {
         });
 
         Self {
-            mode: Mode::Default,
+            mode: Mode::Default(None),
             focus_handle,
             scroll_handle: ScrollHandle::new(),
             dev_server_store,
@@ -94,14 +98,7 @@ impl DevServerProjects {
         dev_server_id: DevServerId,
         cx: &mut ViewContext<Self>,
     ) {
-        let path = self
-            .project_path_input
-            .read(cx)
-            .editor()
-            .read(cx)
-            .text(cx)
-            .trim()
-            .to_string();
+        let path = self.project_path_input.read(cx).text(cx).trim().to_string();
 
         if path == "" {
             return;
@@ -139,16 +136,18 @@ impl DevServerProjects {
 
         cx.spawn(|this, mut cx| async move {
             let result = create.await;
-            let dev_server_project = result
-                .as_ref()
-                .ok()
-                .and_then(|r| r.dev_server_project.clone());
-            this.update(&mut cx, |this, _| {
-                this.mode = Mode::CreateDevServerProject(CreateDevServerProject {
-                    dev_server_id,
-                    creating: false,
-                    dev_server_project,
-                });
+            this.update(&mut cx, |this, cx| {
+                if result.is_ok() {
+                    this.project_path_input.update(cx, |editor, cx| {
+                        editor.set_text("", cx);
+                    });
+                    this.mode = Mode::Default(None);
+                } else {
+                    this.mode = Mode::Default(Some(CreateDevServerProject {
+                        dev_server_id,
+                        creating: false,
+                    }));
+                }
             })
             .log_err();
             result
@@ -166,17 +165,10 @@ impl DevServerProjects {
             }
         });
 
-        self.project_path_input.update(cx, |input, cx| {
-            input.editor().update(cx, |editor, cx| {
-                editor.set_text("", cx);
-            });
-        });
-
-        self.mode = Mode::CreateDevServerProject(CreateDevServerProject {
+        self.mode = Mode::Default(Some(CreateDevServerProject {
             dev_server_id,
             creating: true,
-            dev_server_project: None,
-        });
+        }));
     }
 
     pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
@@ -238,20 +230,74 @@ impl DevServerProjects {
                 return Ok(());
             }
 
+            let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
+                this.dev_server_store.update(cx, |store, _| {
+                    store
+                        .projects_for_server(id)
+                        .into_iter()
+                        .map(|project| project.id)
+                        .collect()
+                })
+            })?;
+
             this.update(&mut cx, |this, cx| {
                 this.dev_server_store
                     .update(cx, |store, cx| store.delete_dev_server(id, cx))
             })?
-            .await
+            .await?;
+
+            for id in project_ids {
+                WORKSPACE_DB
+                    .delete_workspace_by_dev_server_project_id(id)
+                    .await
+                    .log_err();
+            }
+            Ok(())
         })
         .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
     }
 
+    fn delete_dev_server_project(
+        &mut self,
+        id: DevServerProjectId,
+        path: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let answer = cx.prompt(
+            gpui::PromptLevel::Destructive,
+            format!("Delete \"{}\"?", path).as_str(),
+            Some("This will delete the remote project. You can always re-add it later."),
+            &["Delete", "Cancel"],
+        );
+
+        cx.spawn(|this, mut cx| async move {
+            let answer = answer.await?;
+
+            if answer != 0 {
+                return Ok(());
+            }
+
+            this.update(&mut cx, |this, cx| {
+                this.dev_server_store
+                    .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
+            })?
+            .await?;
+
+            WORKSPACE_DB
+                .delete_workspace_by_dev_server_project_id(id)
+                .await
+                .log_err();
+
+            Ok(())
+        })
+        .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
+    }
+
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        match self.mode {
-            Mode::Default => {}
-            Mode::CreateDevServerProject(CreateDevServerProject { dev_server_id, .. }) => {
-                self.create_dev_server_project(dev_server_id, cx);
+        match &self.mode {
+            Mode::Default(None) => {}
+            Mode::Default(Some(create_project)) => {
+                self.create_dev_server_project(create_project.dev_server_id, cx);
             }
             Mode::CreateDevServer(_) => {
                 self.create_dev_server(cx);
@@ -261,9 +307,9 @@ impl DevServerProjects {
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
         match self.mode {
-            Mode::Default => cx.emit(DismissEvent),
-            Mode::CreateDevServerProject(_) | Mode::CreateDevServer(_) => {
-                self.mode = Mode::Default;
+            Mode::Default(None) => cx.emit(DismissEvent),
+            _ => {
+                self.mode = Mode::Default(None);
                 self.focus_handle(cx).focus(cx);
                 cx.notify();
             }
@@ -273,10 +319,17 @@ impl DevServerProjects {
     fn render_dev_server(
         &mut self,
         dev_server: &DevServer,
+        mut create_project: Option<CreateDevServerProject>,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
         let dev_server_id = dev_server.id;
         let status = dev_server.status;
+        if create_project
+            .as_ref()
+            .is_some_and(|cp| cp.dev_server_id != dev_server.id)
+        {
+            create_project = None;
+        }
 
         v_flex()
             .w_full()
@@ -341,12 +394,12 @@ impl DevServerProjects {
                             .tooltip(|cx| Tooltip::text("Add a remote project", cx))
                             .on_click(cx.listener(
                                 move |this, _, cx| {
-                                    this.mode =
-                                        Mode::CreateDevServerProject(CreateDevServerProject {
+                                    if let Mode::Default(project) = &mut this.mode {
+                                        *project = Some(CreateDevServerProject {
                                             dev_server_id,
                                             creating: false,
-                                            dev_server_project: None,
                                         });
+                                    }
                                     this.project_path_input.read(cx).focus_handle(cx).focus(cx);
                                     cx.notify();
                                 },
@@ -365,17 +418,49 @@ impl DevServerProjects {
                     .py_0p5()
                     .px_3()
                     .child(
-                        List::new().empty_message("No projects.").children(
-                            self.dev_server_store
-                                .read(cx)
-                                .projects_for_server(dev_server.id)
-                                .iter()
-                                .map(|p| self.render_dev_server_project(p, cx)),
-                        ),
+                        List::new()
+                            .empty_message("No projects.")
+                            .children(
+                                self.dev_server_store
+                                    .read(cx)
+                                    .projects_for_server(dev_server.id)
+                                    .iter()
+                                    .map(|p| self.render_dev_server_project(p, cx)),
+                            )
+                            .when_some(create_project, |el, create_project| {
+                                el.child(self.render_create_new_project(&create_project, cx))
+                            }),
                     ),
             )
     }
 
+    fn render_create_new_project(
+        &mut self,
+        create_project: &CreateDevServerProject,
+        _: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        ListItem::new("create-remote-project")
+            .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
+            .child(self.project_path_input.clone())
+            .child(
+                div()
+                    .w(IconSize::Medium.rems())
+                    .when(create_project.creating, |el| {
+                        el.child(
+                            Icon::new(IconName::ArrowCircle)
+                                .size(IconSize::Medium)
+                                .with_animation(
+                                    "arrow-circle",
+                                    Animation::new(Duration::from_secs(2)).repeat(),
+                                    |icon, delta| {
+                                        icon.transform(Transformation::rotate(percentage(delta)))
+                                    },
+                                ),
+                        )
+                    }),
+            )
+    }
+
     fn render_dev_server_project(
         &mut self,
         project: &DevServerProject,
@@ -384,6 +469,7 @@ impl DevServerProjects {
         let dev_server_project_id = project.id;
         let project_id = project.project_id;
         let is_online = project_id.is_some();
+        let project_path = project.path.clone();
 
         ListItem::new(("remote-project", dev_server_project_id.0))
             .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
@@ -402,6 +488,11 @@ impl DevServerProjects {
                     }).detach();
                 }
             }))
+            .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
+                .on_click(cx.listener(move |this, _, cx| {
+                    this.delete_dev_server_project(dev_server_project_id, &project_path, cx)
+                }))
+                .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
     }
 
     fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
@@ -559,6 +650,11 @@ impl DevServerProjects {
     fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let dev_servers = self.dev_server_store.read(cx).dev_servers();
 
+        let Mode::Default(create_dev_server_project) = &self.mode else {
+            unreachable!()
+        };
+        let create_dev_server_project = create_dev_server_project.clone();
+
         v_flex()
             .id("scroll-container")
             .h_full()
@@ -597,126 +693,131 @@ impl DevServerProjects {
                             ),
                         ))
                         .children(dev_servers.iter().map(|dev_server| {
-                            self.render_dev_server(dev_server, cx).into_any_element()
+                            self.render_dev_server(
+                                dev_server,
+                                create_dev_server_project.clone(),
+                                cx,
+                            )
+                            .into_any_element()
                         })),
                 ),
             )
     }
 
-    fn render_create_dev_server_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let Mode::CreateDevServerProject(CreateDevServerProject {
-            dev_server_id,
-            creating,
-            dev_server_project,
-        }) = &self.mode
-        else {
-            unreachable!()
-        };
-
-        let dev_server = self
-            .dev_server_store
-            .read(cx)
-            .dev_server(*dev_server_id)
-            .cloned();
-
-        let (dev_server_name, dev_server_status) = dev_server
-            .map(|server| (server.name, server.status))
-            .unwrap_or((SharedString::from(""), DevServerStatus::Offline));
-
-        v_flex()
-            .px_1()
-            .pt_0p5()
-            .gap_px()
-            .child(
-                v_flex().py_0p5().px_1().child(
-                    h_flex()
-                        .px_1()
-                        .py_0p5()
-                        .child(
-                            IconButton::new("back", IconName::ArrowLeft)
-                                .style(ButtonStyle::Transparent)
-                                .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| {
-                                    cx.dispatch_action(menu::Cancel.boxed_clone())
-                                })),
-                        )
-                        .child(Headline::new("Add remote project").size(HeadlineSize::Small)),
-                ),
-            )
-            .child(
-                h_flex()
-                    .ml_5()
-                    .gap_2()
-                    .child(
-                        div()
-                            .id(("status", dev_server_id.0))
-                            .relative()
-                            .child(Icon::new(IconName::Server))
-                            .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
-                                Indicator::dot().color(match dev_server_status {
-                                    DevServerStatus::Online => Color::Created,
-                                    DevServerStatus::Offline => Color::Hidden,
-                                }),
-                            ))
-                            .tooltip(move |cx| {
-                                Tooltip::text(
-                                    match dev_server_status {
-                                        DevServerStatus::Online => "Online",
-                                        DevServerStatus::Offline => "Offline",
-                                    },
-                                    cx,
-                                )
-                            }),
-                    )
-                    .child(dev_server_name.clone()),
-            )
-            .child(
-                h_flex()
-                    .ml_5()
-                    .gap_2()
-                    .child(self.project_path_input.clone())
-                    .when(!*creating && dev_server_project.is_none(), |div| {
-                        div.child(Button::new("create-remote-server", "Create").on_click({
-                            let dev_server_id = *dev_server_id;
-                            cx.listener(move |this, _, cx| {
-                                this.create_dev_server_project(dev_server_id, cx)
-                            })
-                        }))
-                    })
-                    .when(*creating, |div| {
-                        div.child(Button::new("create-dev-server", "Creating...").disabled(true))
-                    }),
-            )
-            .when_some(dev_server_project.clone(), |div, dev_server_project| {
-                let status = self
-                    .dev_server_store
-                    .read(cx)
-                    .dev_server_project(DevServerProjectId(dev_server_project.id))
-                    .map(|project| {
-                        if project.project_id.is_some() {
-                            DevServerStatus::Online
-                        } else {
-                            DevServerStatus::Offline
-                        }
-                    })
-                    .unwrap_or(DevServerStatus::Offline);
-                div.child(
-                    v_flex()
-                        .ml_5()
-                        .ml_8()
-                        .gap_2()
-                        .when(status == DevServerStatus::Offline, |this| {
-                            this.child(Label::new("Waiting for project..."))
-                        })
-                        .when(status == DevServerStatus::Online, |this| {
-                            this.child(Label::new("Project online! 🎊")).child(
-                                Button::new("done", "Done").on_click(cx.listener(|_, _, cx| {
-                                    cx.dispatch_action(menu::Cancel.boxed_clone())
-                                })),
-                            )
-                        }),
-                )
-            })
-    }
+    // fn render_create_dev_server_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+    //     let Mode::CreateDevServerProject(CreateDevServerProject {
+    //         dev_server_id,
+    //         creating,
+    //         dev_server_project,
+    //     }) = &self.mode
+    //     else {
+    //         unreachable!()
+    //     };
+
+    //     let dev_server = self
+    //         .dev_server_store
+    //         .read(cx)
+    //         .dev_server(*dev_server_id)
+    //         .cloned();
+
+    //     let (dev_server_name, dev_server_status) = dev_server
+    //         .map(|server| (server.name, server.status))
+    //         .unwrap_or((SharedString::from(""), DevServerStatus::Offline));
+
+    //     v_flex()
+    //         .px_1()
+    //         .pt_0p5()
+    //         .gap_px()
+    //         .child(
+    //             v_flex().py_0p5().px_1().child(
+    //                 h_flex()
+    //                     .px_1()
+    //                     .py_0p5()
+    //                     .child(
+    //                         IconButton::new("back", IconName::ArrowLeft)
+    //                             .style(ButtonStyle::Transparent)
+    //                             .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| {
+    //                                 cx.dispatch_action(menu::Cancel.boxed_clone())
+    //                             })),
+    //                     )
+    //                     .child(Headline::new("Add remote project").size(HeadlineSize::Small)),
+    //             ),
+    //         )
+    //         .child(
+    //             h_flex()
+    //                 .ml_5()
+    //                 .gap_2()
+    //                 .child(
+    //                     div()
+    //                         .id(("status", dev_server_id.0))
+    //                         .relative()
+    //                         .child(Icon::new(IconName::Server))
+    //                         .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
+    //                             Indicator::dot().color(match dev_server_status {
+    //                                 DevServerStatus::Online => Color::Created,
+    //                                 DevServerStatus::Offline => Color::Hidden,
+    //                             }),
+    //                         ))
+    //                         .tooltip(move |cx| {
+    //                             Tooltip::text(
+    //                                 match dev_server_status {
+    //                                     DevServerStatus::Online => "Online",
+    //                                     DevServerStatus::Offline => "Offline",
+    //                                 },
+    //                                 cx,
+    //                             )
+    //                         }),
+    //                 )
+    //                 .child(dev_server_name.clone()),
+    //         )
+    //         .child(
+    //             h_flex()
+    //                 .ml_5()
+    //                 .gap_2()
+    //                 .child(self.project_path_input.clone())
+    //                 .when(!*creating && dev_server_project.is_none(), |div| {
+    //                     div.child(Button::new("create-remote-server", "Create").on_click({
+    //                         let dev_server_id = *dev_server_id;
+    //                         cx.listener(move |this, _, cx| {
+    //                             this.create_dev_server_project(dev_server_id, cx)
+    //                         })
+    //                     }))
+    //                 })
+    //                 .when(*creating, |div| {
+    //                     div.child(Button::new("create-dev-server", "Creating...").disabled(true))
+    //                 }),
+    //         )
+    //         .when_some(dev_server_project.clone(), |div, dev_server_project| {
+    //             let status = self
+    //                 .dev_server_store
+    //                 .read(cx)
+    //                 .dev_server_project(DevServerProjectId(dev_server_project.id))
+    //                 .map(|project| {
+    //                     if project.project_id.is_some() {
+    //                         DevServerStatus::Online
+    //                     } else {
+    //                         DevServerStatus::Offline
+    //                     }
+    //                 })
+    //                 .unwrap_or(DevServerStatus::Offline);
+    //             div.child(
+    //                 v_flex()
+    //                     .ml_5()
+    //                     .ml_8()
+    //                     .gap_2()
+    //                     .when(status == DevServerStatus::Offline, |this| {
+    //                         this.child(Label::new("Waiting for project..."))
+    //                     })
+    //                     .when(status == DevServerStatus::Online, |this| {
+    //                         this.child(Label::new("Project online! 🎊")).child(
+    //                             Button::new("done", "Done").on_click(cx.listener(|_, _, cx| {
+    //                                 cx.dispatch_action(menu::Cancel.boxed_clone())
+    //                             })),
+    //                         )
+    //                     }),
+    //             )
+    //         })
+    // }
 }
 impl ModalView for DevServerProjects {}
 
@@ -737,7 +838,7 @@ impl Render for DevServerProjects {
             .on_action(cx.listener(Self::cancel))
             .on_action(cx.listener(Self::confirm))
             .on_mouse_down_out(cx.listener(|this, _, cx| {
-                if matches!(this.mode, Mode::Default) {
+                if matches!(this.mode, Mode::Default(None)) {
                     cx.emit(DismissEvent)
                 }
             }))
@@ -746,10 +847,7 @@ impl Render for DevServerProjects {
             .min_h(rems(20.))
             .max_h(rems(40.))
             .child(match &self.mode {
-                Mode::Default => self.render_default(cx).into_any_element(),
-                Mode::CreateDevServerProject(_) => {
-                    self.render_create_dev_server_project(cx).into_any_element()
-                }
+                Mode::Default(_) => self.render_default(cx).into_any_element(),
                 Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
             })
     }

crates/rpc/proto/zed.proto 🔗

@@ -237,7 +237,8 @@ message Envelope {
         DevServerProjectsUpdate dev_server_projects_update = 193;
         ValidateDevServerProjectRequest validate_dev_server_project_request = 194;
         DeleteDevServer delete_dev_server = 195;
-        OpenNewBuffer open_new_buffer = 196; // Current max
+        OpenNewBuffer open_new_buffer = 196;
+        DeleteDevServerProject delete_dev_server_project = 197; // Current max
     }
 
     reserved 158 to 161;
@@ -498,6 +499,10 @@ message DeleteDevServer {
     uint64 dev_server_id = 1;
 }
 
+message DeleteDevServerProject {
+    uint64 dev_server_project_id = 1;
+}
+
 message ReconnectDevServer {
     repeated UpdateProject reshared_projects = 1;
 }

crates/rpc/src/proto.rs 🔗

@@ -320,6 +320,7 @@ messages!(
     (DevServerProjectsUpdate, Foreground),
     (ValidateDevServerProjectRequest, Background),
     (DeleteDevServer, Foreground),
+    (DeleteDevServerProject, Foreground),
     (OpenNewBuffer, Foreground)
 );
 
@@ -425,6 +426,7 @@ request_messages!(
     (ValidateDevServerProjectRequest, Ack),
     (MultiLspQuery, MultiLspQueryResponse),
     (DeleteDevServer, Ack),
+    (DeleteDevServerProject, Ack),
 );
 
 entity_messages!(

crates/workspace/src/persistence.rs 🔗

@@ -576,6 +576,22 @@ impl WorkspaceDb {
         }
     }
 
+    pub async fn delete_workspace_by_dev_server_project_id(
+        &self,
+        id: DevServerProjectId,
+    ) -> Result<()> {
+        self.write(move |conn| {
+            conn.exec_bound(sql!(
+                DELETE FROM dev_server_projects WHERE id = ?
+            ))?(id.0)?;
+            conn.exec_bound(sql!(
+                DELETE FROM workspaces
+                WHERE dev_server_project_id IS ?
+            ))?(id.0)
+        })
+        .await
+    }
+
     // Returns the recent locations which are still valid on disk and deletes ones which no longer
     // exist.
     pub async fn recent_workspaces_on_disk(