1use std::path::PathBuf;
2
3use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
4use project::project_settings::ProjectSettings;
5use remote::SshConnectionOptions;
6use settings::Settings;
7use ui::{
8 div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder,
9 Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
10 ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, ViewContext,
11};
12use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
13
14use crate::open_ssh_project;
15
16enum Host {
17 RemoteProject,
18 SshRemoteProject(SshConnectionOptions),
19}
20
21pub struct DisconnectedOverlay {
22 workspace: WeakView<Workspace>,
23 host: Host,
24 focus_handle: FocusHandle,
25 finished: bool,
26}
27
28impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
29impl FocusableView for DisconnectedOverlay {
30 fn focus_handle(&self, _cx: &gpui::AppContext) -> gpui::FocusHandle {
31 self.focus_handle.clone()
32 }
33}
34impl ModalView for DisconnectedOverlay {
35 fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> workspace::DismissDecision {
36 return workspace::DismissDecision::Dismiss(self.finished);
37 }
38 fn fade_out_background(&self) -> bool {
39 true
40 }
41}
42
43impl DisconnectedOverlay {
44 pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
45 cx.subscribe(workspace.project(), |workspace, project, event, cx| {
46 if !matches!(
47 event,
48 project::Event::DisconnectedFromHost | project::Event::DisconnectedFromSshRemote
49 ) {
50 return;
51 }
52 let handle = cx.view().downgrade();
53
54 let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
55 let host = if let Some(ssh_connection_options) = ssh_connection_options {
56 Host::SshRemoteProject(ssh_connection_options)
57 } else {
58 Host::RemoteProject
59 };
60
61 workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
62 finished: false,
63 workspace: handle,
64 host,
65 focus_handle: cx.focus_handle(),
66 });
67 })
68 .detach();
69 }
70
71 fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
72 self.finished = true;
73 cx.emit(DismissEvent);
74
75 match &self.host {
76 Host::SshRemoteProject(ssh_connection_options) => {
77 self.reconnect_to_ssh_remote(ssh_connection_options.clone(), cx);
78 }
79 _ => {}
80 }
81 }
82
83 fn reconnect_to_ssh_remote(
84 &self,
85 connection_options: SshConnectionOptions,
86 cx: &mut ViewContext<Self>,
87 ) {
88 let Some(workspace) = self.workspace.upgrade() else {
89 return;
90 };
91
92 let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else {
93 return;
94 };
95
96 let Some(window) = cx.window_handle().downcast::<Workspace>() else {
97 return;
98 };
99
100 let app_state = workspace.read(cx).app_state().clone();
101
102 let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
103
104 cx.spawn(move |_, mut cx| async move {
105 open_ssh_project(
106 connection_options,
107 paths,
108 app_state,
109 OpenOptions {
110 replace_window: Some(window),
111 ..Default::default()
112 },
113 &mut cx,
114 )
115 .await?;
116 Ok(())
117 })
118 .detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
119 }
120
121 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
122 self.finished = true;
123 cx.emit(DismissEvent)
124 }
125}
126
127impl Render for DisconnectedOverlay {
128 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
129 let can_reconnect = matches!(self.host, Host::SshRemoteProject(_));
130
131 let message = match &self.host {
132 Host::RemoteProject => {
133 "Your connection to the remote project has been lost.".to_string()
134 }
135 Host::SshRemoteProject(options) => {
136 let autosave = if ProjectSettings::get_global(cx)
137 .session
138 .restore_unsaved_buffers
139 {
140 "\nUnsaved changes are stored locally."
141 } else {
142 ""
143 };
144 format!(
145 "Your connection to {} has been lost.{}",
146 options.host, autosave
147 )
148 }
149 };
150
151 div()
152 .track_focus(&self.focus_handle(cx))
153 .elevation_3(cx)
154 .on_action(cx.listener(Self::cancel))
155 .occlude()
156 .w(rems(24.))
157 .max_h(rems(40.))
158 .child(
159 Modal::new("disconnected", None)
160 .header(
161 ModalHeader::new()
162 .show_dismiss_button(true)
163 .child(Headline::new("Disconnected").size(HeadlineSize::Small)),
164 )
165 .section(Section::new().child(Label::new(message)))
166 .footer(
167 ModalFooter::new().end_slot(
168 h_flex()
169 .gap_2()
170 .child(
171 Button::new("close-window", "Close Window")
172 .style(ButtonStyle::Filled)
173 .layer(ElevationIndex::ModalSurface)
174 .on_click(cx.listener(move |_, _, cx| {
175 cx.remove_window();
176 })),
177 )
178 .when(can_reconnect, |el| {
179 el.child(
180 Button::new("reconnect", "Reconnect")
181 .style(ButtonStyle::Filled)
182 .layer(ElevationIndex::ModalSurface)
183 .icon(IconName::ArrowCircle)
184 .icon_position(IconPosition::Start)
185 .on_click(cx.listener(Self::handle_reconnect)),
186 )
187 }),
188 ),
189 ),
190 )
191 }
192}