1399 lines · 52.2 KB · Rust
RawDownload
use std::collections::HashMap;use std::path::PathBuf;use std::sync::Arc;use std::time::Duration; use anyhow::anyhow;use anyhow::Context;use anyhow::Result;use dev_server_projects::{DevServer, DevServerId, DevServerProjectId};use editor::Editor;use file_finder::OpenPathDelegate;use futures::channel::oneshot;use futures::future::Shared;use futures::FutureExt;use gpui::canvas;use gpui::AsyncWindowContext;use gpui::ClipboardItem;use gpui::Task;use gpui::WeakView;use gpui::{    AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, FontWeight,    Model, PromptLevel, ScrollHandle, View, ViewContext,};use picker::Picker;use project::terminals::wrap_for_ssh;use project::terminals::SshCommand;use project::Project;use remote::SshConnectionOptions;use rpc::proto::DevServerStatus;use settings::update_settings_file;use settings::Settings;use task::HideStrategy;use task::RevealStrategy;use task::SpawnInTerminal;use terminal_view::terminal_panel::TerminalPanel;use ui::Scrollbar;use ui::ScrollbarState;use ui::Section;use ui::{prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Tooltip};use util::ResultExt;use workspace::notifications::NotificationId;use workspace::OpenOptions;use workspace::Toast;use workspace::{notifications::DetachAndPromptErr, ModalView, Workspace}; use crate::open_dev_server_project;use crate::ssh_connections::connect_over_ssh;use crate::ssh_connections::open_ssh_project;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::OpenRemote; pub struct DevServerProjects {    mode: Mode,    focus_handle: FocusHandle,    scroll_handle: ScrollHandle,    workspace: WeakView<Workspace>,    selectable_items: SelectableItemList,} struct CreateDevServer {    address_editor: View<Editor>,    address_error: Option<SharedString>,    ssh_prompt: Option<View<SshPrompt>>,    _creating: Option<Task<Option<()>>>,} impl CreateDevServer {    fn new(cx: &mut WindowContext<'_>) -> Self {        let address_editor = cx.new_view(Editor::single_line);        address_editor.update(cx, |this, cx| {            this.focus_handle(cx).focus(cx);        });        Self {            address_editor,            address_error: None,            ssh_prompt: None,            _creating: None,        }    }} struct ProjectPicker {    connection_string: SharedString,    picker: View<Picker<OpenPathDelegate>>,    _path_task: Shared<Task<Option<()>>>,} type SelectedItemCallback =    Box<dyn Fn(&mut DevServerProjects, &mut ViewContext<DevServerProjects>) + 'static>; /// Used to implement keyboard navigation for SSH modal.#[derive(Default)]struct SelectableItemList {    items: Vec<SelectedItemCallback>,    active_item: Option<usize>,} struct EditNicknameState {    index: usize,    editor: View<Editor>,} impl EditNicknameState {    fn new(index: usize, cx: &mut WindowContext<'_>) -> Self {        let this = Self {            index,            editor: cx.new_view(Editor::single_line),        };        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, cx);            }        });        this.editor.focus_handle(cx).focus(cx);        this    }} impl SelectableItemList {    fn reset(&mut self) {        self.items.clear();    }     fn reset_selection(&mut self) {        self.active_item.take();    }     fn prev(&mut self, _: &mut WindowContext<'_>) {        match self.active_item.as_mut() {            Some(active_index) => {                *active_index = active_index.checked_sub(1).unwrap_or(self.items.len() - 1)            }            None => {                self.active_item = Some(self.items.len() - 1);            }        }    }     fn next(&mut self, _: &mut WindowContext<'_>) {        match self.active_item.as_mut() {            Some(active_index) => {                if *active_index + 1 < self.items.len() {                    *active_index += 1;                } else {                    *active_index = 0;                }            }            None => {                self.active_item = Some(0);            }        }    }     fn add_item(&mut self, callback: SelectedItemCallback) {        self.items.push(callback)    }     fn is_selected(&self) -> bool {        self.active_item == self.items.len().checked_sub(1)    }     fn confirm(&self, dev_modal: &mut DevServerProjects, cx: &mut ViewContext<DevServerProjects>) {        if let Some(active_item) = self.active_item.and_then(|ix| self.items.get(ix)) {            active_item(dev_modal, cx);        }    }} impl ProjectPicker {    fn new(        ix: usize,        connection_string: SharedString,        project: Model<Project>,        workspace: WeakView<Workspace>,        cx: &mut ViewContext<DevServerProjects>,    ) -> View<Self> {        let (tx, rx) = oneshot::channel();        let lister = project::DirectoryLister::Project(project.clone());        let query = lister.default_query(cx);        let delegate = file_finder::OpenPathDelegate::new(tx, lister);         let picker = cx.new_view(|cx| {            let picker = Picker::uniform_list(delegate, cx)                .width(rems(34.))                .modal(false);            picker.set_query(query, cx);            picker        });        cx.new_view(|cx| {            let _path_task = cx                .spawn({                    let workspace = workspace.clone();                    move |_, mut cx| async move {                        let Ok(Some(paths)) = rx.await else {                            workspace                                .update(&mut cx, |workspace, cx| {                                    let weak = cx.view().downgrade();                                    workspace                                        .toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));                                })                                .log_err()?;                            return None;                        };                         let app_state = workspace                            .update(&mut cx, |workspace, _| workspace.app_state().clone())                            .ok()?;                        let options = cx                            .update(|cx| (app_state.build_window_options)(None, cx))                            .log_err()?;                         cx.open_window(options, |cx| {                            cx.activate_window();                             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.push(SshProject { paths })                                    }                                }                            });                             let tasks = paths                                .into_iter()                                .map(|path| {                                    project.update(cx, |project, cx| {                                        project.find_or_create_worktree(&path, true, cx)                                    })                                })                                .collect::<Vec<_>>();                            cx.spawn(|_| async move {                                for task in tasks {                                    task.await?;                                }                                Ok(())                            })                            .detach_and_prompt_err(                                "Failed to open path",                                cx,                                |_, _| None,                            );                             cx.new_view(|cx| {                                let workspace =                                    Workspace::new(None, project.clone(), app_state.clone(), cx);                                 workspace                                    .client()                                    .telemetry()                                    .report_app_event("create ssh project".to_string());                                 workspace                            })                        })                        .log_err();                        Some(())                    }                })                .shared();             Self {                _path_task,                picker,                connection_string,            }        })    }} impl gpui::Render for ProjectPicker {    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {        v_flex()            .child(                SshConnectionHeader {                    connection_string: self.connection_string.clone(),                    nickname: None,                }                .render(cx),            )            .child(self.picker.clone())    }}enum Mode {    Default(ScrollbarState),    ViewServerOptions(usize, SshConnection),    EditNickname(EditNicknameState),    ProjectPicker(View<ProjectPicker>),    CreateDevServer(CreateDevServer),} impl Mode {    fn default_mode() -> Self {        let handle = ScrollHandle::new();        Self::Default(ScrollbarState::new(handle))    }}impl DevServerProjects {    pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {        workspace.register_action(|workspace, _: &OpenRemote, cx| {            let handle = cx.view().downgrade();            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))        });    }     pub fn open(workspace: View<Workspace>, cx: &mut WindowContext) {        workspace.update(cx, |workspace, cx| {            let handle = cx.view().downgrade();            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))        })    }     pub fn new(cx: &mut ViewContext<Self>, workspace: WeakView<Workspace>) -> Self {        let focus_handle = cx.focus_handle();         let mut base_style = cx.text_style();        base_style.refine(&gpui::TextStyleRefinement {            color: Some(cx.theme().colors().editor_foreground),            ..Default::default()        });         Self {            mode: Mode::default_mode(),            focus_handle,            scroll_handle: ScrollHandle::new(),            workspace,            selectable_items: Default::default(),        }    }     fn next_item(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {        if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {            return;        }        self.selectable_items.next(cx);    }    fn prev_item(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {        if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {            return;        }        self.selectable_items.prev(cx);    }    pub fn project_picker(        ix: usize,        connection_options: remote::SshConnectionOptions,        project: Model<Project>,        cx: &mut ViewContext<Self>,        workspace: WeakView<Workspace>,    ) -> Self {        let mut this = Self::new(cx, workspace.clone());        this.mode = Mode::ProjectPicker(ProjectPicker::new(            ix,            connection_options.connection_string().into(),            project,            workspace,            cx,        ));         this    }     fn create_ssh_server(&mut self, editor: View<Editor>, cx: &mut ViewContext<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::CreateDevServer(CreateDevServer {                    address_editor: editor,                    address_error: Some(format!("could not parse: {:?}", e).into()),                    ssh_prompt: None,                    _creating: None,                });                return;            }        };        let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));         let connection = connect_over_ssh(            connection_options.dev_server_identifier(),            connection_options.clone(),            ssh_prompt.clone(),            cx,        )        .prompt_err("Failed to connect", cx, |_, _| None);         let address_editor = editor.clone();        let creating = cx.spawn(move |this, mut cx| async move {            match connection.await {                Some(_) => this                    .update(&mut cx, |this, cx| {                        let _ = this.workspace.update(cx, |workspace, _| {                            workspace                                .client()                                .telemetry()                                .report_app_event("create ssh server".to_string())                        });                         this.add_ssh_server(connection_options, cx);                        this.mode = Mode::default_mode();                        this.selectable_items.reset_selection();                        cx.notify()                    })                    .log_err(),                None => this                    .update(&mut cx, |this, cx| {                        address_editor.update(cx, |this, _| {                            this.set_read_only(false);                        });                        this.mode = Mode::CreateDevServer(CreateDevServer {                            address_editor,                            address_error: None,                            ssh_prompt: None,                            _creating: None,                        });                        cx.notify()                    })                    .log_err(),            };            None        });         editor.update(cx, |this, _| {            this.set_read_only(true);        });        self.mode = Mode::CreateDevServer(CreateDevServer {            address_editor: editor,            address_error: None,            ssh_prompt: Some(ssh_prompt.clone()),            _creating: Some(creating),        });    }     fn view_server_options(        &mut self,        (index, connection): (usize, SshConnection),        cx: &mut ViewContext<Self>,    ) {        self.selectable_items.reset_selection();        self.mode = Mode::ViewServerOptions(index, connection);        cx.notify();    }     fn create_ssh_project(        &mut self,        ix: usize,        ssh_connection: SshConnection,        cx: &mut ViewContext<Self>,    ) {        let Some(workspace) = self.workspace.upgrade() else {            return;        };         let connection_options = ssh_connection.into();        workspace.update(cx, |_, cx| {            cx.defer(move |workspace, cx| {                workspace.toggle_modal(cx, |cx| {                    SshConnectionModal::new(&connection_options, false, cx)                });                let prompt = workspace                    .active_modal::<SshConnectionModal>(cx)                    .unwrap()                    .read(cx)                    .prompt                    .clone();                 let connect = connect_over_ssh(                    connection_options.dev_server_identifier(),                    connection_options.clone(),                    prompt,                    cx,                )                .prompt_err("Failed to connect", cx, |_, _| None);                cx.spawn(move |workspace, mut cx| async move {                    let Some(session) = connect.await else {                        workspace                            .update(&mut cx, |workspace, cx| {

Lines 1–500 of 1399.