Reconnect button for remote projects (#12669)
Release Notes: - N/A --------- Co-authored-by: Max <max@zed.dev>
This commit is contained in:
parent
1914a42b1c
commit
4e98c23463
15 changed files with 437 additions and 136 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2363,6 +2363,7 @@ dependencies = [
|
||||||
"prometheus",
|
"prometheus",
|
||||||
"prost",
|
"prost",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"recent_projects",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rpc",
|
"rpc",
|
||||||
|
|
|
@ -96,6 +96,7 @@ node_runtime.workspace = true
|
||||||
notifications = { workspace = true, features = ["test-support"] }
|
notifications = { workspace = true, features = ["test-support"] }
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
|
recent_projects = { workspace = true }
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
dev_server_projects.workspace = true
|
dev_server_projects.workspace = true
|
||||||
rpc = { workspace = true, features = ["test-support"] }
|
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.len(), 1);
|
||||||
assert_eq!(projects[0].path, "/remote");
|
assert_eq!(projects[0].path, "/remote");
|
||||||
workspace::join_dev_server_project(
|
workspace::join_dev_server_project(
|
||||||
|
projects[0].id,
|
||||||
projects[0].project_id.unwrap(),
|
projects[0].project_id.unwrap(),
|
||||||
client.app_state.clone(),
|
client.app_state.clone(),
|
||||||
None,
|
None,
|
||||||
|
@ -207,6 +208,7 @@ async fn create_dev_server_project(
|
||||||
assert_eq!(projects.len(), 1);
|
assert_eq!(projects.len(), 1);
|
||||||
assert_eq!(projects[0].path, "/remote");
|
assert_eq!(projects[0].path, "/remote");
|
||||||
workspace::join_dev_server_project(
|
workspace::join_dev_server_project(
|
||||||
|
projects[0].id,
|
||||||
projects[0].project_id.unwrap(),
|
projects[0].project_id.unwrap(),
|
||||||
client_app_state,
|
client_app_state,
|
||||||
None,
|
None,
|
||||||
|
@ -491,6 +493,7 @@ async fn test_dev_server_reconnect(
|
||||||
.update(cx2, |store, cx| {
|
.update(cx2, |store, cx| {
|
||||||
let projects = store.dev_server_projects();
|
let projects = store.dev_server_projects();
|
||||||
workspace::join_dev_server_project(
|
workspace::join_dev_server_project(
|
||||||
|
projects[0].id,
|
||||||
projects[0].project_id.unwrap(),
|
projects[0].project_id.unwrap(),
|
||||||
client2.app_state.clone(),
|
client2.app_state.clone(),
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -30,6 +30,7 @@ use project::{
|
||||||
project_settings::{InlineBlameSettings, ProjectSettings},
|
project_settings::{InlineBlameSettings, ProjectSettings},
|
||||||
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
|
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
|
||||||
};
|
};
|
||||||
|
use recent_projects::disconnected_overlay::DisconnectedOverlay;
|
||||||
use rpc::RECEIVE_TIMEOUT;
|
use rpc::RECEIVE_TIMEOUT;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
|
@ -59,6 +60,7 @@ async fn test_host_disconnect(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
cx_b.update(editor::init);
|
cx_b.update(editor::init);
|
||||||
|
cx_b.update(recent_projects::init);
|
||||||
|
|
||||||
client_a
|
client_a
|
||||||
.fs()
|
.fs()
|
||||||
|
@ -123,11 +125,10 @@ async fn test_host_disconnect(
|
||||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
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.
|
// Ensure client B's edited state is reset and that the whole window is blurred.
|
||||||
|
|
||||||
workspace_b
|
workspace_b
|
||||||
.update(cx_b, |workspace, cx| {
|
.update(cx_b, |workspace, cx| {
|
||||||
assert_eq!(cx.focused(), None);
|
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
|
||||||
assert!(!workspace.is_edited())
|
assert!(!workspace.is_edited());
|
||||||
})
|
})
|
||||||
.unwrap();
|
.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 = self.project.read(cx).host()?;
|
||||||
let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
|
let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
|
||||||
let participant_index = self
|
let participant_index = self
|
||||||
|
|
|
@ -1816,6 +1816,9 @@ impl Project {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
if self.is_disconnected() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.disconnected_from_host_internal(cx);
|
self.disconnected_from_host_internal(cx);
|
||||||
cx.emit(Event::DisconnectedFromHost);
|
cx.emit(Event::DisconnectedFromHost);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -1863,7 +1866,10 @@ impl Project {
|
||||||
for open_buffer in self.opened_buffers.values_mut() {
|
for open_buffer in self.opened_buffers.values_mut() {
|
||||||
// Wake up any tasks waiting for peers' edits to this buffer.
|
// Wake up any tasks waiting for peers' edits to this buffer.
|
||||||
if let Some(buffer) = open_buffer.upgrade() {
|
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 {
|
if let OpenBuffer::Strong(buffer) = open_buffer {
|
||||||
|
@ -2127,6 +2133,9 @@ impl Project {
|
||||||
let remote_worktree_id = worktree.read(cx).id();
|
let remote_worktree_id = worktree.read(cx).id();
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
let path_string = path.to_string_lossy().to_string();
|
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 {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let response = rpc
|
let response = rpc
|
||||||
.request(proto::OpenBufferByPath {
|
.request(proto::OpenBufferByPath {
|
||||||
|
|
|
@ -337,22 +337,10 @@ fn prepare_ssh_shell(
|
||||||
"exec $SHELL -l".to_string()
|
"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 {
|
let commands = if let Some(path) = path {
|
||||||
// I've found that `ssh -t dev sh -c 'cd; cd /tmp; pwd'` gives /tmp
|
format!("cd {path}; {to_run}")
|
||||||
// but `ssh -t dev sh -c 'cd /tmp; pwd'` gives /root
|
|
||||||
format!("cd {path}; {local_dev_env} {to_run}")
|
|
||||||
} else {
|
} else {
|
||||||
format!("cd; {local_dev_env} {to_run}")
|
format!("cd; {to_run}")
|
||||||
};
|
};
|
||||||
let shell_invocation = &format!("sh -c {}", shlex::try_quote(&commands)?);
|
let shell_invocation = &format!("sh -c {}", shlex::try_quote(&commands)?);
|
||||||
|
|
||||||
|
@ -361,10 +349,9 @@ fn prepare_ssh_shell(
|
||||||
// be run instead.
|
// be run instead.
|
||||||
write!(
|
write!(
|
||||||
&mut ssh_file,
|
&mut ssh_file,
|
||||||
"#!/bin/sh\nexec {} \"$@\" {} {} {}",
|
"#!/bin/sh\nexec {} \"$@\" {} {}",
|
||||||
real_ssh.to_string_lossy(),
|
real_ssh.to_string_lossy(),
|
||||||
if spawn_task.is_none() { "-t" } else { "" },
|
if spawn_task.is_none() { "-t" } else { "" },
|
||||||
port_forward,
|
|
||||||
shlex::try_quote(shell_invocation)?,
|
shlex::try_quote(shell_invocation)?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
@ -317,6 +317,7 @@ impl ProjectPanel {
|
||||||
)
|
)
|
||||||
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
|
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
|
||||||
match e.error_code() {
|
match e.error_code() {
|
||||||
|
ErrorCode::Disconnected => Some("Disconnected from remote project".to_string()),
|
||||||
ErrorCode::UnsharedItem => Some(format!(
|
ErrorCode::UnsharedItem => Some(format!(
|
||||||
"{} is not shared by the host. This could be because it has been marked as `private`",
|
"{} is not shared by the host. This could be because it has been marked as `private`",
|
||||||
file_path.display()
|
file_path.display()
|
||||||
|
|
|
@ -23,6 +23,7 @@ markdown.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
project.workspace = true
|
||||||
dev_server_projects.workspace = true
|
dev_server_projects.workspace = true
|
||||||
rpc.workspace = true
|
rpc.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -35,6 +35,7 @@ use ui_text_field::{FieldLabelLayout, TextField};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
|
use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
|
||||||
|
|
||||||
|
use crate::open_dev_server_project;
|
||||||
use crate::OpenRemote;
|
use crate::OpenRemote;
|
||||||
|
|
||||||
pub struct DevServerProjects {
|
pub struct DevServerProjects {
|
||||||
|
@ -211,7 +212,11 @@ impl DevServerProjects {
|
||||||
this.mode = Mode::Default(None);
|
this.mode = Mode::Default(None);
|
||||||
if let Some(app_state) = AppState::global(cx).upgrade() {
|
if let Some(app_state) = AppState::global(cx).upgrade() {
|
||||||
workspace::join_dev_server_project(
|
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(
|
.detach_and_prompt_err(
|
||||||
"Could not join project",
|
"Could not join project",
|
||||||
|
@ -558,7 +563,27 @@ impl DevServerProjects {
|
||||||
h_flex()
|
h_flex()
|
||||||
.visible_on_hover("dev-server")
|
.visible_on_hover("dev-server")
|
||||||
.gap_1()
|
.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)
|
IconButton::new("edit-dev-server", IconName::Pencil)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
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({
|
.child({
|
||||||
let dev_server_id = dev_server.id;
|
let dev_server_id = dev_server.id;
|
||||||
IconButton::new("remove-dev-server", IconName::Trash)
|
IconButton::new("remove-dev-server", IconName::Trash)
|
||||||
|
@ -681,7 +706,7 @@ impl DevServerProjects {
|
||||||
.on_click(cx.listener(move |_, _, cx| {
|
.on_click(cx.listener(move |_, _, cx| {
|
||||||
if let Some(project_id) = project_id {
|
if let Some(project_id) = project_id {
|
||||||
if let Some(app_state) = AppState::global(cx).upgrade() {
|
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)
|
.detach_and_prompt_err("Could not join project", cx, |_, _| None)
|
||||||
}
|
}
|
||||||
} else {
|
} 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(
|
pub fn reconnect_to_dev_server(
|
||||||
workspace: View<Workspace>,
|
workspace: View<Workspace>,
|
||||||
dev_server: DevServer,
|
dev_server: DevServer,
|
||||||
|
|
155
crates/recent_projects/src/disconnected_overlay.rs
Normal file
155
crates/recent_projects/src/disconnected_overlay.rs
Normal file
|
@ -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;
|
mod dev_servers;
|
||||||
|
pub mod disconnected_overlay;
|
||||||
|
|
||||||
use client::ProjectId;
|
use client::{DevServerProjectId, ProjectId};
|
||||||
use dev_servers::reconnect_to_dev_server;
|
use dev_servers::reconnect_to_dev_server_project;
|
||||||
pub use dev_servers::DevServerProjects;
|
pub use dev_servers::DevServerProjects;
|
||||||
|
use disconnected_overlay::DisconnectedOverlay;
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -19,7 +21,6 @@ use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
|
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
|
||||||
|
@ -46,6 +47,7 @@ gpui::actions!(projects, [OpenRemote]);
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(RecentProjects::register).detach();
|
cx.observe_new_views(RecentProjects::register).detach();
|
||||||
cx.observe_new_views(DevServerProjects::register).detach();
|
cx.observe_new_views(DevServerProjects::register).detach();
|
||||||
|
cx.observe_new_views(DisconnectedOverlay::register).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RecentProjects {
|
pub struct RecentProjects {
|
||||||
|
@ -314,23 +316,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
else {
|
else {
|
||||||
let server = store.read(cx).dev_server_for_project(dev_server_project.id);
|
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()) {
|
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);
|
return reconnect_to_dev_server_project(cx.view().clone(), server.unwrap().clone(), dev_server_project.id, replace_current_window, 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(())
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
let dev_server_name = dev_server_project.dev_server_name.clone();
|
let dev_server_name = dev_server_project.dev_server_name.clone();
|
||||||
return cx.spawn(|workspace, mut cx| async move {
|
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(
|
fn open_dev_server_project(
|
||||||
replace_current_window: bool,
|
replace_current_window: bool,
|
||||||
|
dev_server_project_id: DevServerProjectId,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
|
@ -565,6 +552,7 @@ fn open_dev_server_project(
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |_workspace, cx| {
|
.update(&mut cx, |_workspace, cx| {
|
||||||
workspace::join_dev_server_project(
|
workspace::join_dev_server_project(
|
||||||
|
dev_server_project_id,
|
||||||
project_id,
|
project_id,
|
||||||
app_state,
|
app_state,
|
||||||
Some(handle),
|
Some(handle),
|
||||||
|
@ -576,7 +564,13 @@ fn open_dev_server_project(
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
} else {
|
} 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 {
|
cx.spawn(|_, _| async move {
|
||||||
task.await?;
|
task.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -2,6 +2,7 @@ use gpui::{
|
||||||
div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription,
|
div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription,
|
||||||
View, ViewContext, WindowContext,
|
View, ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
use theme::ActiveTheme as _;
|
||||||
use ui::{h_flex, v_flex};
|
use ui::{h_flex, v_flex};
|
||||||
|
|
||||||
pub enum DismissDecision {
|
pub enum DismissDecision {
|
||||||
|
@ -13,11 +14,16 @@ pub trait ModalView: ManagedView {
|
||||||
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> DismissDecision {
|
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> DismissDecision {
|
||||||
DismissDecision::Dismiss(true)
|
DismissDecision::Dismiss(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fade_out_background(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ModalViewHandle {
|
trait ModalViewHandle {
|
||||||
fn on_before_dismiss(&mut self, cx: &mut WindowContext) -> DismissDecision;
|
fn on_before_dismiss(&mut self, cx: &mut WindowContext) -> DismissDecision;
|
||||||
fn view(&self) -> AnyView;
|
fn view(&self) -> AnyView;
|
||||||
|
fn fade_out_background(&self, cx: &WindowContext) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V: ModalView> ModalViewHandle for View<V> {
|
impl<V: ModalView> ModalViewHandle for View<V> {
|
||||||
|
@ -28,6 +34,10 @@ impl<V: ModalView> ModalViewHandle for View<V> {
|
||||||
fn view(&self) -> AnyView {
|
fn view(&self) -> AnyView {
|
||||||
self.clone().into()
|
self.clone().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fade_out_background(&self, cx: &WindowContext) -> bool {
|
||||||
|
self.read(cx).fade_out_background()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ActiveModal {
|
pub struct ActiveModal {
|
||||||
|
@ -134,20 +144,34 @@ impl ModalLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for 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 {
|
let Some(active_modal) = &self.active_modal else {
|
||||||
return div();
|
return div();
|
||||||
};
|
};
|
||||||
|
|
||||||
div().absolute().size_full().top_0().left_0().child(
|
div()
|
||||||
v_flex()
|
.absolute()
|
||||||
.h(px(0.0))
|
.size_full()
|
||||||
.top_20()
|
.top_0()
|
||||||
.flex()
|
.left_0()
|
||||||
.flex_col()
|
.when(active_modal.modal.fade_out_background(cx), |el| {
|
||||||
.items_center()
|
let mut background = cx.theme().colors().elevated_surface_background;
|
||||||
.track_focus(&active_modal.focus_handle)
|
background.fade_out(0.2);
|
||||||
.child(h_flex().occlude().child(active_modal.modal.view())),
|
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
|
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
|
||||||
/// that used this workspace previously
|
/// that used this workspace previously
|
||||||
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
|
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 call::{call_settings::CallSettings, ActiveCall};
|
||||||
use client::{
|
use client::{
|
||||||
proto::{self, ErrorCode, PeerId},
|
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 collections::{hash_map, HashMap, HashSet};
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
|
@ -29,10 +29,9 @@ use futures::{
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, canvas, impl_actions, point, relative, size, Action, AnyElement, AnyView, AnyWeakView,
|
actions, canvas, impl_actions, point, relative, size, Action, AnyElement, AnyView, AnyWeakView,
|
||||||
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DevicePixels, DragMoveEvent,
|
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DevicePixels, DragMoveEvent,
|
||||||
ElementId, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
|
Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, KeyContext, Keystroke,
|
||||||
GlobalElementId, KeyContext, Keystroke, LayoutId, ManagedView, Model, ModelContext,
|
ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render, Size,
|
||||||
PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView,
|
Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions,
|
||||||
WindowBounds, WindowHandle, WindowOptions,
|
|
||||||
};
|
};
|
||||||
use item::{
|
use item::{
|
||||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||||
|
@ -80,8 +79,8 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
|
||||||
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||||
pub use ui;
|
pub use ui;
|
||||||
use ui::{
|
use ui::{
|
||||||
div, h_flex, Context as _, Div, Element, FluentBuilder, InteractiveElement as _, IntoElement,
|
div, h_flex, Context as _, Div, FluentBuilder, InteractiveElement as _, IntoElement,
|
||||||
Label, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
|
ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt};
|
||||||
|
@ -600,6 +599,8 @@ pub struct Workspace {
|
||||||
centered_layout: bool,
|
centered_layout: bool,
|
||||||
bounds_save_task_queued: Option<Task<()>>,
|
bounds_save_task_queued: Option<Task<()>>,
|
||||||
on_prompt_for_new_path: Option<PromptForNewPath>,
|
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 {}
|
impl EventEmitter<Event> for Workspace {}
|
||||||
|
@ -650,7 +651,6 @@ impl Workspace {
|
||||||
for pane in panes_to_unfollow {
|
for pane in panes_to_unfollow {
|
||||||
this.unfollow(&pane, cx);
|
this.unfollow(&pane, cx);
|
||||||
}
|
}
|
||||||
cx.disable_focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
project::Event::Closed => {
|
project::Event::Closed => {
|
||||||
|
@ -879,10 +879,11 @@ impl Workspace {
|
||||||
centered_layout: false,
|
centered_layout: false,
|
||||||
bounds_save_task_queued: None,
|
bounds_save_task_queued: None,
|
||||||
on_prompt_for_new_path: None,
|
on_prompt_for_new_path: None,
|
||||||
|
render_disconnected_overlay: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_local(
|
pub fn new_local(
|
||||||
abs_paths: Vec<PathBuf>,
|
abs_paths: Vec<PathBuf>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
requesting_window: Option<WindowHandle<Workspace>>,
|
requesting_window: Option<WindowHandle<Workspace>>,
|
||||||
|
@ -1255,6 +1256,13 @@ impl Workspace {
|
||||||
self.on_prompt_for_new_path = Some(prompt)
|
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(
|
pub fn prompt_for_new_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
|
@ -4285,7 +4293,13 @@ impl Render for Workspace {
|
||||||
)
|
)
|
||||||
.child(self.status_bar.clone())
|
.child(self.status_bar.clone())
|
||||||
.children(if self.project.read(cx).is_disconnected() {
|
.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 {
|
} else {
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
|
@ -4935,6 +4949,7 @@ pub fn join_hosted_project(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn join_dev_server_project(
|
pub fn join_dev_server_project(
|
||||||
|
dev_server_project_id: DevServerProjectId,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
window_to_replace: Option<WindowHandle<Workspace>>,
|
window_to_replace: Option<WindowHandle<Workspace>>,
|
||||||
|
@ -4969,10 +4984,19 @@ pub fn join_dev_server_project(
|
||||||
)
|
)
|
||||||
.await?;
|
.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 {
|
if let Some(window_to_replace) = window_to_replace {
|
||||||
cx.update_window(window_to_replace.into(), |_, cx| {
|
cx.update_window(window_to_replace.into(), |_, cx| {
|
||||||
cx.replace_root_view(|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
|
window_to_replace
|
||||||
|
@ -4984,7 +5008,7 @@ pub fn join_dev_server_project(
|
||||||
window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
|
window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
|
||||||
cx.open_window(options, |cx| {
|
cx.open_window(options, |cx| {
|
||||||
cx.new_view(|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()))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue