Fix remoting things (#19587)

- Fixes modal closing when using the remote modal folder 
- Fixes a bug with local terminals where they could open in / instead of
~
- Fixes a bug where SSH connections would continue running after their
window is closed
- Hides SSH Terminal process details from Zed UI
- Implement `cmd-o` for remote projects
- Implement LanguageServerPromptRequest for remote LSPs

Release Notes:

- N/A
This commit is contained in:
Mikayla Maki 2024-10-23 00:14:43 -07:00 committed by GitHub
parent fabc14355c
commit d0bc84eb33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 318 additions and 109 deletions

View file

@ -22,7 +22,7 @@ pub use environment::EnvironmentErrorMessage;
pub mod search_history;
mod yarn;
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context as _, Result};
use buffer_store::{BufferStore, BufferStoreEvent};
use client::{
proto, Client, Collaborator, DevServerProjectId, PendingEntitySubscription, ProjectId,
@ -40,8 +40,8 @@ use futures::{
use git::{blame::Blame, repository::GitRepository};
use gpui::{
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context, EventEmitter, Hsla, Model,
ModelContext, SharedString, Task, WeakModel, WindowContext,
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
};
use itertools::Itertools;
use language::{
@ -52,6 +52,7 @@ use language::{
};
use lsp::{
CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId,
MessageActionItem,
};
use lsp_command::*;
use node_runtime::NodeRuntime;
@ -59,7 +60,10 @@ use parking_lot::{Mutex, RwLock};
pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
use remote::{SshConnectionOptions, SshRemoteClient};
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
use rpc::{
proto::{LanguageServerPromptResponse, SSH_PROJECT_ID},
AnyProtoClient, ErrorCode,
};
use search::{SearchInputKind, SearchQuery, SearchResult};
use search_history::SearchHistory;
use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore};
@ -810,6 +814,7 @@ impl Project {
ssh_proto.add_model_message_handler(Self::handle_update_worktree);
ssh_proto.add_model_message_handler(Self::handle_update_project);
ssh_proto.add_model_message_handler(Self::handle_toast);
ssh_proto.add_model_request_handler(Self::handle_language_server_prompt_request);
ssh_proto.add_model_message_handler(Self::handle_hide_toast);
ssh_proto.add_model_request_handler(BufferStore::handle_update_buffer);
BufferStore::init(&ssh_proto);
@ -1180,6 +1185,7 @@ impl Project {
cx: &mut gpui::TestAppContext,
) -> Model<Project> {
use clock::FakeSystemClock;
use gpui::Context;
let languages = LanguageRegistry::test(cx.executor());
let clock = Arc::new(FakeSystemClock::default());
@ -3622,6 +3628,45 @@ impl Project {
})?
}
async fn handle_language_server_prompt_request(
this: Model<Self>,
envelope: TypedEnvelope<proto::LanguageServerPromptRequest>,
mut cx: AsyncAppContext,
) -> Result<proto::LanguageServerPromptResponse> {
let (tx, mut rx) = smol::channel::bounded(1);
let actions: Vec<_> = envelope
.payload
.actions
.into_iter()
.map(|action| MessageActionItem {
title: action,
properties: Default::default(),
})
.collect();
this.update(&mut cx, |_, cx| {
cx.emit(Event::LanguageServerPrompt(LanguageServerPromptRequest {
level: proto_to_prompt(envelope.payload.level.context("Invalid prompt level")?),
message: envelope.payload.message,
actions: actions.clone(),
lsp_name: envelope.payload.lsp_name,
response_channel: tx,
}));
anyhow::Ok(())
})??;
let answer = rx.next().await;
Ok(LanguageServerPromptResponse {
action_response: answer.and_then(|answer| {
actions
.iter()
.position(|action| *action == answer)
.map(|index| index as u64)
}),
})
}
async fn handle_hide_toast(
this: Model<Self>,
envelope: TypedEnvelope<proto::HideToast>,
@ -4257,3 +4302,11 @@ pub fn sort_worktree_entries(entries: &mut [Entry]) {
)
});
}
fn proto_to_prompt(level: proto::language_server_prompt_request::Level) -> gpui::PromptLevel {
match level {
proto::language_server_prompt_request::Level::Info(_) => gpui::PromptLevel::Info,
proto::language_server_prompt_request::Level::Warning(_) => gpui::PromptLevel::Warning,
proto::language_server_prompt_request::Level::Critical(_) => gpui::PromptLevel::Critical,
}
}

