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:
张小白 2025-07-08 22:34:57 +08:00 committed by GitHub
parent 8bd739d869
commit 0ca0914cca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1435 additions and 354 deletions

View file

@ -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}")