ssh remote: Handle disconnect on project and show overlay (#19014)
Demo: https://github.com/user-attachments/assets/e5edf8f3-8c15-482e-a792-6eb619f83de4 Release Notes: - N/A --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
e3ff2ced79
commit
b75532fad7
16 changed files with 264 additions and 78 deletions
|
@ -1178,7 +1178,7 @@ impl Room {
|
|||
this.update(&mut cx, |this, cx| {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
!project.read(cx).is_disconnected()
|
||||
!project.read(cx).is_disconnected(cx)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ async fn test_channel_guests(
|
|||
project_b.read_with(cx_b, |project, _| project.remote_id()),
|
||||
Some(project_id),
|
||||
);
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
|
||||
assert!(project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
|
||||
|
@ -103,7 +103,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
|||
workspace.active_item_as::<Editor>(cx).unwrap(),
|
||||
)
|
||||
});
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
assert!(room_b
|
||||
|
@ -127,7 +127,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
|||
cx_a.run_until_parked();
|
||||
|
||||
// project and buffers are now editable
|
||||
assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only()));
|
||||
assert!(project_b.read_with(cx_b, |project, cx| !project.is_read_only(cx)));
|
||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
|
||||
// B sees themselves as muted, and can unmute.
|
||||
|
@ -153,7 +153,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
|||
cx_a.run_until_parked();
|
||||
|
||||
// project and buffers are no longer editable
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
|
||||
assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx)));
|
||||
assert!(room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
|
|
|
@ -262,7 +262,7 @@ async fn test_dev_server_leave_room(
|
|||
cx1.executor().run_until_parked();
|
||||
|
||||
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -308,7 +308,7 @@ async fn test_dev_server_delete(
|
|||
cx1.executor().run_until_parked();
|
||||
|
||||
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
|
||||
|
||||
cx1.update(|cx| {
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
|
@ -418,12 +418,12 @@ async fn test_dev_server_refresh_access_token(
|
|||
|
||||
// Assert that the other client was disconnected
|
||||
let (workspace, cx2) = client2.active_workspace(cx2);
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected()));
|
||||
cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected(cx)));
|
||||
|
||||
// Assert that the owner of the dev server does not see the dev server as online anymore
|
||||
let (workspace, cx1) = client1.active_workspace(cx1);
|
||||
cx1.update(|cx| {
|
||||
assert!(workspace.read(cx).project().read(cx).is_disconnected());
|
||||
assert!(workspace.read(cx).project().read(cx).is_disconnected(cx));
|
||||
dev_server_projects::Store::global(cx).update(cx, |store, _| {
|
||||
assert_eq!(
|
||||
store.dev_servers().first().unwrap().status,
|
||||
|
|
|
@ -114,7 +114,7 @@ async fn test_host_disconnect(
|
|||
|
||||
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
||||
|
||||
project_b.read_with(cx_b, |project, _| project.is_read_only());
|
||||
project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));
|
||||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
||||
|
||||
|
|
|
@ -1389,7 +1389,7 @@ async fn test_unshare_project(
|
|||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected()));
|
||||
assert!(project_b.read_with(cx_b, |project, cx| project.is_disconnected(cx)));
|
||||
|
||||
// Client C opens the project.
|
||||
let project_c = client_c.join_remote_project(project_id, cx_c).await;
|
||||
|
@ -1402,7 +1402,7 @@ async fn test_unshare_project(
|
|||
|
||||
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
|
||||
|
||||
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
|
||||
assert!(project_c.read_with(cx_c, |project, cx| project.is_disconnected(cx)));
|
||||
|
||||
// Client C can open the project again after client A re-shares.
|
||||
let project_id = active_call_a
|
||||
|
@ -1427,8 +1427,8 @@ async fn test_unshare_project(
|
|||
|
||||
project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
||||
|
||||
project_c2.read_with(cx_c, |project, _| {
|
||||
assert!(project.is_disconnected());
|
||||
project_c2.read_with(cx_c, |project, cx| {
|
||||
assert!(project.is_disconnected(cx));
|
||||
assert!(project.collaborators().is_empty());
|
||||
});
|
||||
}
|
||||
|
@ -1560,8 +1560,8 @@ async fn test_project_reconnect(
|
|||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
project_b1.read_with(cx_b, |project, _| {
|
||||
assert!(!project.is_disconnected());
|
||||
project_b1.read_with(cx_b, |project, cx| {
|
||||
assert!(!project.is_disconnected(cx));
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
|
@ -1661,7 +1661,7 @@ async fn test_project_reconnect(
|
|||
});
|
||||
|
||||
project_b1.read_with(cx_b, |project, cx| {
|
||||
assert!(!project.is_disconnected());
|
||||
assert!(!project.is_disconnected(cx));
|
||||
assert_eq!(
|
||||
project
|
||||
.worktree_for_id(worktree1_id, cx)
|
||||
|
@ -1695,9 +1695,9 @@ async fn test_project_reconnect(
|
|||
);
|
||||
});
|
||||
|
||||
project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
|
||||
project_b2.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
|
||||
|
||||
project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected()));
|
||||
project_b3.read_with(cx_b, |project, cx| assert!(!project.is_disconnected(cx)));
|
||||
|
||||
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ"));
|
||||
|
||||
|
@ -1754,7 +1754,7 @@ async fn test_project_reconnect(
|
|||
executor.run_until_parked();
|
||||
|
||||
project_b1.read_with(cx_b, |project, cx| {
|
||||
assert!(!project.is_disconnected());
|
||||
assert!(!project.is_disconnected(cx));
|
||||
assert_eq!(
|
||||
project
|
||||
.worktree_for_id(worktree1_id, cx)
|
||||
|
@ -1788,7 +1788,7 @@ async fn test_project_reconnect(
|
|||
);
|
||||
});
|
||||
|
||||
project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected()));
|
||||
project_b3.read_with(cx_b, |project, cx| assert!(project.is_disconnected(cx)));
|
||||
|
||||
buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ"));
|
||||
|
||||
|
@ -3816,8 +3816,8 @@ async fn test_leaving_project(
|
|||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
project_b2.read_with(cx_b, |project, _| {
|
||||
assert!(project.is_disconnected());
|
||||
project_b2.read_with(cx_b, |project, cx| {
|
||||
assert!(project.is_disconnected(cx));
|
||||
});
|
||||
|
||||
project_c.read_with(cx_c, |project, _| {
|
||||
|
@ -3849,12 +3849,12 @@ async fn test_leaving_project(
|
|||
assert_eq!(project.collaborators().len(), 0);
|
||||
});
|
||||
|
||||
project_b2.read_with(cx_b, |project, _| {
|
||||
assert!(project.is_disconnected());
|
||||
project_b2.read_with(cx_b, |project, cx| {
|
||||
assert!(project.is_disconnected(cx));
|
||||
});
|
||||
|
||||
project_c.read_with(cx_c, |project, _| {
|
||||
assert!(project.is_disconnected());
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert!(project.is_disconnected(cx));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1168,7 +1168,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
Some((project, cx))
|
||||
});
|
||||
|
||||
if !guest_project.is_disconnected() {
|
||||
if !guest_project.is_disconnected(cx) {
|
||||
if let Some((host_project, host_cx)) = host_project {
|
||||
let host_worktree_snapshots =
|
||||
host_project.read_with(host_cx, |host_project, cx| {
|
||||
|
@ -1254,8 +1254,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
|
||||
let buffers = client.buffers().clone();
|
||||
for (guest_project, guest_buffers) in &buffers {
|
||||
let project_id = if guest_project.read_with(client_cx, |project, _| {
|
||||
project.is_local() || project.is_disconnected()
|
||||
let project_id = if guest_project.read_with(client_cx, |project, cx| {
|
||||
project.is_local() || project.is_disconnected(cx)
|
||||
}) {
|
||||
continue;
|
||||
} else {
|
||||
|
|
|
@ -532,9 +532,9 @@ impl<T: RandomizedTest> TestPlan<T> {
|
|||
server.allow_connections();
|
||||
|
||||
for project in client.dev_server_projects().iter() {
|
||||
project.read_with(&client_cx, |project, _| {
|
||||
project.read_with(&client_cx, |project, cx| {
|
||||
assert!(
|
||||
project.is_disconnected(),
|
||||
project.is_disconnected(cx),
|
||||
"project {:?} should be read only",
|
||||
project.remote_id()
|
||||
)
|
||||
|
|
|
@ -647,17 +647,10 @@ pub struct FormattableBuffer {
|
|||
}
|
||||
|
||||
pub struct RemoteLspStore {
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_client: Option<AnyProtoClient>,
|
||||
upstream_project_id: u64,
|
||||
}
|
||||
|
||||
impl RemoteLspStore {}
|
||||
|
||||
// pub struct SshLspStore {
|
||||
// upstream_client: AnyProtoClient,
|
||||
// current_lsp_settings: HashMap<LanguageServerName, LspSettings>,
|
||||
// }
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum LspStoreMode {
|
||||
Local(LocalLspStore), // ssh host and collab host
|
||||
|
@ -808,10 +801,15 @@ impl LspStore {
|
|||
pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {
|
||||
match &self.mode {
|
||||
LspStoreMode::Remote(RemoteLspStore {
|
||||
upstream_client,
|
||||
upstream_client: Some(upstream_client),
|
||||
upstream_project_id,
|
||||
..
|
||||
}) => Some((upstream_client.clone(), *upstream_project_id)),
|
||||
|
||||
LspStoreMode::Remote(RemoteLspStore {
|
||||
upstream_client: None,
|
||||
..
|
||||
}) => None,
|
||||
LspStoreMode::Local(_) => None,
|
||||
}
|
||||
}
|
||||
|
@ -924,7 +922,7 @@ impl LspStore {
|
|||
|
||||
Self {
|
||||
mode: LspStoreMode::Remote(RemoteLspStore {
|
||||
upstream_client,
|
||||
upstream_client: Some(upstream_client),
|
||||
upstream_project_id: project_id,
|
||||
}),
|
||||
downstream_client: None,
|
||||
|
@ -3099,6 +3097,15 @@ impl LspStore {
|
|||
self.downstream_client.take();
|
||||
}
|
||||
|
||||
pub fn disconnected_from_ssh_remote(&mut self) {
|
||||
if let LspStoreMode::Remote(RemoteLspStore {
|
||||
upstream_client, ..
|
||||
}) = &mut self.mode
|
||||
{
|
||||
upstream_client.take();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_language_server_statuses_from_proto(
|
||||
&mut self,
|
||||
language_servers: Vec<proto::LanguageServer>,
|
||||
|
|
|
@ -58,7 +58,7 @@ use node_runtime::NodeRuntime;
|
|||
use parking_lot::{Mutex, RwLock};
|
||||
pub use prettier_store::PrettierStore;
|
||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||
use remote::SshRemoteClient;
|
||||
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
|
||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||
use search_history::SearchHistory;
|
||||
|
@ -245,6 +245,7 @@ pub enum Event {
|
|||
},
|
||||
RemoteIdChanged(Option<u64>),
|
||||
DisconnectedFromHost,
|
||||
DisconnectedFromSshRemote,
|
||||
Closed,
|
||||
DeletedEntry(ProjectEntryId),
|
||||
CollaboratorUpdated {
|
||||
|
@ -755,6 +756,8 @@ impl Project {
|
|||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&ssh, Self::on_ssh_event).detach();
|
||||
cx.observe(&ssh, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
let this = Self {
|
||||
|
@ -1313,6 +1316,12 @@ impl Project {
|
|||
.map(|ssh| ssh.read(cx).connection_state())
|
||||
}
|
||||
|
||||
pub fn ssh_connection_options(&self, cx: &AppContext) -> Option<SshConnectionOptions> {
|
||||
self.ssh_client
|
||||
.as_ref()
|
||||
.map(|ssh| ssh.read(cx).connection_options())
|
||||
}
|
||||
|
||||
pub fn replica_id(&self) -> ReplicaId {
|
||||
match self.client_state {
|
||||
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
||||
|
@ -1658,7 +1667,7 @@ impl Project {
|
|||
}
|
||||
|
||||
pub fn disconnected_from_host(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.is_disconnected() {
|
||||
if self.is_disconnected(cx) {
|
||||
return;
|
||||
}
|
||||
self.disconnected_from_host_internal(cx);
|
||||
|
@ -1708,16 +1717,24 @@ impl Project {
|
|||
cx.emit(Event::Closed);
|
||||
}
|
||||
|
||||
pub fn is_disconnected(&self) -> bool {
|
||||
pub fn is_disconnected(&self, cx: &AppContext) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Remote {
|
||||
sharing_has_stopped,
|
||||
..
|
||||
} => *sharing_has_stopped,
|
||||
ProjectClientState::Local if self.is_via_ssh() => self.ssh_is_disconnected(cx),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn ssh_is_disconnected(&self, cx: &AppContext) -> bool {
|
||||
self.ssh_client
|
||||
.as_ref()
|
||||
.map(|ssh| ssh.read(cx).is_disconnected())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn capability(&self) -> Capability {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Remote { capability, .. } => *capability,
|
||||
|
@ -1725,8 +1742,8 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_read_only(&self) -> bool {
|
||||
self.is_disconnected() || self.capability() == Capability::ReadOnly
|
||||
pub fn is_read_only(&self, cx: &AppContext) -> bool {
|
||||
self.is_disconnected(cx) || self.capability() == Capability::ReadOnly
|
||||
}
|
||||
|
||||
pub fn is_local(&self) -> bool {
|
||||
|
@ -1807,7 +1824,7 @@ impl Project {
|
|||
path: impl Into<ProjectPath>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
if self.is_via_collab() && self.is_disconnected() {
|
||||
if (self.is_via_collab() || self.is_via_ssh()) && self.is_disconnected(cx) {
|
||||
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
|
||||
}
|
||||
|
||||
|
@ -2114,6 +2131,30 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_ssh_event(
|
||||
&mut self,
|
||||
_: Model<SshRemoteClient>,
|
||||
event: &remote::SshRemoteEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
remote::SshRemoteEvent::Disconnected => {
|
||||
// if self.is_via_ssh() {
|
||||
// self.collaborators.clear();
|
||||
self.worktree_store.update(cx, |store, cx| {
|
||||
store.disconnected_from_host(cx);
|
||||
});
|
||||
self.buffer_store.update(cx, |buffer_store, cx| {
|
||||
buffer_store.disconnected_from_host(cx)
|
||||
});
|
||||
self.lsp_store.update(cx, |lsp_store, _cx| {
|
||||
lsp_store.disconnected_from_ssh_remote()
|
||||
});
|
||||
cx.emit(Event::DisconnectedFromSshRemote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_settings_observer_event(
|
||||
&mut self,
|
||||
_: Model<SettingsObserver>,
|
||||
|
|
|
@ -509,6 +509,11 @@ impl WorktreeStore {
|
|||
for worktree in &self.worktrees {
|
||||
if let Some(worktree) = worktree.upgrade() {
|
||||
worktree.update(cx, |worktree, _| {
|
||||
println!(
|
||||
"worktree. is_local: {:?}, is_remote: {:?}",
|
||||
worktree.is_local(),
|
||||
worktree.is_remote()
|
||||
);
|
||||
if let Some(worktree) = worktree.as_remote_mut() {
|
||||
worktree.disconnected_from_host();
|
||||
}
|
||||
|
|
|
@ -344,16 +344,17 @@ impl ProjectPanel {
|
|||
let worktree_id = worktree.read(cx).id();
|
||||
let entry_id = entry.id;
|
||||
|
||||
project_panel.update(cx, |this, _| {
|
||||
if !mark_selected {
|
||||
this.marked_entries.clear();
|
||||
}
|
||||
this.marked_entries.insert(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id
|
||||
});
|
||||
}).ok();
|
||||
project_panel.update(cx, |this, _| {
|
||||
if !mark_selected {
|
||||
this.marked_entries.clear();
|
||||
}
|
||||
this.marked_entries.insert(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id
|
||||
});
|
||||
}).ok();
|
||||
|
||||
let is_via_ssh = project.read(cx).is_via_ssh();
|
||||
|
||||
workspace
|
||||
.open_path_preview(
|
||||
|
@ -368,7 +369,11 @@ 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::Disconnected => if is_via_ssh {
|
||||
Some("Disconnected from SSH host".to_string())
|
||||
} else {
|
||||
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()
|
||||
|
@ -493,7 +498,7 @@ impl ProjectPanel {
|
|||
let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
|
||||
let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
|
||||
let worktree_id = worktree.id();
|
||||
let is_read_only = project.is_read_only();
|
||||
let is_read_only = project.is_read_only(cx);
|
||||
let is_remote = project.is_via_collab() && project.dev_server_project_id().is_none();
|
||||
let is_local = project.is_local();
|
||||
|
||||
|
@ -2901,7 +2906,7 @@ impl Render for ProjectPanel {
|
|||
.on_action(cx.listener(Self::new_search_in_directory))
|
||||
.on_action(cx.listener(Self::unfold_directory))
|
||||
.on_action(cx.listener(Self::fold_directory))
|
||||
.when(!project.is_read_only(), |el| {
|
||||
.when(!project.is_read_only(cx), |el| {
|
||||
el.on_action(cx.listener(Self::new_file))
|
||||
.on_action(cx.listener(Self::new_directory))
|
||||
.on_action(cx.listener(Self::rename))
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use dev_server_projects::DevServer;
|
||||
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
|
||||
use remote::SshConnectionOptions;
|
||||
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 workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
|
||||
|
||||
use crate::{
|
||||
dev_servers::reconnect_to_dev_server_project, open_dev_server_project, DevServerProjects,
|
||||
dev_servers::reconnect_to_dev_server_project, open_dev_server_project, open_ssh_project,
|
||||
DevServerProjects,
|
||||
};
|
||||
|
||||
enum Host {
|
||||
RemoteProject,
|
||||
DevServerProject(DevServer),
|
||||
SshRemoteProject(SshConnectionOptions),
|
||||
}
|
||||
|
||||
pub struct DisconnectedOverlay {
|
||||
workspace: WeakView<Workspace>,
|
||||
dev_server: Option<DevServer>,
|
||||
host: Host,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
|
@ -32,7 +42,10 @@ impl ModalView for DisconnectedOverlay {
|
|||
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) {
|
||||
if !matches!(
|
||||
event,
|
||||
project::Event::DisconnectedFromHost | project::Event::DisconnectedFromSshRemote
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let handle = cx.view().downgrade();
|
||||
|
@ -45,9 +58,19 @@ impl DisconnectedOverlay {
|
|||
.dev_server_for_project(id)
|
||||
})
|
||||
.cloned();
|
||||
|
||||
let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
|
||||
let host = if let Some(dev_server) = dev_server {
|
||||
Host::DevServerProject(dev_server)
|
||||
} else if let Some(ssh_connection_options) = ssh_connection_options {
|
||||
Host::SshRemoteProject(ssh_connection_options)
|
||||
} else {
|
||||
Host::RemoteProject
|
||||
};
|
||||
|
||||
workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
|
||||
workspace: handle,
|
||||
dev_server,
|
||||
host,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
})
|
||||
|
@ -56,12 +79,22 @@ impl DisconnectedOverlay {
|
|||
|
||||
fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
match &self.host {
|
||||
Host::DevServerProject(dev_server) => {
|
||||
self.reconnect_to_dev_server(dev_server.clone(), cx);
|
||||
}
|
||||
Host::SshRemoteProject(ssh_connection_options) => {
|
||||
self.reconnect_to_ssh_remote(ssh_connection_options.clone(), cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn reconnect_to_dev_server(&self, dev_server: DevServer, cx: &mut ViewContext<Self>) {
|
||||
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()
|
||||
|
@ -102,6 +135,44 @@ impl DisconnectedOverlay {
|
|||
}
|
||||
}
|
||||
|
||||
fn reconnect_to_ssh_remote(
|
||||
&self,
|
||||
connection_options: SshConnectionOptions,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(window) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let app_state = workspace.read(cx).app_state().clone();
|
||||
|
||||
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
|
||||
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
open_ssh_project(
|
||||
connection_options,
|
||||
paths,
|
||||
app_state,
|
||||
OpenOptions {
|
||||
replace_window: Some(window),
|
||||
..Default::default()
|
||||
},
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
|
@ -109,6 +180,23 @@ impl DisconnectedOverlay {
|
|||
|
||||
impl Render for DisconnectedOverlay {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let can_reconnect = matches!(
|
||||
self.host,
|
||||
Host::DevServerProject(_) | Host::SshRemoteProject(_)
|
||||
);
|
||||
|
||||
let message = match &self.host {
|
||||
Host::RemoteProject | Host::DevServerProject(_) => {
|
||||
"Your connection to the remote project has been lost.".to_string()
|
||||
}
|
||||
Host::SshRemoteProject(options) => {
|
||||
format!(
|
||||
"Your connection to {} has been lost",
|
||||
options.connection_string()
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.elevation_3(cx)
|
||||
|
@ -123,9 +211,7 @@ impl Render for DisconnectedOverlay {
|
|||
.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.",
|
||||
)))
|
||||
.section(Section::new().child(Label::new(message)))
|
||||
.footer(
|
||||
ModalFooter::new().end_slot(
|
||||
h_flex()
|
||||
|
@ -138,7 +224,7 @@ impl Render for DisconnectedOverlay {
|
|||
cx.remove_window();
|
||||
})),
|
||||
)
|
||||
.when_some(self.dev_server.clone(), |el, _| {
|
||||
.when(can_reconnect, |el| {
|
||||
el.child(
|
||||
Button::new("reconnect", "Reconnect")
|
||||
.style(ButtonStyle::Filled)
|
||||
|
|
|
@ -5,4 +5,5 @@ pub mod ssh_session;
|
|||
|
||||
pub use ssh_session::{
|
||||
ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient,
|
||||
SshRemoteEvent,
|
||||
};
|
||||
|
|
|
@ -17,7 +17,8 @@ use futures::{
|
|||
StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, Model, ModelContext, SemanticVersion, Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SemanticVersion, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use rpc::{
|
||||
|
@ -315,6 +316,10 @@ impl State {
|
|||
matches!(self, Self::ReconnectFailed { .. })
|
||||
}
|
||||
|
||||
fn is_reconnect_exhausted(&self) -> bool {
|
||||
matches!(self, Self::ReconnectExhausted { .. })
|
||||
}
|
||||
|
||||
fn is_reconnecting(&self) -> bool {
|
||||
matches!(self, Self::Reconnecting { .. })
|
||||
}
|
||||
|
@ -376,7 +381,7 @@ impl State {
|
|||
}
|
||||
|
||||
/// The state of the ssh connection.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
Connecting,
|
||||
Connected,
|
||||
|
@ -411,6 +416,13 @@ impl Drop for SshRemoteClient {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SshRemoteEvent {
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
impl EventEmitter<SshRemoteEvent> for SshRemoteClient {}
|
||||
|
||||
impl SshRemoteClient {
|
||||
pub fn new(
|
||||
unique_identifier: String,
|
||||
|
@ -672,6 +684,9 @@ impl SshRemoteClient {
|
|||
|
||||
if this.state_is(State::is_reconnect_failed) {
|
||||
this.reconnect(cx)
|
||||
} else if this.state_is(State::is_reconnect_exhausted) {
|
||||
cx.emit(SshRemoteEvent::Disconnected);
|
||||
Ok(())
|
||||
} else {
|
||||
log::debug!("State has transition from Reconnecting into new state while attempting reconnect. Ignoring new state.");
|
||||
Ok(())
|
||||
|
@ -851,11 +866,15 @@ impl SshRemoteClient {
|
|||
log::error!("failed to reconnect because server is not running");
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_state(State::ServerNotRunning, cx);
|
||||
cx.emit(SshRemoteEvent::Disconnected);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
} else if exit_code > 0 {
|
||||
log::error!("proxy process terminated unexpectedly");
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.reconnect(cx).ok();
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
|
@ -963,6 +982,11 @@ impl SshRemoteClient {
|
|||
self.connection_options.connection_string()
|
||||
}
|
||||
|
||||
pub fn connection_options(&self) -> SshConnectionOptions {
|
||||
self.connection_options.clone()
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub fn connection_state(&self) -> ConnectionState {
|
||||
self.state
|
||||
.lock()
|
||||
|
@ -971,6 +995,15 @@ impl SshRemoteClient {
|
|||
.unwrap_or(ConnectionState::Disconnected)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn connection_state(&self) -> ConnectionState {
|
||||
ConnectionState::Connected
|
||||
}
|
||||
|
||||
pub fn is_disconnected(&self) -> bool {
|
||||
self.connection_state() == ConnectionState::Disconnected
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake(
|
||||
client_cx: &mut gpui::TestAppContext,
|
||||
|
|
|
@ -366,7 +366,7 @@ impl TitleBar {
|
|||
return self.render_ssh_project_host(cx);
|
||||
}
|
||||
|
||||
if self.project.read(cx).is_disconnected() {
|
||||
if self.project.read(cx).is_disconnected(cx) {
|
||||
return Some(
|
||||
Button::new("disconnected", "Disconnected")
|
||||
.disabled(true)
|
||||
|
|
|
@ -823,6 +823,10 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
project::Event::DisconnectedFromSshRemote => {
|
||||
this.update_window_edited(cx);
|
||||
}
|
||||
|
||||
project::Event::Closed => {
|
||||
cx.remove_window();
|
||||
}
|
||||
|
@ -1464,6 +1468,10 @@ impl Workspace {
|
|||
self.on_prompt_for_open_path = Some(prompt)
|
||||
}
|
||||
|
||||
pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
|
||||
self.serialized_ssh_project.clone()
|
||||
}
|
||||
|
||||
pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
|
||||
self.serialized_ssh_project = Some(serialized_ssh_project);
|
||||
}
|
||||
|
@ -1791,7 +1799,7 @@ impl Workspace {
|
|||
mut save_intent: SaveIntent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
if self.project.read(cx).is_disconnected() {
|
||||
if self.project.read(cx).is_disconnected(cx) {
|
||||
return Task::ready(Ok(true));
|
||||
}
|
||||
let dirty_items = self
|
||||
|
@ -3447,7 +3455,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
fn update_window_edited(&mut self, cx: &mut WindowContext) {
|
||||
let is_edited = !self.project.read(cx).is_disconnected()
|
||||
let is_edited = !self.project.read(cx).is_disconnected(cx)
|
||||
&& self
|
||||
.items(cx)
|
||||
.any(|item| item.has_conflict(cx) || item.is_dirty(cx));
|
||||
|
@ -4858,7 +4866,7 @@ impl Render for Workspace {
|
|||
.children(self.render_notifications(cx)),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.children(if self.project.read(cx).is_disconnected() {
|
||||
.children(if self.project.read(cx).is_disconnected(cx) {
|
||||
if let Some(render) = self.render_disconnected_overlay.take() {
|
||||
let result = render(self, cx);
|
||||
self.render_disconnected_overlay = Some(render);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue