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