use std::time::Duration; use anyhow::anyhow;use anyhow::Context;use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};use editor::Editor;use feature_flags::FeatureFlagAppExt;use feature_flags::FeatureFlagViewExt;use gpui::AsyncWindowContext;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 rpc::proto::RegenerateDevServerTokenResponse;use rpc::{ proto::{CreateDevServerResponse, DevServerStatus}, ErrorCode, ErrorExt,};use task::RevealStrategy;use task::SpawnInTerminal;use task::TerminalWorkDir;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::ResultExt;use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB}; use crate::open_dev_server_project;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<()>>, dev_server_id: Option<DevServerId>, access_token: Option<String>, manual_setup: bool,} struct CreateDevServerProject { dev_server_id: DevServerId, creating: bool, _opening: Option<Subscription>,} enum Mode { Default(Option<CreateDevServerProject>), CreateDevServer(CreateDevServer),} impl DevServerProjects { pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) { cx.observe_flag::<feature_flags::Remoting, _>(|enabled, workspace, _| { if enabled { Self::register_open_remote_action(workspace); } }) .detach(); if cx.has_flag::<feature_flags::Remoting>() { Self::register_open_remote_action(workspace); } } fn register_open_remote_action(workspace: &mut 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 markdown_style = MarkdownStyle { code_block: gpui::TextStyleRefinement { font_family: Some("Zed Plex Mono".into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(cx.theme().colors().editor_background), ..Default::default() }, inline_code: Default::default(), block_quote: Default::default(), link: gpui::TextStyleRefinement { color: Some(Color::Accent.color(cx)), ..Default::default() }, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: cx.theme().syntax().clone(), selection_background_color: cx.theme().players().local().selection, }; let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx)); 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.path == path) { cx.spawn(|_, mut cx| async move { cx.prompt( gpui::PromptLevel::Critical, "Failed to create project", Some(&format!( "Project {} already exists for 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, })); } pub fn create_or_update_dev_server( &mut self, manual_setup: bool, 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 ssh_connection_string = if manual_setup { None } else if name.contains(' ') { Some(name.clone()) } else { Some(format!("ssh {}", name)) }; let dev_server = self.dev_server_store.update(cx, { let access_token = access_token.clone(); |store, cx| { let ssh_connection_string = ssh_connection_string.clone(); if let Some(dev_server_id) = existing_id { let rename = store.rename_dev_server( dev_server_id, name.clone(), ssh_connection_string, cx, ); let token = if let Some(access_token) = access_token { Task::ready(Ok(RegenerateDevServerTokenResponse { dev_server_id: dev_server_id.0, access_token, })) } else { store.regenerate_dev_server_token(dev_server_id, cx) }; cx.spawn(|_, _| async move { rename.await?; let response = token.await?; Ok(CreateDevServerResponse { dev_server_id: dev_server_id.0, name, access_token: response.access_token, }) }) } else { store.create_dev_server(name, ssh_connection_string.clone(), cx) } } }); let workspace = self.workspace.clone(); let store = dev_server_projects::Store::global(cx); let task = cx .spawn({ |this, mut cx| async move { let result = dev_server.await; match result { Ok(dev_server) => { if let Some(ssh_connection_string) = ssh_connection_string { this.update(&mut cx, |this, cx| { if let Mode::CreateDevServer(CreateDevServer { access_token, dev_server_id, .. }) = &mut this.mode { access_token.replace(dev_server.access_token.clone()); dev_server_id .replace(DevServerId(dev_server.dev_server_id)); } cx.notify(); })?; spawn_ssh_task( workspace .upgrade() .ok_or_else(|| anyhow!("workspace dropped"))?, store, DevServerId(dev_server.dev_server_id), ssh_connection_string, dev_server.access_token.clone(), &mut cx, ) .await .log_err(); } this.update(&mut cx, |this, cx| { this.focus_handle.focus(cx); this.mode = Mode::CreateDevServer(CreateDevServer { creating: None, dev_server_id: Some(DevServerId(dev_server.dev_server_id)), access_token: Some(dev_server.access_token), manual_setup, }); cx.notify(); })?; Ok(()) } Err(e) => { this.update(&mut cx, |this, cx| { this.mode = Mode::CreateDevServer(CreateDevServer { creating: None, dev_server_id: existing_id, access_token: None, manual_setup, }); cx.notify() }) .log_err(); return Err(e); } } } }) .prompt_err("Failed to create server", cx, |_, _| None); self.mode = Mode::CreateDevServer(CreateDevServer { creating: Some(task), dev_server_id: existing_id, access_token, manual_setup, }); cx.notify() } fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) { let store = self.dev_server_store.read(cx); let prompt = if store.projects_for_server(id).is_empty() && store .dev_server(id) .is_some_and(|server| server.status == DevServerStatus::Offline) { None } else { Some(cx.prompt( gpui::PromptLevel::Warning, "Are you sure?", Some("This will delete the dev server and all of its remote projects."), &["Delete", "Cancel"], )) }; cx.spawn(|this, mut cx| async move { if let Some(prompt) = prompt { if prompt.await? != 0 { return Ok(()); } } let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| { this.dev_server_store.update(cx, |store, _| { store .projects_for_server(id) .into_iter() .map(|project| project.id) .collect() }) })?; this.update(&mut cx, |this, cx| { this.dev_server_store .update(cx, |store, cx| store.delete_dev_server(id, cx)) })? .await?; for id in project_ids { WORKSPACE_DB .delete_workspace_by_dev_server_project_id(id) .await .log_err(); } Ok(()) }) .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None); } fn delete_dev_server_project( &mut self, id: DevServerProjectId, path: &str, cx: &mut ViewContext<Self>, ) { let answer = cx.prompt( gpui::PromptLevel::Warning, format!("Delete \"{}\"?", path).as_str(), Some("This will delete the remote project. You can always re-add it later."), &["Delete", "Cancel"], ); cx.spawn(|this, mut cx| async move { let answer = answer.await?; if answer != 0 { return Ok(()); } this.update(&mut cx, |this, cx| { this.dev_server_store .update(cx, |store, cx| store.delete_dev_server_project(id, cx)) })? .await?; WORKSPACE_DB .delete_workspace_by_dev_server_project_id(id) .await .log_err(); Ok(()) }) .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { match &self.mode { Mode::Default(None) => {} Mode::Default(Some(create_project)) => { self.create_dev_server_project(create_project.dev_server_id, cx); } Mode::CreateDevServer(state) => { if state.creating.is_none() || state.dev_server_id.is_some() { self.create_or_update_dev_server( state.manual_setup, state.dev_server_id, state.access_token.clone(), cx, );Lines 1–500 of 1207. Load more
use std::time::Duration; use anyhow::anyhow; use anyhow::Context; use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId}; use editor::Editor; use feature_flags::FeatureFlagAppExt; use feature_flags::FeatureFlagViewExt; use gpui::AsyncWindowContext; 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 rpc::proto::RegenerateDevServerTokenResponse; use rpc::{ proto::{CreateDevServerResponse, DevServerStatus}, ErrorCode, ErrorExt, }; use task::RevealStrategy; use task::SpawnInTerminal; use task::TerminalWorkDir; 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::ResultExt; use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB}; use crate::open_dev_server_project; 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<()>>, dev_server_id: Option<DevServerId>, access_token: Option<String>, manual_setup: bool, } struct CreateDevServerProject { dev_server_id: DevServerId, creating: bool, _opening: Option<Subscription>, } enum Mode { Default(Option<CreateDevServerProject>), CreateDevServer(CreateDevServer), } impl DevServerProjects { pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) { cx.observe_flag::<feature_flags::Remoting, _>(|enabled, workspace, _| { if enabled { Self::register_open_remote_action(workspace); } }) .detach(); if cx.has_flag::<feature_flags::Remoting>() { Self::register_open_remote_action(workspace); } } fn register_open_remote_action(workspace: &mut 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 markdown_style = MarkdownStyle { code_block: gpui::TextStyleRefinement { font_family: Some("Zed Plex Mono".into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(cx.theme().colors().editor_background), ..Default::default() }, inline_code: Default::default(), block_quote: Default::default(), link: gpui::TextStyleRefinement { color: Some(Color::Accent.color(cx)), ..Default::default() }, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: cx.theme().syntax().clone(), selection_background_color: cx.theme().players().local().selection, }; let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx)); 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.path == path) { cx.spawn(|_, mut cx| async move { cx.prompt( gpui::PromptLevel::Critical, "Failed to create project", Some(&format!( "Project {} already exists for 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, })); } pub fn create_or_update_dev_server( &mut self, manual_setup: bool, 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 ssh_connection_string = if manual_setup { None } else if name.contains(' ') { Some(name.clone()) } else { Some(format!("ssh {}", name)) }; let dev_server = self.dev_server_store.update(cx, { let access_token = access_token.clone(); |store, cx| { let ssh_connection_string = ssh_connection_string.clone(); if let Some(dev_server_id) = existing_id { let rename = store.rename_dev_server( dev_server_id, name.clone(), ssh_connection_string, cx, ); let token = if let Some(access_token) = access_token { Task::ready(Ok(RegenerateDevServerTokenResponse { dev_server_id: dev_server_id.0, access_token, })) } else { store.regenerate_dev_server_token(dev_server_id, cx) }; cx.spawn(|_, _| async move { rename.await?; let response = token.await?; Ok(CreateDevServerResponse { dev_server_id: dev_server_id.0, name, access_token: response.access_token, }) }) } else { store.create_dev_server(name, ssh_connection_string.clone(), cx) } } }); let workspace = self.workspace.clone(); let store = dev_server_projects::Store::global(cx); let task = cx .spawn({ |this, mut cx| async move { let result = dev_server.await; match result { Ok(dev_server) => { if let Some(ssh_connection_string) = ssh_connection_string { this.update(&mut cx, |this, cx| { if let Mode::CreateDevServer(CreateDevServer { access_token, dev_server_id, .. }) = &mut this.mode { access_token.replace(dev_server.access_token.clone()); dev_server_id .replace(DevServerId(dev_server.dev_server_id)); } cx.notify(); })?; spawn_ssh_task( workspace .upgrade() .ok_or_else(|| anyhow!("workspace dropped"))?, store, DevServerId(dev_server.dev_server_id), ssh_connection_string, dev_server.access_token.clone(), &mut cx, ) .await .log_err(); } this.update(&mut cx, |this, cx| { this.focus_handle.focus(cx); this.mode = Mode::CreateDevServer(CreateDevServer { creating: None, dev_server_id: Some(DevServerId(dev_server.dev_server_id)), access_token: Some(dev_server.access_token), manual_setup, }); cx.notify(); })?; Ok(()) } Err(e) => { this.update(&mut cx, |this, cx| { this.mode = Mode::CreateDevServer(CreateDevServer { creating: None, dev_server_id: existing_id, access_token: None, manual_setup, }); cx.notify() }) .log_err(); return Err(e); } } } }) .prompt_err("Failed to create server", cx, |_, _| None); self.mode = Mode::CreateDevServer(CreateDevServer { creating: Some(task), dev_server_id: existing_id, access_token, manual_setup, }); cx.notify() } fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) { let store = self.dev_server_store.read(cx); let prompt = if store.projects_for_server(id).is_empty() && store .dev_server(id) .is_some_and(|server| server.status == DevServerStatus::Offline) { None } else { Some(cx.prompt( gpui::PromptLevel::Warning, "Are you sure?", Some("This will delete the dev server and all of its remote projects."), &["Delete", "Cancel"], )) }; cx.spawn(|this, mut cx| async move { if let Some(prompt) = prompt { if prompt.await? != 0 { return Ok(()); } } let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| { this.dev_server_store.update(cx, |store, _| { store .projects_for_server(id) .into_iter() .map(|project| project.id) .collect() }) })?; this.update(&mut cx, |this, cx| { this.dev_server_store .update(cx, |store, cx| store.delete_dev_server(id, cx)) })? .await?; for id in project_ids { WORKSPACE_DB .delete_workspace_by_dev_server_project_id(id) .await .log_err(); } Ok(()) }) .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None); } fn delete_dev_server_project( &mut self, id: DevServerProjectId, path: &str, cx: &mut ViewContext<Self>, ) { let answer = cx.prompt( gpui::PromptLevel::Warning, format!("Delete \"{}\"?", path).as_str(), Some("This will delete the remote project. You can always re-add it later."), &["Delete", "Cancel"], ); cx.spawn(|this, mut cx| async move { let answer = answer.await?; if answer != 0 { return Ok(()); } this.update(&mut cx, |this, cx| { this.dev_server_store .update(cx, |store, cx| store.delete_dev_server_project(id, cx)) })? .await?; WORKSPACE_DB .delete_workspace_by_dev_server_project_id(id) .await .log_err(); Ok(()) }) .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { match &self.mode { Mode::Default(None) => {} Mode::Default(Some(create_project)) => { self.create_dev_server_project(create_project.dev_server_id, cx); } Mode::CreateDevServer(state) => { if state.creating.is_none() || state.dev_server_id.is_some() { self.create_or_update_dev_server( state.manual_setup, state.dev_server_id, state.access_token.clone(), cx, );