View file

@ -67,13 +67,15 @@ impl Project {
}
}
fn ssh_command(&self, cx: &AppContext) -> Option<SshCommand> {
if let Some(args) = self
.ssh_client
.as_ref()
.and_then(|session| session.read(cx).ssh_args())
{
return Some(SshCommand::Direct(args));
fn ssh_details(&self, cx: &AppContext) -> Option<(String, SshCommand)> {
if let Some(ssh_client) = &self.ssh_client {
let ssh_client = ssh_client.read(cx);
if let Some(args) = ssh_client.ssh_args() {
return Some((
ssh_client.connection_options().host.clone(),
SshCommand::Direct(args),
));
}
}
let dev_server_project_id = self.dev_server_project_id()?;
@ -83,7 +85,7 @@ impl Project {
.ssh_connection_string
.as_ref()?
.to_string();
Some(SshCommand::DevServer(ssh_command))
Some(("".to_string(), SshCommand::DevServer(ssh_command)))
}
pub fn create_terminal(
@ -102,7 +104,7 @@ impl Project {
}
}
};
let ssh_command = self.ssh_command(cx);
let ssh_details = self.ssh_details(cx);
let mut settings_location = None;
if let Some(path) = path.as_ref() {
@ -127,7 +129,7 @@ impl Project {
// precedence.
env.extend(settings.env.clone());
let local_path = if ssh_command.is_none() {
let local_path = if ssh_details.is_none() {
path.clone()
} else {
None
@ -144,8 +146,8 @@ impl Project {
self.python_activate_command(&python_venv_directory, settings);
}
match &ssh_command {
Some(ssh_command) => {
match &ssh_details {
Some((host, ssh_command)) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
@ -158,7 +160,14 @@ impl Project {
let (program, args) =
wrap_for_ssh(ssh_command, None, path.as_deref(), env, None);
env = HashMap::default();
(None, Shell::WithArguments { program, args })
(
None,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
}
None => (None, settings.shell.clone()),
}
@ -183,8 +192,8 @@ impl Project {
);
}
match &ssh_command {
Some(ssh_command) => {
match &ssh_details {
Some((host, ssh_command)) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
@ -196,7 +205,14 @@ impl Project {
python_venv_directory,
);
env = HashMap::default();
(task_state, Shell::WithArguments { program, args })
(
task_state,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
}
None => {
if let Some(venv_path) = &python_venv_directory {
@ -208,6 +224,7 @@ impl Project {
Shell::WithArguments {
program: spawn_task.command,
args: spawn_task.args,
title_override: None,
},
)
}

View file

@ -2939,7 +2939,7 @@ impl Render for ProjectPanel {
.key_binding(KeyBinding::for_action(&workspace::Open, cx))
.on_click(cx.listener(|this, _, cx| {
this.workspace
.update(cx, |workspace, cx| workspace.open(&workspace::Open, cx))
.update(cx, |_, cx| cx.dispatch_action(Box::new(workspace::Open)))
.log_err();
})),
)

View file

@ -299,6 +299,9 @@ message Envelope {
GetPermalinkToLineResponse get_permalink_to_line_response = 265;
FlushBufferedMessages flush_buffered_messages = 267;
LanguageServerPromptRequest language_server_prompt_request = 268;
LanguageServerPromptResponse language_server_prompt_response = 269; // current max
}
reserved 87 to 88;
@ -2528,3 +2531,25 @@ message GetPermalinkToLineResponse {
message FlushBufferedMessages {}
message FlushBufferedMessagesResponse {}
message LanguageServerPromptRequest {
uint64 project_id = 1;
oneof level {
Info info = 2;
Warning warning = 3;
Critical critical = 4;
}
message Info {}
message Warning {}
message Critical {}
string message = 5;
repeated string actions = 6;
string lsp_name = 7;
}
message LanguageServerPromptResponse {
optional uint64 action_response = 1;
}

View file

@ -373,6 +373,8 @@ messages!(
(GetPermalinkToLine, Foreground),
(GetPermalinkToLineResponse, Foreground),
(FlushBufferedMessages, Foreground),
(LanguageServerPromptRequest, Foreground),
(LanguageServerPromptResponse, Foreground),
);
request_messages!(
@ -500,6 +502,7 @@ request_messages!(
(OpenServerSettings, OpenBufferResponse),
(GetPermalinkToLine, GetPermalinkToLineResponse),
(FlushBufferedMessages, Ack),
(LanguageServerPromptRequest, LanguageServerPromptResponse),
);
entity_messages!(
@ -577,6 +580,7 @@ entity_messages!(
HideToast,
OpenServerSettings,
GetPermalinkToLine,
LanguageServerPromptRequest
);
entity_messages!(

View file

@ -386,6 +386,7 @@ impl RemoteServerProjects {
if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {
return;
}
self.selectable_items.next(cx);
cx.notify();
self.scroll_to_selected(cx);
@ -768,7 +769,7 @@ impl RemoteServerProjects {
};
let project = project.clone();
let server = server.clone();
cx.spawn(|_, mut cx| async move {
cx.spawn(|remote_server_projects, mut cx| async move {
let nickname = server.nickname.clone();
let result = open_ssh_project(
server.into(),
@ -789,6 +790,10 @@ impl RemoteServerProjects {
)
.await
.ok();
} else {
remote_server_projects
.update(&mut cx, |_, cx| cx.emit(DismissEvent))
.ok();
}
})
.detach();

View file

@ -1,13 +1,13 @@
use std::{path::PathBuf, sync::Arc, time::Duration};
use anyhow::Result;
use anyhow::{anyhow, Result};
use auto_update::AutoUpdater;
use editor::Editor;
use futures::channel::oneshot;
use gpui::{
percentage, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
EventEmitter, FocusableView, ParentElement as _, PromptLevel, Render, SemanticVersion,
SharedString, Task, TextStyleRefinement, Transformation, View,
SharedString, Task, TextStyleRefinement, Transformation, View, WeakView,
};
use gpui::{AppContext, Model};
@ -128,6 +128,14 @@ pub struct SshPrompt {
editor: View<Editor>,
}
impl Drop for SshPrompt {
fn drop(&mut self) {
if let Some(cancel) = self.cancellation.take() {
cancel.send(()).ok();
}
}
}
pub struct SshConnectionModal {
pub(crate) prompt: View<SshPrompt>,
paths: Vec<PathBuf>,
@ -393,7 +401,7 @@ impl ModalView for SshConnectionModal {
#[derive(Clone)]
pub struct SshClientDelegate {
window: AnyWindowHandle,
ui: View<SshPrompt>,
ui: WeakView<SshPrompt>,
known_password: Option<String>,
}
@ -493,7 +501,7 @@ impl SshClientDelegate {
)
.await
.map_err(|e| {
anyhow::anyhow!(
anyhow!(
"failed to download remote server binary (os: {}, arch: {}): {}",
platform.os,
platform.arch,
@ -520,7 +528,7 @@ impl SshClientDelegate {
.output()
.await?;
if !output.status.success() {
Err(anyhow::anyhow!("failed to run command: {:?}", command))?;
Err(anyhow!("failed to run command: {:?}", command))?;
}
Ok(())
}
@ -629,7 +637,7 @@ pub fn connect_over_ssh(
rx,
Arc::new(SshClientDelegate {
window,
ui,
ui: ui.downgrade(),
known_password,
}),
cx,
@ -686,7 +694,7 @@ pub async fn open_ssh_project(
Some(Arc::new(SshClientDelegate {
window: cx.window_handle(),
ui,
ui: ui.downgrade(),
known_password: connection_options.password.clone(),
}))
}

View file

@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
use fs::Fs;
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, PromptLevel};
use http_client::HttpClient;
use language::{proto::serialize_operation, Buffer, BufferEvent, LanguageRegistry};
use node_runtime::NodeRuntime;
@ -206,7 +206,7 @@ impl HeadlessProject {
&mut self,
_lsp_store: Model<LspStore>,
event: &LspStoreEvent,
_cx: &mut ModelContext<Self>,
cx: &mut ModelContext<Self>,
) {
match event {
LspStoreEvent::LanguageServerUpdate {
@ -240,6 +240,29 @@ impl HeadlessProject {
})
.log_err();
}
LspStoreEvent::LanguageServerPrompt(prompt) => {
let prompt = prompt.clone();
let request = self.session.request(proto::LanguageServerPromptRequest {
project_id: SSH_PROJECT_ID,
actions: prompt
.actions
.iter()
.map(|action| action.title.to_string())
.collect(),
level: Some(prompt_to_proto(&prompt)),
lsp_name: Default::default(),
message: Default::default(),
});
cx.background_executor()
.spawn(async move {
let response = request.await?;
if let Some(action_response) = response.action_response {
prompt.respond(action_response as usize).await;
}
anyhow::Ok(())
})
.detach();
}
_ => {}
}
}
@ -540,3 +563,19 @@ impl HeadlessProject {
Ok(proto::Ack {})
}
}
fn prompt_to_proto(
prompt: &project::LanguageServerPromptRequest,
) -> proto::language_server_prompt_request::Level {
match prompt.level {
PromptLevel::Info => proto::language_server_prompt_request::Level::Info(
proto::language_server_prompt_request::Info {},
),
PromptLevel::Warning => proto::language_server_prompt_request::Level::Warning(
proto::language_server_prompt_request::Warning {},
),
PromptLevel::Critical => proto::language_server_prompt_request::Level::Critical(
proto::language_server_prompt_request::Critical {},
),
}
}

View file

@ -269,5 +269,7 @@ pub enum Shell {
program: String,
/// The arguments to pass to the program.
args: Vec<String>,
/// An optional string to override the title of the terminal tab
title_override: Option<SharedString>,
},
}

View file

@ -45,7 +45,7 @@ use smol::channel::{Receiver, Sender};
use task::{HideStrategy, Shell, TaskId};
use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
use theme::{ActiveTheme, Theme};
use util::truncate_and_trailoff;
use util::{paths::home_dir, truncate_and_trailoff};
use std::{
cmp::{self, min},
@ -60,7 +60,7 @@ use thiserror::Error;
use gpui::{
actions, black, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla,
Keystroke, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, Point, Rgba, ScrollWheelEvent, Size, Task, TouchPhase,
Pixels, Point, Rgba, ScrollWheelEvent, SharedString, Size, Task, TouchPhase,
};
use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
@ -274,19 +274,21 @@ impl TerminalError {
})
}
pub fn shell_to_string(&self) -> String {
match &self.shell {
Shell::System => "<system shell>".to_string(),
Shell::Program(p) => p.to_string(),
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
}
}
pub fn fmt_shell(&self) -> String {
match &self.shell {
Shell::System => "<system defined shell>".to_string(),
Shell::Program(s) => s.to_string(),
Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
Shell::WithArguments {
program,
args,
title_override,
} => {
if let Some(title_override) = title_override {
format!("{} {} ({})", program, args.join(" "), title_override)
} else {
format!("{} {}", program, args.join(" "))
}
}
}
}
}
@ -348,20 +350,29 @@ impl TerminalBuilder {
release_channel::AppVersion::global(cx).to_string(),
);
let mut terminal_title_override = None;
let pty_options = {
let alac_shell = match shell.clone() {
Shell::System => None,
Shell::Program(program) => {
Some(alacritty_terminal::tty::Shell::new(program, Vec::new()))
}
Shell::WithArguments { program, args } => {
Shell::WithArguments {
program,
args,
title_override,
} => {
terminal_title_override = title_override;
Some(alacritty_terminal::tty::Shell::new(program, args))
}
};
alacritty_terminal::tty::Options {
shell: alac_shell,
working_directory: working_directory.clone(),
working_directory: working_directory
.clone()
.or_else(|| Some(home_dir().to_path_buf())),
hold: false,
env: env.into_iter().collect(),
}
@ -441,6 +452,7 @@ impl TerminalBuilder {
completion_tx,
term,
term_config: config,
title_override: terminal_title_override,
events: VecDeque::with_capacity(10), //Should never get this high.
last_content: Default::default(),
last_mouse: None,
@ -604,6 +616,7 @@ pub struct Terminal {
pub selection_head: Option<AlacPoint>,
pub breadcrumb_text: String,
pub pty_info: PtyProcessInfo,
title_override: Option<SharedString>,
scroll_px: Pixels,
next_link_id: usize,
selection_phase: SelectionPhase,
@ -1640,37 +1653,42 @@ impl Terminal {
}
}
None => self
.pty_info
.current
.title_override
.as_ref()
.map(|fpi| {
let process_file = fpi
.cwd
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default();
.map(|title_override| title_override.to_string())
.unwrap_or_else(|| {
self.pty_info
.current
.as_ref()
.map(|fpi| {
let process_file = fpi
.cwd
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default();
let argv = fpi.argv.clone();
let process_name = format!(
"{}{}",
fpi.name,
if !argv.is_empty() {
format!(" {}", (argv[1..]).join(" "))
} else {
"".to_string()
}
);
let (process_file, process_name) = if truncate {
(
truncate_and_trailoff(&process_file, MAX_CHARS),
truncate_and_trailoff(&process_name, MAX_CHARS),
)
} else {
(process_file, process_name)
};
format!("{process_file}{process_name}")
})
.unwrap_or_else(|| "Terminal".to_string()),
let argv = fpi.argv.clone();
let process_name = format!(
"{}{}",
fpi.name,
if !argv.is_empty() {
format!(" {}", (argv[1..]).join(" "))
} else {
"".to_string()
}
);
let (process_file, process_name) = if truncate {
(
truncate_and_trailoff(&process_file, MAX_CHARS),
truncate_and_trailoff(&process_name, MAX_CHARS),
)
} else {
(process_file, process_name)
};
format!("{process_file}{process_name}")
})
.unwrap_or_else(|| "Terminal".to_string())
}),
}
}

View file

@ -414,7 +414,7 @@ impl TerminalPanel {
}
}
Shell::Program(shell) => Some((shell, Vec::new())),
Shell::WithArguments { program, args } => Some((program, args)),
Shell::WithArguments { program, args, .. } => Some((program, args)),
}) else {
return;
};

View file

@ -1877,37 +1877,6 @@ impl Workspace {
})
}
pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
self.client()
.telemetry()
.report_app_event("open project".to_string());
let paths = self.prompt_for_open_path(
PathPromptOptions {
files: true,
directories: true,
multiple: true,
},
DirectoryLister::Local(self.app_state.fs.clone()),
cx,
);
cx.spawn(|this, mut cx| async move {
let Some(paths) = paths.await.log_err().flatten() else {
return;
};
if let Some(task) = this
.update(&mut cx, |this, cx| {
this.open_workspace_for_paths(false, paths, cx)
})
.log_err()
{
task.await.log_err();
}
})
.detach()
}
pub fn open_workspace_for_paths(
&mut self,
replace_current_window: bool,
@ -4345,7 +4314,6 @@ impl Workspace {
.on_action(cx.listener(Self::send_keystrokes))
.on_action(cx.listener(Self::add_folder_to_project))
.on_action(cx.listener(Self::follow_next_collaborator))
.on_action(cx.listener(Self::open))
.on_action(cx.listener(Self::close_window))
.on_action(cx.listener(Self::activate_pane_at_index))
.on_action(cx.listener(|workspace, _: &Unfollow, cx| {

View file

@ -18,8 +18,9 @@ use editor::ProposedChangesEditorToolbar;
use editor::{scroll::Autoscroll, Editor, MultiBuffer};
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PromptLevel,
ReadGlobal, TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem,
PathPromptOptions, PromptLevel, ReadGlobal, Task, TitlebarOptions, View, ViewContext,
VisualContext, WindowKind, WindowOptions,
};
pub use open_listener::*;
@ -27,9 +28,10 @@ use anyhow::Context as _;
use assets::Assets;
use futures::{channel::mpsc, select_biased, StreamExt};
use outline_panel::OutlinePanel;
use project::Item;
use project::{DirectoryLister, Item};
use project_panel::ProjectPanel;
use quick_action_bar::QuickActionBar;
use recent_projects::open_ssh_project;
use release_channel::{AppCommitSha, ReleaseChannel};
use rope::Rope;
use search::project_search::ProjectSearchBar;
@ -38,6 +40,7 @@ use settings::{
DEFAULT_KEYMAP_PATH,
};
use std::any::TypeId;
use std::path::PathBuf;
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
use theme::ActiveTheme;
use workspace::notifications::NotificationId;
@ -296,6 +299,40 @@ pub fn initialize_workspace(
.register_action(move |_, _: &zed_actions::IncreaseBufferFontSize, cx| {
theme::adjust_buffer_font_size(cx, |size| *size += px(1.0))
})
.register_action(|workspace, _: &workspace::Open, cx| {
workspace.client()
.telemetry()
.report_app_event("open project".to_string());
let paths = workspace.prompt_for_open_path(
PathPromptOptions {
files: true,
directories: true,
multiple: true,
},
DirectoryLister::Project(workspace.project().clone()),
cx,
);
cx.spawn(|this, mut cx| async move {
let Some(paths) = paths.await.log_err().flatten() else {
return;
};
if let Some(task) = this
.update(&mut cx, |this, cx| {
if this.project().read(cx).is_local() {
this.open_workspace_for_paths(false, paths, cx)
} else {
open_new_ssh_project_from_project(this, paths, cx)
}
})
.log_err()
{
task.await.log_err();
}
})
.detach()
})
.register_action(move |_, _: &zed_actions::DecreaseBufferFontSize, cx| {
theme::adjust_buffer_font_size(cx, |size| *size -= px(1.0))
})
@ -834,6 +871,39 @@ pub fn load_default_keymap(cx: &mut AppContext) {
}
}
pub fn open_new_ssh_project_from_project(
workspace: &mut Workspace,
paths: Vec<PathBuf>,
cx: &mut ViewContext<Workspace>,
) -> Task<anyhow::Result<()>> {
let app_state = workspace.app_state().clone();
let Some(ssh_client) = workspace.project().read(cx).ssh_client() else {
return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
};
let connection_options = ssh_client.read(cx).connection_options();
let nickname = recent_projects::SshSettings::get_global(cx).nickname_for(
&connection_options.host,
connection_options.port,
&connection_options.username,
);
cx.spawn(|_, mut cx| async move {
open_ssh_project(
connection_options,
paths,
app_state,
workspace::OpenOptions {
open_new_workspace: Some(true),
replace_window: None,
env: None,
},
nickname,
&mut cx,
)
.await
})
}
fn open_project_settings_file(
workspace: &mut Workspace,
_: &OpenProjectSettings,