1use std::path::PathBuf;
2
3use dev_server_projects::DevServer;
4use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
5use project::project_settings::ProjectSettings;
6use remote::SshConnectionOptions;
7use settings::Settings;
8use ui::{
9 div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder,
10 Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
11 ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, ViewContext,
12};
13use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
14
15use crate::{
16 open_dev_server_project, open_ssh_project, remote_servers::reconnect_to_dev_server_project,
17 RemoteServerProjects, SshSettings,
18};
19
20enum Host {
21 RemoteProject,
22 DevServerProject(DevServer),
23 SshRemoteProject(SshConnectionOptions),
24}
25
26pub struct DisconnectedOverlay {
27 workspace: WeakView<Workspace>,
28 host: Host,
29 focus_handle: FocusHandle,
30 finished: bool,
31}
32
33impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
34impl FocusableView for DisconnectedOverlay {
35 fn focus_handle(&self, _cx: &gpui::AppContext) -> gpui::FocusHandle {
36 self.focus_handle.clone()
37 }
38}
39impl ModalView for DisconnectedOverlay {
40 fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> workspace::DismissDecision {
41 return workspace::DismissDecision::Dismiss(self.finished);
42 }
43 fn fade_out_background(&self) -> bool {
44 true
45 }
46}
47
48impl DisconnectedOverlay {
49 pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
50 cx.subscribe(workspace.project(), |workspace, project, event, cx| {
51 if !matches!(
52 event,
53 project::Event::DisconnectedFromHost | project::Event::DisconnectedFromSshRemote
54 ) {
55 return;
56 }
57 let handle = cx.view().downgrade();
58 let dev_server = project
59 .read(cx)
60 .dev_server_project_id()
61 .and_then(|id| {
62 dev_server_projects::Store::global(cx)
63 .read(cx)
64 .dev_server_for_project(id)
65 })
66 .cloned();
67
68 let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
69 let host = if let Some(dev_server) = dev_server {
70 Host::DevServerProject(dev_server)
71 } else if let Some(ssh_connection_options) = ssh_connection_options {
72 Host::SshRemoteProject(ssh_connection_options)
73 } else {
74 Host::RemoteProject
75 };
76
77 workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
78 finished: false,
79 workspace: handle,
80 host,
81 focus_handle: cx.focus_handle(),
82 });
83 })
84 .detach();
85 }
86
87 fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
88 self.finished = true;
89 cx.emit(DismissEvent);
90
91 match &self.host {
92 Host::DevServerProject(dev_server) => {
93 self.reconnect_to_dev_server(dev_server.clone(), cx);
94 }
95 Host::SshRemoteProject(ssh_connection_options) => {
96 self.reconnect_to_ssh_remote(ssh_connection_options.clone(), cx);
97 }
98 _ => {}
99 }
100 }
101
102 fn reconnect_to_dev_server(&self, dev_server: DevServer, cx: &mut ViewContext<Self>) {
103 let Some(workspace) = self.workspace.upgrade() else {
104 return;
105 };
106 let Some(dev_server_project_id) = workspace
107 .read(cx)
108 .project()
109 .read(cx)
110 .dev_server_project_id()
111 else {
112 return;
113 };
114
115 if let Some(project_id) = dev_server_projects::Store::global(cx)
116 .read(cx)
117 .dev_server_project(dev_server_project_id)
118 .and_then(|project| project.project_id)
119 {
120 return workspace.update(cx, move |_, cx| {
121 open_dev_server_project(true, dev_server_project_id, project_id, cx)
122 .detach_and_prompt_err("Failed to reconnect", cx, |_, _| None)
123 });
124 }
125
126 if dev_server.ssh_connection_string.is_some() {
127 let task = workspace.update(cx, |_, cx| {
128 reconnect_to_dev_server_project(
129 cx.view().clone(),
130 dev_server,
131 dev_server_project_id,
132 true,
133 cx,
134 )
135 });
136
137 task.detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
138 } else {
139 return workspace.update(cx, |workspace, cx| {
140 let handle = cx.view().downgrade();
141 workspace.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, handle))
142 });
143 }
144 }
145
146 fn reconnect_to_ssh_remote(
147 &self,
148 connection_options: SshConnectionOptions,
149 cx: &mut ViewContext<Self>,
150 ) {
151 let Some(workspace) = self.workspace.upgrade() else {
152 return;
153 };
154
155 let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else {
156 return;
157 };
158
159 let Some(window) = cx.window_handle().downcast::<Workspace>() else {
160 return;
161 };
162
163 let app_state = workspace.read(cx).app_state().clone();
164
165 let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
166
167 cx.spawn(move |_, mut cx| async move {
168 let nickname = cx
169 .update(|cx| {
170 SshSettings::get_global(cx).nickname_for(
171 &connection_options.host,
172 connection_options.port,
173 &connection_options.username,
174 )
175 })
176 .ok()
177 .flatten();
178 open_ssh_project(
179 connection_options,
180 paths,
181 app_state,
182 OpenOptions {
183 replace_window: Some(window),
184 ..Default::default()
185 },
186 nickname,
187 &mut cx,
188 )
189 .await?;
190 Ok(())
191 })
192 .detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
193 }
194
195 fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
196 self.finished = true;
197 cx.emit(DismissEvent)
198 }
199}
200
201impl Render for DisconnectedOverlay {
202 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
203 let can_reconnect = matches!(
204 self.host,
205 Host::DevServerProject(_) | Host::SshRemoteProject(_)
206 );
207
208 let message = match &self.host {
209 Host::RemoteProject | Host::DevServerProject(_) => {
210 "Your connection to the remote project has been lost.".to_string()
211 }
212 Host::SshRemoteProject(options) => {
213 let autosave = if ProjectSettings::get_global(cx)
214 .session
215 .restore_unsaved_buffers
216 {
217 "\nUnsaved changes are stored locally."
218 } else {
219 ""
220 };
221 format!(
222 "Your connection to {} has been lost.{}",
223 options.host, autosave
224 )
225 }
226 };
227
228 div()
229 .track_focus(&self.focus_handle)
230 .elevation_3(cx)
231 .on_action(cx.listener(Self::cancel))
232 .occlude()
233 .w(rems(24.))
234 .max_h(rems(40.))
235 .child(
236 Modal::new("disconnected", None)
237 .header(
238 ModalHeader::new()
239 .show_dismiss_button(true)
240 .child(Headline::new("Disconnected").size(HeadlineSize::Small)),
241 )
242 .section(Section::new().child(Label::new(message)))
243 .footer(
244 ModalFooter::new().end_slot(
245 h_flex()
246 .gap_2()
247 .child(
248 Button::new("close-window", "Close Window")
249 .style(ButtonStyle::Filled)
250 .layer(ElevationIndex::ModalSurface)
251 .on_click(cx.listener(move |_, _, cx| {
252 cx.remove_window();
253 })),
254 )
255 .when(can_reconnect, |el| {
256 el.child(
257 Button::new("reconnect", "Reconnect")
258 .style(ButtonStyle::Filled)
259 .layer(ElevationIndex::ModalSurface)
260 .icon(IconName::ArrowCircle)
261 .icon_position(IconPosition::Start)
262 .on_click(cx.listener(Self::handle_reconnect)),
263 )
264 }),
265 ),
266 ),
267 )
268 }
269}