use std::any::Any;use std::collections::BTreeSet;use std::path::PathBuf;use std::sync::Arc; use editor::Editor;use file_finder::OpenPathDelegate;use futures::FutureExt;use futures::channel::oneshot;use futures::future::Shared;use gpui::ClipboardItem;use gpui::Task;use gpui::WeakEntity;use gpui::canvas;use gpui::{ AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Window,};use picker::Picker;use project::Project;use remote::SshConnectionOptions;use remote::SshRemoteClient;use remote::ssh_session::ConnectionIdentifier;use settings::Settings;use settings::update_settings_file;use ui::Navigable;use ui::NavigableEntry;use ui::{ IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, Section, Tooltip, prelude::*,};use util::ResultExt;use workspace::OpenOptions;use workspace::Toast;use workspace::notifications::NotificationId;use workspace::{ ModalView, Workspace, notifications::DetachAndPromptErr, open_ssh_project_with_existing_connection,}; use crate::OpenRemote;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::ssh_connections::connect_over_ssh;use crate::ssh_connections::open_ssh_project; mod navigation_base {}pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakEntity<Workspace>, retained_connections: Vec<Entity<SshRemoteClient>>,} struct CreateRemoteServer { address_editor: Entity<Editor>, address_error: Option<SharedString>, ssh_prompt: Option<Entity<SshPrompt>>, _creating: Option<Task<Option<()>>>,} impl CreateRemoteServer { fn new(window: &mut Window, cx: &mut App) -> Self { let address_editor = cx.new(|cx| Editor::single_line(window, cx)); address_editor.update(cx, |this, cx| { this.focus_handle(cx).focus(window); }); Self { address_editor, address_error: None, ssh_prompt: None, _creating: None, } }} struct ProjectPicker { connection_string: SharedString, nickname: Option<SharedString>, picker: Entity<Picker<OpenPathDelegate>>, _path_task: Shared<Task<Option<()>>>,} struct EditNicknameState { index: usize, editor: Entity<Editor>,} impl EditNicknameState { fn new(index: usize, window: &mut Window, cx: &mut App) -> Self { let this = Self { index, editor: cx.new(|cx| Editor::single_line(window, cx)), }; 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, window, cx); } }); this.editor.focus_handle(cx).focus(window); this }} impl Focusable for ProjectPicker { fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) }} impl ProjectPicker { fn new( ix: usize, connection: SshConnectionOptions, project: Entity<Project>, workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<RemoteServerProjects>, ) -> Entity<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(|cx| { let picker = Picker::uniform_list(delegate, window, cx) .width(rems(34.)) .modal(false); picker.set_query(query, window, cx); picker }); let connection_string = connection.connection_string().into(); let nickname = connection.nickname.clone().map(|nick| nick.into()); let _path_task = cx .spawn_in(window, { let workspace = workspace.clone(); async move |this, cx| { let Ok(Some(paths)) = rx.await else { workspace .update_in(cx, |workspace, window, cx| { let weak = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { RemoteServerProjects::new(window, cx, weak) }); }) .log_err()?; return None; }; let app_state = workspace .update(cx, |workspace, _| workspace.app_state().clone()) .ok()?; cx.update(|_, cx| { 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.insert(SshProject { paths }); } } }); }) .log_err(); let options = cx .update(|_, cx| (app_state.build_window_options)(None, cx)) .log_err()?; let window = cx .open_window(options, |window, cx| { cx.new(|cx| { telemetry::event!("SSH Project Created"); Workspace::new(None, project.clone(), app_state.clone(), window, cx) }) }) .log_err()?; open_ssh_project_with_existing_connection( connection, project, paths, app_state, window, cx, ) .await .log_err(); this.update(cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); Some(()) } }) .shared(); cx.new(|_| Self { _path_task, picker, connection_string, nickname, }) }} impl gpui::Render for ProjectPicker { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { v_flex() .child( SshConnectionHeader { connection_string: self.connection_string.clone(), paths: Default::default(), nickname: self.nickname.clone(), } .render(window, cx), ) .child( div() .border_t_1() .border_color(cx.theme().colors().border_variant) .child(self.picker.clone()), ) }} #[derive(Clone)]struct ProjectEntry { open_folder: NavigableEntry, projects: Vec<(NavigableEntry, SshProject)>, configure: NavigableEntry, connection: SshConnection,} #[derive(Clone)]struct DefaultState { scrollbar: ScrollbarState, add_new_server: NavigableEntry, servers: Vec<ProjectEntry>,}impl DefaultState { fn new(cx: &mut App) -> Self { let handle = ScrollHandle::new(); let scrollbar = ScrollbarState::new(handle.clone()); let add_new_server = NavigableEntry::new(&handle, cx); let servers = SshSettings::get_global(cx) .ssh_connections() .map(|connection| { let open_folder = NavigableEntry::new(&handle, cx); let configure = NavigableEntry::new(&handle, cx); let projects = connection .projects .iter() .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) .collect(); ProjectEntry { open_folder, configure, projects, connection, } }) .collect(); Self { scrollbar, add_new_server, servers, } }} #[derive(Clone)]struct ViewServerOptionsState { server_index: usize, connection: SshConnection, entries: [NavigableEntry; 4],}enum Mode { Default(DefaultState), ViewServerOptions(ViewServerOptionsState), EditNickname(EditNicknameState), ProjectPicker(Entity<ProjectPicker>), CreateRemoteServer(CreateRemoteServer),} impl Mode { fn default_mode(cx: &mut App) -> Self { Self::Default(DefaultState::new(cx)) }}impl RemoteServerProjects { pub fn register( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { workspace.register_action(|workspace, _: &OpenRemote, window, cx| { let handle = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| Self::new(window, cx, handle)) }); } pub fn open(workspace: Entity<Workspace>, window: &mut Window, cx: &mut App) { workspace.update(cx, |workspace, cx| { let handle = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| Self::new(window, cx, handle)) }) } pub fn new( window: &mut Window, cx: &mut Context<Self>, workspace: WeakEntity<Workspace>, ) -> Self { let focus_handle = cx.focus_handle(); let mut base_style = window.text_style(); base_style.refine(&gpui::TextStyleRefinement { color: Some(cx.theme().colors().editor_foreground), ..Default::default() }); Self { mode: Mode::default_mode(cx), focus_handle, workspace, retained_connections: Vec::new(), } } pub fn project_picker( ix: usize, connection_options: remote::SshConnectionOptions, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, workspace: WeakEntity<Workspace>, ) -> Self { let mut this = Self::new(window, cx, workspace.clone()); this.mode = Mode::ProjectPicker(ProjectPicker::new( ix, connection_options, project, workspace, window, cx, )); cx.notify(); this } fn create_ssh_server( &mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<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::CreateRemoteServer(CreateRemoteServer { address_editor: editor, address_error: Some(format!("could not parse: {:?}", e).into()), ssh_prompt: None, _creating: None, }); return; } }; let ssh_prompt = cx.new(|cx| SshPrompt::new(&connection_options, window, cx)); let connection = connect_over_ssh( ConnectionIdentifier::setup(), connection_options.clone(), ssh_prompt.clone(), window, cx, ) .prompt_err("Failed to connect", window, cx, |_, _, _| None); let address_editor = editor.clone(); let creating = cx.spawn(async move |this, cx| { match connection.await { Some(Some(client)) => this .update(cx, |this, cx| { telemetry::event!("SSH Server Created"); this.retained_connections.push(client); this.add_ssh_server(connection_options, cx); this.mode = Mode::default_mode(cx); cx.notify() }) .log_err(), _ => this .update(cx, |this, cx| { address_editor.update(cx, |this, _| { this.set_read_only(false); }); this.mode = Mode::CreateRemoteServer(CreateRemoteServer { 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::CreateRemoteServer(CreateRemoteServer { address_editor: editor, address_error: None, ssh_prompt: Some(ssh_prompt.clone()), _creating: Some(creating), }); } fn view_server_options( &mut self, (server_index, connection): (usize, SshConnection), window: &mut Window, cx: &mut Context<Self>, ) { self.mode = Mode::ViewServerOptions(ViewServerOptionsState { server_index, connection, entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), }); self.focus_handle(cx).focus(window); cx.notify(); } fn create_ssh_project( &mut self, ix: usize, ssh_connection: SshConnection, window: &mut Window, cx: &mut Context<Self>, ) { let Some(workspace) = self.workspace.upgrade() else { return; }; let connection_options = ssh_connection.into(); workspace.update(cx, |_, cx| { cx.defer_in(window, move |workspace, window, cx| { workspace.toggle_modal(window, cx, |window, cx| { SshConnectionModal::new(&connection_options, Vec::new(), window, cx) }); let prompt = workspace .active_modal::<SshConnectionModal>(cx) .unwrap() .read(cx) .prompt .clone(); let connect = connect_over_ssh( ConnectionIdentifier::setup(), connection_options.clone(), prompt, window, cx, ) .prompt_err("Failed to connect", window, cx, |_, _, _| None); cx.spawn_in(window, async move |workspace, cx| { let session = connect.await; workspace .update(cx, |workspace, cx| { if let Some(prompt) = workspace.active_modal::<SshConnectionModal>(cx) { prompt.update(cx, |prompt, cx| prompt.finished(cx)) } }) .ok(); let Some(Some(session)) = session else {Lines 1–500 of 1431. Load more
use std::any::Any; use std::collections::BTreeSet; use std::path::PathBuf; use std::sync::Arc; use editor::Editor; use file_finder::OpenPathDelegate; use futures::FutureExt; use futures::channel::oneshot; use futures::future::Shared; use gpui::ClipboardItem; use gpui::Task; use gpui::WeakEntity; use gpui::canvas; use gpui::{ AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Window, }; use picker::Picker; use project::Project; use remote::SshConnectionOptions; use remote::SshRemoteClient; use remote::ssh_session::ConnectionIdentifier; use settings::Settings; use settings::update_settings_file; use ui::Navigable; use ui::NavigableEntry; use ui::{ IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, Section, Tooltip, prelude::*, }; use util::ResultExt; use workspace::OpenOptions; use workspace::Toast; use workspace::notifications::NotificationId; use workspace::{ ModalView, Workspace, notifications::DetachAndPromptErr, open_ssh_project_with_existing_connection, }; use crate::OpenRemote; 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::ssh_connections::connect_over_ssh; use crate::ssh_connections::open_ssh_project; mod navigation_base {} pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakEntity<Workspace>, retained_connections: Vec<Entity<SshRemoteClient>>, } struct CreateRemoteServer { address_editor: Entity<Editor>, address_error: Option<SharedString>, ssh_prompt: Option<Entity<SshPrompt>>, _creating: Option<Task<Option<()>>>, } impl CreateRemoteServer { fn new(window: &mut Window, cx: &mut App) -> Self { let address_editor = cx.new(|cx| Editor::single_line(window, cx)); address_editor.update(cx, |this, cx| { this.focus_handle(cx).focus(window); }); Self { address_editor, address_error: None, ssh_prompt: None, _creating: None, } } } struct ProjectPicker { connection_string: SharedString, nickname: Option<SharedString>, picker: Entity<Picker<OpenPathDelegate>>, _path_task: Shared<Task<Option<()>>>, } struct EditNicknameState { index: usize, editor: Entity<Editor>, } impl EditNicknameState { fn new(index: usize, window: &mut Window, cx: &mut App) -> Self { let this = Self { index, editor: cx.new(|cx| Editor::single_line(window, cx)), }; 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, window, cx); } }); this.editor.focus_handle(cx).focus(window); this } } impl Focusable for ProjectPicker { fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl ProjectPicker { fn new( ix: usize, connection: SshConnectionOptions, project: Entity<Project>, workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<RemoteServerProjects>, ) -> Entity<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(|cx| { let picker = Picker::uniform_list(delegate, window, cx) .width(rems(34.)) .modal(false); picker.set_query(query, window, cx); picker }); let connection_string = connection.connection_string().into(); let nickname = connection.nickname.clone().map(|nick| nick.into()); let _path_task = cx .spawn_in(window, { let workspace = workspace.clone(); async move |this, cx| { let Ok(Some(paths)) = rx.await else { workspace .update_in(cx, |workspace, window, cx| { let weak = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { RemoteServerProjects::new(window, cx, weak) }); }) .log_err()?; return None; }; let app_state = workspace .update(cx, |workspace, _| workspace.app_state().clone()) .ok()?; cx.update(|_, cx| { 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.insert(SshProject { paths }); } } }); }) .log_err(); let options = cx .update(|_, cx| (app_state.build_window_options)(None, cx)) .log_err()?; let window = cx .open_window(options, |window, cx| { cx.new(|cx| { telemetry::event!("SSH Project Created"); Workspace::new(None, project.clone(), app_state.clone(), window, cx) }) }) .log_err()?; open_ssh_project_with_existing_connection( connection, project, paths, app_state, window, cx, ) .await .log_err(); this.update(cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); Some(()) } }) .shared(); cx.new(|_| Self { _path_task, picker, connection_string, nickname, }) } } impl gpui::Render for ProjectPicker { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { v_flex() .child( SshConnectionHeader { connection_string: self.connection_string.clone(), paths: Default::default(), nickname: self.nickname.clone(), } .render(window, cx), ) .child( div() .border_t_1() .border_color(cx.theme().colors().border_variant) .child(self.picker.clone()), ) } } #[derive(Clone)] struct ProjectEntry { open_folder: NavigableEntry, projects: Vec<(NavigableEntry, SshProject)>, configure: NavigableEntry, connection: SshConnection, } #[derive(Clone)] struct DefaultState { scrollbar: ScrollbarState, add_new_server: NavigableEntry, servers: Vec<ProjectEntry>, } impl DefaultState { fn new(cx: &mut App) -> Self { let handle = ScrollHandle::new(); let scrollbar = ScrollbarState::new(handle.clone()); let add_new_server = NavigableEntry::new(&handle, cx); let servers = SshSettings::get_global(cx) .ssh_connections() .map(|connection| { let open_folder = NavigableEntry::new(&handle, cx); let configure = NavigableEntry::new(&handle, cx); let projects = connection .projects .iter() .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) .collect(); ProjectEntry { open_folder, configure, projects, connection, } }) .collect(); Self { scrollbar, add_new_server, servers, } } } #[derive(Clone)] struct ViewServerOptionsState { server_index: usize, connection: SshConnection, entries: [NavigableEntry; 4], } enum Mode { Default(DefaultState), ViewServerOptions(ViewServerOptionsState), EditNickname(EditNicknameState), ProjectPicker(Entity<ProjectPicker>), CreateRemoteServer(CreateRemoteServer), } impl Mode { fn default_mode(cx: &mut App) -> Self { Self::Default(DefaultState::new(cx)) } } impl RemoteServerProjects { pub fn register( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { workspace.register_action(|workspace, _: &OpenRemote, window, cx| { let handle = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| Self::new(window, cx, handle)) }); } pub fn open(workspace: Entity<Workspace>, window: &mut Window, cx: &mut App) { workspace.update(cx, |workspace, cx| { let handle = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| Self::new(window, cx, handle)) }) } pub fn new( window: &mut Window, cx: &mut Context<Self>, workspace: WeakEntity<Workspace>, ) -> Self { let focus_handle = cx.focus_handle(); let mut base_style = window.text_style(); base_style.refine(&gpui::TextStyleRefinement { color: Some(cx.theme().colors().editor_foreground), ..Default::default() }); Self { mode: Mode::default_mode(cx), focus_handle, workspace, retained_connections: Vec::new(), } } pub fn project_picker( ix: usize, connection_options: remote::SshConnectionOptions, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, workspace: WeakEntity<Workspace>, ) -> Self { let mut this = Self::new(window, cx, workspace.clone()); this.mode = Mode::ProjectPicker(ProjectPicker::new( ix, connection_options, project, workspace, window, cx, )); cx.notify(); this } fn create_ssh_server( &mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<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::CreateRemoteServer(CreateRemoteServer { address_editor: editor, address_error: Some(format!("could not parse: {:?}", e).into()), ssh_prompt: None, _creating: None, }); return; } }; let ssh_prompt = cx.new(|cx| SshPrompt::new(&connection_options, window, cx)); let connection = connect_over_ssh( ConnectionIdentifier::setup(), connection_options.clone(), ssh_prompt.clone(), window, cx, ) .prompt_err("Failed to connect", window, cx, |_, _, _| None); let address_editor = editor.clone(); let creating = cx.spawn(async move |this, cx| { match connection.await { Some(Some(client)) => this .update(cx, |this, cx| { telemetry::event!("SSH Server Created"); this.retained_connections.push(client); this.add_ssh_server(connection_options, cx); this.mode = Mode::default_mode(cx); cx.notify() }) .log_err(), _ => this .update(cx, |this, cx| { address_editor.update(cx, |this, _| { this.set_read_only(false); }); this.mode = Mode::CreateRemoteServer(CreateRemoteServer { 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::CreateRemoteServer(CreateRemoteServer { address_editor: editor, address_error: None, ssh_prompt: Some(ssh_prompt.clone()), _creating: Some(creating), }); } fn view_server_options( &mut self, (server_index, connection): (usize, SshConnection), window: &mut Window, cx: &mut Context<Self>, ) { self.mode = Mode::ViewServerOptions(ViewServerOptionsState { server_index, connection, entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), }); self.focus_handle(cx).focus(window); cx.notify(); } fn create_ssh_project( &mut self, ix: usize, ssh_connection: SshConnection, window: &mut Window, cx: &mut Context<Self>, ) { let Some(workspace) = self.workspace.upgrade() else { return; }; let connection_options = ssh_connection.into(); workspace.update(cx, |_, cx| { cx.defer_in(window, move |workspace, window, cx| { workspace.toggle_modal(window, cx, |window, cx| { SshConnectionModal::new(&connection_options, Vec::new(), window, cx) }); let prompt = workspace .active_modal::<SshConnectionModal>(cx) .unwrap() .read(cx) .prompt .clone(); let connect = connect_over_ssh( ConnectionIdentifier::setup(), connection_options.clone(), prompt, window, cx, ) .prompt_err("Failed to connect", window, cx, |_, _, _| None); cx.spawn_in(window, async move |workspace, cx| { let session = connect.await; workspace .update(cx, |workspace, cx| { if let Some(prompt) = workspace.active_modal::<SshConnectionModal>(cx) { prompt.update(cx, |prompt, cx| prompt.finished(cx)) } }) .ok(); let Some(Some(session)) = session else {