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