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