diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 3c36c5f877..a637bfd43f 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -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 } diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index 8df318dc29..06b14bee5e 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -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::(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)) diff --git a/crates/collab/src/tests/dev_server_tests.rs b/crates/collab/src/tests/dev_server_tests.rs index 5acdeb706e..cbeb2a85a0 100644 --- a/crates/collab/src/tests/dev_server_tests.rs +++ b/crates/collab/src/tests/dev_server_tests.rs @@ -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, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index f9bc21efb1..16deef70d5 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -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())); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 2859113634..cc8c35fbc3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -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)); }); } diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 19d37f8786..47f6a38073 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -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 { diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index c788dd28e0..7bf1034cea 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -532,9 +532,9 @@ impl TestPlan { 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() ) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b33416228e..7a7bd2863b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -647,17 +647,10 @@ pub struct FormattableBuffer { } pub struct RemoteLspStore { - upstream_client: AnyProtoClient, + upstream_client: Option, upstream_project_id: u64, } -impl RemoteLspStore {} - -// pub struct SshLspStore { -// upstream_client: AnyProtoClient, -// current_lsp_settings: HashMap, -// } - #[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, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 7f02d3f6dd..7f452e8643 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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), 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 { + 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) { - 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, cx: &mut ModelContext, ) -> Task>> { - 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, + event: &remote::SshRemoteEvent, + cx: &mut ModelContext, + ) { + 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, diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 1fc04a0d0b..41b475d785 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -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(); } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 39130513ee..26a2bf27e0 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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)) diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index f488150c83..cec38ee598 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -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, - dev_server: Option, + host: Host, focus_handle: FocusHandle, } @@ -32,7 +42,10 @@ impl ModalView for DisconnectedOverlay { impl DisconnectedOverlay { pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { cx.subscribe(workspace.project(), |workspace, project, event, cx| { - if !matches!(event, project::Event::DisconnectedFromHost) { + 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) { 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) { 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, + ) { + 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::() 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) { cx.emit(DismissEvent) } @@ -109,6 +180,23 @@ impl DisconnectedOverlay { impl Render for DisconnectedOverlay { fn render(&mut self, cx: &mut ViewContext) -> 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) diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 382adf0dac..43eb59c0ae 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -5,4 +5,5 @@ pub mod ssh_session; pub use ssh_session::{ ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient, + SshRemoteEvent, }; diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index e709fd726e..48b1641f3d 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -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 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, diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 999165b9d8..4e196b8bbf 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -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) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 814c7fa915..11acee8493 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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 { + 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, ) -> Task> { - 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);