diff --git a/Cargo.lock b/Cargo.lock index 3fbf4163ff..ee1b95a986 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2363,6 +2363,7 @@ dependencies = [ "prometheus", "prost", "rand 0.8.5", + "recent_projects", "release_channel", "reqwest", "rpc", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index d7a2e458f6..a95682ede0 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -96,6 +96,7 @@ node_runtime.workspace = true notifications = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } +recent_projects = { workspace = true } release_channel.workspace = true dev_server_projects.workspace = true rpc = { workspace = true, features = ["test-support"] } diff --git a/crates/collab/src/tests/dev_server_tests.rs b/crates/collab/src/tests/dev_server_tests.rs index c759cbc3db..c5a13a431d 100644 --- a/crates/collab/src/tests/dev_server_tests.rs +++ b/crates/collab/src/tests/dev_server_tests.rs @@ -68,6 +68,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC assert_eq!(projects.len(), 1); assert_eq!(projects[0].path, "/remote"); workspace::join_dev_server_project( + projects[0].id, projects[0].project_id.unwrap(), client.app_state.clone(), None, @@ -207,6 +208,7 @@ async fn create_dev_server_project( assert_eq!(projects.len(), 1); assert_eq!(projects[0].path, "/remote"); workspace::join_dev_server_project( + projects[0].id, projects[0].project_id.unwrap(), client_app_state, None, @@ -491,6 +493,7 @@ async fn test_dev_server_reconnect( .update(cx2, |store, cx| { let projects = store.dev_server_projects(); workspace::join_dev_server_project( + projects[0].id, projects[0].project_id.unwrap(), client2.app_state.clone(), None, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 739adec242..fe0ce82abe 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -30,6 +30,7 @@ use project::{ project_settings::{InlineBlameSettings, ProjectSettings}, SERVER_PROGRESS_DEBOUNCE_TIMEOUT, }; +use recent_projects::disconnected_overlay::DisconnectedOverlay; use rpc::RECEIVE_TIMEOUT; use serde_json::json; use settings::SettingsStore; @@ -59,6 +60,7 @@ async fn test_host_disconnect( .await; cx_b.update(editor::init); + cx_b.update(recent_projects::init); client_a .fs() @@ -123,11 +125,10 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer())); // Ensure client B's edited state is reset and that the whole window is blurred. - workspace_b .update(cx_b, |workspace, cx| { - assert_eq!(cx.focused(), None); - assert!(!workspace.is_edited()) + assert!(workspace.active_modal::(cx).is_some()); + assert!(!workspace.is_edited()); }) .unwrap(); diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 8747a54d07..3c95bc66ab 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -413,6 +413,17 @@ impl CollabTitlebarItem { ); } + if self.project.read(cx).is_disconnected() { + return Some( + Button::new("disconnected", "Disconnected") + .disabled(true) + .color(Color::Disabled) + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .into_any_element(), + ); + } + let host = self.project.read(cx).host()?; let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?; let participant_index = self diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a70e15ac75..5c6c74e408 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1816,6 +1816,9 @@ impl Project { } pub fn disconnected_from_host(&mut self, cx: &mut ModelContext) { + if self.is_disconnected() { + return; + } self.disconnected_from_host_internal(cx); cx.emit(Event::DisconnectedFromHost); cx.notify(); @@ -1863,7 +1866,10 @@ impl Project { for open_buffer in self.opened_buffers.values_mut() { // Wake up any tasks waiting for peers' edits to this buffer. if let Some(buffer) = open_buffer.upgrade() { - buffer.update(cx, |buffer, _| buffer.give_up_waiting()); + buffer.update(cx, |buffer, cx| { + buffer.give_up_waiting(); + buffer.set_capability(Capability::ReadOnly, cx) + }); } if let OpenBuffer::Strong(buffer) = open_buffer { @@ -2127,6 +2133,9 @@ impl Project { let remote_worktree_id = worktree.read(cx).id(); let path = path.clone(); let path_string = path.to_string_lossy().to_string(); + if self.is_disconnected() { + return Task::ready(Err(anyhow!(ErrorCode::Disconnected))); + } cx.spawn(move |this, mut cx| async move { let response = rpc .request(proto::OpenBufferByPath { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index ebe6438cea..91851b80b3 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -337,22 +337,10 @@ fn prepare_ssh_shell( "exec $SHELL -l".to_string() }; - let (port_forward, local_dev_env) = - if env::var("ZED_RPC_URL").as_deref() == Ok("http://localhost:8080/rpc") { - ( - "-R 8080:localhost:8080", - "export ZED_RPC_URL=http://localhost:8080/rpc;", - ) - } else { - ("", "") - }; - let commands = if let Some(path) = path { - // I've found that `ssh -t dev sh -c 'cd; cd /tmp; pwd'` gives /tmp - // but `ssh -t dev sh -c 'cd /tmp; pwd'` gives /root - format!("cd {path}; {local_dev_env} {to_run}") + format!("cd {path}; {to_run}") } else { - format!("cd; {local_dev_env} {to_run}") + format!("cd; {to_run}") }; let shell_invocation = &format!("sh -c {}", shlex::try_quote(&commands)?); @@ -361,10 +349,9 @@ fn prepare_ssh_shell( // be run instead. write!( &mut ssh_file, - "#!/bin/sh\nexec {} \"$@\" {} {} {}", + "#!/bin/sh\nexec {} \"$@\" {} {}", real_ssh.to_string_lossy(), if spawn_task.is_none() { "-t" } else { "" }, - port_forward, shlex::try_quote(shell_invocation)?, )?; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index cecb3aee5a..6323a4fd3c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -317,6 +317,7 @@ impl ProjectPanel { ) .detach_and_prompt_err("Failed to open file", cx, move |e, _| { match e.error_code() { + ErrorCode::Disconnected => Some("Disconnected from remote project".to_string()), ErrorCode::UnsharedItem => Some(format!( "{} is not shared by the host. This could be because it has been marked as `private`", file_path.display() diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 47277f2a06..b454501788 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -23,6 +23,7 @@ markdown.workspace = true menu.workspace = true ordered-float.workspace = true picker.workspace = true +project.workspace = true dev_server_projects.workspace = true rpc.workspace = true serde.workspace = true diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 826314f356..440f609f4e 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -35,6 +35,7 @@ use ui_text_field::{FieldLabelLayout, TextField}; use util::ResultExt; use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB}; +use crate::open_dev_server_project; use crate::OpenRemote; pub struct DevServerProjects { @@ -211,7 +212,11 @@ impl DevServerProjects { this.mode = Mode::Default(None); if let Some(app_state) = AppState::global(cx).upgrade() { workspace::join_dev_server_project( - project_id, app_state, None, cx, + DevServerProjectId(dev_server_project_id), + project_id, + app_state, + None, + cx, ) .detach_and_prompt_err( "Could not join project", @@ -558,7 +563,27 @@ impl DevServerProjects { h_flex() .visible_on_hover("dev-server") .gap_1() - .child( + .child(if dev_server.ssh_connection_string.is_some() { + let dev_server = dev_server.clone(); + IconButton::new("reconnect-dev-server", IconName::ArrowCircle) + .on_click(cx.listener(move |this, _, cx| { + let Some(workspace) = this.workspace.upgrade() else { + return; + }; + + reconnect_to_dev_server( + workspace, + dev_server.clone(), + cx, + ) + .detach_and_prompt_err( + "Failed to reconnect", + cx, + |_, _| None, + ); + })) + .tooltip(|cx| Tooltip::text("Reconnect", cx)) + } else { IconButton::new("edit-dev-server", IconName::Pencil) .on_click(cx.listener(move |this, _, cx| { this.mode = Mode::CreateDevServer(CreateDevServer { @@ -577,8 +602,8 @@ impl DevServerProjects { }, ) })) - .tooltip(|cx| Tooltip::text("Edit dev server", cx)), - ) + .tooltip(|cx| Tooltip::text("Edit dev server", cx)) + }) .child({ let dev_server_id = dev_server.id; IconButton::new("remove-dev-server", IconName::Trash) @@ -681,7 +706,7 @@ impl DevServerProjects { .on_click(cx.listener(move |_, _, cx| { if let Some(project_id) = project_id { if let Some(app_state) = AppState::global(cx).upgrade() { - workspace::join_dev_server_project(project_id, app_state, None, cx) + workspace::join_dev_server_project(dev_server_project_id, project_id, app_state, None, cx) .detach_and_prompt_err("Could not join project", cx, |_, _| None) } } else { @@ -1044,6 +1069,43 @@ impl Render for DevServerProjects { } } +pub fn reconnect_to_dev_server_project( + workspace: View, + dev_server: DevServer, + dev_server_project_id: DevServerProjectId, + replace_current_window: bool, + cx: &mut WindowContext, +) -> Task> { + let store = dev_server_projects::Store::global(cx); + let reconnect = reconnect_to_dev_server(workspace.clone(), dev_server, cx); + cx.spawn(|mut cx| async move { + reconnect.await?; + + cx.background_executor() + .timer(Duration::from_millis(1000)) + .await; + + if let Some(project_id) = store.update(&mut cx, |store, _| { + store + .dev_server_project(dev_server_project_id) + .and_then(|p| p.project_id) + })? { + workspace + .update(&mut cx, move |_, cx| { + open_dev_server_project( + replace_current_window, + dev_server_project_id, + project_id, + cx, + ) + })? + .await?; + } + + Ok(()) + }) +} + pub fn reconnect_to_dev_server( workspace: View, dev_server: DevServer, diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs new file mode 100644 index 0000000000..f488150c83 --- /dev/null +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -0,0 +1,155 @@ +use dev_server_projects::DevServer; +use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView}; +use ui::{ + div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder, + Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal, + ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, ViewContext, +}; +use workspace::{notifications::DetachAndPromptErr, ModalView, Workspace}; + +use crate::{ + dev_servers::reconnect_to_dev_server_project, open_dev_server_project, DevServerProjects, +}; + +pub struct DisconnectedOverlay { + workspace: WeakView, + dev_server: Option, + focus_handle: FocusHandle, +} + +impl EventEmitter for DisconnectedOverlay {} +impl FocusableView for DisconnectedOverlay { + fn focus_handle(&self, _cx: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} +impl ModalView for DisconnectedOverlay { + fn fade_out_background(&self) -> bool { + true + } +} + +impl DisconnectedOverlay { + pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { + cx.subscribe(workspace.project(), |workspace, project, event, cx| { + if !matches!(event, project::Event::DisconnectedFromHost) { + return; + } + let handle = cx.view().downgrade(); + let dev_server = project + .read(cx) + .dev_server_project_id() + .and_then(|id| { + dev_server_projects::Store::global(cx) + .read(cx) + .dev_server_for_project(id) + }) + .cloned(); + workspace.toggle_modal(cx, |cx| DisconnectedOverlay { + workspace: handle, + dev_server, + focus_handle: cx.focus_handle(), + }); + }) + .detach(); + } + + fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + cx.emit(DismissEvent); + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let Some(dev_server) = self.dev_server.clone() else { + return; + }; + let Some(dev_server_project_id) = workspace + .read(cx) + .project() + .read(cx) + .dev_server_project_id() + else { + return; + }; + + if let Some(project_id) = dev_server_projects::Store::global(cx) + .read(cx) + .dev_server_project(dev_server_project_id) + .and_then(|project| project.project_id) + { + return workspace.update(cx, move |_, cx| { + open_dev_server_project(true, dev_server_project_id, project_id, cx) + .detach_and_prompt_err("Failed to reconnect", cx, |_, _| None) + }); + } + + if dev_server.ssh_connection_string.is_some() { + let task = workspace.update(cx, |_, cx| { + reconnect_to_dev_server_project( + cx.view().clone(), + dev_server, + dev_server_project_id, + true, + cx, + ) + }); + + task.detach_and_prompt_err("Failed to reconnect", cx, |_, _| None); + } else { + return workspace.update(cx, |workspace, cx| { + let handle = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, handle)) + }); + } + } + + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(DismissEvent) + } +} + +impl Render for DisconnectedOverlay { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .track_focus(&self.focus_handle) + .elevation_3(cx) + .on_action(cx.listener(Self::cancel)) + .occlude() + .w(rems(24.)) + .max_h(rems(40.)) + .child( + Modal::new("disconnected", None) + .header( + ModalHeader::new() + .show_dismiss_button(true) + .child(Headline::new("Disconnected").size(HeadlineSize::Small)), + ) + .section(Section::new().child(Label::new( + "Your connection to the remote project has been lost.", + ))) + .footer( + ModalFooter::new().end_slot( + h_flex() + .gap_2() + .child( + Button::new("close-window", "Close Window") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .on_click(cx.listener(move |_, _, cx| { + cx.remove_window(); + })), + ) + .when_some(self.dev_server.clone(), |el, _| { + el.child( + Button::new("reconnect", "Reconnect") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .icon(IconName::ArrowCircle) + .icon_position(IconPosition::Start) + .on_click(cx.listener(Self::handle_reconnect)), + ) + }), + ), + ), + ) + } +} diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 5e6a2bca24..da25b83f4d 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,8 +1,10 @@ mod dev_servers; +pub mod disconnected_overlay; -use client::ProjectId; -use dev_servers::reconnect_to_dev_server; +use client::{DevServerProjectId, ProjectId}; +use dev_servers::reconnect_to_dev_server_project; pub use dev_servers::DevServerProjects; +use disconnected_overlay::DisconnectedOverlay; use feature_flags::FeatureFlagAppExt; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -19,7 +21,6 @@ use serde::Deserialize; use std::{ path::{Path, PathBuf}, sync::Arc, - time::Duration, }; use ui::{ prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem, @@ -46,6 +47,7 @@ gpui::actions!(projects, [OpenRemote]); pub fn init(cx: &mut AppContext) { cx.observe_new_views(RecentProjects::register).detach(); cx.observe_new_views(DevServerProjects::register).detach(); + cx.observe_new_views(DisconnectedOverlay::register).detach(); } pub struct RecentProjects { @@ -314,23 +316,7 @@ impl PickerDelegate for RecentProjectsDelegate { else { let server = store.read(cx).dev_server_for_project(dev_server_project.id); if server.is_some_and(|server| server.ssh_connection_string.is_some()) { - let reconnect = reconnect_to_dev_server(cx.view().clone(), server.unwrap().clone(), cx); - let id = dev_server_project.id; - return cx.spawn(|workspace, mut cx| async move { - reconnect.await?; - - cx.background_executor().timer(Duration::from_millis(1000)).await; - - if let Some(project_id) = store.update(&mut cx, |store, _| { - store.dev_server_project(id) - .and_then(|p| p.project_id) - })? { - workspace.update(&mut cx, move |_, cx| { - open_dev_server_project(replace_current_window, project_id, cx) - })?.await?; - } - Ok(()) - }) + return reconnect_to_dev_server_project(cx.view().clone(), server.unwrap().clone(), dev_server_project.id, replace_current_window, cx); } else { let dev_server_name = dev_server_project.dev_server_name.clone(); return cx.spawn(|workspace, mut cx| async move { @@ -354,7 +340,7 @@ impl PickerDelegate for RecentProjectsDelegate { }) } }; - open_dev_server_project(replace_current_window, project_id, cx) + open_dev_server_project(replace_current_window, dev_server_project.id, project_id, cx) } } } @@ -544,6 +530,7 @@ impl PickerDelegate for RecentProjectsDelegate { fn open_dev_server_project( replace_current_window: bool, + dev_server_project_id: DevServerProjectId, project_id: ProjectId, cx: &mut ViewContext, ) -> Task> { @@ -565,6 +552,7 @@ fn open_dev_server_project( workspace .update(&mut cx, |_workspace, cx| { workspace::join_dev_server_project( + dev_server_project_id, project_id, app_state, Some(handle), @@ -576,7 +564,13 @@ fn open_dev_server_project( Ok(()) }) } else { - let task = workspace::join_dev_server_project(project_id, app_state, None, cx); + let task = workspace::join_dev_server_project( + dev_server_project_id, + project_id, + app_state, + None, + cx, + ); cx.spawn(|_, _| async move { task.await?; Ok(()) diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index f64df94cae..44923d783e 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -2,6 +2,7 @@ use gpui::{ div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription, View, ViewContext, WindowContext, }; +use theme::ActiveTheme as _; use ui::{h_flex, v_flex}; pub enum DismissDecision { @@ -13,11 +14,16 @@ pub trait ModalView: ManagedView { fn on_before_dismiss(&mut self, _: &mut ViewContext) -> DismissDecision { DismissDecision::Dismiss(true) } + + fn fade_out_background(&self) -> bool { + false + } } trait ModalViewHandle { fn on_before_dismiss(&mut self, cx: &mut WindowContext) -> DismissDecision; fn view(&self) -> AnyView; + fn fade_out_background(&self, cx: &WindowContext) -> bool; } impl ModalViewHandle for View { @@ -28,6 +34,10 @@ impl ModalViewHandle for View { fn view(&self) -> AnyView { self.clone().into() } + + fn fade_out_background(&self, cx: &WindowContext) -> bool { + self.read(cx).fade_out_background() + } } pub struct ActiveModal { @@ -134,20 +144,34 @@ impl ModalLayer { } impl Render for ModalLayer { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let Some(active_modal) = &self.active_modal else { return div(); }; - div().absolute().size_full().top_0().left_0().child( - v_flex() - .h(px(0.0)) - .top_20() - .flex() - .flex_col() - .items_center() - .track_focus(&active_modal.focus_handle) - .child(h_flex().occlude().child(active_modal.modal.view())), - ) + div() + .absolute() + .size_full() + .top_0() + .left_0() + .when(active_modal.modal.fade_out_background(cx), |el| { + let mut background = cx.theme().colors().elevated_surface_background; + background.fade_out(0.2); + el.bg(background) + .occlude() + .on_mouse_down_out(cx.listener(|this, _, cx| { + this.hide_modal(cx); + })) + }) + .child( + v_flex() + .h(px(0.0)) + .top_20() + .flex() + .flex_col() + .items_center() + .track_focus(&active_modal.focus_handle) + .child(h_flex().occlude().child(active_modal.modal.view())), + ) } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 96f79c95f2..03bc9ca961 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -468,6 +468,99 @@ impl WorkspaceDb { }) } + pub(crate) fn workspace_for_dev_server_project( + &self, + dev_server_project_id: DevServerProjectId, + ) -> Option { + // Note that we re-assign the workspace_id here in case it's empty + // and we've grabbed the most recent workspace + let ( + workspace_id, + local_paths, + local_paths_order, + dev_server_project_id, + window_bounds, + display, + centered_layout, + docks, + ): ( + WorkspaceId, + Option, + Option, + Option, + Option, + Option, + Option, + DockStructure, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + local_paths, + local_paths_order, + dev_server_project_id, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + centered_layout, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom + FROM workspaces + WHERE dev_server_project_id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0)) + .context("No workspaces found") + .warn_on_err() + .flatten()?; + + let location = if let Some(dev_server_project_id) = dev_server_project_id { + let dev_server_project: SerializedDevServerProject = self + .select_row_bound(sql! { + SELECT id, path, dev_server_name + FROM dev_server_projects + WHERE id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id)) + .context("No remote project found") + .warn_on_err() + .flatten()?; + SerializedWorkspaceLocation::DevServer(dev_server_project) + } else if let Some(local_paths) = local_paths { + match local_paths_order { + Some(order) => SerializedWorkspaceLocation::Local(local_paths, order), + None => { + let order = LocalPathsOrder::default_for_paths(&local_paths); + SerializedWorkspaceLocation::Local(local_paths, order) + } + } + } else { + return None; + }; + + Some(SerializedWorkspace { + id: workspace_id, + location, + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + window_bounds, + centered_layout: centered_layout.unwrap_or(false), + display, + docks, + }) + } + /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8fbeeebb10..f35933029d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -16,7 +16,7 @@ use anyhow::{anyhow, Context as _, Result}; use call::{call_settings::CallSettings, ActiveCall}; use client::{ proto::{self, ErrorCode, PeerId}, - ChannelId, Client, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore, + ChannelId, Client, DevServerProjectId, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use derive_more::{Deref, DerefMut}; @@ -29,10 +29,9 @@ use futures::{ use gpui::{ actions, canvas, impl_actions, point, relative, size, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DevicePixels, DragMoveEvent, - ElementId, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, - GlobalElementId, KeyContext, Keystroke, LayoutId, ManagedView, Model, ModelContext, - PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView, - WindowBounds, WindowHandle, WindowOptions, + Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, KeyContext, Keystroke, + ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render, Size, + Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions, }; use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, @@ -80,8 +79,8 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use ui::{ - div, h_flex, Context as _, Div, Element, FluentBuilder, InteractiveElement as _, IntoElement, - Label, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, + div, h_flex, Context as _, Div, FluentBuilder, InteractiveElement as _, IntoElement, + ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, WindowContext, }; use util::{maybe, ResultExt}; @@ -600,6 +599,8 @@ pub struct Workspace { centered_layout: bool, bounds_save_task_queued: Option>, on_prompt_for_new_path: Option, + render_disconnected_overlay: + Option) -> AnyElement>>, } impl EventEmitter for Workspace {} @@ -650,7 +651,6 @@ impl Workspace { for pane in panes_to_unfollow { this.unfollow(&pane, cx); } - cx.disable_focus(); } project::Event::Closed => { @@ -879,10 +879,11 @@ impl Workspace { centered_layout: false, bounds_save_task_queued: None, on_prompt_for_new_path: None, + render_disconnected_overlay: None, } } - fn new_local( + pub fn new_local( abs_paths: Vec, app_state: Arc, requesting_window: Option>, @@ -1255,6 +1256,13 @@ impl Workspace { self.on_prompt_for_new_path = Some(prompt) } + pub fn set_render_disconnected_overlay( + &mut self, + render: impl Fn(&mut Self, &mut ViewContext) -> AnyElement + 'static, + ) { + self.render_disconnected_overlay = Some(Box::new(render)) + } + pub fn prompt_for_new_path( &mut self, cx: &mut ViewContext, @@ -4285,7 +4293,13 @@ impl Render for Workspace { ) .child(self.status_bar.clone()) .children(if self.project.read(cx).is_disconnected() { - Some(DisconnectedOverlay) + if let Some(render) = self.render_disconnected_overlay.take() { + let result = render(self, cx); + self.render_disconnected_overlay = Some(render); + Some(result) + } else { + None + } } else { None }) @@ -4935,6 +4949,7 @@ pub fn join_hosted_project( } pub fn join_dev_server_project( + dev_server_project_id: DevServerProjectId, project_id: ProjectId, app_state: Arc, window_to_replace: Option>, @@ -4969,10 +4984,19 @@ pub fn join_dev_server_project( ) .await?; + let serialized_workspace: Option = + persistence::DB.workspace_for_dev_server_project(dev_server_project_id); + + let workspace_id = if let Some(serialized_workspace) = serialized_workspace { + serialized_workspace.id + } else { + persistence::DB.next_id().await? + }; + if let Some(window_to_replace) = window_to_replace { cx.update_window(window_to_replace.into(), |_, cx| { cx.replace_root_view(|cx| { - Workspace::new(Default::default(), project, app_state.clone(), cx) + Workspace::new(Some(workspace_id), project, app_state.clone(), cx) }); })?; window_to_replace @@ -4984,7 +5008,7 @@ pub fn join_dev_server_project( window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds)); cx.open_window(options, |cx| { cx.new_view(|cx| { - Workspace::new(Default::default(), project, app_state.clone(), cx) + Workspace::new(Some(workspace_id), project, app_state.clone(), cx) }) }) })? @@ -5150,72 +5174,6 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { Some(size((width as i32).into(), (height as i32).into())) } -struct DisconnectedOverlay; - -impl Element for DisconnectedOverlay { - type RequestLayoutState = AnyElement; - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - cx: &mut WindowContext, - ) -> (LayoutId, Self::RequestLayoutState) { - let mut background = cx.theme().colors().elevated_surface_background; - background.fade_out(0.2); - let mut overlay = div() - .bg(background) - .absolute() - .left_0() - .top(ui::TitleBar::height(cx)) - .size_full() - .flex() - .items_center() - .justify_center() - .capture_any_mouse_down(|_, cx| cx.stop_propagation()) - .capture_any_mouse_up(|_, cx| cx.stop_propagation()) - .child(Label::new( - "Your connection to the remote project has been lost.", - )) - .into_any(); - (overlay.request_layout(cx), overlay) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - bounds: Bounds, - overlay: &mut Self::RequestLayoutState, - cx: &mut WindowContext, - ) { - cx.insert_hitbox(bounds, true); - overlay.prepaint(cx); - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _: Bounds, - overlay: &mut Self::RequestLayoutState, - _: &mut Self::PrepaintState, - cx: &mut WindowContext, - ) { - overlay.paint(cx) - } -} - -impl IntoElement for DisconnectedOverlay { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - #[cfg(test)] mod tests { use std::{cell::RefCell, rc::Rc};