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