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. Load more
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 {