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