use crate::{ remote_connections::{ Connection, RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettings, SshConnection, SshConnectionHeader, connect, determine_paths_with_positions, open_remote_project, }, ssh_config::parse_ssh_config_hosts,};use dev_container::{ DevContainerConfig, DevContainerContext, find_devcontainer_configs, start_dev_container_with_config,};use editor::Editor; use futures::{FutureExt, channel::oneshot, future::Shared};use gpui::{ Action, AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, canvas,};use log::{debug, info};use open_path_prompt::OpenPathDelegate;use paths::{global_ssh_config_file, user_ssh_config_file};use picker::{Picker, PickerDelegate};use project::{Fs, Project};use remote::{ RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions, remote_client::ConnectionIdentifier,};use settings::{ RemoteProject, RemoteSettingsContent, Settings as _, SettingsStore, update_settings_file, watch_config_file,};use smol::stream::StreamExt as _;use std::{ borrow::Cow, collections::BTreeSet, path::PathBuf, rc::Rc, sync::{ Arc, atomic::{self, AtomicUsize}, },};use ui::{ CommonAnimationExt, IconButtonShape, KeyBinding, List, ListItem, ListSeparator, Modal, ModalFooter, ModalHeader, Navigable, NavigableEntry, Section, Tooltip, WithScrollbar, prelude::*,};use util::{ ResultExt, paths::{PathStyle, RemotePathBuf}, rel_path::RelPath,};use workspace::{ AppState, ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace, notifications::{DetachAndPromptErr, NotificationId}, open_remote_project_with_existing_connection,}; pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakEntity<Workspace>, retained_connections: Vec<Entity<RemoteClient>>, ssh_config_updates: Task<()>, ssh_config_servers: BTreeSet<SharedString>, create_new_window: bool, dev_container_picker: Option<Entity<Picker<DevContainerPickerDelegate>>>, _subscription: Subscription,} struct CreateRemoteServer { address_editor: Entity<Editor>, address_error: Option<SharedString>, ssh_prompt: Option<Entity<RemoteConnectionPrompt>>, _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, cx); }); Self { address_editor, address_error: None, ssh_prompt: None, _creating: None, } }} #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]enum DevContainerCreationProgress { SelectingConfig, Creating, Error(String),} #[derive(Clone)]struct CreateRemoteDevContainer { view_logs_entry: NavigableEntry, back_entry: NavigableEntry, progress: DevContainerCreationProgress,} impl CreateRemoteDevContainer { fn new(progress: DevContainerCreationProgress, cx: &mut Context<RemoteServerProjects>) -> Self { let view_logs_entry = NavigableEntry::focusable(cx); let back_entry = NavigableEntry::focusable(cx); Self { view_logs_entry, back_entry, progress, } }} #[cfg(target_os = "windows")]struct AddWslDistro { picker: Entity<Picker<crate::wsl_picker::WslPickerDelegate>>, connection_prompt: Option<Entity<RemoteConnectionPrompt>>, _creating: Option<Task<()>>,} #[cfg(target_os = "windows")]impl AddWslDistro { fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self { use crate::wsl_picker::{WslDistroSelected, WslPickerDelegate, WslPickerDismissed}; let delegate = WslPickerDelegate::new(); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); cx.subscribe_in( &picker, window, |this, _, _: &WslDistroSelected, window, cx| { this.confirm(&menu::Confirm, window, cx); }, ) .detach(); cx.subscribe_in( &picker, window, |this, _, _: &WslPickerDismissed, window, cx| { this.cancel(&menu::Cancel, window, cx); }, ) .detach(); AddWslDistro { picker, connection_prompt: None, _creating: None, } }} enum ProjectPickerData { Ssh { connection_string: SharedString, nickname: Option<SharedString>, }, Wsl { distro_name: SharedString, },} struct ProjectPicker { data: ProjectPickerData, picker: Entity<Picker<OpenPathDelegate>>, _path_task: Shared<Task<Option<()>>>,} struct EditNicknameState { index: SshServerIndex, editor: Entity<Editor>,} struct DevContainerPickerDelegate { selected_index: usize, candidates: Vec<DevContainerConfig>, matching_candidates: Vec<DevContainerConfig>, parent_modal: WeakEntity<RemoteServerProjects>,}impl DevContainerPickerDelegate { fn new( candidates: Vec<DevContainerConfig>, parent_modal: WeakEntity<RemoteServerProjects>, ) -> Self { Self { selected_index: 0, matching_candidates: candidates.clone(), candidates, parent_modal, } }} impl PickerDelegate for DevContainerPickerDelegate { type ListItem = AnyElement; fn match_count(&self) -> usize { self.matching_candidates.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index( &mut self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) { self.selected_index = ix; } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> { "Select Dev Container Configuration".into() } fn update_matches( &mut self, query: String, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) -> Task<()> { let query_lower = query.to_lowercase(); self.matching_candidates = self .candidates .iter() .filter(|c| { c.name.to_lowercase().contains(&query_lower) || c.config_path .to_string_lossy() .to_lowercase() .contains(&query_lower) }) .cloned() .collect(); self.selected_index = std::cmp::min( self.selected_index, self.matching_candidates.len().saturating_sub(1), ); Task::ready(()) } fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { let selected_config = self.matching_candidates.get(self.selected_index).cloned(); self.parent_modal .update(cx, move |modal, cx| { if secondary { modal.edit_in_dev_container_json(selected_config.clone(), window, cx); } else if let Some((app_state, context)) = modal .workspace .read_with(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); let context = DevContainerContext::from_workspace(workspace, cx)?; Some((app_state, context)) }) .ok() .flatten() { modal.open_dev_container(selected_config, app_state, context, window, cx); modal.view_in_progress_dev_container(window, cx); } else { log::error!("No active project directory for Dev Container"); } }) .ok(); } fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) { self.parent_modal .update(cx, |modal, cx| { modal.cancel(&menu::Cancel, window, cx); }) .ok(); } fn render_match( &self, ix: usize, selected: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { let candidate = self.matching_candidates.get(ix)?; let config_path = candidate.config_path.display().to_string(); Some( ListItem::new(SharedString::from(format!("li-devcontainer-config-{}", ix))) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .toggle_state(selected) .start_slot(Icon::new(IconName::FileToml).color(Color::Muted)) .child( v_flex().child(Label::new(candidate.name.clone())).child( Label::new(config_path) .size(ui::LabelSize::Small) .color(Color::Muted), ), ) .into_any_element(), ) } fn render_footer( &self, _window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<AnyElement> { Some( h_flex() .w_full() .p_1p5() .gap_1() .justify_start() .border_t_1() .border_color(cx.theme().colors().border_variant) .child( Button::new("run-action", "Start Dev Container") .key_binding( KeyBinding::for_action(&menu::Confirm, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { window.dispatch_action(menu::Confirm.boxed_clone(), cx) }), ) .child( Button::new("run-action-secondary", "Open devcontainer.json") .key_binding( KeyBinding::for_action(&menu::SecondaryConfirm, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) }), ) .into_any_element(), ) }} impl EditNicknameState { fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self { let this = Self { index, editor: cx.new(|cx| Editor::single_line(window, cx)), }; let starting_text = RemoteSettings::get_global(cx) .ssh_connections() .nth(index.0) .and_then(|state| state.nickname) .filter(|text| !text.is_empty()); this.editor.update(cx, |this, cx| { this.set_placeholder_text("Add a nickname for this server", window, cx); if let Some(starting_text) = starting_text { this.set_text(starting_text, window, cx); } }); this.editor.focus_handle(cx).focus(window, cx); this }} impl Focusable for ProjectPicker { fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) }} impl ProjectPicker { fn new( create_new_window: bool, index: ServerIndex, connection: RemoteConnectionOptions, project: Entity<Project>, home_dir: RemotePathBuf, 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 delegate = open_path_prompt::OpenPathDelegate::new(tx, lister, false, cx).show_hidden(); let picker = cx.new(|cx| { let picker = Picker::uniform_list(delegate, window, cx) .width(rems(34.)) .modal(false); picker.set_query(&home_dir.to_string(), window, cx); picker }); let data = match &connection { RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh { connection_string: connection.connection_string().into(), nickname: connection.nickname.clone().map(|nick| nick.into()), }, RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl { distro_name: connection.distro_name.clone().into(), }, RemoteConnectionOptions::Docker(_) => ProjectPickerData::Ssh { // Not implemented as a project picker at this time connection_string: "".into(), nickname: None, }, #[cfg(any(test, feature = "test-support"))] RemoteConnectionOptions::Mock(options) => ProjectPickerData::Ssh { connection_string: format!("mock-{}", options.id).into(), nickname: None, }, }; let _path_task = cx .spawn_in(window, { let workspace = workspace; async move |this, cx| { let Ok(Some(paths)) = rx.await else { workspace .update_in(cx, |workspace, window, cx| { let fs = workspace.project().read(cx).fs().clone(); let weak = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { RemoteServerProjects::new( create_new_window, fs, window, weak, cx, ) }); }) .log_err()?; return None; }; let app_state = workspace .read_with(cx, |workspace, _| workspace.app_state().clone()) .ok()?; let remote_connection = project.read_with(cx, |project, cx| { project.remote_client()?.read(cx).connection() })?; let (paths, paths_with_positions) = determine_paths_with_positions(&remote_connection, paths).await; cx.update(|_, cx| { let fs = app_state.fs.clone(); update_settings_file(fs, cx, { let paths = paths .iter() .map(|path| path.to_string_lossy().into_owned()) .collect(); move |settings, _| match index { ServerIndex::Ssh(index) => { if let Some(server) = settings .remote .ssh_connections .as_mut() .and_then(|connections| connections.get_mut(index.0)) { server.projects.insert(RemoteProject { paths }); }; } ServerIndex::Wsl(index) => { if let Some(server) = settings .remote .wsl_connections .as_mut() .and_then(|connections| connections.get_mut(index.0)) { server.projects.insert(RemoteProject { 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| { let workspace = cx.new(|cx| { telemetry::event!("SSH Project Created"); Workspace::new(None, project.clone(), app_state.clone(), window, cx) }); cx.new(|cx| MultiWorkspace::new(workspace, window, cx)) }) .log_err()?; Lines 1–500 of 2989. Load more
use crate::{ remote_connections::{ Connection, RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettings, SshConnection, SshConnectionHeader, connect, determine_paths_with_positions, open_remote_project, }, ssh_config::parse_ssh_config_hosts, }; use dev_container::{ DevContainerConfig, DevContainerContext, find_devcontainer_configs, start_dev_container_with_config, }; use editor::Editor; use futures::{FutureExt, channel::oneshot, future::Shared}; use gpui::{ Action, AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, canvas, }; use log::{debug, info}; use open_path_prompt::OpenPathDelegate; use paths::{global_ssh_config_file, user_ssh_config_file}; use picker::{Picker, PickerDelegate}; use project::{Fs, Project}; use remote::{ RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions, remote_client::ConnectionIdentifier, }; use settings::{ RemoteProject, RemoteSettingsContent, Settings as _, SettingsStore, update_settings_file, watch_config_file, }; use smol::stream::StreamExt as _; use std::{ borrow::Cow, collections::BTreeSet, path::PathBuf, rc::Rc, sync::{ Arc, atomic::{self, AtomicUsize}, }, }; use ui::{ CommonAnimationExt, IconButtonShape, KeyBinding, List, ListItem, ListSeparator, Modal, ModalFooter, ModalHeader, Navigable, NavigableEntry, Section, Tooltip, WithScrollbar, prelude::*, }; use util::{ ResultExt, paths::{PathStyle, RemotePathBuf}, rel_path::RelPath, }; use workspace::{ AppState, ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace, notifications::{DetachAndPromptErr, NotificationId}, open_remote_project_with_existing_connection, }; pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, workspace: WeakEntity<Workspace>, retained_connections: Vec<Entity<RemoteClient>>, ssh_config_updates: Task<()>, ssh_config_servers: BTreeSet<SharedString>, create_new_window: bool, dev_container_picker: Option<Entity<Picker<DevContainerPickerDelegate>>>, _subscription: Subscription, } struct CreateRemoteServer { address_editor: Entity<Editor>, address_error: Option<SharedString>, ssh_prompt: Option<Entity<RemoteConnectionPrompt>>, _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, cx); }); Self { address_editor, address_error: None, ssh_prompt: None, _creating: None, } } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] enum DevContainerCreationProgress { SelectingConfig, Creating, Error(String), } #[derive(Clone)] struct CreateRemoteDevContainer { view_logs_entry: NavigableEntry, back_entry: NavigableEntry, progress: DevContainerCreationProgress, } impl CreateRemoteDevContainer { fn new(progress: DevContainerCreationProgress, cx: &mut Context<RemoteServerProjects>) -> Self { let view_logs_entry = NavigableEntry::focusable(cx); let back_entry = NavigableEntry::focusable(cx); Self { view_logs_entry, back_entry, progress, } } } #[cfg(target_os = "windows")] struct AddWslDistro { picker: Entity<Picker<crate::wsl_picker::WslPickerDelegate>>, connection_prompt: Option<Entity<RemoteConnectionPrompt>>, _creating: Option<Task<()>>, } #[cfg(target_os = "windows")] impl AddWslDistro { fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self { use crate::wsl_picker::{WslDistroSelected, WslPickerDelegate, WslPickerDismissed}; let delegate = WslPickerDelegate::new(); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); cx.subscribe_in( &picker, window, |this, _, _: &WslDistroSelected, window, cx| { this.confirm(&menu::Confirm, window, cx); }, ) .detach(); cx.subscribe_in( &picker, window, |this, _, _: &WslPickerDismissed, window, cx| { this.cancel(&menu::Cancel, window, cx); }, ) .detach(); AddWslDistro { picker, connection_prompt: None, _creating: None, } } } enum ProjectPickerData { Ssh { connection_string: SharedString, nickname: Option<SharedString>, }, Wsl { distro_name: SharedString, }, } struct ProjectPicker { data: ProjectPickerData, picker: Entity<Picker<OpenPathDelegate>>, _path_task: Shared<Task<Option<()>>>, } struct EditNicknameState { index: SshServerIndex, editor: Entity<Editor>, } struct DevContainerPickerDelegate { selected_index: usize, candidates: Vec<DevContainerConfig>, matching_candidates: Vec<DevContainerConfig>, parent_modal: WeakEntity<RemoteServerProjects>, } impl DevContainerPickerDelegate { fn new( candidates: Vec<DevContainerConfig>, parent_modal: WeakEntity<RemoteServerProjects>, ) -> Self { Self { selected_index: 0, matching_candidates: candidates.clone(), candidates, parent_modal, } } } impl PickerDelegate for DevContainerPickerDelegate { type ListItem = AnyElement; fn match_count(&self) -> usize { self.matching_candidates.len() } fn selected_index(&self) -> usize { self.selected_index } fn set_selected_index( &mut self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) { self.selected_index = ix; } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> { "Select Dev Container Configuration".into() } fn update_matches( &mut self, query: String, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) -> Task<()> { let query_lower = query.to_lowercase(); self.matching_candidates = self .candidates .iter() .filter(|c| { c.name.to_lowercase().contains(&query_lower) || c.config_path .to_string_lossy() .to_lowercase() .contains(&query_lower) }) .cloned() .collect(); self.selected_index = std::cmp::min( self.selected_index, self.matching_candidates.len().saturating_sub(1), ); Task::ready(()) } fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) { let selected_config = self.matching_candidates.get(self.selected_index).cloned(); self.parent_modal .update(cx, move |modal, cx| { if secondary { modal.edit_in_dev_container_json(selected_config.clone(), window, cx); } else if let Some((app_state, context)) = modal .workspace .read_with(cx, |workspace, cx| { let app_state = workspace.app_state().clone(); let context = DevContainerContext::from_workspace(workspace, cx)?; Some((app_state, context)) }) .ok() .flatten() { modal.open_dev_container(selected_config, app_state, context, window, cx); modal.view_in_progress_dev_container(window, cx); } else { log::error!("No active project directory for Dev Container"); } }) .ok(); } fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) { self.parent_modal .update(cx, |modal, cx| { modal.cancel(&menu::Cancel, window, cx); }) .ok(); } fn render_match( &self, ix: usize, selected: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>, ) -> Option<Self::ListItem> { let candidate = self.matching_candidates.get(ix)?; let config_path = candidate.config_path.display().to_string(); Some( ListItem::new(SharedString::from(format!("li-devcontainer-config-{}", ix))) .inset(true) .spacing(ui::ListItemSpacing::Sparse) .toggle_state(selected) .start_slot(Icon::new(IconName::FileToml).color(Color::Muted)) .child( v_flex().child(Label::new(candidate.name.clone())).child( Label::new(config_path) .size(ui::LabelSize::Small) .color(Color::Muted), ), ) .into_any_element(), ) } fn render_footer( &self, _window: &mut Window, cx: &mut Context<Picker<Self>>, ) -> Option<AnyElement> { Some( h_flex() .w_full() .p_1p5() .gap_1() .justify_start() .border_t_1() .border_color(cx.theme().colors().border_variant) .child( Button::new("run-action", "Start Dev Container") .key_binding( KeyBinding::for_action(&menu::Confirm, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { window.dispatch_action(menu::Confirm.boxed_clone(), cx) }), ) .child( Button::new("run-action-secondary", "Open devcontainer.json") .key_binding( KeyBinding::for_action(&menu::SecondaryConfirm, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) }), ) .into_any_element(), ) } } impl EditNicknameState { fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self { let this = Self { index, editor: cx.new(|cx| Editor::single_line(window, cx)), }; let starting_text = RemoteSettings::get_global(cx) .ssh_connections() .nth(index.0) .and_then(|state| state.nickname) .filter(|text| !text.is_empty()); this.editor.update(cx, |this, cx| { this.set_placeholder_text("Add a nickname for this server", window, cx); if let Some(starting_text) = starting_text { this.set_text(starting_text, window, cx); } }); this.editor.focus_handle(cx).focus(window, cx); this } } impl Focusable for ProjectPicker { fn focus_handle(&self, cx: &App) -> FocusHandle { self.picker.focus_handle(cx) } } impl ProjectPicker { fn new( create_new_window: bool, index: ServerIndex, connection: RemoteConnectionOptions, project: Entity<Project>, home_dir: RemotePathBuf, 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 delegate = open_path_prompt::OpenPathDelegate::new(tx, lister, false, cx).show_hidden(); let picker = cx.new(|cx| { let picker = Picker::uniform_list(delegate, window, cx) .width(rems(34.)) .modal(false); picker.set_query(&home_dir.to_string(), window, cx); picker }); let data = match &connection { RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh { connection_string: connection.connection_string().into(), nickname: connection.nickname.clone().map(|nick| nick.into()), }, RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl { distro_name: connection.distro_name.clone().into(), }, RemoteConnectionOptions::Docker(_) => ProjectPickerData::Ssh { // Not implemented as a project picker at this time connection_string: "".into(), nickname: None, }, #[cfg(any(test, feature = "test-support"))] RemoteConnectionOptions::Mock(options) => ProjectPickerData::Ssh { connection_string: format!("mock-{}", options.id).into(), nickname: None, }, }; let _path_task = cx .spawn_in(window, { let workspace = workspace; async move |this, cx| { let Ok(Some(paths)) = rx.await else { workspace .update_in(cx, |workspace, window, cx| { let fs = workspace.project().read(cx).fs().clone(); let weak = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { RemoteServerProjects::new( create_new_window, fs, window, weak, cx, ) }); }) .log_err()?; return None; }; let app_state = workspace .read_with(cx, |workspace, _| workspace.app_state().clone()) .ok()?; let remote_connection = project.read_with(cx, |project, cx| { project.remote_client()?.read(cx).connection() })?; let (paths, paths_with_positions) = determine_paths_with_positions(&remote_connection, paths).await; cx.update(|_, cx| { let fs = app_state.fs.clone(); update_settings_file(fs, cx, { let paths = paths .iter() .map(|path| path.to_string_lossy().into_owned()) .collect(); move |settings, _| match index { ServerIndex::Ssh(index) => { if let Some(server) = settings .remote .ssh_connections .as_mut() .and_then(|connections| connections.get_mut(index.0)) { server.projects.insert(RemoteProject { paths }); }; } ServerIndex::Wsl(index) => { if let Some(server) = settings .remote .wsl_connections .as_mut() .and_then(|connections| connections.get_mut(index.0)) { server.projects.insert(RemoteProject { 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| { let workspace = cx.new(|cx| { telemetry::event!("SSH Project Created"); Workspace::new(None, project.clone(), app_state.clone(), window, cx) }); cx.new(|cx| MultiWorkspace::new(workspace, window, cx)) }) .log_err()?;