disconnected_overlay.rs

  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}