Cargo.lock 🔗
@@ -2363,6 +2363,7 @@ dependencies = [
"prometheus",
"prost",
"rand 0.8.5",
+ "recent_projects",
"release_channel",
"reqwest",
"rpc",
Conrad Irwin and Max created
Release Notes:
- N/A
---------
Co-authored-by: Max <max@zed.dev>
Cargo.lock | 1
crates/collab/Cargo.toml | 1
crates/collab/src/tests/dev_server_tests.rs | 3
crates/collab/src/tests/editor_tests.rs | 7
crates/collab_ui/src/collab_titlebar_item.rs | 11 +
crates/project/src/project.rs | 11 +
crates/project/src/terminals.rs | 19 -
crates/project_panel/src/project_panel.rs | 1
crates/recent_projects/Cargo.toml | 1
crates/recent_projects/src/dev_servers.rs | 72 ++++++
crates/recent_projects/src/disconnected_overlay.rs | 155 ++++++++++++++++
crates/recent_projects/src/recent_projects.rs | 38 +--
crates/workspace/src/modal_layer.rs | 46 +++-
crates/workspace/src/persistence.rs | 93 +++++++++
crates/workspace/src/workspace.rs | 114 +++--------
15 files changed, 437 insertions(+), 136 deletions(-)
@@ -2363,6 +2363,7 @@ dependencies = [
"prometheus",
"prost",
"rand 0.8.5",
+ "recent_projects",
"release_channel",
"reqwest",
"rpc",
@@ -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"] }
@@ -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,
@@ -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::<DisconnectedOverlay>(cx).is_some());
+ assert!(!workspace.is_edited());
})
.unwrap();
@@ -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
@@ -1816,6 +1816,9 @@ impl Project {
}
pub fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
+ 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 {
@@ -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)?,
)?;
@@ -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()
@@ -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
@@ -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<Workspace>,
+ dev_server: DevServer,
+ dev_server_project_id: DevServerProjectId,
+ replace_current_window: bool,
+ cx: &mut WindowContext,
+) -> Task<anyhow::Result<()>> {
+ 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<Workspace>,
dev_server: DevServer,
@@ -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<Workspace>,
+ dev_server: Option<DevServer>,
+ focus_handle: FocusHandle,
+}
+
+impl EventEmitter<DismissEvent> 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<Workspace>) {
+ 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<Self>) {
+ 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<Self>) {
+ cx.emit(DismissEvent)
+ }
+}
+
+impl Render for DisconnectedOverlay {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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)),
+ )
+ }),
+ ),
+ ),
+ )
+ }
+}
@@ -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<Workspace>,
) -> Task<anyhow::Result<()>> {
@@ -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(())
@@ -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<Self>) -> 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<V: ModalView> ModalViewHandle for View<V> {
@@ -28,6 +34,10 @@ impl<V: ModalView> ModalViewHandle for View<V> {
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<Self>) -> impl IntoElement {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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())),
+ )
}
}
@@ -468,6 +468,99 @@ impl WorkspaceDb {
})
}
+ pub(crate) fn workspace_for_dev_server_project(
+ &self,
+ dev_server_project_id: DevServerProjectId,
+ ) -> Option<SerializedWorkspace> {
+ // 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<LocalPaths>,
+ Option<LocalPathsOrder>,
+ Option<u64>,
+ Option<SerializedWindowBounds>,
+ Option<Uuid>,
+ Option<bool>,
+ 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) {
@@ -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<Task<()>>,
on_prompt_for_new_path: Option<PromptForNewPath>,
+ render_disconnected_overlay:
+ Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
}
impl EventEmitter<Event> 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<PathBuf>,
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
@@ -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<Self>) -> AnyElement + 'static,
+ ) {
+ self.render_disconnected_overlay = Some(Box::new(render))
+ }
+
pub fn prompt_for_new_path(
&mut self,
cx: &mut ViewContext<Self>,
@@ -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<AppState>,
window_to_replace: Option<WindowHandle<Workspace>>,
@@ -4969,10 +4984,19 @@ pub fn join_dev_server_project(
)
.await?;
+ let serialized_workspace: Option<SerializedWorkspace> =
+ 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<Size<DevicePixels>> {
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<ElementId> {
- 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<Pixels>,
- overlay: &mut Self::RequestLayoutState,
- cx: &mut WindowContext,
- ) {
- cx.insert_hitbox(bounds, true);
- overlay.prepaint(cx);
- }
-
- fn paint(
- &mut self,
- _id: Option<&GlobalElementId>,
- _: Bounds<Pixels>,
- 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};