1728 lines · 68.9 KB · Rust
RawDownload
use std::any::Any;use std::borrow::Cow;use std::collections::BTreeSet;use std::path::PathBuf;use std::rc::Rc;use std::sync::Arc;use std::sync::atomic;use std::sync::atomic::AtomicUsize; use editor::Editor;use file_finder::OpenPathDelegate;use futures::FutureExt;use futures::channel::oneshot;use futures::future::Shared;use futures::select;use gpui::ClickEvent;use gpui::ClipboardItem;use gpui::Subscription;use gpui::Task;use gpui::WeakEntity;use gpui::canvas;use gpui::{    AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,    PromptLevel, ScrollHandle, Window,};use paths::global_ssh_config_file;use paths::user_ssh_config_file;use picker::Picker;use project::Fs;use project::Project;use remote::SshConnectionOptions;use remote::SshRemoteClient;use remote::ssh_session::ConnectionIdentifier;use settings::Settings;use settings::SettingsStore;use settings::update_settings_file;use settings::watch_config_file;use smol::stream::StreamExt as _;use ui::Navigable;use ui::NavigableEntry;use ui::{    IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState,    Section, Tooltip, prelude::*,};use util::ResultExt;use workspace::OpenOptions;use workspace::Toast;use workspace::notifications::NotificationId;use workspace::{    ModalView, Workspace, notifications::DetachAndPromptErr,    open_ssh_project_with_existing_connection,}; use crate::ssh_config::parse_ssh_config_hosts;use crate::ssh_connections::RemoteSettingsContent;use crate::ssh_connections::SshConnection;use crate::ssh_connections::SshConnectionHeader;use crate::ssh_connections::SshConnectionModal;use crate::ssh_connections::SshProject;use crate::ssh_connections::SshPrompt;use crate::ssh_connections::SshSettings;use crate::ssh_connections::connect_over_ssh;use crate::ssh_connections::open_ssh_project; mod navigation_base {}pub struct RemoteServerProjects {    mode: Mode,    focus_handle: FocusHandle,    workspace: WeakEntity<Workspace>,    retained_connections: Vec<Entity<SshRemoteClient>>,    ssh_config_updates: Task<()>,    ssh_config_servers: BTreeSet<SharedString>,    create_new_window: bool,    _subscription: Subscription,} struct CreateRemoteServer {    address_editor: Entity<Editor>,    address_error: Option<SharedString>,    ssh_prompt: Option<Entity<SshPrompt>>,    _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);        });        Self {            address_editor,            address_error: None,            ssh_prompt: None,            _creating: None,        }    }} struct ProjectPicker {    connection_string: SharedString,    nickname: Option<SharedString>,    picker: Entity<Picker<OpenPathDelegate>>,    _path_task: Shared<Task<Option<()>>>,} struct EditNicknameState {    index: usize,    editor: Entity<Editor>,} impl EditNicknameState {    fn new(index: usize, window: &mut Window, cx: &mut App) -> Self {        let this = Self {            index,            editor: cx.new(|cx| Editor::single_line(window, cx)),        };        let starting_text = SshSettings::get_global(cx)            .ssh_connections()            .nth(index)            .and_then(|state| state.nickname.clone())            .filter(|text| !text.is_empty());        this.editor.update(cx, |this, cx| {            this.set_placeholder_text("Add a nickname for this server", cx);            if let Some(starting_text) = starting_text {                this.set_text(starting_text, window, cx);            }        });        this.editor.focus_handle(cx).focus(window);        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,        ix: usize,        connection: SshConnectionOptions,        project: Entity<Project>,        home_dir: PathBuf,        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 = file_finder::OpenPathDelegate::new(tx, lister, false);         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_lossy().to_string(), window, cx);            picker        });        let connection_string = connection.connection_string().into();        let nickname = connection.nickname.clone().map(|nick| nick.into());        let _path_task = cx            .spawn_in(window, {                let workspace = workspace.clone();                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()?;                     cx.update(|_, cx| {                        let fs = app_state.fs.clone();                        update_settings_file::<SshSettings>(fs, cx, {                            let paths = paths                                .iter()                                .map(|path| path.to_string_lossy().to_string())                                .collect();                            move |setting, _| {                                if let Some(server) = setting                                    .ssh_connections                                    .as_mut()                                    .and_then(|connections| connections.get_mut(ix))                                {                                    server.projects.insert(SshProject { 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| {                            cx.new(|cx| {                                telemetry::event!("SSH Project Created");                                Workspace::new(None, project.clone(), app_state.clone(), window, cx)                            })                        })                        .log_err()?;                     open_ssh_project_with_existing_connection(                        connection, project, paths, app_state, window, cx,                    )                    .await                    .log_err();                     this.update(cx, |_, cx| {                        cx.emit(DismissEvent);                    })                    .ok();                    Some(())                }            })            .shared();        cx.new(|_| Self {            _path_task,            picker,            connection_string,            nickname,        })    }} impl gpui::Render for ProjectPicker {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        v_flex()            .child(                SshConnectionHeader {                    connection_string: self.connection_string.clone(),                    paths: Default::default(),                    nickname: self.nickname.clone(),                }                .render(window, cx),            )            .child(                div()                    .border_t_1()                    .border_color(cx.theme().colors().border_variant)                    .child(self.picker.clone()),            )    }} #[derive(Clone)]enum RemoteEntry {    Project {        open_folder: NavigableEntry,        projects: Vec<(NavigableEntry, SshProject)>,        configure: NavigableEntry,        connection: SshConnection,    },    SshConfig {        open_folder: NavigableEntry,        host: SharedString,    },} impl RemoteEntry {    fn is_from_zed(&self) -> bool {        matches!(self, Self::Project { .. })    }     fn connection(&self) -> Cow<'_, SshConnection> {        match self {            Self::Project { connection, .. } => Cow::Borrowed(connection),            Self::SshConfig { host, .. } => Cow::Owned(SshConnection {                host: host.clone(),                ..SshConnection::default()            }),        }    }} #[derive(Clone)]struct DefaultState {    scrollbar: ScrollbarState,    add_new_server: NavigableEntry,    servers: Vec<RemoteEntry>,} impl DefaultState {    fn new(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self {        let handle = ScrollHandle::new();        let scrollbar = ScrollbarState::new(handle.clone());        let add_new_server = NavigableEntry::new(&handle, cx);         let ssh_settings = SshSettings::get_global(cx);        let read_ssh_config = ssh_settings.read_ssh_config;         let mut servers: Vec<RemoteEntry> = ssh_settings            .ssh_connections()            .map(|connection| {                let open_folder = NavigableEntry::new(&handle, cx);                let configure = NavigableEntry::new(&handle, cx);                let projects = connection                    .projects                    .iter()                    .map(|project| (NavigableEntry::new(&handle, cx), project.clone()))                    .collect();                RemoteEntry::Project {                    open_folder,                    configure,                    projects,                    connection,                }            })            .collect();         if read_ssh_config {            let mut extra_servers_from_config = ssh_config_servers.clone();            for server in &servers {                if let RemoteEntry::Project { connection, .. } = server {                    extra_servers_from_config.remove(&connection.host);                }            }            servers.extend(extra_servers_from_config.into_iter().map(|host| {                RemoteEntry::SshConfig {                    open_folder: NavigableEntry::new(&handle, cx),                    host,                }            }));        }         Self {            scrollbar,            add_new_server,            servers,        }    }} #[derive(Clone)]struct ViewServerOptionsState {    server_index: usize,    connection: SshConnection,    entries: [NavigableEntry; 4],}enum Mode {    Default(DefaultState),    ViewServerOptions(ViewServerOptionsState),    EditNickname(EditNicknameState),    ProjectPicker(Entity<ProjectPicker>),    CreateRemoteServer(CreateRemoteServer),} impl Mode {    fn default_mode(ssh_config_servers: &BTreeSet<SharedString>, cx: &mut App) -> Self {        Self::Default(DefaultState::new(ssh_config_servers, cx))    }}impl RemoteServerProjects {    pub fn new(        create_new_window: bool,        fs: Arc<dyn Fs>,        window: &mut Window,        workspace: WeakEntity<Workspace>,        cx: &mut Context<Self>,    ) -> Self {        let focus_handle = cx.focus_handle();        let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config;        let ssh_config_updates = if read_ssh_config {            spawn_ssh_config_watch(fs.clone(), cx)        } else {            Task::ready(())        };         let mut base_style = window.text_style();        base_style.refine(&gpui::TextStyleRefinement {            color: Some(cx.theme().colors().editor_foreground),            ..Default::default()        });         let _subscription =            cx.observe_global_in::<SettingsStore>(window, move |recent_projects, _, cx| {                let new_read_ssh_config = SshSettings::get_global(cx).read_ssh_config;                if read_ssh_config != new_read_ssh_config {                    read_ssh_config = new_read_ssh_config;                    if read_ssh_config {                        recent_projects.ssh_config_updates = spawn_ssh_config_watch(fs.clone(), cx);                    } else {                        recent_projects.ssh_config_servers.clear();                        recent_projects.ssh_config_updates = Task::ready(());                    }                }            });         Self {            mode: Mode::default_mode(&BTreeSet::new(), cx),            focus_handle,            workspace,            retained_connections: Vec::new(),            ssh_config_updates,            ssh_config_servers: BTreeSet::new(),            create_new_window,            _subscription,        }    }     pub fn project_picker(        create_new_window: bool,        ix: usize,        connection_options: remote::SshConnectionOptions,        project: Entity<Project>,        home_dir: PathBuf,        window: &mut Window,        cx: &mut Context<Self>,        workspace: WeakEntity<Workspace>,    ) -> Self {        let fs = project.read(cx).fs().clone();        let mut this = Self::new(create_new_window, fs, window, workspace.clone(), cx);        this.mode = Mode::ProjectPicker(ProjectPicker::new(            create_new_window,            ix,            connection_options,            project,            home_dir,            workspace,            window,            cx,        ));        cx.notify();         this    }     fn create_ssh_server(        &mut self,        editor: Entity<Editor>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let input = get_text(&editor, cx);        if input.is_empty() {            return;        }         let connection_options = match SshConnectionOptions::parse_command_line(&input) {            Ok(c) => c,            Err(e) => {                self.mode = Mode::CreateRemoteServer(CreateRemoteServer {                    address_editor: editor,                    address_error: Some(format!("could not parse: {:?}", e).into()),                    ssh_prompt: None,                    _creating: None,                });                return;            }        };        let ssh_prompt = cx.new(|cx| SshPrompt::new(&connection_options, window, cx));         let connection = connect_over_ssh(            ConnectionIdentifier::setup(),            connection_options.clone(),            ssh_prompt.clone(),            window,            cx,        )        .prompt_err("Failed to connect", window, cx, |_, _, _| None);         let address_editor = editor.clone();        let creating = cx.spawn_in(window, async move |this, cx| {            match connection.await {                Some(Some(client)) => this                    .update_in(cx, |this, window, cx| {                        telemetry::event!("SSH Server Created");                        this.retained_connections.push(client);                        this.add_ssh_server(connection_options, cx);                        this.mode = Mode::default_mode(&this.ssh_config_servers, cx);                        this.focus_handle(cx).focus(window);                        cx.notify()                    })                    .log_err(),                _ => this                    .update(cx, |this, cx| {                        address_editor.update(cx, |this, _| {                            this.set_read_only(false);                        });                        this.mode = Mode::CreateRemoteServer(CreateRemoteServer {                            address_editor,

Lines 1–500 of 1728.