1694 lines · 68.4 KB · Rust
RawDownload
use std::collections::HashMap;use std::path::PathBuf;use std::time::Duration; use anyhow::anyhow;use anyhow::Context;use anyhow::Result;use client::Client;use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};use editor::Editor;use gpui::AsyncWindowContext;use gpui::PathPromptOptions;use gpui::Subscription;use gpui::Task;use gpui::WeakView;use gpui::{    percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter,    FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext,};use markdown::Markdown;use markdown::MarkdownStyle;use project::terminals::wrap_for_ssh;use project::terminals::SshCommand;use rpc::proto::RegenerateDevServerTokenResponse;use rpc::{    proto::{CreateDevServerResponse, DevServerStatus},    ErrorCode, ErrorExt,};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::ElevationIndex;use ui::Section;use ui::{    prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader,    RadioWithLabel, Tooltip,};use ui_input::{FieldLabelLayout, TextField};use util::paths::PathWithPosition;use util::ResultExt;use workspace::notifications::NotifyResultExt;use workspace::OpenOptions;use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB}; 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::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,    dev_server_store: Model<dev_server_projects::Store>,    workspace: WeakView<Workspace>,    project_path_input: View<Editor>,    dev_server_name_input: View<TextField>,    markdown: View<Markdown>,    _dev_server_subscription: Subscription,} #[derive(Default)]struct CreateDevServer {    creating: Option<Task<Option<()>>>,    dev_server_id: Option<DevServerId>,    access_token: Option<String>,    ssh_prompt: Option<View<SshPrompt>>,    kind: NewServerKind,} struct CreateDevServerProject {    dev_server_id: DevServerId,    creating: bool,    _opening: Option<Subscription>,} enum Mode {    Default(Option<CreateDevServerProject>),    CreateDevServer(CreateDevServer),} #[derive(Default, PartialEq, Eq, Clone, Copy)]enum NewServerKind {    DirectSSH,    #[default]    LegacySSH,    Manual,} 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 project_path_input = cx.new_view(|cx| {            let mut editor = Editor::single_line(cx);            editor.set_placeholder_text("Project path (~/work/zed, /workspace/zed, …)", cx);            editor        });        let dev_server_name_input = cx.new_view(|cx| {            TextField::new(cx, "Name", "192.168.0.1").with_label(FieldLabelLayout::Hidden)        });         let focus_handle = cx.focus_handle();        let dev_server_store = dev_server_projects::Store::global(cx);         let subscription = cx.observe(&dev_server_store, |_, _, cx| {            cx.notify();        });         let mut base_style = cx.text_style();        base_style.refine(&gpui::TextStyleRefinement {            color: Some(cx.theme().colors().editor_foreground),            ..Default::default()        });         let markdown_style = MarkdownStyle {            base_text_style: base_style,            code_block: gpui::StyleRefinement {                text: Some(gpui::TextStyleRefinement {                    font_family: Some("Zed Plex Mono".into()),                    ..Default::default()                }),                ..Default::default()            },            link: gpui::TextStyleRefinement {                color: Some(Color::Accent.color(cx)),                ..Default::default()            },            syntax: cx.theme().syntax().clone(),            selection_background_color: cx.theme().players().local().selection,            ..Default::default()        };        let markdown =            cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));         Self {            mode: Mode::Default(None),            focus_handle,            scroll_handle: ScrollHandle::new(),            dev_server_store,            project_path_input,            dev_server_name_input,            markdown,            workspace,            _dev_server_subscription: subscription,        }    }     pub fn create_dev_server_project(        &mut self,        dev_server_id: DevServerId,        cx: &mut ViewContext<Self>,    ) {        let mut path = self.project_path_input.read(cx).text(cx).trim().to_string();         if path == "" {            return;        }         if !path.starts_with('/') && !path.starts_with('~') {            path = format!("~/{}", path);        }         if self            .dev_server_store            .read(cx)            .projects_for_server(dev_server_id)            .iter()            .any(|p| p.paths.iter().any(|p| p == &path))        {            cx.spawn(|_, mut cx| async move {                cx.prompt(                    gpui::PromptLevel::Critical,                    "Failed to create project",                    Some(&format!("{} is already open on this dev server.", path)),                    &["Ok"],                )                .await            })            .detach_and_log_err(cx);            return;        }         let create = {            let path = path.clone();            self.dev_server_store.update(cx, |store, cx| {                store.create_dev_server_project(dev_server_id, path, cx)            })        };         cx.spawn(|this, mut cx| async move {            let result = create.await;            this.update(&mut cx, |this, cx| {                if let Ok(result) = &result {                    if let Some(dev_server_project_id) =                        result.dev_server_project.as_ref().map(|p| p.id)                    {                        let subscription =                            cx.observe(&this.dev_server_store, move |this, store, cx| {                                if let Some(project_id) = store                                    .read(cx)                                    .dev_server_project(DevServerProjectId(dev_server_project_id))                                    .and_then(|p| p.project_id)                                {                                    this.project_path_input.update(cx, |editor, cx| {                                        editor.set_text("", cx);                                    });                                    this.mode = Mode::Default(None);                                    if let Some(app_state) = AppState::global(cx).upgrade() {                                        workspace::join_dev_server_project(                                            DevServerProjectId(dev_server_project_id),                                            project_id,                                            app_state,                                            None,                                            cx,                                        )                                        .detach_and_prompt_err(                                            "Could not join project",                                            cx,                                            |_, _| None,                                        )                                    }                                }                            });                         this.mode = Mode::Default(Some(CreateDevServerProject {                            dev_server_id,                            creating: true,                            _opening: Some(subscription),                        }));                    }                } else {                    this.mode = Mode::Default(Some(CreateDevServerProject {                        dev_server_id,                        creating: false,                        _opening: None,                    }));                }            })            .log_err();            result        })        .detach_and_prompt_err("Failed to create project", cx, move |e, _| {            match e.error_code() {                ErrorCode::DevServerOffline => Some(                    "The dev server is offline. Please log in and check it is connected."                        .to_string(),                ),                ErrorCode::DevServerProjectPathDoesNotExist => {                    Some(format!("The path `{}` does not exist on the server.", path))                }                _ => None,            }        });         self.mode = Mode::Default(Some(CreateDevServerProject {            dev_server_id,            creating: true,            _opening: None,        }));    }     fn create_ssh_server(&mut self, cx: &mut ViewContext<Self>) {        let host = get_text(&self.dev_server_name_input, cx);        if host.is_empty() {            return;        }         let mut host = host.trim_start_matches("ssh ");        let mut username: Option<String> = None;        let mut port: Option<u16> = None;         if let Some((u, rest)) = host.split_once('@') {            host = rest;            username = Some(u.to_string());        }        if let Some((rest, p)) = host.split_once(':') {            host = rest;            port = p.parse().ok()        }         if let Some((rest, p)) = host.split_once(" -p") {            host = rest;            port = p.trim().parse().ok()        }         let connection_options = remote::SshConnectionOptions {            host: host.to_string(),            username,            port,            password: None,        };        let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));        let connection = connect_over_ssh(connection_options.clone(), ssh_prompt.clone(), cx)            .prompt_err("Failed to connect", cx, |_, _| None);         let creating = cx.spawn(move |this, mut cx| async move {            match connection.await {                Some(_) => this                    .update(&mut cx, |this, cx| {                        this.add_ssh_server(connection_options, cx);                        this.mode = Mode::Default(None);                        cx.notify()                    })                    .log_err(),                None => this                    .update(&mut cx, |this, cx| {                        this.mode = Mode::CreateDevServer(CreateDevServer {                            kind: NewServerKind::DirectSSH,                            ..Default::default()                        });                        cx.notify()                    })                    .log_err(),            };            None        });        self.mode = Mode::CreateDevServer(CreateDevServer {            kind: NewServerKind::DirectSSH,            ssh_prompt: Some(ssh_prompt.clone()),            creating: Some(creating),            ..Default::default()        });    }     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, cx));                let prompt = workspace                    .active_modal::<SshConnectionModal>(cx)                    .unwrap()                    .read(cx)                    .prompt                    .clone();                 let connect = connect_over_ssh(connection_options, prompt, cx).prompt_err(                    "Failed to connect",                    cx,                    |_, _| None,                );                cx.spawn(|workspace, mut cx| async move {                    let Some(session) = connect.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;                    };                    let Ok((app_state, project, paths)) =                        workspace.update(&mut cx, |workspace, cx| {                            let app_state = workspace.app_state().clone();                            let project = project::Project::ssh(                                session,                                app_state.client.clone(),                                app_state.node_runtime.clone(),                                app_state.user_store.clone(),                                app_state.languages.clone(),                                app_state.fs.clone(),                                cx,                            );                            let paths = workspace.prompt_for_open_path(                                PathPromptOptions {                                    files: true,                                    directories: true,                                    multiple: true,                                },                                project::DirectoryLister::Project(project.clone()),                                cx,                            );                            (app_state, project, paths)                        })                    else {                        return;                    };                     let Ok(Some(paths)) = paths.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;                    };                     let Some(options) = cx                        .update(|cx| (app_state.build_window_options)(None, cx))                        .log_err()                    else {                        return;                    };                     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| {                            Workspace::new(None, project.clone(), app_state.clone(), cx)                        })                    })                    .log_err();                })                .detach()            })        })    }     fn create_or_update_dev_server(        &mut self,        kind: NewServerKind,        existing_id: Option<DevServerId>,        access_token: Option<String>,        cx: &mut ViewContext<Self>,    ) {        let name = get_text(&self.dev_server_name_input, cx);        if name.is_empty() {            return;        }         let manual_setup = match kind {            NewServerKind::DirectSSH => unreachable!(),            NewServerKind::LegacySSH => false,            NewServerKind::Manual => true,        };         let ssh_connection_string = if manual_setup {            None        } else if name.contains(' ') {            Some(name.clone())        } else {

Lines 1–500 of 1694.