3017 lines · 115.1 KB · Rust
RawDownload
use crate::{    remote_connections::{        Connection, RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettings, SshConnection,        SshConnectionHeader, connect, determine_paths_with_positions, open_remote_project,    },    ssh_config::parse_ssh_config_hosts,};use dev_container::{    DevContainerConfig, DevContainerContext, find_devcontainer_configs,    start_dev_container_with_config,};use editor::Editor; use extension_host::ExtensionStore;use futures::{FutureExt, channel::oneshot, future::Shared};use gpui::{    Action, AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity,    EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task,    WeakEntity, Window, canvas,};use log::{debug, info};use open_path_prompt::OpenPathDelegate;use paths::{global_ssh_config_file, user_ssh_config_file};use picker::{Picker, PickerDelegate};use project::{Fs, Project};use remote::{    RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,    remote_client::ConnectionIdentifier,};use settings::{    RemoteProject, RemoteSettingsContent, Settings as _, SettingsStore, update_settings_file,    watch_config_file,};use smol::stream::StreamExt as _;use std::{    borrow::Cow,    collections::BTreeSet,    path::PathBuf,    rc::Rc,    sync::{        Arc,        atomic::{self, AtomicUsize},    },}; use ui::{    CommonAnimationExt, IconButtonShape, KeyBinding, List, ListItem, ListSeparator, Modal,    ModalFooter, ModalHeader, Navigable, NavigableEntry, Section, Tooltip, WithScrollbar,    prelude::*,};use util::{    ResultExt,    paths::{PathStyle, RemotePathBuf},    rel_path::RelPath,};use workspace::{    AppState, DismissDecision, ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,    notifications::{DetachAndPromptErr, NotificationId},    open_remote_project_with_existing_connection,}; pub struct RemoteServerProjects {    mode: Mode,    focus_handle: FocusHandle,    workspace: WeakEntity<Workspace>,    retained_connections: Vec<Entity<RemoteClient>>,    ssh_config_updates: Task<()>,    ssh_config_servers: BTreeSet<SharedString>,    create_new_window: bool,    dev_container_picker: Option<Entity<Picker<DevContainerPickerDelegate>>>,    _subscription: Subscription,    allow_dismissal: bool,} struct CreateRemoteServer {    address_editor: Entity<Editor>,    address_error: Option<SharedString>,    ssh_prompt: Option<Entity<RemoteConnectionPrompt>>,    _creating: Option<Task<Option<()>>>,} impl CreateRemoteServer {    fn new(window: &mut Window, cx: &mut App) -> Self {        let address_editor = cx.new(|cx| Editor::single_line(window, cx));        address_editor.update(cx, |this, cx| {            this.focus_handle(cx).focus(window, cx);        });        Self {            address_editor,            address_error: None,            ssh_prompt: None,            _creating: None,        }    }} #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]enum DevContainerCreationProgress {    SelectingConfig,    Creating,    Error(String),} #[derive(Clone)]struct CreateRemoteDevContainer {    view_logs_entry: NavigableEntry,    back_entry: NavigableEntry,    progress: DevContainerCreationProgress,} impl CreateRemoteDevContainer {    fn new(progress: DevContainerCreationProgress, cx: &mut Context<RemoteServerProjects>) -> Self {        let view_logs_entry = NavigableEntry::focusable(cx);        let back_entry = NavigableEntry::focusable(cx);        Self {            view_logs_entry,            back_entry,            progress,        }    }} #[cfg(target_os = "windows")]struct AddWslDistro {    picker: Entity<Picker<crate::wsl_picker::WslPickerDelegate>>,    connection_prompt: Option<Entity<RemoteConnectionPrompt>>,    _creating: Option<Task<()>>,} #[cfg(target_os = "windows")]impl AddWslDistro {    fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self {        use crate::wsl_picker::{WslDistroSelected, WslPickerDelegate, WslPickerDismissed};         let delegate = WslPickerDelegate::new();        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));         cx.subscribe_in(            &picker,            window,            |this, _, _: &WslDistroSelected, window, cx| {                this.confirm(&menu::Confirm, window, cx);            },        )        .detach();         cx.subscribe_in(            &picker,            window,            |this, _, _: &WslPickerDismissed, window, cx| {                this.cancel(&menu::Cancel, window, cx);            },        )        .detach();         AddWslDistro {            picker,            connection_prompt: None,            _creating: None,        }    }} enum ProjectPickerData {    Ssh {        connection_string: SharedString,        nickname: Option<SharedString>,    },    Wsl {        distro_name: SharedString,    },} struct ProjectPicker {    data: ProjectPickerData,    picker: Entity<Picker<OpenPathDelegate>>,    _path_task: Shared<Task<Option<()>>>,} struct EditNicknameState {    index: SshServerIndex,    editor: Entity<Editor>,} struct DevContainerPickerDelegate {    selected_index: usize,    candidates: Vec<DevContainerConfig>,    matching_candidates: Vec<DevContainerConfig>,    parent_modal: WeakEntity<RemoteServerProjects>,}impl DevContainerPickerDelegate {    fn new(        candidates: Vec<DevContainerConfig>,        parent_modal: WeakEntity<RemoteServerProjects>,    ) -> Self {        Self {            selected_index: 0,            matching_candidates: candidates.clone(),            candidates,            parent_modal,        }    }} impl PickerDelegate for DevContainerPickerDelegate {    type ListItem = AnyElement;     fn match_count(&self) -> usize {        self.matching_candidates.len()    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(        &mut self,        ix: usize,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) {        self.selected_index = ix;    }     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Select Dev Container Configuration".into()    }     fn update_matches(        &mut self,        query: String,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        let query_lower = query.to_lowercase();        self.matching_candidates = self            .candidates            .iter()            .filter(|c| {                c.name.to_lowercase().contains(&query_lower)                    || c.config_path                        .to_string_lossy()                        .to_lowercase()                        .contains(&query_lower)            })            .cloned()            .collect();         self.selected_index = std::cmp::min(            self.selected_index,            self.matching_candidates.len().saturating_sub(1),        );         Task::ready(())    }     fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let selected_config = self.matching_candidates.get(self.selected_index).cloned();        self.parent_modal            .update(cx, move |modal, cx| {                if secondary {                    modal.edit_in_dev_container_json(selected_config.clone(), window, cx);                } else if let Some((app_state, context)) = modal                    .workspace                    .read_with(cx, |workspace, cx| {                        let app_state = workspace.app_state().clone();                        let context = DevContainerContext::from_workspace(workspace, cx)?;                        Some((app_state, context))                    })                    .ok()                    .flatten()                {                    modal.open_dev_container(selected_config, app_state, context, window, cx);                    modal.view_in_progress_dev_container(window, cx);                } else {                    log::error!("No active project directory for Dev Container");                }            })            .ok();    }     fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {        self.parent_modal            .update(cx, |modal, cx| {                modal.cancel(&menu::Cancel, window, cx);            })            .ok();    }     fn render_match(        &self,        ix: usize,        selected: bool,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) -> Option<Self::ListItem> {        let candidate = self.matching_candidates.get(ix)?;        let config_path = candidate.config_path.display().to_string();        Some(            ListItem::new(SharedString::from(format!("li-devcontainer-config-{}", ix)))                .inset(true)                .spacing(ui::ListItemSpacing::Sparse)                .toggle_state(selected)                .start_slot(Icon::new(IconName::FileToml).color(Color::Muted))                .child(                    v_flex().child(Label::new(candidate.name.clone())).child(                        Label::new(config_path)                            .size(ui::LabelSize::Small)                            .color(Color::Muted),                    ),                )                .into_any_element(),        )    }     fn render_footer(        &self,        _window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Option<AnyElement> {        Some(            h_flex()                .w_full()                .p_1p5()                .gap_1()                .justify_start()                .border_t_1()                .border_color(cx.theme().colors().border_variant)                .child(                    Button::new("run-action", "Start Dev Container")                        .key_binding(                            KeyBinding::for_action(&menu::Confirm, cx)                                .map(|kb| kb.size(rems_from_px(12.))),                        )                        .on_click(|_, window, cx| {                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)                        }),                )                .child(                    Button::new("run-action-secondary", "Open devcontainer.json")                        .key_binding(                            KeyBinding::for_action(&menu::SecondaryConfirm, cx)                                .map(|kb| kb.size(rems_from_px(12.))),                        )                        .on_click(|_, window, cx| {                            window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)                        }),                )                .into_any_element(),        )    }} impl EditNicknameState {    fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self {        let this = Self {            index,            editor: cx.new(|cx| Editor::single_line(window, cx)),        };        let starting_text = RemoteSettings::get_global(cx)            .ssh_connections()            .nth(index.0)            .and_then(|state| state.nickname)            .filter(|text| !text.is_empty());        this.editor.update(cx, |this, cx| {            this.set_placeholder_text("Add a nickname for this server", window, cx);            if let Some(starting_text) = starting_text {                this.set_text(starting_text, window, cx);            }        });        this.editor.focus_handle(cx).focus(window, cx);        this    }} impl Focusable for ProjectPicker {    fn focus_handle(&self, cx: &App) -> FocusHandle {        self.picker.focus_handle(cx)    }} impl ProjectPicker {    fn new(        create_new_window: bool,        index: ServerIndex,        connection: RemoteConnectionOptions,        project: Entity<Project>,        home_dir: RemotePathBuf,        workspace: WeakEntity<Workspace>,        window: &mut Window,        cx: &mut Context<RemoteServerProjects>,    ) -> Entity<Self> {        let (tx, rx) = oneshot::channel();        let lister = project::DirectoryLister::Project(project.clone());        let delegate = open_path_prompt::OpenPathDelegate::new(tx, lister, false, cx).show_hidden();         let picker = cx.new(|cx| {            let picker = Picker::uniform_list(delegate, window, cx)                .width(rems(34.))                .modal(false);            picker.set_query(&home_dir.to_string(), window, cx);            picker        });         let data = match &connection {            RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh {                connection_string: connection.connection_string().into(),                nickname: connection.nickname.clone().map(|nick| nick.into()),            },            RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl {                distro_name: connection.distro_name.clone().into(),            },            RemoteConnectionOptions::Docker(_) => ProjectPickerData::Ssh {                // Not implemented as a project picker at this time                connection_string: "".into(),                nickname: None,            },            #[cfg(any(test, feature = "test-support"))]            RemoteConnectionOptions::Mock(options) => ProjectPickerData::Ssh {                connection_string: format!("mock-{}", options.id).into(),                nickname: None,            },        };        let _path_task = cx            .spawn_in(window, {                let workspace = workspace;                async move |this, cx| {                    let Ok(Some(paths)) = rx.await else {                        workspace                            .update_in(cx, |workspace, window, cx| {                                let fs = workspace.project().read(cx).fs().clone();                                let weak = cx.entity().downgrade();                                workspace.toggle_modal(window, cx, |window, cx| {                                    RemoteServerProjects::new(                                        create_new_window,                                        fs,                                        window,                                        weak,                                        cx,                                    )                                });                            })                            .log_err()?;                        return None;                    };                     let app_state = workspace                        .read_with(cx, |workspace, _| workspace.app_state().clone())                        .ok()?;                     let remote_connection = project.read_with(cx, |project, cx| {                        project.remote_client()?.read(cx).connection()                    })?;                     let (paths, paths_with_positions) =                        determine_paths_with_positions(&remote_connection, paths).await;                     cx.update(|_, cx| {                        let fs = app_state.fs.clone();                        update_settings_file(fs, cx, {                            let paths = paths                                .iter()                                .map(|path| path.to_string_lossy().into_owned())                                .collect();                            move |settings, _| match index {                                ServerIndex::Ssh(index) => {                                    if let Some(server) = settings                                        .remote                                        .ssh_connections                                        .as_mut()                                        .and_then(|connections| connections.get_mut(index.0))                                    {                                        server.projects.insert(RemoteProject { paths });                                    };                                }                                ServerIndex::Wsl(index) => {                                    if let Some(server) = settings                                        .remote                                        .wsl_connections                                        .as_mut()                                        .and_then(|connections| connections.get_mut(index.0))                                    {                                        server.projects.insert(RemoteProject { paths });                                    };                                }                            }                        });                    })                    .log_err();                     let options = cx                        .update(|_, cx| (app_state.build_window_options)(None, cx))                        .log_err()?;                    let window = cx                        .open_window(options, |window, cx| {                            let workspace = cx.new(|cx| {                                telemetry::event!("SSH Project Created");                                Workspace::new(None, project.clone(), app_state.clone(), window, cx)                            });                            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))

Lines 1–500 of 3017.