remote_connections.rs

  1use std::{
  2    path::{Path, PathBuf},
  3    sync::Arc,
  4};
  5
  6use anyhow::{Context as _, Result};
  7use askpass::EncryptedPassword;
  8use auto_update::AutoUpdater;
  9use editor::Editor;
 10use extension_host::ExtensionStore;
 11use futures::channel::oneshot;
 12use gpui::{
 13    AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures,
 14    ParentElement as _, PromptLevel, Render, SharedString, Task, TextStyleRefinement, WeakEntity,
 15};
 16
 17use language::{CursorShape, Point};
 18use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 19use release_channel::ReleaseChannel;
 20use remote::{
 21    ConnectionIdentifier, DockerConnectionOptions, RemoteClient, RemoteConnection,
 22    RemoteConnectionOptions, RemotePlatform, SshConnectionOptions,
 23};
 24use semver::Version;
 25pub use settings::SshConnection;
 26use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
 27use theme::ThemeSettings;
 28use ui::{
 29    ActiveTheme, Color, CommonAnimationExt, Context, InteractiveElement, IntoElement, KeyBinding,
 30    LabelCommon, ListItem, Styled, Window, prelude::*,
 31};
 32use util::paths::PathWithPosition;
 33use workspace::{AppState, ModalView, Workspace};
 34
 35#[derive(RegisterSetting)]
 36pub struct SshSettings {
 37    pub ssh_connections: ExtendingVec<SshConnection>,
 38    pub wsl_connections: ExtendingVec<WslConnection>,
 39    /// Whether to read ~/.ssh/config for ssh connection sources.
 40    pub read_ssh_config: bool,
 41}
 42
 43impl SshSettings {
 44    pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {
 45        self.ssh_connections.clone().0.into_iter()
 46    }
 47
 48    pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {
 49        self.wsl_connections.clone().0.into_iter()
 50    }
 51
 52    pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
 53        for conn in self.ssh_connections() {
 54            if conn.host == options.host
 55                && conn.username == options.username
 56                && conn.port == options.port
 57            {
 58                options.nickname = conn.nickname;
 59                options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default();
 60                options.args = Some(conn.args);
 61                options.port_forwards = conn.port_forwards;
 62                break;
 63            }
 64        }
 65    }
 66
 67    pub fn connection_options_for(
 68        &self,
 69        host: String,
 70        port: Option<u16>,
 71        username: Option<String>,
 72    ) -> SshConnectionOptions {
 73        let mut options = SshConnectionOptions {
 74            host,
 75            port,
 76            username,
 77            ..Default::default()
 78        };
 79        self.fill_connection_options_from_settings(&mut options);
 80        options
 81    }
 82}
 83
 84#[derive(Clone, PartialEq)]
 85pub enum Connection {
 86    Ssh(SshConnection),
 87    Wsl(WslConnection),
 88    DevContainer(DevContainerConnection),
 89}
 90
 91impl From<Connection> for RemoteConnectionOptions {
 92    fn from(val: Connection) -> Self {
 93        match val {
 94            Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()),
 95            Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()),
 96            Connection::DevContainer(conn) => {
 97                RemoteConnectionOptions::Docker(DockerConnectionOptions {
 98                    name: conn.name.to_string(),
 99                    container_id: conn.container_id.to_string(),
100                    upload_binary_over_docker_exec: false,
101                })
102            }
103        }
104    }
105}
106
107impl From<SshConnection> for Connection {
108    fn from(val: SshConnection) -> Self {
109        Connection::Ssh(val)
110    }
111}
112
113impl From<WslConnection> for Connection {
114    fn from(val: WslConnection) -> Self {
115        Connection::Wsl(val)
116    }
117}
118
119impl Settings for SshSettings {
120    fn from_settings(content: &settings::SettingsContent) -> Self {
121        let remote = &content.remote;
122        Self {
123            ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(),
124            wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(),
125            read_ssh_config: remote.read_ssh_config.unwrap(),
126        }
127    }
128}
129
130pub struct RemoteConnectionPrompt {
131    connection_string: SharedString,
132    nickname: Option<SharedString>,
133    is_wsl: bool,
134    is_devcontainer: bool,
135    status_message: Option<SharedString>,
136    prompt: Option<(Entity<Markdown>, oneshot::Sender<EncryptedPassword>)>,
137    cancellation: Option<oneshot::Sender<()>>,
138    editor: Entity<Editor>,
139}
140
141impl Drop for RemoteConnectionPrompt {
142    fn drop(&mut self) {
143        if let Some(cancel) = self.cancellation.take() {
144            cancel.send(()).ok();
145        }
146    }
147}
148
149pub struct RemoteConnectionModal {
150    pub prompt: Entity<RemoteConnectionPrompt>,
151    paths: Vec<PathBuf>,
152    finished: bool,
153}
154
155impl RemoteConnectionPrompt {
156    pub(crate) fn new(
157        connection_string: String,
158        nickname: Option<String>,
159        is_wsl: bool,
160        is_devcontainer: bool,
161        window: &mut Window,
162        cx: &mut Context<Self>,
163    ) -> Self {
164        Self {
165            connection_string: connection_string.into(),
166            nickname: nickname.map(|nickname| nickname.into()),
167            is_wsl,
168            is_devcontainer,
169            editor: cx.new(|cx| Editor::single_line(window, cx)),
170            status_message: None,
171            cancellation: None,
172            prompt: None,
173        }
174    }
175
176    pub fn set_cancellation_tx(&mut self, tx: oneshot::Sender<()>) {
177        self.cancellation = Some(tx);
178    }
179
180    fn set_prompt(
181        &mut self,
182        prompt: String,
183        tx: oneshot::Sender<EncryptedPassword>,
184        window: &mut Window,
185        cx: &mut Context<Self>,
186    ) {
187        let theme = ThemeSettings::get_global(cx);
188
189        let refinement = TextStyleRefinement {
190            font_family: Some(theme.buffer_font.family.clone()),
191            font_features: Some(FontFeatures::disable_ligatures()),
192            font_size: Some(theme.buffer_font_size(cx).into()),
193            color: Some(cx.theme().colors().editor_foreground),
194            background_color: Some(gpui::transparent_black()),
195            ..Default::default()
196        };
197
198        self.editor.update(cx, |editor, cx| {
199            if prompt.contains("yes/no") {
200                editor.set_masked(false, cx);
201            } else {
202                editor.set_masked(true, cx);
203            }
204            editor.set_text_style_refinement(refinement);
205            editor.set_cursor_shape(CursorShape::Block, cx);
206        });
207
208        let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx));
209        self.prompt = Some((markdown, tx));
210        self.status_message.take();
211        window.focus(&self.editor.focus_handle(cx));
212        cx.notify();
213    }
214
215    pub fn set_status(&mut self, status: Option<String>, cx: &mut Context<Self>) {
216        self.status_message = status.map(|s| s.into());
217        cx.notify();
218    }
219
220    pub fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
221        if let Some((_, tx)) = self.prompt.take() {
222            self.status_message = Some("Connecting".into());
223
224            self.editor.update(cx, |editor, cx| {
225                let pw = editor.text(cx);
226                if let Ok(secure) = EncryptedPassword::try_from(pw.as_ref()) {
227                    tx.send(secure).ok();
228                }
229                editor.clear(window, cx);
230            });
231        }
232    }
233}
234
235impl Render for RemoteConnectionPrompt {
236    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
237        let theme = ThemeSettings::get_global(cx);
238
239        let mut text_style = window.text_style();
240        let refinement = TextStyleRefinement {
241            font_family: Some(theme.buffer_font.family.clone()),
242            font_features: Some(FontFeatures::disable_ligatures()),
243            font_size: Some(theme.buffer_font_size(cx).into()),
244            color: Some(cx.theme().colors().editor_foreground),
245            background_color: Some(gpui::transparent_black()),
246            ..Default::default()
247        };
248
249        text_style.refine(&refinement);
250        let markdown_style = MarkdownStyle {
251            base_text_style: text_style,
252            selection_background_color: cx.theme().colors().element_selection_background,
253            ..Default::default()
254        };
255
256        v_flex()
257            .key_context("PasswordPrompt")
258            .p_2()
259            .size_full()
260            .text_buffer(cx)
261            .when_some(self.status_message.clone(), |el, status_message| {
262                el.child(
263                    h_flex()
264                        .gap_2()
265                        .child(
266                            Icon::new(IconName::ArrowCircle)
267                                .color(Color::Muted)
268                                .with_rotate_animation(2),
269                        )
270                        .child(
271                            div()
272                                .text_ellipsis()
273                                .overflow_x_hidden()
274                                .child(format!("{}", status_message)),
275                        ),
276                )
277            })
278            .when_some(self.prompt.as_ref(), |el, prompt| {
279                el.child(
280                    div()
281                        .size_full()
282                        .overflow_hidden()
283                        .child(MarkdownElement::new(prompt.0.clone(), markdown_style))
284                        .child(self.editor.clone()),
285                )
286                .when(window.capslock().on, |el| {
287                    el.child(Label::new("⚠️ ⇪ is on"))
288                })
289            })
290    }
291}
292
293impl RemoteConnectionModal {
294    pub fn new(
295        connection_options: &RemoteConnectionOptions,
296        paths: Vec<PathBuf>,
297        window: &mut Window,
298        cx: &mut Context<Self>,
299    ) -> Self {
300        let (connection_string, nickname, is_wsl, is_devcontainer) = match connection_options {
301            RemoteConnectionOptions::Ssh(options) => (
302                options.connection_string(),
303                options.nickname.clone(),
304                false,
305                false,
306            ),
307            RemoteConnectionOptions::Wsl(options) => {
308                (options.distro_name.clone(), None, true, false)
309            }
310            RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true),
311        };
312        Self {
313            prompt: cx.new(|cx| {
314                RemoteConnectionPrompt::new(
315                    connection_string,
316                    nickname,
317                    is_wsl,
318                    is_devcontainer,
319                    window,
320                    cx,
321                )
322            }),
323            finished: false,
324            paths,
325        }
326    }
327
328    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
329        self.prompt
330            .update(cx, |prompt, cx| prompt.confirm(window, cx))
331    }
332
333    pub fn finished(&mut self, cx: &mut Context<Self>) {
334        self.finished = true;
335        cx.emit(DismissEvent);
336    }
337
338    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
339        if let Some(tx) = self
340            .prompt
341            .update(cx, |prompt, _cx| prompt.cancellation.take())
342        {
343            tx.send(()).ok();
344        }
345        self.finished(cx);
346    }
347}
348
349pub(crate) struct SshConnectionHeader {
350    pub(crate) connection_string: SharedString,
351    pub(crate) paths: Vec<PathBuf>,
352    pub(crate) nickname: Option<SharedString>,
353    pub(crate) is_wsl: bool,
354    pub(crate) is_devcontainer: bool,
355}
356
357impl RenderOnce for SshConnectionHeader {
358    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
359        let theme = cx.theme();
360
361        let mut header_color = theme.colors().text;
362        header_color.fade_out(0.96);
363
364        let (main_label, meta_label) = if let Some(nickname) = self.nickname {
365            (nickname, Some(format!("({})", self.connection_string)))
366        } else {
367            (self.connection_string, None)
368        };
369
370        let icon = if self.is_wsl {
371            IconName::Linux
372        } else if self.is_devcontainer {
373            IconName::Box
374        } else {
375            IconName::Server
376        };
377
378        h_flex()
379            .px(DynamicSpacing::Base12.rems(cx))
380            .pt(DynamicSpacing::Base08.rems(cx))
381            .pb(DynamicSpacing::Base04.rems(cx))
382            .rounded_t_sm()
383            .w_full()
384            .gap_1p5()
385            .child(Icon::new(icon).size(IconSize::Small))
386            .child(
387                h_flex()
388                    .gap_1()
389                    .overflow_x_hidden()
390                    .child(
391                        div()
392                            .max_w_96()
393                            .overflow_x_hidden()
394                            .text_ellipsis()
395                            .child(Headline::new(main_label).size(HeadlineSize::XSmall)),
396                    )
397                    .children(
398                        meta_label.map(|label| {
399                            Label::new(label).color(Color::Muted).size(LabelSize::Small)
400                        }),
401                    )
402                    .child(div().overflow_x_hidden().text_ellipsis().children(
403                        self.paths.into_iter().map(|path| {
404                            Label::new(path.to_string_lossy().into_owned())
405                                .size(LabelSize::Small)
406                                .color(Color::Muted)
407                        }),
408                    )),
409            )
410    }
411}
412
413impl Render for RemoteConnectionModal {
414    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
415        let nickname = self.prompt.read(cx).nickname.clone();
416        let connection_string = self.prompt.read(cx).connection_string.clone();
417        let is_wsl = self.prompt.read(cx).is_wsl;
418        let is_devcontainer = self.prompt.read(cx).is_devcontainer;
419
420        let theme = cx.theme().clone();
421        let body_color = theme.colors().editor_background;
422
423        v_flex()
424            .elevation_3(cx)
425            .w(rems(34.))
426            .border_1()
427            .border_color(theme.colors().border)
428            .key_context("SshConnectionModal")
429            .track_focus(&self.focus_handle(cx))
430            .on_action(cx.listener(Self::dismiss))
431            .on_action(cx.listener(Self::confirm))
432            .child(
433                SshConnectionHeader {
434                    paths: self.paths.clone(),
435                    connection_string,
436                    nickname,
437                    is_wsl,
438                    is_devcontainer,
439                }
440                .render(window, cx),
441            )
442            .child(
443                div()
444                    .w_full()
445                    .bg(body_color)
446                    .border_y_1()
447                    .border_color(theme.colors().border_variant)
448                    .child(self.prompt.clone()),
449            )
450            .child(
451                div().w_full().py_1().child(
452                    ListItem::new("li-devcontainer-go-back")
453                        .inset(true)
454                        .spacing(ui::ListItemSpacing::Sparse)
455                        .start_slot(Icon::new(IconName::Close).color(Color::Muted))
456                        .child(Label::new("Cancel"))
457                        .end_slot(
458                            KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle(cx), cx)
459                                .size(rems_from_px(12.)),
460                        )
461                        .on_click(cx.listener(|this, _, window, cx| {
462                            this.dismiss(&menu::Cancel, window, cx);
463                        })),
464                ),
465            )
466    }
467}
468
469impl Focusable for RemoteConnectionModal {
470    fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
471        self.prompt.read(cx).editor.focus_handle(cx)
472    }
473}
474
475impl EventEmitter<DismissEvent> for RemoteConnectionModal {}
476
477impl ModalView for RemoteConnectionModal {
478    fn on_before_dismiss(
479        &mut self,
480        _window: &mut Window,
481        _: &mut Context<Self>,
482    ) -> workspace::DismissDecision {
483        workspace::DismissDecision::Dismiss(self.finished)
484    }
485
486    fn fade_out_background(&self) -> bool {
487        true
488    }
489}
490
491#[derive(Clone)]
492pub struct RemoteClientDelegate {
493    window: AnyWindowHandle,
494    ui: WeakEntity<RemoteConnectionPrompt>,
495    known_password: Option<EncryptedPassword>,
496}
497
498impl remote::RemoteClientDelegate for RemoteClientDelegate {
499    fn ask_password(
500        &self,
501        prompt: String,
502        tx: oneshot::Sender<EncryptedPassword>,
503        cx: &mut AsyncApp,
504    ) {
505        let mut known_password = self.known_password.clone();
506        if let Some(password) = known_password.take() {
507            tx.send(password).ok();
508        } else {
509            self.window
510                .update(cx, |_, window, cx| {
511                    self.ui.update(cx, |modal, cx| {
512                        modal.set_prompt(prompt, tx, window, cx);
513                    })
514                })
515                .ok();
516        }
517    }
518
519    fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
520        self.update_status(status, cx)
521    }
522
523    fn download_server_binary_locally(
524        &self,
525        platform: RemotePlatform,
526        release_channel: ReleaseChannel,
527        version: Option<Version>,
528        cx: &mut AsyncApp,
529    ) -> Task<anyhow::Result<PathBuf>> {
530        let this = self.clone();
531        cx.spawn(async move |cx| {
532            AutoUpdater::download_remote_server_release(
533                release_channel,
534                version.clone(),
535                platform.os,
536                platform.arch,
537                move |status, cx| this.set_status(Some(status), cx),
538                cx,
539            )
540            .await
541            .with_context(|| {
542                format!(
543                    "Downloading remote server binary (version: {}, os: {}, arch: {})",
544                    version
545                        .as_ref()
546                        .map(|v| format!("{}", v))
547                        .unwrap_or("unknown".to_string()),
548                    platform.os,
549                    platform.arch,
550                )
551            })
552        })
553    }
554
555    fn get_download_url(
556        &self,
557        platform: RemotePlatform,
558        release_channel: ReleaseChannel,
559        version: Option<Version>,
560        cx: &mut AsyncApp,
561    ) -> Task<Result<Option<String>>> {
562        cx.spawn(async move |cx| {
563            AutoUpdater::get_remote_server_release_url(
564                release_channel,
565                version,
566                platform.os,
567                platform.arch,
568                cx,
569            )
570            .await
571        })
572    }
573}
574
575impl RemoteClientDelegate {
576    fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
577        self.window
578            .update(cx, |_, _, cx| {
579                self.ui.update(cx, |modal, cx| {
580                    modal.set_status(status.map(|s| s.to_string()), cx);
581                })
582            })
583            .ok();
584    }
585}
586
587pub fn connect(
588    unique_identifier: ConnectionIdentifier,
589    connection_options: RemoteConnectionOptions,
590    ui: Entity<RemoteConnectionPrompt>,
591    window: &mut Window,
592    cx: &mut App,
593) -> Task<Result<Option<Entity<RemoteClient>>>> {
594    let window = window.window_handle();
595    let known_password = match &connection_options {
596        RemoteConnectionOptions::Ssh(ssh_connection_options) => ssh_connection_options
597            .password
598            .as_deref()
599            .and_then(|pw| pw.try_into().ok()),
600        _ => None,
601    };
602    let (tx, rx) = oneshot::channel();
603    ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
604
605    let delegate = Arc::new(RemoteClientDelegate {
606        window,
607        ui: ui.downgrade(),
608        known_password,
609    });
610
611    cx.spawn(async move |cx| {
612        let connection = remote::connect(connection_options, delegate.clone(), cx).await?;
613        cx.update(|cx| remote::RemoteClient::new(unique_identifier, connection, rx, delegate, cx))?
614            .await
615    })
616}
617
618pub async fn open_remote_project(
619    connection_options: RemoteConnectionOptions,
620    paths: Vec<PathBuf>,
621    app_state: Arc<AppState>,
622    open_options: workspace::OpenOptions,
623    cx: &mut AsyncApp,
624) -> Result<()> {
625    let created_new_window = open_options.replace_window.is_none();
626    let window = if let Some(window) = open_options.replace_window {
627        window
628    } else {
629        let workspace_position = cx
630            .update(|cx| {
631                // todo: These paths are wrong they may have column and line information
632                workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
633            })?
634            .await
635            .context("fetching ssh workspace position from db")?;
636
637        let mut options =
638            cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx))?;
639        options.window_bounds = workspace_position.window_bounds;
640
641        cx.open_window(options, |window, cx| {
642            let project = project::Project::local(
643                app_state.client.clone(),
644                app_state.node_runtime.clone(),
645                app_state.user_store.clone(),
646                app_state.languages.clone(),
647                app_state.fs.clone(),
648                None,
649                cx,
650            );
651            cx.new(|cx| {
652                let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);
653                workspace.centered_layout = workspace_position.centered_layout;
654                workspace
655            })
656        })?
657    };
658
659    loop {
660        let (cancel_tx, cancel_rx) = oneshot::channel();
661        let delegate = window.update(cx, {
662            let paths = paths.clone();
663            let connection_options = connection_options.clone();
664            move |workspace, window, cx| {
665                window.activate_window();
666                workspace.toggle_modal(window, cx, |window, cx| {
667                    RemoteConnectionModal::new(&connection_options, paths, window, cx)
668                });
669
670                let ui = workspace
671                    .active_modal::<RemoteConnectionModal>(cx)?
672                    .read(cx)
673                    .prompt
674                    .clone();
675
676                ui.update(cx, |ui, _cx| {
677                    ui.set_cancellation_tx(cancel_tx);
678                });
679
680                Some(Arc::new(RemoteClientDelegate {
681                    window: window.window_handle(),
682                    ui: ui.downgrade(),
683                    known_password: if let RemoteConnectionOptions::Ssh(options) =
684                        &connection_options
685                    {
686                        options
687                            .password
688                            .as_deref()
689                            .and_then(|pw| EncryptedPassword::try_from(pw).ok())
690                    } else {
691                        None
692                    },
693                }))
694            }
695        })?;
696
697        let Some(delegate) = delegate else { break };
698
699        let remote_connection =
700            match remote::connect(connection_options.clone(), delegate.clone(), cx).await {
701                Ok(connection) => connection,
702                Err(e) => {
703                    window
704                        .update(cx, |workspace, _, cx| {
705                            if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
706                                ui.update(cx, |modal, cx| modal.finished(cx))
707                            }
708                        })
709                        .ok();
710                    log::error!("Failed to open project: {e:#}");
711                    let response = window
712                        .update(cx, |_, window, cx| {
713                            window.prompt(
714                                PromptLevel::Critical,
715                                match connection_options {
716                                    RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
717                                    RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
718                                    RemoteConnectionOptions::Docker(_) => {
719                                        "Failed to connect to Dev Container"
720                                    }
721                                },
722                                Some(&format!("{e:#}")),
723                                &["Retry", "Cancel"],
724                                cx,
725                            )
726                        })?
727                        .await;
728
729                    if response == Ok(0) {
730                        continue;
731                    }
732
733                    if created_new_window {
734                        window
735                            .update(cx, |_, window, _| window.remove_window())
736                            .ok();
737                    }
738                    break;
739                }
740            };
741
742        let (paths, paths_with_positions) =
743            determine_paths_with_positions(&remote_connection, paths.clone()).await;
744
745        let opened_items = cx
746            .update(|cx| {
747                workspace::open_remote_project_with_new_connection(
748                    window,
749                    remote_connection,
750                    cancel_rx,
751                    delegate.clone(),
752                    app_state.clone(),
753                    paths.clone(),
754                    cx,
755                )
756            })?
757            .await;
758
759        window
760            .update(cx, |workspace, _, cx| {
761                if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
762                    ui.update(cx, |modal, cx| modal.finished(cx))
763                }
764            })
765            .ok();
766
767        match opened_items {
768            Err(e) => {
769                log::error!("Failed to open project: {e:#}");
770                let response = window
771                    .update(cx, |_, window, cx| {
772                        window.prompt(
773                            PromptLevel::Critical,
774                            match connection_options {
775                                RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
776                                RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
777                                RemoteConnectionOptions::Docker(_) => {
778                                    "Failed to connect to Dev Container"
779                                }
780                            },
781                            Some(&format!("{e:#}")),
782                            &["Retry", "Cancel"],
783                            cx,
784                        )
785                    })?
786                    .await;
787                if response == Ok(0) {
788                    continue;
789                }
790
791                if created_new_window {
792                    window
793                        .update(cx, |_, window, _| window.remove_window())
794                        .ok();
795                }
796            }
797
798            Ok(items) => {
799                for (item, path) in items.into_iter().zip(paths_with_positions) {
800                    let Some(item) = item else {
801                        continue;
802                    };
803                    let Some(row) = path.row else {
804                        continue;
805                    };
806                    if let Some(active_editor) = item.downcast::<Editor>() {
807                        window
808                            .update(cx, |_, window, cx| {
809                                active_editor.update(cx, |editor, cx| {
810                                    let row = row.saturating_sub(1);
811                                    let col = path.column.unwrap_or(0).saturating_sub(1);
812                                    editor.go_to_singleton_buffer_point(
813                                        Point::new(row, col),
814                                        window,
815                                        cx,
816                                    );
817                                });
818                            })
819                            .ok();
820                    }
821                }
822            }
823        }
824
825        window
826            .update(cx, |workspace, _, cx| {
827                if let Some(client) = workspace.project().read(cx).remote_client() {
828                    ExtensionStore::global(cx)
829                        .update(cx, |store, cx| store.register_remote_client(client, cx));
830                }
831            })
832            .ok();
833
834        break;
835    }
836
837    // Already showed the error to the user
838    Ok(())
839}
840
841pub(crate) async fn determine_paths_with_positions(
842    remote_connection: &Arc<dyn RemoteConnection>,
843    mut paths: Vec<PathBuf>,
844) -> (Vec<PathBuf>, Vec<PathWithPosition>) {
845    let mut paths_with_positions = Vec::<PathWithPosition>::new();
846    for path in &mut paths {
847        if let Some(path_str) = path.to_str() {
848            let path_with_position = PathWithPosition::parse_str(&path_str);
849            if path_with_position.row.is_some() {
850                if !path_exists(&remote_connection, &path).await {
851                    *path = path_with_position.path.clone();
852                    paths_with_positions.push(path_with_position);
853                    continue;
854                }
855            }
856        }
857        paths_with_positions.push(PathWithPosition::from_path(path.clone()))
858    }
859    (paths, paths_with_positions)
860}
861
862async fn path_exists(connection: &Arc<dyn RemoteConnection>, path: &Path) -> bool {
863    let Ok(command) = connection.build_command(
864        Some("test".to_string()),
865        &["-e".to_owned(), path.to_string_lossy().to_string()],
866        &Default::default(),
867        None,
868        None,
869    ) else {
870        return false;
871    };
872    let Ok(mut child) = util::command::new_smol_command(command.program)
873        .args(command.args)
874        .envs(command.env)
875        .spawn()
876    else {
877        return false;
878    };
879    child.status().await.is_ok_and(|status| status.success())
880}