992 lines · 34.3 KB · Rust
RawDownload
use std::{    path::{Path, PathBuf},    sync::Arc,}; use anyhow::{Context as _, Result};use askpass::EncryptedPassword;use editor::Editor;use extension_host::ExtensionStore;use futures::{FutureExt as _, channel::oneshot, select};use gpui::{AppContext, AsyncApp, PromptLevel, WindowHandle}; use project::trusted_worktrees;use remote::{    DockerConnectionOptions, Interactive, RemoteConnection, RemoteConnectionOptions,    SshConnectionOptions,};pub use settings::SshConnection;use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};use util::paths::PathWithPosition;use workspace::{    AppState, MultiWorkspace, OpenOptions, SerializedWorkspaceLocation, Workspace,    find_existing_workspace,}; pub use remote_connection::{    RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,    connect,}; #[derive(RegisterSetting)]pub struct RemoteSettings {    pub ssh_connections: ExtendingVec<SshConnection>,    pub wsl_connections: ExtendingVec<WslConnection>,    /// Whether to read ~/.ssh/config for ssh connection sources.    pub read_ssh_config: bool,} impl RemoteSettings {    pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {        self.ssh_connections.clone().0.into_iter()    }     pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {        self.wsl_connections.clone().0.into_iter()    }     pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {        for conn in self.ssh_connections() {            if conn.host == options.host.to_string()                && conn.username == options.username                && conn.port == options.port            {                options.nickname = conn.nickname;                options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default();                options.args = Some(conn.args);                options.port_forwards = conn.port_forwards;                break;            }        }    }     pub fn connection_options_for(        &self,        host: String,        port: Option<u16>,        username: Option<String>,    ) -> SshConnectionOptions {        let mut options = SshConnectionOptions {            host: host.into(),            port,            username,            ..Default::default()        };        self.fill_connection_options_from_settings(&mut options);        options    }} #[derive(Clone, PartialEq)]pub enum Connection {    Ssh(SshConnection),    Wsl(WslConnection),    DevContainer(DevContainerConnection),} impl From<Connection> for RemoteConnectionOptions {    fn from(val: Connection) -> Self {        match val {            Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()),            Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()),            Connection::DevContainer(conn) => {                RemoteConnectionOptions::Docker(DockerConnectionOptions {                    name: conn.name,                    remote_user: conn.remote_user,                    container_id: conn.container_id,                    upload_binary_over_docker_exec: false,                    use_podman: conn.use_podman,                    remote_env: conn.remote_env,                })            }        }    }} impl From<SshConnection> for Connection {    fn from(val: SshConnection) -> Self {        Connection::Ssh(val)    }} impl From<WslConnection> for Connection {    fn from(val: WslConnection) -> Self {        Connection::Wsl(val)    }} impl Settings for RemoteSettings {    fn from_settings(content: &settings::SettingsContent) -> Self {        let remote = &content.remote;        Self {            ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(),            wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(),            read_ssh_config: remote.read_ssh_config.unwrap(),        }    }} pub async fn open_remote_project(    connection_options: RemoteConnectionOptions,    paths: Vec<PathBuf>,    app_state: Arc<AppState>,    open_options: workspace::OpenOptions,    cx: &mut AsyncApp,) -> Result<WindowHandle<MultiWorkspace>> {    let created_new_window = open_options.requesting_window.is_none();     let (existing, open_visible) = find_existing_workspace(        &paths,        &open_options,        &SerializedWorkspaceLocation::Remote(connection_options.clone()),        cx,    )    .await;     if let Some((existing_window, existing_workspace)) = existing {        let remote_connection = cx.update(|cx| {            existing_workspace                .read(cx)                .project()                .read(cx)                .remote_client()                .and_then(|client| client.read(cx).remote_connection())        });         if let Some(remote_connection) = remote_connection {            let (resolved_paths, paths_with_positions) =                determine_paths_with_positions(&remote_connection, paths).await;             let open_results = existing_window                .update(cx, |multi_workspace, window, cx| {                    window.activate_window();                    multi_workspace.activate(existing_workspace.clone(), None, window, cx);                    existing_workspace.update(cx, |workspace, cx| {                        workspace.open_paths(                            resolved_paths,                            OpenOptions {                                visible: Some(open_visible),                                ..Default::default()                            },                            None,                            window,                            cx,                        )                    })                })?                .await;             _ = existing_window.update(cx, |multi_workspace, _, cx| {                let workspace = multi_workspace.workspace().clone();                workspace.update(cx, |workspace, cx| {                    for item in open_results.iter().flatten() {                        if let Err(e) = item {                            workspace.show_error(format!("{e}"), cx);                        }                    }                });            });             let items = open_results                .into_iter()                .map(|r| r.and_then(|r| r.ok()))                .collect::<Vec<_>>();            navigate_to_positions(&existing_window, items, &paths_with_positions, cx);             return Ok(existing_window);        }        // If the remote connection is dead (e.g. server not running after failed reconnect),        // fall through to establish a fresh connection instead of showing an error.        log::info!(            "existing remote workspace found but connection is dead, starting fresh connection"        );    }     let (window, initial_workspace) = if let Some(window) = open_options.requesting_window {        let workspace = window.update(cx, |multi_workspace, _, _| {            multi_workspace.workspace().clone()        })?;        (window, workspace)    } else {        let workspace_position = cx            .update(|cx| {                workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)            })            .await            .context("fetching remote workspace position from db")?;         let mut options =            cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));        options.window_bounds = workspace_position.window_bounds;         let window = cx.open_window(options, |window, cx| {            let project = project::Project::local(                app_state.client.clone(),                app_state.node_runtime.clone(),                app_state.user_store.clone(),                app_state.languages.clone(),                app_state.fs.clone(),                None,                project::LocalProjectFlags {                    init_worktree_trust: false,                    ..Default::default()                },                cx,            );            let workspace = cx.new(|cx| {                let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);                workspace.centered_layout = workspace_position.centered_layout;                workspace            });            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))        })?;        let workspace = window.update(cx, |multi_workspace, _, _cx| {            multi_workspace.workspace().clone()        })?;        (window, workspace)    };     loop {        let (cancel_tx, mut cancel_rx) = oneshot::channel();        let delegate = window.update(cx, {            let paths = paths.clone();            let connection_options = connection_options.clone();            let initial_workspace = initial_workspace.clone();            move |_multi_workspace: &mut MultiWorkspace, window, cx| {                window.activate_window();                initial_workspace.update(cx, |workspace, cx| {                    workspace.hide_modal(window, cx);                    workspace.toggle_modal(window, cx, |window, cx| {                        RemoteConnectionModal::new(&connection_options, paths, window, cx)                    });                     let ui = workspace                        .active_modal::<RemoteConnectionModal>(cx)?                        .read(cx)                        .prompt                        .clone();                     ui.update(cx, |ui, _cx| {                        ui.set_cancellation_tx(cancel_tx);                    });                     Some(Arc::new(RemoteClientDelegate::new(                        window.window_handle(),                        ui.downgrade(),                        if let RemoteConnectionOptions::Ssh(options) = &connection_options {                            options                                .password                                .as_deref()                                .and_then(|pw| EncryptedPassword::try_from(pw).ok())                        } else {                            None                        },                    )))                })            }        })?;         let Some(delegate) = delegate else { break };         let connection = remote::connect(connection_options.clone(), delegate.clone(), cx);        let connection = select! {            _ = cancel_rx => {                initial_workspace.update(cx, |workspace, cx| {                    if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {                        ui.update(cx, |modal, cx| modal.finished(cx))                    }                });                 break;            },            result = connection.fuse() => result,        };        let remote_connection = match connection {            Ok(connection) => connection,            Err(e) => {                initial_workspace.update(cx, |workspace, cx| {                    if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {                        ui.update(cx, |modal, cx| modal.finished(cx))                    }                });                log::error!("Failed to open project: {e:#}");                let response = window                    .update(cx, |_, window, cx| {                        window.prompt(                            PromptLevel::Critical,                            match connection_options {                                RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",                                RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",                                RemoteConnectionOptions::Docker(_) => {                                    "Failed to connect to Dev Container"                                }                                #[cfg(any(test, feature = "test-support"))]                                RemoteConnectionOptions::Mock(_) => {                                    "Failed to connect to mock server"                                }                            },                            Some(&format!("{e:#}")),                            &["Retry", "Cancel"],                            cx,                        )                    })?                    .await;                 if response == Ok(0) {                    continue;                }                 if created_new_window {                    window                        .update(cx, |_, window, _| window.remove_window())                        .ok();                }                return Ok(window);            }        };         let (paths, paths_with_positions) =            determine_paths_with_positions(&remote_connection, paths.clone()).await;         let opened_items = cx            .update(|cx| {                workspace::open_remote_project_with_new_connection(                    window,                    remote_connection,                    cancel_rx,                    delegate.clone(),                    app_state.clone(),                    paths.clone(),                    cx,                )            })            .await;         initial_workspace.update(cx, |workspace, cx| {            if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {                ui.update(cx, |modal, cx| modal.finished(cx))            }        });         match opened_items {            Err(e) => {                log::error!("Failed to open project: {e:#}");                let response = window                    .update(cx, |_, window, cx| {                        window.prompt(                            PromptLevel::Critical,                            match connection_options {                                RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",                                RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",                                RemoteConnectionOptions::Docker(_) => {                                    "Failed to connect to Dev Container"                                }                                #[cfg(any(test, feature = "test-support"))]                                RemoteConnectionOptions::Mock(_) => {                                    "Failed to connect to mock server"                                }                            },                            Some(&format!("{e:#}")),                            &["Retry", "Cancel"],                            cx,                        )                    })?                    .await;                if response == Ok(0) {                    continue;                }                 if created_new_window {                    window                        .update(cx, |_, window, _| window.remove_window())                        .ok();                }                initial_workspace.update(cx, |workspace, cx| {                    trusted_worktrees::track_worktree_trust(                        workspace.project().read(cx).worktree_store(),                        None,                        None,                        None,                        cx,                    );                });            }             Ok(items) => {                navigate_to_positions(&window, items, &paths_with_positions, cx);            }        }         break;    }     // Register the remote client with extensions. We use `multi_workspace.workspace()` here    // (not `initial_workspace`) because `open_remote_project_inner` activated the new remote    // workspace, so the active workspace is now the one with the remote project.    window        .update(cx, |multi_workspace: &mut MultiWorkspace, _, cx| {            let workspace = multi_workspace.workspace().clone();            workspace.update(cx, |workspace, cx| {                if let Some(client) = workspace.project().read(cx).remote_client() {                    if let Some(extension_store) = ExtensionStore::try_global(cx) {                        extension_store                            .update(cx, |store, cx| store.register_remote_client(client, cx));                    }                }            });        })        .ok();    Ok(window)} pub fn navigate_to_positions(    window: &WindowHandle<MultiWorkspace>,    items: impl IntoIterator<Item = Option<Box<dyn workspace::item::ItemHandle>>>,    positions: &[PathWithPosition],    cx: &mut AsyncApp,) {    for (item, path) in items.into_iter().zip(positions) {        let Some(item) = item else {            continue;        };        let Some(row) = path.row else {            continue;        };        if let Some(active_editor) = item.downcast::<Editor>() {            window                .update(cx, |_, window, cx| {                    active_editor.update(cx, |editor, cx| {                        let row = row.saturating_sub(1);                        let col = path.column.unwrap_or(0).saturating_sub(1);                        let Some(buffer) = editor.buffer().read(cx).as_singleton() else {                            return;                        };                        let buffer_snapshot = buffer.read(cx).snapshot();                        let point = buffer_snapshot.point_from_external_input(row, col);                        editor.go_to_singleton_buffer_point(point, window, cx);                    });                })                .ok();        }    }} pub(crate) async fn determine_paths_with_positions(    remote_connection: &Arc<dyn RemoteConnection>,    mut paths: Vec<PathBuf>,) -> (Vec<PathBuf>, Vec<PathWithPosition>) {    let mut paths_with_positions = Vec::<PathWithPosition>::new();    for path in &mut paths {        if let Some(path_str) = path.to_str() {            let path_with_position = PathWithPosition::parse_str(&path_str);            if path_with_position.row.is_some() {                if !path_exists(&remote_connection, &path).await {                    *path = path_with_position.path.clone();                    paths_with_positions.push(path_with_position);                    continue;                }            }        }        paths_with_positions.push(PathWithPosition::from_path(path.clone()))    }    (paths, paths_with_positions)} async fn path_exists(connection: &Arc<dyn RemoteConnection>, path: &Path) -> bool {    let Ok(command) = connection.build_command(        Some("test".to_string()),        &["-e".to_owned(), path.to_string_lossy().to_string()],        &Default::default(),        None,

Lines 1–500 of 992.