use std::collections::BTreeSet;use std::path::PathBuf;use std::sync::Arc; use editor::Editor;use file_finder::OpenPathDelegate;use futures::channel::oneshot;use futures::future::Shared;use futures::FutureExt;use gpui::canvas;use gpui::ClipboardItem;use gpui::Task;use gpui::WeakView;use gpui::{ AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, PromptLevel, ScrollHandle, View, ViewContext,};use picker::Picker;use project::Project;use remote::ssh_session::ConnectionIdentifier;use remote::SshConnectionOptions;use remote::SshRemoteClient;use settings::update_settings_file;use settings::Settings;use ui::Navigable;use ui::NavigableEntry;use ui::{ prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, Section, Tooltip,};use util::ResultExt;use workspace::notifications::NotificationId;use workspace::OpenOptions;use workspace::Toast;use workspace::{notifications::DetachAndPromptErr, ModalView, Workspace}; 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; mod navigation_base {}pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakView<Workspace>, retained_connections: Vec<Model<SshRemoteClient>>,} struct CreateRemoteServer { address_editor: View<Editor>, address_error: Option<SharedString>, ssh_prompt: Option<View<SshPrompt>>, _creating: Option<Task<Option<()>>>,} impl CreateRemoteServer { 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, nickname: Option<SharedString>, picker: View<Picker<OpenPathDelegate>>, _path_task: Shared<Task<Option<()>>>,} 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 FocusableView for ProjectPicker { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.picker.focus_handle(cx) }} impl ProjectPicker { fn new( ix: usize, connection: SshConnectionOptions, project: Model<Project>, workspace: WeakView<Workspace>, cx: &mut ViewContext<RemoteServerProjects>, ) -> 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 }); let connection_string = connection.connection_string().into(); let nickname = connection.nickname.clone().map(|nick| nick.into()); let _path_task = cx .spawn({ let workspace = workspace.clone(); move |this, 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| RemoteServerProjects::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.insert(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(); this.update(&mut cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); Some(()) } }) .shared(); cx.new_view(|_| Self { _path_task, picker, connection_string, nickname, }) }} 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(), paths: Default::default(), nickname: self.nickname.clone(), } .render(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: &WindowContext<'_>) -> 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(View<ProjectPicker>), CreateRemoteServer(CreateRemoteServer),} impl Mode { fn default_mode(cx: &WindowContext<'_>) -> Self { Self::Default(DefaultState::new(cx)) }}impl RemoteServerProjects { 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(cx), focus_handle, workspace, retained_connections: Vec::new(), } } 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, project, workspace, cx, )); cx.notify(); 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::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_view(|cx| SshPrompt::new(&connection_options, cx)); let connection = connect_over_ssh( ConnectionIdentifier::setup(), 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(Some(client)) => this .update(&mut cx, |this, cx| { let _ = this.workspace.update(cx, |workspace, _| { workspace .client() .telemetry() .report_app_event("create ssh server".to_string()) }); this.retained_connections.push(client); this.add_ssh_server(connection_options, cx); this.mode = Mode::default_mode(cx); cx.notify() }) .log_err(), _ => this .update(&mut 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), cx: &mut ViewContext<Self>, ) { self.mode = Mode::ViewServerOptions(ViewServerOptionsState { server_index, connection, entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), }); self.focus_handle(cx).focus(cx); 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, Vec::new(), cx) }); let prompt = workspace .active_modal::<SshConnectionModal>(cx) .unwrap() .read(cx) .prompt .clone(); let connect = connect_over_ssh( ConnectionIdentifier::setup(), connection_options.clone(), prompt, cx, ) .prompt_err("Failed to connect", cx, |_, _| None); cx.spawn(move |workspace, mut cx| async move { let session = connect.await; workspace .update(&mut 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 { workspace .update(&mut cx, |workspace, cx| {Lines 1–500 of 1390. Load more
use std::collections::BTreeSet; use std::path::PathBuf; use std::sync::Arc; use editor::Editor; use file_finder::OpenPathDelegate; use futures::channel::oneshot; use futures::future::Shared; use futures::FutureExt; use gpui::canvas; use gpui::ClipboardItem; use gpui::Task; use gpui::WeakView; use gpui::{ AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, PromptLevel, ScrollHandle, View, ViewContext, }; use picker::Picker; use project::Project; use remote::ssh_session::ConnectionIdentifier; use remote::SshConnectionOptions; use remote::SshRemoteClient; use settings::update_settings_file; use settings::Settings; use ui::Navigable; use ui::NavigableEntry; use ui::{ prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, Section, Tooltip, }; use util::ResultExt; use workspace::notifications::NotificationId; use workspace::OpenOptions; use workspace::Toast; use workspace::{notifications::DetachAndPromptErr, ModalView, Workspace}; 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; mod navigation_base {} pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakView<Workspace>, retained_connections: Vec<Model<SshRemoteClient>>, } struct CreateRemoteServer { address_editor: View<Editor>, address_error: Option<SharedString>, ssh_prompt: Option<View<SshPrompt>>, _creating: Option<Task<Option<()>>>, } impl CreateRemoteServer { 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, nickname: Option<SharedString>, picker: View<Picker<OpenPathDelegate>>, _path_task: Shared<Task<Option<()>>>, } 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 FocusableView for ProjectPicker { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.picker.focus_handle(cx) } } impl ProjectPicker { fn new( ix: usize, connection: SshConnectionOptions, project: Model<Project>, workspace: WeakView<Workspace>, cx: &mut ViewContext<RemoteServerProjects>, ) -> 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 }); let connection_string = connection.connection_string().into(); let nickname = connection.nickname.clone().map(|nick| nick.into()); let _path_task = cx .spawn({ let workspace = workspace.clone(); move |this, 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| RemoteServerProjects::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.insert(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(); this.update(&mut cx, |_, cx| { cx.emit(DismissEvent); }) .ok(); Some(()) } }) .shared(); cx.new_view(|_| Self { _path_task, picker, connection_string, nickname, }) } } 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(), paths: Default::default(), nickname: self.nickname.clone(), } .render(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: &WindowContext<'_>) -> 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(View<ProjectPicker>), CreateRemoteServer(CreateRemoteServer), } impl Mode { fn default_mode(cx: &WindowContext<'_>) -> Self { Self::Default(DefaultState::new(cx)) } } impl RemoteServerProjects { 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(cx), focus_handle, workspace, retained_connections: Vec::new(), } } 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, project, workspace, cx, )); cx.notify(); 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::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_view(|cx| SshPrompt::new(&connection_options, cx)); let connection = connect_over_ssh( ConnectionIdentifier::setup(), 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(Some(client)) => this .update(&mut cx, |this, cx| { let _ = this.workspace.update(cx, |workspace, _| { workspace .client() .telemetry() .report_app_event("create ssh server".to_string()) }); this.retained_connections.push(client); this.add_ssh_server(connection_options, cx); this.mode = Mode::default_mode(cx); cx.notify() }) .log_err(), _ => this .update(&mut 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), cx: &mut ViewContext<Self>, ) { self.mode = Mode::ViewServerOptions(ViewServerOptionsState { server_index, connection, entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), }); self.focus_handle(cx).focus(cx); 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, Vec::new(), cx) }); let prompt = workspace .active_modal::<SshConnectionModal>(cx) .unwrap() .read(cx) .prompt .clone(); let connect = connect_over_ssh( ConnectionIdentifier::setup(), connection_options.clone(), prompt, cx, ) .prompt_err("Failed to connect", cx, |_, _| None); cx.spawn(move |workspace, mut cx| async move { let session = connect.await; workspace .update(&mut 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 { workspace .update(&mut cx, |workspace, cx| {