Detailed changes
@@ -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);