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 if let Host::SshRemoteProject(ssh_connection_options) = &self.host {
92 self.reconnect_to_ssh_remote(ssh_connection_options.clone(), window, cx);
93 }
94 }
95
96 fn reconnect_to_ssh_remote(
97 &self,
98 connection_options: SshConnectionOptions,
99 window: &mut Window,
100 cx: &mut Context<Self>,
101 ) {
102 let Some(workspace) = self.workspace.upgrade() else {
103 return;
104 };
105
106 let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else {
107 return;
108 };
109
110 let Some(window_handle) = window.window_handle().downcast::<Workspace>() else {
111 return;
112 };
113
114 let app_state = workspace.read(cx).app_state().clone();
115
116 let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
117
118 cx.spawn_in(window, async move |_, cx| {
119 open_ssh_project(
120 connection_options,
121 paths,
122 app_state,
123 OpenOptions {
124 replace_window: Some(window_handle),
125 ..Default::default()
126 },
127 cx,
128 )
129 .await?;
130 Ok(())
131 })
132 .detach_and_prompt_err("Failed to reconnect", window, cx, |_, _, _| None);
133 }
134
135 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
136 self.finished = true;
137 cx.emit(DismissEvent)
138 }
139}
140
141impl Render for DisconnectedOverlay {
142 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
143 let can_reconnect = matches!(self.host, Host::SshRemoteProject(_));
144
145 let message = match &self.host {
146 Host::RemoteProject => {
147 "Your connection to the remote project has been lost.".to_string()
148 }
149 Host::SshRemoteProject(options) => {
150 let autosave = if ProjectSettings::get_global(cx)
151 .session
152 .restore_unsaved_buffers
153 {
154 "\nUnsaved changes are stored locally."
155 } else {
156 ""
157 };
158 format!(
159 "Your connection to {} has been lost.{}",
160 options.host, autosave
161 )
162 }
163 };
164
165 div()
166 .track_focus(&self.focus_handle(cx))
167 .elevation_3(cx)
168 .on_action(cx.listener(Self::cancel))
169 .occlude()
170 .w(rems(24.))
171 .max_h(rems(40.))
172 .child(
173 Modal::new("disconnected", None)
174 .header(
175 ModalHeader::new()
176 .show_dismiss_button(true)
177 .child(Headline::new("Disconnected").size(HeadlineSize::Small)),
178 )
179 .section(Section::new().child(Label::new(message)))
180 .footer(
181 ModalFooter::new().end_slot(
182 h_flex()
183 .gap_2()
184 .child(
185 Button::new("close-window", "Close Window")
186 .style(ButtonStyle::Filled)
187 .layer(ElevationIndex::ModalSurface)
188 .on_click(cx.listener(move |_, _, window, _| {
189 window.remove_window();
190 })),
191 )
192 .when(can_reconnect, |el| {
193 el.child(
194 Button::new("reconnect", "Reconnect")
195 .style(ButtonStyle::Filled)
196 .layer(ElevationIndex::ModalSurface)
197 .icon(IconName::ArrowCircle)
198 .icon_position(IconPosition::Start)
199 .on_click(cx.listener(Self::handle_reconnect)),
200 )
201 }),
202 ),
203 ),
204 )
205 }
206}