SSH remoting: terminal & tasks (#15321)

This also rolls back the `TerminalWorkDir` abstraction I added for the
original remoting, and tidies up the terminal creation code to be clear
about whether we're creating a task *or* a terminal. The previous logic
was a little muddy because it assumed we could be doing both at the same
time (which was not true).

Release Notes:

- remoting alpha: Removed the ability to specify `gh cs ssh` or `gcloud
compute ssh` etc. See https://zed.dev/docs/remote-development for
alternatives.
- remoting alpha: Added support for terminal and tasks to new
experimental ssh remoting
This commit is contained in:
Conrad Irwin 2024-07-28 22:45:00 -06:00 committed by GitHub
parent 26d0a33e79
commit 583b6235fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 404 additions and 454 deletions

View file

@ -515,7 +515,8 @@ impl Project {
buffer: &Model<Buffer>, buffer: &Model<Buffer>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Option<(Option<PathBuf>, PrettierTask)>> { ) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
if !self.is_local() { // todo(ssh remote): prettier support
if self.is_remote() || self.ssh_session.is_some() {
return Task::ready(None); return Task::ready(None);
} }
let buffer = buffer.read(cx); let buffer = buffer.read(cx);

View file

@ -1914,6 +1914,13 @@ impl Project {
} }
} }
pub fn is_ssh(&self) -> bool {
match &self.client_state {
ProjectClientState::Local | ProjectClientState::Shared { .. } => true,
ProjectClientState::Remote { .. } => false,
}
}
pub fn is_remote(&self) -> bool { pub fn is_remote(&self) -> bool {
!self.is_local() !self.is_local()
} }
@ -7687,11 +7694,7 @@ impl Project {
) -> Option<(Model<Worktree>, PathBuf)> { ) -> Option<(Model<Worktree>, PathBuf)> {
self.worktree_store.read_with(cx, |worktree_store, cx| { self.worktree_store.read_with(cx, |worktree_store, cx| {
for tree in worktree_store.worktrees() { for tree in worktree_store.worktrees() {
if let Some(relative_path) = tree if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) {
.read(cx)
.as_local()
.and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
{
return Some((tree.clone(), relative_path.into())); return Some((tree.clone(), relative_path.into()));
} }
} }

View file

@ -1,21 +1,18 @@
use crate::Project; use crate::Project;
use anyhow::Context as _; use anyhow::Context as _;
use collections::HashMap; use collections::HashMap;
use gpui::{ use gpui::{AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, WeakModel};
AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, SharedString, WeakModel,
};
use itertools::Itertools; use itertools::Itertools;
use settings::{Settings, SettingsLocation}; use settings::{Settings, SettingsLocation};
use smol::channel::bounded; use smol::channel::bounded;
use std::{ use std::{
env, env::{self},
fs::File, iter,
io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use task::{Shell, SpawnInTerminal, TerminalWorkDir}; use task::{Shell, SpawnInTerminal};
use terminal::{ use terminal::{
terminal_settings::{self, TerminalSettings, VenvSettingsContent}, terminal_settings::{self, TerminalSettings},
TaskState, TaskStatus, Terminal, TerminalBuilder, TaskState, TaskStatus, Terminal, TerminalBuilder,
}; };
use util::ResultExt; use util::ResultExt;
@ -27,21 +24,53 @@ pub struct Terminals {
pub(crate) local_handles: Vec<WeakModel<terminal::Terminal>>, pub(crate) local_handles: Vec<WeakModel<terminal::Terminal>>,
} }
#[derive(Debug, Clone)] /// Terminals are opened either for the users shell, or to run a task.
pub struct ConnectRemoteTerminal { #[allow(clippy::large_enum_variant)]
pub ssh_connection_string: SharedString, #[derive(Debug)]
pub project_path: SharedString, pub enum TerminalKind {
/// Run a shell at the given path (or $HOME if None)
Shell(Option<PathBuf>),
/// Run a task.
Task(SpawnInTerminal),
}
/// SshCommand describes how to connect to a remote server
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SshCommand {
/// DevServers give a string from the user
DevServer(String),
/// Direct ssh has a list of arguments to pass to ssh
Direct(Vec<String>),
} }
impl Project { impl Project {
pub fn terminal_work_dir_for( pub fn active_project_directory(&self, cx: &AppContext) -> Option<PathBuf> {
&self, let worktree = self
pathbuf: Option<&Path>, .active_entry()
cx: &AppContext, .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
) -> Option<TerminalWorkDir> { .or_else(|| self.worktrees(cx).next())?;
if self.is_local() { let worktree = worktree.read(cx);
return Some(TerminalWorkDir::Local(pathbuf?.to_owned())); if !worktree.root_entry()?.is_dir() {
return None;
} }
Some(worktree.abs_path().to_path_buf())
}
pub fn first_project_directory(&self, cx: &AppContext) -> Option<PathBuf> {
let worktree = self.worktrees(cx).next()?;
let worktree = worktree.read(cx);
if worktree.root_entry()?.is_dir() {
return Some(worktree.abs_path().to_path_buf());
} else {
None
}
}
fn ssh_command(&self, cx: &AppContext) -> Option<SshCommand> {
if let Some(ssh_session) = self.ssh_session.as_ref() {
return Some(SshCommand::Direct(ssh_session.ssh_args()));
}
let dev_server_project_id = self.dev_server_project_id()?; let dev_server_project_id = self.dev_server_project_id()?;
let projects_store = dev_server_projects::Store::global(cx).read(cx); let projects_store = dev_server_projects::Store::global(cx).read(cx);
let ssh_command = projects_store let ssh_command = projects_store
@ -49,133 +78,132 @@ impl Project {
.ssh_connection_string .ssh_connection_string
.as_ref()? .as_ref()?
.to_string(); .to_string();
Some(SshCommand::DevServer(ssh_command))
let path = if let Some(pathbuf) = pathbuf {
pathbuf.to_string_lossy().to_string()
} else {
projects_store
.dev_server_project(dev_server_project_id)?
.paths
.get(0)
.unwrap()
.to_string()
};
Some(TerminalWorkDir::Ssh {
ssh_command,
path: Some(path),
})
} }
pub fn create_terminal( pub fn create_terminal(
&mut self, &mut self,
working_directory: Option<TerminalWorkDir>, kind: TerminalKind,
spawn_task: Option<SpawnInTerminal>,
window: AnyWindowHandle, window: AnyWindowHandle,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> anyhow::Result<Model<Terminal>> { ) -> anyhow::Result<Model<Terminal>> {
// used only for TerminalSettings::get let path = match &kind {
let worktree = { TerminalKind::Shell(path) => path.as_ref().map(|path| path.to_path_buf()),
let terminal_cwd = working_directory.as_ref().and_then(|cwd| cwd.local_path()); TerminalKind::Task(spawn_task) => {
let task_cwd = spawn_task if let Some(cwd) = &spawn_task.cwd {
.as_ref() Some(cwd.clone())
.and_then(|spawn_task| spawn_task.cwd.as_ref()) } else {
.and_then(|cwd| cwd.local_path()); self.active_project_directory(cx)
}
terminal_cwd }
.and_then(|terminal_cwd| self.find_worktree(&terminal_cwd, cx))
.or_else(|| task_cwd.and_then(|spawn_cwd| self.find_worktree(&spawn_cwd, cx)))
}; };
let ssh_command = self.ssh_command(cx);
let settings_location = worktree.as_ref().map(|(worktree, path)| SettingsLocation { let mut settings_location = None;
worktree_id: worktree.read(cx).id().to_usize(), if let Some(path) = path.as_ref() {
path, if let Some((worktree, _)) = self.find_worktree(path, cx) {
}); settings_location = Some(SettingsLocation {
worktree_id: worktree.read(cx).id().to_usize(),
let is_terminal = spawn_task.is_none() path,
&& working_directory });
.as_ref() }
.map_or(true, |work_dir| work_dir.is_local()); }
let settings = TerminalSettings::get(settings_location, cx); let settings = TerminalSettings::get(settings_location, cx);
let python_settings = settings.detect_venv.clone();
let (completion_tx, completion_rx) = bounded(1); let (completion_tx, completion_rx) = bounded(1);
let mut env = settings.env.clone(); let mut env = settings.env.clone();
// Alacritty uses parent project's working directory when no working directory is provided
// https://github.com/alacritty/alacritty/blob/fd1a3cc79192d1d03839f0fd8c72e1f8d0fce42e/extra/man/alacritty.5.scd?plain=1#L47-L52
let mut retained_script = None; let local_path = if ssh_command.is_none() {
path.clone()
let venv_base_directory = working_directory } else {
None
};
let python_venv_directory = path
.as_ref() .as_ref()
.and_then(|cwd| cwd.local_path()) .and_then(|path| self.python_venv_directory(path, settings, cx));
.unwrap_or_else(|| Path::new("")); let mut python_venv_activate_command = None;
let (spawn_task, shell) = match working_directory.as_ref() { let (spawn_task, shell) = match kind {
Some(TerminalWorkDir::Ssh { ssh_command, path }) => { TerminalKind::Shell(_) => {
log::debug!("Connecting to a remote server: {ssh_command:?}"); if let Some(python_venv_directory) = python_venv_directory {
let tmp_dir = tempfile::tempdir()?; python_venv_activate_command =
let ssh_shell_result = prepare_ssh_shell( self.python_activate_command(&python_venv_directory, settings);
&mut env, }
tmp_dir.path(),
spawn_task.as_ref(),
ssh_command,
path.as_deref(),
);
retained_script = Some(tmp_dir);
let ssh_shell = ssh_shell_result?;
( match &ssh_command {
spawn_task.map(|spawn_task| TaskState { Some(ssh_command) => {
id: spawn_task.id, log::debug!("Connecting to a remote server: {ssh_command:?}");
full_label: spawn_task.full_label,
label: spawn_task.label, // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
command_label: spawn_task.command_label, // to properly display colors.
hide: spawn_task.hide, // We do not have the luxury of assuming the host has it installed,
status: TaskStatus::Running, // so we set it to a default that does not break the highlighting via ssh.
completion_rx, env.entry("TERM".to_string())
}), .or_insert_with(|| "xterm-256color".to_string());
ssh_shell,
) let (program, args) =
} wrap_for_ssh(ssh_command, None, path.as_deref(), env, None);
_ => { env = HashMap::default();
if let Some(spawn_task) = spawn_task { (None, Shell::WithArguments { program, args })
log::debug!("Spawning task: {spawn_task:?}"); }
env.extend(spawn_task.env); None => (None, settings.shell.clone()),
// Activate minimal Python virtual environment }
if let Some(python_settings) = &python_settings.as_option() { }
self.set_python_venv_path_for_tasks( TerminalKind::Task(spawn_task) => {
python_settings, let task_state = Some(TaskState {
&venv_base_directory, id: spawn_task.id,
&mut env, full_label: spawn_task.full_label,
); label: spawn_task.label,
command_label: spawn_task.command_label,
hide: spawn_task.hide,
status: TaskStatus::Running,
completion_rx,
});
env.extend(spawn_task.env);
if let Some(venv_path) = &python_venv_directory {
env.insert(
"VIRTUAL_ENV".to_string(),
venv_path.to_string_lossy().to_string(),
);
}
match &ssh_command {
Some(ssh_command) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) = wrap_for_ssh(
ssh_command,
Some((&spawn_task.command, &spawn_task.args)),
path.as_deref(),
env,
python_venv_directory,
);
env = HashMap::default();
(task_state, Shell::WithArguments { program, args })
}
None => {
if let Some(venv_path) = &python_venv_directory {
add_environment_path(&mut env, &venv_path.join("bin")).log_err();
}
(
task_state,
Shell::WithArguments {
program: spawn_task.command,
args: spawn_task.args,
},
)
} }
(
Some(TaskState {
id: spawn_task.id,
full_label: spawn_task.full_label,
label: spawn_task.label,
command_label: spawn_task.command_label,
hide: spawn_task.hide,
status: TaskStatus::Running,
completion_rx,
}),
Shell::WithArguments {
program: spawn_task.command,
args: spawn_task.args,
},
)
} else {
(None, settings.shell.clone())
} }
} }
}; };
let terminal = TerminalBuilder::new( let terminal = TerminalBuilder::new(
working_directory local_path,
.as_ref()
.and_then(|cwd| cwd.local_path())
.map(ToOwned::to_owned),
spawn_task, spawn_task,
shell, shell,
env, env,
@ -195,7 +223,6 @@ impl Project {
let id = terminal_handle.entity_id(); let id = terminal_handle.entity_id();
cx.observe_release(&terminal_handle, move |project, _terminal, cx| { cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
drop(retained_script);
let handles = &mut project.terminals.local_handles; let handles = &mut project.terminals.local_handles;
if let Some(index) = handles if let Some(index) = handles
@ -208,20 +235,8 @@ impl Project {
}) })
.detach(); .detach();
// if the terminal is not a task, activate full Python virtual environment if let Some(activate_command) = python_venv_activate_command {
if is_terminal { self.activate_python_virtual_environment(activate_command, &terminal_handle, cx);
if let Some(python_settings) = &python_settings.as_option() {
if let Some(activate_script_path) =
self.find_activate_script_path(python_settings, &venv_base_directory)
{
self.activate_python_virtual_environment(
Project::get_activate_command(python_settings),
activate_script_path,
&terminal_handle,
cx,
);
}
}
} }
terminal_handle terminal_handle
}); });
@ -229,80 +244,58 @@ impl Project {
terminal terminal
} }
pub fn find_activate_script_path( pub fn python_venv_directory(
&mut self, &self,
settings: &VenvSettingsContent, abs_path: &Path,
venv_base_directory: &Path, settings: &TerminalSettings,
cx: &AppContext,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
let activate_script_name = match settings.activate_script { let venv_settings = settings.detect_venv.as_option()?;
venv_settings
.directories
.into_iter()
.map(|virtual_environment_name| abs_path.join(virtual_environment_name))
.find(|venv_path| {
self.find_worktree(&venv_path, cx)
.and_then(|(worktree, relative_path)| {
worktree.read(cx).entry_for_path(&relative_path)
})
.is_some()
})
}
fn python_activate_command(
&self,
venv_base_directory: &Path,
settings: &TerminalSettings,
) -> Option<String> {
let venv_settings = settings.detect_venv.as_option()?;
let activate_script_name = match venv_settings.activate_script {
terminal_settings::ActivateScript::Default => "activate", terminal_settings::ActivateScript::Default => "activate",
terminal_settings::ActivateScript::Csh => "activate.csh", terminal_settings::ActivateScript::Csh => "activate.csh",
terminal_settings::ActivateScript::Fish => "activate.fish", terminal_settings::ActivateScript::Fish => "activate.fish",
terminal_settings::ActivateScript::Nushell => "activate.nu", terminal_settings::ActivateScript::Nushell => "activate.nu",
}; };
let path = venv_base_directory
.join("bin")
.join(activate_script_name)
.to_string_lossy()
.to_string();
let quoted = shlex::try_quote(&path).ok()?;
settings Some(match venv_settings.activate_script {
.directories terminal_settings::ActivateScript::Nushell => format!("overlay use {}\n", quoted),
.into_iter() _ => format!("source {}\n", quoted),
.find_map(|virtual_environment_name| { })
let path = venv_base_directory
.join(virtual_environment_name)
.join("bin")
.join(activate_script_name);
path.exists().then_some(path)
})
}
pub fn set_python_venv_path_for_tasks(
&mut self,
settings: &VenvSettingsContent,
venv_base_directory: &Path,
env: &mut HashMap<String, String>,
) {
let activate_path = settings
.directories
.into_iter()
.find_map(|virtual_environment_name| {
let path = venv_base_directory.join(virtual_environment_name);
path.exists().then_some(path)
});
if let Some(path) = activate_path {
// Some tools use VIRTUAL_ENV to detect the virtual environment
env.insert(
"VIRTUAL_ENV".to_string(),
path.to_string_lossy().to_string(),
);
// We need to set the PATH to include the virtual environment's bin directory
add_environment_path(env, &path.join("bin")).log_err();
}
}
fn get_activate_command(settings: &VenvSettingsContent) -> &'static str {
match settings.activate_script {
terminal_settings::ActivateScript::Nushell => "overlay use",
_ => "source",
}
} }
fn activate_python_virtual_environment( fn activate_python_virtual_environment(
&mut self, &self,
activate_command: &'static str, command: String,
activate_script: PathBuf,
terminal_handle: &Model<Terminal>, terminal_handle: &Model<Terminal>,
cx: &mut ModelContext<Project>, cx: &mut ModelContext<Project>,
) { ) {
// Paths are not strings so we need to jump through some hoops to format the command without `format!` terminal_handle.update(cx, |this, _| this.input_bytes(command.into_bytes()));
let mut command = Vec::from(activate_command.as_bytes());
command.push(b' ');
// Wrapping path in double quotes to catch spaces in folder name
command.extend_from_slice(b"\"");
command.extend_from_slice(activate_script.as_os_str().as_encoded_bytes());
command.extend_from_slice(b"\"");
command.push(b'\n');
terminal_handle.update(cx, |this, _| this.input_bytes(command));
} }
pub fn local_terminal_handles(&self) -> &Vec<WeakModel<terminal::Terminal>> { pub fn local_terminal_handles(&self) -> &Vec<WeakModel<terminal::Terminal>> {
@ -310,65 +303,55 @@ impl Project {
} }
} }
fn prepare_ssh_shell( pub fn wrap_for_ssh(
env: &mut HashMap<String, String>, ssh_command: &SshCommand,
tmp_dir: &Path, command: Option<(&String, &Vec<String>)>,
spawn_task: Option<&SpawnInTerminal>, path: Option<&Path>,
ssh_command: &str, env: HashMap<String, String>,
path: Option<&str>, venv_directory: Option<PathBuf>,
) -> anyhow::Result<Shell> { ) -> (String, Vec<String>) {
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed let to_run = if let Some((command, args)) = command {
// to properly display colors. iter::once(command)
// We do not have the luxury of assuming the host has it installed, .chain(args)
// so we set it to a default that does not break the highlighting via ssh. .filter_map(|arg| shlex::try_quote(arg).ok())
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let real_ssh = which::which("ssh")?;
let ssh_path = tmp_dir.join("ssh");
let mut ssh_file = File::create(&ssh_path)?;
let to_run = if let Some(spawn_task) = spawn_task {
Some(shlex::try_quote(&spawn_task.command)?)
.into_iter()
.chain(
spawn_task
.args
.iter()
.filter_map(|arg| shlex::try_quote(arg).ok()),
)
.join(" ") .join(" ")
} else { } else {
"exec $SHELL -l".to_string() "exec ${SHELL:-sh} -l".to_string()
}; };
let mut env_changes = String::new();
for (k, v) in env.iter() {
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
env_changes.push_str(&format!("{}={} ", k, v));
}
}
if let Some(venv_directory) = venv_directory {
if let Some(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref()).ok() {
env_changes.push_str(&format!("PATH={}:$PATH ", str));
}
}
let commands = if let Some(path) = path { let commands = if let Some(path) = path {
format!("cd {path}; {to_run}") format!("cd {:?}; {} {}", path, env_changes, to_run)
} else { } else {
format!("cd; {to_run}") format!("cd; {env_changes} {to_run}")
}; };
let shell_invocation = &format!("sh -c {}", shlex::try_quote(&commands)?); let shell_invocation = format!("sh -c {}", shlex::try_quote(&commands).unwrap());
// To support things like `gh cs ssh`/`coder ssh`, we run whatever command let (program, mut args) = match ssh_command {
// you have configured, but place our custom script on the path so that it will SshCommand::DevServer(ssh_command) => {
// be run instead. let mut args = shlex::split(&ssh_command).unwrap_or_default();
write!( let program = args.drain(0..1).next().unwrap_or("ssh".to_string());
&mut ssh_file, (program, args)
"#!/bin/sh\nexec {} \"$@\" {} {}", }
real_ssh.to_string_lossy(), SshCommand::Direct(ssh_args) => ("ssh".to_string(), ssh_args.clone()),
if spawn_task.is_none() { "-t" } else { "" }, };
shlex::try_quote(shell_invocation)?,
)?;
// todo(windows) if command.is_none() {
#[cfg(not(target_os = "windows"))] args.push("-t".to_string())
std::fs::set_permissions(ssh_path, smol::fs::unix::PermissionsExt::from_mode(0o755))?; }
args.push(shell_invocation);
add_environment_path(env, tmp_dir)?; (program, args)
let mut args = shlex::split(&ssh_command).unwrap_or_default();
let program = args.drain(0..1).next().unwrap_or("ssh".to_string());
Ok(Shell::WithArguments { program, args })
} }
fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> anyhow::Result<()> { fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> anyhow::Result<()> {

View file

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
@ -18,6 +19,8 @@ use gpui::{
}; };
use markdown::Markdown; use markdown::Markdown;
use markdown::MarkdownStyle; use markdown::MarkdownStyle;
use project::terminals::wrap_for_ssh;
use project::terminals::SshCommand;
use rpc::proto::RegenerateDevServerTokenResponse; use rpc::proto::RegenerateDevServerTokenResponse;
use rpc::{ use rpc::{
proto::{CreateDevServerResponse, DevServerStatus}, proto::{CreateDevServerResponse, DevServerStatus},
@ -28,7 +31,6 @@ use settings::Settings;
use task::HideStrategy; use task::HideStrategy;
use task::RevealStrategy; use task::RevealStrategy;
use task::SpawnInTerminal; use task::SpawnInTerminal;
use task::TerminalWorkDir;
use terminal_view::terminal_panel::TerminalPanel; use terminal_view::terminal_panel::TerminalPanel;
use ui::ElevationIndex; use ui::ElevationIndex;
use ui::Section; use ui::Section;
@ -1638,6 +1640,13 @@ pub async fn spawn_ssh_task(
]; ];
let ssh_connection_string = ssh_connection_string.to_string(); let ssh_connection_string = ssh_connection_string.to_string();
let (command, args) = wrap_for_ssh(
&SshCommand::DevServer(ssh_connection_string.clone()),
Some((&command, &args)),
None,
HashMap::default(),
None,
);
let terminal = terminal_panel let terminal = terminal_panel
.update(cx, |terminal_panel, cx| { .update(cx, |terminal_panel, cx| {
@ -1649,10 +1658,7 @@ pub async fn spawn_ssh_task(
command, command,
args, args,
command_label: ssh_connection_string.clone(), command_label: ssh_connection_string.clone(),
cwd: Some(TerminalWorkDir::Ssh { cwd: None,
ssh_command: ssh_connection_string,
path: None,
}),
use_new_terminal: true, use_new_terminal: true,
allow_concurrent_runs: false, allow_concurrent_runs: false,
reveal: RevealStrategy::Always, reveal: RevealStrategy::Always,

View file

@ -36,11 +36,18 @@ use std::{
}; };
use tempfile::TempDir; use tempfile::TempDir;
#[derive(Clone)]
pub struct SshSocket {
connection_options: SshConnectionOptions,
socket_path: PathBuf,
}
pub struct SshSession { pub struct SshSession {
next_message_id: AtomicU32, next_message_id: AtomicU32,
response_channels: ResponseChannels, response_channels: ResponseChannels,
outgoing_tx: mpsc::UnboundedSender<Envelope>, outgoing_tx: mpsc::UnboundedSender<Envelope>,
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>, spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
client_socket: Option<SshSocket>,
message_handlers: Mutex< message_handlers: Mutex<
HashMap< HashMap<
TypeId, TypeId,
@ -58,8 +65,7 @@ pub struct SshSession {
} }
struct SshClientState { struct SshClientState {
connection_options: SshConnectionOptions, socket: SshSocket,
socket_path: PathBuf,
_master_process: process::Child, _master_process: process::Child,
_temp_dir: TempDir, _temp_dir: TempDir,
} }
@ -162,9 +168,10 @@ impl SshSession {
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>(); let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>(); let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
run_cmd(client_state.ssh_command(&remote_binary_path).arg("version")).await?; let socket = client_state.socket.clone();
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
let mut remote_server_child = client_state let mut remote_server_child = socket
.ssh_command(&format!( .ssh_command(&format!(
"RUST_LOG={} {:?} run", "RUST_LOG={} {:?} run",
std::env::var("RUST_LOG").unwrap_or(String::new()), std::env::var("RUST_LOG").unwrap_or(String::new()),
@ -202,7 +209,7 @@ impl SshSession {
}; };
log::info!("spawn process: {:?}", request.command); log::info!("spawn process: {:?}", request.command);
let child = client_state let child = client_state.socket
.ssh_command(&request.command) .ssh_command(&request.command)
.spawn() .spawn()
.context("failed to create channel")?; .context("failed to create channel")?;
@ -268,7 +275,7 @@ impl SshSession {
} }
}).detach(); }).detach();
cx.update(|cx| Self::new(incoming_rx, outgoing_tx, spawn_process_tx, cx)) cx.update(|cx| Self::new(incoming_rx, outgoing_tx, spawn_process_tx, Some(socket), cx))
} }
pub fn server( pub fn server(
@ -277,7 +284,7 @@ impl SshSession {
cx: &AppContext, cx: &AppContext,
) -> Arc<SshSession> { ) -> Arc<SshSession> {
let (tx, _rx) = mpsc::unbounded(); let (tx, _rx) = mpsc::unbounded();
Self::new(incoming_rx, outgoing_tx, tx, cx) Self::new(incoming_rx, outgoing_tx, tx, None, cx)
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -289,10 +296,24 @@ impl SshSession {
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded(); let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
let (tx, _rx) = mpsc::unbounded(); let (tx, _rx) = mpsc::unbounded();
( (
client_cx client_cx.update(|cx| {
.update(|cx| Self::new(server_to_client_rx, client_to_server_tx, tx.clone(), cx)), Self::new(
server_cx server_to_client_rx,
.update(|cx| Self::new(client_to_server_rx, server_to_client_tx, tx.clone(), cx)), client_to_server_tx,
tx.clone(),
None, // todo()
cx,
)
}),
server_cx.update(|cx| {
Self::new(
client_to_server_rx,
server_to_client_tx,
tx.clone(),
None,
cx,
)
}),
) )
} }
@ -300,6 +321,7 @@ impl SshSession {
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>, mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
outgoing_tx: mpsc::UnboundedSender<Envelope>, outgoing_tx: mpsc::UnboundedSender<Envelope>,
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>, spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
client_socket: Option<SshSocket>,
cx: &AppContext, cx: &AppContext,
) -> Arc<SshSession> { ) -> Arc<SshSession> {
let this = Arc::new(Self { let this = Arc::new(Self {
@ -307,6 +329,7 @@ impl SshSession {
response_channels: ResponseChannels::default(), response_channels: ResponseChannels::default(),
outgoing_tx, outgoing_tx,
spawn_process_tx, spawn_process_tx,
client_socket,
message_handlers: Default::default(), message_handlers: Default::default(),
}); });
@ -400,6 +423,10 @@ impl SshSession {
process_rx.await.unwrap() process_rx.await.unwrap()
} }
pub fn ssh_args(&self) -> Vec<String> {
self.client_socket.as_ref().unwrap().ssh_args()
}
pub fn add_message_handler<M, E, H, F>(&self, entity: WeakModel<E>, handler: H) pub fn add_message_handler<M, E, H, F>(&self, entity: WeakModel<E>, handler: H)
where where
M: EnvelopedMessage, M: EnvelopedMessage,
@ -559,8 +586,10 @@ impl SshClientState {
} }
Ok(Self { Ok(Self {
connection_options, socket: SshSocket {
socket_path, connection_options,
socket_path,
},
_master_process: master_process, _master_process: master_process,
_temp_dir: temp_dir, _temp_dir: temp_dir,
}) })
@ -578,12 +607,13 @@ impl SshClientState {
dst_path_gz.set_extension("gz"); dst_path_gz.set_extension("gz");
if let Some(parent) = dst_path.parent() { if let Some(parent) = dst_path.parent() {
run_cmd(self.ssh_command("mkdir").arg("-p").arg(parent)).await?; run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
} }
let mut server_binary_exists = false; let mut server_binary_exists = false;
if cfg!(not(debug_assertions)) { if cfg!(not(debug_assertions)) {
if let Ok(installed_version) = run_cmd(self.ssh_command(&dst_path).arg("version")).await if let Ok(installed_version) =
run_cmd(self.socket.ssh_command(&dst_path).arg("version")).await
{ {
if installed_version.trim() == version.to_string() { if installed_version.trim() == version.to_string() {
server_binary_exists = true; server_binary_exists = true;
@ -609,11 +639,18 @@ impl SshClientState {
log::info!("uploaded remote development server in {:?}", t0.elapsed()); log::info!("uploaded remote development server in {:?}", t0.elapsed());
delegate.set_status(Some("extracting remote development server"), cx); delegate.set_status(Some("extracting remote development server"), cx);
run_cmd(self.ssh_command("gunzip").arg("--force").arg(&dst_path_gz)).await?; run_cmd(
self.socket
.ssh_command("gunzip")
.arg("--force")
.arg(&dst_path_gz),
)
.await?;
delegate.set_status(Some("unzipping remote development server"), cx); delegate.set_status(Some("unzipping remote development server"), cx);
run_cmd( run_cmd(
self.ssh_command("chmod") self.socket
.ssh_command("chmod")
.arg(format!("{:o}", server_mode)) .arg(format!("{:o}", server_mode))
.arg(&dst_path), .arg(&dst_path),
) )
@ -623,8 +660,8 @@ impl SshClientState {
} }
async fn query_platform(&self) -> Result<SshPlatform> { async fn query_platform(&self) -> Result<SshPlatform> {
let os = run_cmd(self.ssh_command("uname").arg("-s")).await?; let os = run_cmd(self.socket.ssh_command("uname").arg("-s")).await?;
let arch = run_cmd(self.ssh_command("uname").arg("-m")).await?; let arch = run_cmd(self.socket.ssh_command("uname").arg("-m")).await?;
let os = match os.trim() { let os = match os.trim() {
"Darwin" => "macos", "Darwin" => "macos",
@ -645,9 +682,11 @@ impl SshClientState {
async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> { async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> {
let mut command = process::Command::new("scp"); let mut command = process::Command::new("scp");
let output = self let output = self
.socket
.ssh_options(&mut command) .ssh_options(&mut command)
.args( .args(
self.connection_options self.socket
.connection_options
.port .port
.map(|port| vec!["-P".to_string(), port.to_string()]) .map(|port| vec!["-P".to_string(), port.to_string()])
.unwrap_or_default(), .unwrap_or_default(),
@ -655,7 +694,7 @@ impl SshClientState {
.arg(&src_path) .arg(&src_path)
.arg(&format!( .arg(&format!(
"{}:{}", "{}:{}",
self.connection_options.scp_url(), self.socket.connection_options.scp_url(),
dest_path.display() dest_path.display()
)) ))
.output() .output()
@ -672,7 +711,9 @@ impl SshClientState {
)) ))
} }
} }
}
impl SshSocket {
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command { fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
let mut command = process::Command::new("ssh"); let mut command = process::Command::new("ssh");
self.ssh_options(&mut command) self.ssh_options(&mut command)
@ -689,6 +730,16 @@ impl SshClientState {
.args(["-o", "ControlMaster=no", "-o"]) .args(["-o", "ControlMaster=no", "-o"])
.arg(format!("ControlPath={}", self.socket_path.display())) .arg(format!("ControlPath={}", self.socket_path.display()))
} }
fn ssh_args(&self) -> Vec<String> {
vec![
"-o".to_string(),
"ControlMaster=no".to_string(),
"-o".to_string(),
format!("ControlPath={}", self.socket_path.display()),
self.connection_options.ssh_url(),
]
}
} }
async fn run_cmd(command: &mut process::Command) -> Result<String> { async fn run_cmd(command: &mut process::Command) -> Result<String> {

View file

@ -9,9 +9,9 @@ use collections::{hash_map, HashMap, HashSet};
use gpui::SharedString; use gpui::SharedString;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::{borrow::Cow, path::Path};
pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates}; pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
pub use vscode_format::VsCodeTaskFile; pub use vscode_format::VsCodeTaskFile;
@ -21,38 +21,6 @@ pub use vscode_format::VsCodeTaskFile;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
pub struct TaskId(pub String); pub struct TaskId(pub String);
/// TerminalWorkDir describes where a task should be run
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminalWorkDir {
/// Local is on this machine
Local(PathBuf),
/// SSH runs the terminal over ssh
Ssh {
/// The command to run to connect
ssh_command: String,
/// The path on the remote server
path: Option<String>,
},
}
impl TerminalWorkDir {
/// Returns whether the terminal task is supposed to be spawned on a local machine or not.
pub fn is_local(&self) -> bool {
match self {
Self::Local(_) => true,
Self::Ssh { .. } => false,
}
}
/// Returns a local CWD if the terminal is local, None otherwise.
pub fn local_path(&self) -> Option<&Path> {
match self {
Self::Local(path) => Some(path),
Self::Ssh { .. } => None,
}
}
}
/// Contains all information needed by Zed to spawn a new terminal tab for the given task. /// Contains all information needed by Zed to spawn a new terminal tab for the given task.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpawnInTerminal { pub struct SpawnInTerminal {
@ -70,7 +38,7 @@ pub struct SpawnInTerminal {
/// A human-readable label, containing command and all of its arguments, joined and substituted. /// A human-readable label, containing command and all of its arguments, joined and substituted.
pub command_label: String, pub command_label: String,
/// Current working directory to spawn the command into. /// Current working directory to spawn the command into.
pub cwd: Option<TerminalWorkDir>, pub cwd: Option<PathBuf>,
/// Env overrides for the command, will be appended to the terminal's environment from the settings. /// Env overrides for the command, will be appended to the terminal's environment from the settings.
pub env: HashMap<String, String>, pub env: HashMap<String, String>,
/// Whether to use a new terminal tab or reuse the existing one to spawn the process. /// Whether to use a new terminal tab or reuse the existing one to spawn the process.
@ -265,7 +233,7 @@ impl IntoIterator for TaskVariables {
/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function). /// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function).
/// Keeps all Zed-related state inside, used to produce a resolved task out of its template. /// Keeps all Zed-related state inside, used to produce a resolved task out of its template.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TaskContext { pub struct TaskContext {
/// A path to a directory in which the task should be executed. /// A path to a directory in which the task should be executed.
pub cwd: Option<PathBuf>, pub cwd: Option<PathBuf>,

View file

@ -8,7 +8,7 @@ use sha2::{Digest, Sha256};
use util::{truncate_and_remove_front, ResultExt}; use util::{truncate_and_remove_front, ResultExt};
use crate::{ use crate::{
ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, TerminalWorkDir, VariableName, ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName,
ZED_VARIABLE_NAME_PREFIX, ZED_VARIABLE_NAME_PREFIX,
}; };
@ -134,14 +134,11 @@ impl TaskTemplate {
&variable_names, &variable_names,
&mut substituted_variables, &mut substituted_variables,
)?; )?;
Some(TerminalWorkDir::Local(PathBuf::from(substitured_cwd))) Some(PathBuf::from(substitured_cwd))
} }
None => None, None => None,
} }
.or(cx .or(cx.cwd.clone());
.cwd
.as_ref()
.map(|cwd| TerminalWorkDir::Local(cwd.clone())));
let human_readable_label = substitute_all_template_variables_in_str( let human_readable_label = substitute_all_template_variables_in_str(
&self.label, &self.label,
&truncated_variables, &truncated_variables,
@ -421,11 +418,8 @@ mod tests {
project_env: HashMap::default(), project_env: HashMap::default(),
}; };
assert_eq!( assert_eq!(
resolved_task(&task_without_cwd, &cx) resolved_task(&task_without_cwd, &cx).cwd,
.cwd Some(context_cwd.clone()),
.as_ref()
.and_then(|cwd| cwd.local_path()),
Some(context_cwd.as_path()),
"TaskContext's cwd should be taken on resolve if task's cwd is None" "TaskContext's cwd should be taken on resolve if task's cwd is None"
); );
@ -440,11 +434,8 @@ mod tests {
project_env: HashMap::default(), project_env: HashMap::default(),
}; };
assert_eq!( assert_eq!(
resolved_task(&task_with_cwd, &cx) resolved_task(&task_with_cwd, &cx).cwd,
.cwd Some(task_cwd.clone()),
.as_ref()
.and_then(|cwd| cwd.local_path()),
Some(task_cwd.as_path()),
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None" "TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None"
); );
@ -454,11 +445,8 @@ mod tests {
project_env: HashMap::default(), project_env: HashMap::default(),
}; };
assert_eq!( assert_eq!(
resolved_task(&task_with_cwd, &cx) resolved_task(&task_with_cwd, &cx).cwd,
.cwd Some(task_cwd),
.as_ref()
.and_then(|cwd| cwd.local_path()),
Some(task_cwd.as_path()),
"TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None" "TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None"
); );
} }

View file

@ -1,6 +1,6 @@
use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use crate::TerminalView; use crate::{default_working_directory, TerminalView};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use futures::future::join_all; use futures::future::join_all;
@ -10,11 +10,11 @@ use gpui::{
Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
use itertools::Itertools; use itertools::Itertools;
use project::{Fs, ProjectEntryId}; use project::{terminals::TerminalKind, Fs, ProjectEntryId};
use search::{buffer_search::DivRegistrar, BufferSearchBar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId, TerminalWorkDir}; use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
use terminal::{ use terminal::{
terminal_settings::{TerminalDockPosition, TerminalSettings}, terminal_settings::{TerminalDockPosition, TerminalSettings},
Terminal, Terminal,
@ -348,14 +348,13 @@ impl TerminalPanel {
return; return;
}; };
let terminal_work_dir = workspace
.project()
.read(cx)
.terminal_work_dir_for(Some(&action.working_directory), cx);
terminal_panel terminal_panel
.update(cx, |panel, cx| { .update(cx, |panel, cx| {
panel.add_terminal(terminal_work_dir, None, RevealStrategy::Always, cx) panel.add_terminal(
TerminalKind::Shell(Some(action.working_directory.clone())),
RevealStrategy::Always,
cx,
)
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -484,7 +483,7 @@ impl TerminalPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<Model<Terminal>>> { ) -> Task<Result<Model<Terminal>>> {
let reveal = spawn_task.reveal; let reveal = spawn_task.reveal;
self.add_terminal(spawn_task.cwd.clone(), Some(spawn_task), reveal, cx) self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
} }
/// Create a new Terminal in the current working directory or the user's home directory /// Create a new Terminal in the current working directory or the user's home directory
@ -497,9 +496,11 @@ impl TerminalPanel {
return; return;
}; };
let kind = TerminalKind::Shell(default_working_directory(workspace, cx));
terminal_panel terminal_panel
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.add_terminal(None, None, RevealStrategy::Always, cx) this.add_terminal(kind, RevealStrategy::Always, cx)
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
@ -533,22 +534,14 @@ impl TerminalPanel {
fn add_terminal( fn add_terminal(
&mut self, &mut self,
working_directory: Option<TerminalWorkDir>, kind: TerminalKind,
spawn_task: Option<SpawnInTerminal>,
reveal_strategy: RevealStrategy, reveal_strategy: RevealStrategy,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<Model<Terminal>>> { ) -> Task<Result<Model<Terminal>>> {
if !self.enabled { if !self.enabled {
if spawn_task.is_none() return Task::ready(Err(anyhow::anyhow!(
|| !matches!( "terminal not yet supported for remote projects"
spawn_task.as_ref().unwrap().cwd, )));
Some(TerminalWorkDir::Ssh { .. })
)
{
return Task::ready(Err(anyhow::anyhow!(
"terminal not yet supported for remote projects"
)));
}
} }
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
@ -557,18 +550,10 @@ impl TerminalPanel {
cx.spawn(|terminal_panel, mut cx| async move { cx.spawn(|terminal_panel, mut cx| async move {
let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?; let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?;
let result = workspace.update(&mut cx, |workspace, cx| { let result = workspace.update(&mut cx, |workspace, cx| {
let working_directory = if let Some(working_directory) = working_directory {
Some(working_directory)
} else {
let working_directory_strategy =
TerminalSettings::get_global(cx).working_directory.clone();
crate::get_working_directory(workspace, cx, working_directory_strategy)
};
let window = cx.window_handle(); let window = cx.window_handle();
let terminal = workspace.project().update(cx, |project, cx| { let terminal = workspace
project.create_terminal(working_directory, spawn_task, window, cx) .project()
})?; .update(cx, |project, cx| project.create_terminal(kind, window, cx))?;
let terminal_view = Box::new(cx.new_view(|cx| { let terminal_view = Box::new(cx.new_view(|cx| {
TerminalView::new( TerminalView::new(
terminal.clone(), terminal.clone(),
@ -655,7 +640,7 @@ impl TerminalPanel {
let window = cx.window_handle(); let window = cx.window_handle();
let new_terminal = project.update(cx, |project, cx| { let new_terminal = project.update(cx, |project, cx| {
project project
.create_terminal(spawn_task.cwd.clone(), Some(spawn_task), window, cx) .create_terminal(TerminalKind::Task(spawn_task), window, cx)
.log_err() .log_err()
})?; })?;
terminal_to_replace.update(cx, |terminal_to_replace, cx| { terminal_to_replace.update(cx, |terminal_to_replace, cx| {
@ -802,10 +787,19 @@ impl Panel for TerminalPanel {
} }
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) { fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active && self.has_no_terminals(cx) { if !active || !self.has_no_terminals(cx) {
self.add_terminal(None, None, RevealStrategy::Never, cx) return;
.detach_and_log_err(cx)
} }
cx.defer(|this, cx| {
let Ok(kind) = this.workspace.update(cx, |workspace, cx| {
TerminalKind::Shell(default_working_directory(workspace, cx))
}) else {
return;
};
this.add_terminal(kind, RevealStrategy::Never, cx)
.detach_and_log_err(cx)
})
} }
fn icon_label(&self, cx: &WindowContext) -> Option<String> { fn icon_label(&self, cx: &WindowContext) -> Option<String> {

View file

@ -13,8 +13,7 @@ use gpui::{
}; };
use language::Bias; use language::Bias;
use persistence::TERMINAL_DB; use persistence::TERMINAL_DB;
use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project}; use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};
use task::TerminalWorkDir;
use terminal::{ use terminal::{
alacritty_terminal::{ alacritty_terminal::{
index::Point, index::Point,
@ -38,7 +37,6 @@ use workspace::{
}; };
use anyhow::Context; use anyhow::Context;
use dirs::home_dir;
use serde::Deserialize; use serde::Deserialize;
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use smol::Timer; use smol::Timer;
@ -130,15 +128,13 @@ impl TerminalView {
_: &NewCenterTerminal, _: &NewCenterTerminal,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let strategy = TerminalSettings::get_global(cx); let working_directory = default_working_directory(workspace, cx);
let working_directory =
get_working_directory(workspace, cx, strategy.working_directory.clone());
let window = cx.window_handle(); let window = cx.window_handle();
let terminal = workspace let terminal = workspace
.project() .project()
.update(cx, |project, cx| { .update(cx, |project, cx| {
project.create_terminal(working_directory, None, window, cx) project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
}) })
.notify_err(workspace, cx); .notify_err(workspace, cx);
@ -1134,21 +1130,18 @@ impl SerializableItem for TerminalView {
.as_ref() .as_ref()
.is_some_and(|from_db| !from_db.as_os_str().is_empty()) .is_some_and(|from_db| !from_db.as_os_str().is_empty())
{ {
project from_db
.read(cx)
.terminal_work_dir_for(from_db.as_deref(), cx)
} else { } else {
let strategy = TerminalSettings::get_global(cx).working_directory.clone(); workspace
workspace.upgrade().and_then(|workspace| { .upgrade()
get_working_directory(workspace.read(cx), cx, strategy) .and_then(|workspace| default_working_directory(workspace.read(cx), cx))
})
} }
}) })
.ok() .ok()
.flatten(); .flatten();
let terminal = project.update(&mut cx, |project, cx| { let terminal = project.update(&mut cx, |project, cx| {
project.create_terminal(cwd, None, window, cx) project.create_terminal(TerminalKind::Shell(cwd), window, cx)
})??; })??;
pane.update(&mut cx, |_, cx| { pane.update(&mut cx, |_, cx| {
cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx)) cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx))
@ -1276,59 +1269,29 @@ impl SearchableItem for TerminalView {
} }
///Gets the working directory for the given workspace, respecting the user's settings. ///Gets the working directory for the given workspace, respecting the user's settings.
pub fn get_working_directory( /// None implies "~" on whichever machine we end up on.
workspace: &Workspace, pub fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
cx: &AppContext, match &TerminalSettings::get_global(cx).working_directory {
strategy: WorkingDirectory, WorkingDirectory::CurrentProjectDirectory => {
) -> Option<TerminalWorkDir> { workspace.project().read(cx).active_project_directory(cx)
if workspace.project().read(cx).is_local() { }
let res = match strategy { WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx) WorkingDirectory::AlwaysHome => None,
.or_else(|| first_project_directory(workspace, cx)), WorkingDirectory::Always { directory } => {
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), shellexpand::full(&directory) //TODO handle this better
WorkingDirectory::AlwaysHome => None, .ok()
WorkingDirectory::Always { directory } => { .map(|dir| Path::new(&dir.to_string()).to_path_buf())
shellexpand::full(&directory) //TODO handle this better .filter(|dir| dir.is_dir())
.ok() }
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
.filter(|dir| dir.is_dir())
}
};
res.or_else(home_dir).map(|cwd| TerminalWorkDir::Local(cwd))
} else {
workspace.project().read(cx).terminal_work_dir_for(None, cx)
} }
} }
///Gets the first project's home directory, or the home directory ///Gets the first project's home directory, or the home directory
fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> { fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
workspace let worktree = workspace.worktrees(cx).next()?.read(cx);
.worktrees(cx) if !worktree.root_entry()?.is_dir() {
.next() return None;
.and_then(|worktree_handle| worktree_handle.read(cx).as_local()) }
.and_then(get_path_from_wt) Some(worktree.abs_path().to_path_buf())
}
///Gets the intuitively correct working directory from the given workspace
///If there is an active entry for this project, returns that entry's worktree root.
///If there's no active entry but there is a worktree, returns that worktrees root.
///If either of these roots are files, or if there are any other query failures,
/// returns the user's home directory
fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
let project = workspace.project().read(cx);
project
.active_entry()
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
.or_else(|| workspace.worktrees(cx).next())
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
.and_then(get_path_from_wt)
}
fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
wt.root_entry()
.filter(|re| re.is_dir())
.map(|_| wt.abs_path().to_path_buf())
} }
#[cfg(test)] #[cfg(test)]
@ -1353,7 +1316,7 @@ mod tests {
assert!(active_entry.is_none()); assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_none()); assert!(workspace.worktrees(cx).next().is_none());
let res = current_project_directory(workspace, cx); let res = default_working_directory(workspace, cx);
assert_eq!(res, None); assert_eq!(res, None);
let res = first_project_directory(workspace, cx); let res = first_project_directory(workspace, cx);
assert_eq!(res, None); assert_eq!(res, None);
@ -1374,7 +1337,7 @@ mod tests {
assert!(active_entry.is_none()); assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_some()); assert!(workspace.worktrees(cx).next().is_some());
let res = current_project_directory(workspace, cx); let res = default_working_directory(workspace, cx);
assert_eq!(res, None); assert_eq!(res, None);
let res = first_project_directory(workspace, cx); let res = first_project_directory(workspace, cx);
assert_eq!(res, None); assert_eq!(res, None);
@ -1394,7 +1357,7 @@ mod tests {
assert!(active_entry.is_none()); assert!(active_entry.is_none());
assert!(workspace.worktrees(cx).next().is_some()); assert!(workspace.worktrees(cx).next().is_some());
let res = current_project_directory(workspace, cx); let res = default_working_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
let res = first_project_directory(workspace, cx); let res = first_project_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
@ -1416,7 +1379,7 @@ mod tests {
assert!(active_entry.is_some()); assert!(active_entry.is_some());
let res = current_project_directory(workspace, cx); let res = default_working_directory(workspace, cx);
assert_eq!(res, None); assert_eq!(res, None);
let res = first_project_directory(workspace, cx); let res = first_project_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
@ -1438,7 +1401,7 @@ mod tests {
assert!(active_entry.is_some()); assert!(active_entry.is_some());
let res = current_project_directory(workspace, cx); let res = default_working_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
let res = first_project_directory(workspace, cx); let res = first_project_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
@ -1449,6 +1412,7 @@ mod tests {
pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) { pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
let params = cx.update(AppState::test); let params = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
terminal::init(cx);
theme::init(theme::LoadThemes::JustBase, cx); theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx); Project::init_settings(cx);
language::init(cx); language::init(cx);

View file

@ -35,17 +35,9 @@ Once a connection is established, Zed will be downloaded and installed to `~/.lo
If you don't see any output from the Zed command, it is likely that Zed is crashing If you don't see any output from the Zed command, it is likely that Zed is crashing
on startup. You can troubleshoot this by switching to manual mode and passing the `--foreground` flag. Please [file a bug](https://github.com/zed-industries/zed) so we can debug it together. on startup. You can troubleshoot this by switching to manual mode and passing the `--foreground` flag. Please [file a bug](https://github.com/zed-industries/zed) so we can debug it together.
### SSH-like connections If you are trying to connect to a platform like GitHub Codespaces or Google Cloud, you may want to first make sure that your SSH configuration is set up correctly. Once you can `ssh X` to connect to the machine, then Zed will be able to connect.
Zed intercepts `ssh` in a way that should make it possible to intercept connections made by most "ssh wrappers". For example you > **Note:** In an earlier version of remoting, we supported typing in `gh cs ssh` or `gcloud compute ssh` directly. This is no longer supported. Instead you should make sure your SSH configuration is up to date with `gcloud compute ssh --config` or `gh cs ssh --config`, or use Manual setup mode if you cannot ssh directly to the machine.
can specify:
- `user@host` will assume you meant `ssh user@host`
- `ssh -J jump target` to connect via a jump-host
- `gh cs ssh -c example-codespace` to connect to a GitHub codespace
- `doctl compute ssh example-droplet` to connect to a DigitalOcean Droplet
- `gcloud compute ssh` for a Google Cloud instance
- `ssh -i path_to_key_file user@host` to connect to a host using a key file or certificate
### zed --dev-server-token isn't connecting ### zed --dev-server-token isn't connecting