windows: Add support for SSH (#29145)
Closes #19892 This PR builds on top of #20587 and improves upon it. Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
8bd739d869
commit
0ca0914cca
26 changed files with 1435 additions and 354 deletions
|
@ -33,7 +33,7 @@ use http_client::HttpClient;
|
|||
use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
|
||||
use node_runtime::NodeRuntime;
|
||||
|
||||
use remote::SshRemoteClient;
|
||||
use remote::{SshRemoteClient, ssh_session::SshArgs};
|
||||
use rpc::{
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
proto::{self},
|
||||
|
@ -253,11 +253,16 @@ impl DapStore {
|
|||
cx.spawn(async move |_, cx| {
|
||||
let response = request.await?;
|
||||
let binary = DebugAdapterBinary::from_proto(response)?;
|
||||
let mut ssh_command = ssh_client.read_with(cx, |ssh, _| {
|
||||
anyhow::Ok(SshCommand {
|
||||
arguments: ssh.ssh_args().context("SSH arguments not found")?,
|
||||
})
|
||||
})??;
|
||||
let (mut ssh_command, envs, path_style) =
|
||||
ssh_client.read_with(cx, |ssh, _| {
|
||||
let (SshArgs { arguments, envs }, path_style) =
|
||||
ssh.ssh_info().context("SSH arguments not found")?;
|
||||
anyhow::Ok((
|
||||
SshCommand { arguments },
|
||||
envs.unwrap_or_default(),
|
||||
path_style,
|
||||
))
|
||||
})??;
|
||||
|
||||
let mut connection = None;
|
||||
if let Some(c) = binary.connection {
|
||||
|
@ -282,12 +287,13 @@ impl DapStore {
|
|||
binary.cwd.as_deref(),
|
||||
binary.envs,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(program),
|
||||
arguments: args,
|
||||
envs: HashMap::default(),
|
||||
envs,
|
||||
cwd: None,
|
||||
connection,
|
||||
request_args: binary.request_args,
|
||||
|
|
|
@ -117,7 +117,7 @@ use text::{Anchor, BufferId, Point};
|
|||
use toolchain_store::EmptyToolchainStore;
|
||||
use util::{
|
||||
ResultExt as _,
|
||||
paths::{SanitizedPath, compare_paths},
|
||||
paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths},
|
||||
};
|
||||
use worktree::{CreatedEntry, Snapshot, Traversal};
|
||||
pub use worktree::{
|
||||
|
@ -1159,9 +1159,11 @@ impl Project {
|
|||
let snippets =
|
||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
||||
|
||||
let ssh_proto = ssh.read(cx).proto_client();
|
||||
let worktree_store =
|
||||
cx.new(|_| WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID));
|
||||
let (ssh_proto, path_style) =
|
||||
ssh.read_with(cx, |ssh, _| (ssh.proto_client(), ssh.path_style()));
|
||||
let worktree_store = cx.new(|_| {
|
||||
WorktreeStore::remote(false, ssh_proto.clone(), SSH_PROJECT_ID, path_style)
|
||||
});
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
|
@ -1410,8 +1412,15 @@ impl Project {
|
|||
let remote_id = response.payload.project_id;
|
||||
let role = response.payload.role();
|
||||
|
||||
// todo(zjk)
|
||||
// Set the proper path style based on the remote
|
||||
let worktree_store = cx.new(|_| {
|
||||
WorktreeStore::remote(true, client.clone().into(), response.payload.project_id)
|
||||
WorktreeStore::remote(
|
||||
true,
|
||||
client.clone().into(),
|
||||
response.payload.project_id,
|
||||
PathStyle::Posix,
|
||||
)
|
||||
})?;
|
||||
let buffer_store = cx.new(|cx| {
|
||||
BufferStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
||||
|
@ -4039,7 +4048,8 @@ impl Project {
|
|||
})
|
||||
})
|
||||
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||
let request_path = Path::new(path);
|
||||
let path_style = ssh_client.read(cx).path_style();
|
||||
let request_path = RemotePathBuf::from_str(path, path_style);
|
||||
let request = ssh_client
|
||||
.read(cx)
|
||||
.proto_client()
|
||||
|
|
|
@ -4,6 +4,7 @@ use collections::HashMap;
|
|||
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, Task, WeakEntity};
|
||||
use itertools::Itertools;
|
||||
use language::LanguageName;
|
||||
use remote::ssh_session::SshArgs;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use smol::channel::bounded;
|
||||
use std::{
|
||||
|
@ -17,7 +18,10 @@ use terminal::{
|
|||
TaskState, TaskStatus, Terminal, TerminalBuilder,
|
||||
terminal_settings::{self, TerminalSettings, VenvSettings},
|
||||
};
|
||||
use util::ResultExt;
|
||||
use util::{
|
||||
ResultExt,
|
||||
paths::{PathStyle, RemotePathBuf},
|
||||
};
|
||||
|
||||
pub struct Terminals {
|
||||
pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
|
||||
|
@ -47,6 +51,13 @@ impl SshCommand {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct SshDetails {
|
||||
pub host: String,
|
||||
pub ssh_command: SshCommand,
|
||||
pub envs: Option<HashMap<String, String>>,
|
||||
pub path_style: PathStyle,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
|
||||
let worktree = self
|
||||
|
@ -68,14 +79,16 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn ssh_details(&self, cx: &App) -> Option<(String, SshCommand)> {
|
||||
pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
|
||||
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 { arguments: args },
|
||||
));
|
||||
if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() {
|
||||
return Some(SshDetails {
|
||||
host: ssh_client.connection_options().host.clone(),
|
||||
ssh_command: SshCommand { arguments },
|
||||
envs,
|
||||
path_style,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,17 +171,26 @@ impl Project {
|
|||
.unwrap_or_default();
|
||||
env.extend(settings.env.clone());
|
||||
|
||||
match &self.ssh_details(cx) {
|
||||
Some((_, ssh_command)) => {
|
||||
match self.ssh_details(cx) {
|
||||
Some(SshDetails {
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
..
|
||||
}) => {
|
||||
let (command, args) = wrap_for_ssh(
|
||||
ssh_command,
|
||||
&ssh_command,
|
||||
Some((&command, &args)),
|
||||
path.as_deref(),
|
||||
env,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
let mut command = std::process::Command::new(command);
|
||||
command.args(args);
|
||||
if let Some(envs) = envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
command
|
||||
}
|
||||
None => {
|
||||
|
@ -202,6 +224,7 @@ impl Project {
|
|||
}
|
||||
};
|
||||
let ssh_details = this.ssh_details(cx);
|
||||
let is_ssh_terminal = ssh_details.is_some();
|
||||
|
||||
let mut settings_location = None;
|
||||
if let Some(path) = path.as_ref() {
|
||||
|
@ -226,11 +249,7 @@ impl Project {
|
|||
// precedence.
|
||||
env.extend(settings.env.clone());
|
||||
|
||||
let local_path = if ssh_details.is_none() {
|
||||
path.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let local_path = if is_ssh_terminal { None } else { path.clone() };
|
||||
|
||||
let mut python_venv_activate_command = None;
|
||||
|
||||
|
@ -241,8 +260,13 @@ impl Project {
|
|||
this.python_activate_command(python_venv_directory, &settings.detect_venv);
|
||||
}
|
||||
|
||||
match &ssh_details {
|
||||
Some((host, ssh_command)) => {
|
||||
match ssh_details {
|
||||
Some(SshDetails {
|
||||
host,
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
}) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
|
@ -252,9 +276,18 @@ impl Project {
|
|||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
||||
let (program, args) =
|
||||
wrap_for_ssh(&ssh_command, None, path.as_deref(), env, None);
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&ssh_command,
|
||||
None,
|
||||
path.as_deref(),
|
||||
env,
|
||||
None,
|
||||
path_style,
|
||||
);
|
||||
env = HashMap::default();
|
||||
if let Some(envs) = envs {
|
||||
env.extend(envs);
|
||||
}
|
||||
(
|
||||
Option::<TaskState>::None,
|
||||
Shell::WithArguments {
|
||||
|
@ -290,8 +323,13 @@ impl Project {
|
|||
);
|
||||
}
|
||||
|
||||
match &ssh_details {
|
||||
Some((host, ssh_command)) => {
|
||||
match ssh_details {
|
||||
Some(SshDetails {
|
||||
host,
|
||||
ssh_command,
|
||||
envs,
|
||||
path_style,
|
||||
}) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
|
@ -304,8 +342,12 @@ impl Project {
|
|||
path.as_deref(),
|
||||
env,
|
||||
python_venv_directory.as_deref(),
|
||||
path_style,
|
||||
);
|
||||
env = HashMap::default();
|
||||
if let Some(envs) = envs {
|
||||
env.extend(envs);
|
||||
}
|
||||
(
|
||||
task_state,
|
||||
Shell::WithArguments {
|
||||
|
@ -343,7 +385,7 @@ impl Project {
|
|||
settings.cursor_shape.unwrap_or_default(),
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
ssh_details.is_some(),
|
||||
is_ssh_terminal,
|
||||
window,
|
||||
completion_tx,
|
||||
cx,
|
||||
|
@ -533,6 +575,7 @@ pub fn wrap_for_ssh(
|
|||
path: Option<&Path>,
|
||||
env: HashMap<String, String>,
|
||||
venv_directory: Option<&Path>,
|
||||
path_style: PathStyle,
|
||||
) -> (String, Vec<String>) {
|
||||
let to_run = if let Some((command, args)) = command {
|
||||
// DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped
|
||||
|
@ -555,24 +598,25 @@ pub fn wrap_for_ssh(
|
|||
}
|
||||
if let Some(venv_directory) = venv_directory {
|
||||
if let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()) {
|
||||
env_changes.push_str(&format!("PATH={}:$PATH ", str));
|
||||
let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
|
||||
env_changes.push_str(&format!("PATH={}:$PATH ", path));
|
||||
}
|
||||
}
|
||||
|
||||
let commands = if let Some(path) = path {
|
||||
let path_string = path.to_string_lossy().to_string();
|
||||
let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
|
||||
// shlex will wrap the command in single quotes (''), disabling ~ expansion,
|
||||
// replace ith with something that works
|
||||
let tilde_prefix = "~/";
|
||||
if path.starts_with(tilde_prefix) {
|
||||
let trimmed_path = path_string
|
||||
let trimmed_path = path
|
||||
.trim_start_matches("/")
|
||||
.trim_start_matches("~")
|
||||
.trim_start_matches("/");
|
||||
|
||||
format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
|
||||
} else {
|
||||
format!("cd {path:?}; {env_changes} {to_run}")
|
||||
format!("cd {path}; {env_changes} {to_run}")
|
||||
}
|
||||
} else {
|
||||
format!("cd; {env_changes} {to_run}")
|
||||
|
|
|
@ -25,7 +25,10 @@ use smol::{
|
|||
stream::StreamExt,
|
||||
};
|
||||
use text::ReplicaId;
|
||||
use util::{ResultExt, paths::SanitizedPath};
|
||||
use util::{
|
||||
ResultExt,
|
||||
paths::{PathStyle, RemotePathBuf, SanitizedPath},
|
||||
};
|
||||
use worktree::{
|
||||
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
|
||||
WorktreeSettings,
|
||||
|
@ -46,6 +49,7 @@ enum WorktreeStoreState {
|
|||
Remote {
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: u64,
|
||||
path_style: PathStyle,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -100,6 +104,7 @@ impl WorktreeStore {
|
|||
retain_worktrees: bool,
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: u64,
|
||||
path_style: PathStyle,
|
||||
) -> Self {
|
||||
Self {
|
||||
next_entry_id: Default::default(),
|
||||
|
@ -111,6 +116,7 @@ impl WorktreeStore {
|
|||
state: WorktreeStoreState::Remote {
|
||||
upstream_client,
|
||||
upstream_project_id,
|
||||
path_style,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -214,17 +220,16 @@ impl WorktreeStore {
|
|||
if !self.loading_worktrees.contains_key(&abs_path) {
|
||||
let task = match &self.state {
|
||||
WorktreeStoreState::Remote {
|
||||
upstream_client, ..
|
||||
upstream_client,
|
||||
path_style,
|
||||
..
|
||||
} => {
|
||||
if upstream_client.is_via_collab() {
|
||||
Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
|
||||
} else {
|
||||
self.create_ssh_worktree(
|
||||
upstream_client.clone(),
|
||||
abs_path.clone(),
|
||||
visible,
|
||||
cx,
|
||||
)
|
||||
let abs_path =
|
||||
RemotePathBuf::new(abs_path.as_path().to_path_buf(), *path_style);
|
||||
self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
|
||||
}
|
||||
}
|
||||
WorktreeStoreState::Local { fs } => {
|
||||
|
@ -250,11 +255,12 @@ impl WorktreeStore {
|
|||
fn create_ssh_worktree(
|
||||
&mut self,
|
||||
client: AnyProtoClient,
|
||||
abs_path: impl Into<SanitizedPath>,
|
||||
abs_path: RemotePathBuf,
|
||||
visible: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {
|
||||
let mut abs_path = Into::<SanitizedPath>::into(abs_path).to_string();
|
||||
let path_style = abs_path.path_style();
|
||||
let mut abs_path = abs_path.to_string();
|
||||
// If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
|
||||
// in which case want to strip the leading the `/`.
|
||||
// On the host-side, the `~` will get expanded.
|
||||
|
@ -265,10 +271,11 @@ impl WorktreeStore {
|
|||
if abs_path.is_empty() {
|
||||
abs_path = "~/".to_string();
|
||||
}
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let this = this.upgrade().context("Dropped worktree store")?;
|
||||
|
||||
let path = Path::new(abs_path.as_str());
|
||||
let path = RemotePathBuf::new(abs_path.into(), path_style);
|
||||
let response = client
|
||||
.request(proto::AddWorktree {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue