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