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

@ -13,8 +13,7 @@ use gpui::{
};
use language::Bias;
use persistence::TERMINAL_DB;
use project::{search::SearchQuery, Fs, LocalWorktree, Metadata, Project};
use task::TerminalWorkDir;
use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};
use terminal::{
alacritty_terminal::{
index::Point,
@ -38,7 +37,6 @@ use workspace::{
};
use anyhow::Context;
use dirs::home_dir;
use serde::Deserialize;
use settings::{Settings, SettingsStore};
use smol::Timer;
@ -130,15 +128,13 @@ impl TerminalView {
_: &NewCenterTerminal,
cx: &mut ViewContext<Workspace>,
) {
let strategy = TerminalSettings::get_global(cx);
let working_directory =
get_working_directory(workspace, cx, strategy.working_directory.clone());
let working_directory = default_working_directory(workspace, cx);
let window = cx.window_handle();
let terminal = workspace
.project()
.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);
@ -1134,21 +1130,18 @@ impl SerializableItem for TerminalView {
.as_ref()
.is_some_and(|from_db| !from_db.as_os_str().is_empty())
{
project
.read(cx)
.terminal_work_dir_for(from_db.as_deref(), cx)
from_db
} else {
let strategy = TerminalSettings::get_global(cx).working_directory.clone();
workspace.upgrade().and_then(|workspace| {
get_working_directory(workspace.read(cx), cx, strategy)
})
workspace
.upgrade()
.and_then(|workspace| default_working_directory(workspace.read(cx), cx))
}
})
.ok()
.flatten();
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| {
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.
pub fn get_working_directory(
workspace: &Workspace,
cx: &AppContext,
strategy: WorkingDirectory,
) -> Option<TerminalWorkDir> {
if workspace.project().read(cx).is_local() {
let res = match strategy {
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
.or_else(|| first_project_directory(workspace, cx)),
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
WorkingDirectory::AlwaysHome => None,
WorkingDirectory::Always { directory } => {
shellexpand::full(&directory) //TODO handle this better
.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)
/// None implies "~" on whichever machine we end up on.
pub fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
match &TerminalSettings::get_global(cx).working_directory {
WorkingDirectory::CurrentProjectDirectory => {
workspace.project().read(cx).active_project_directory(cx)
}
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
WorkingDirectory::AlwaysHome => None,
WorkingDirectory::Always { directory } => {
shellexpand::full(&directory) //TODO handle this better
.ok()
.map(|dir| Path::new(&dir.to_string()).to_path_buf())
.filter(|dir| dir.is_dir())
}
}
}
///Gets the first project's home directory, or the home directory
fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
workspace
.worktrees(cx)
.next()
.and_then(|worktree_handle| worktree_handle.read(cx).as_local())
.and_then(get_path_from_wt)
}
///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())
let worktree = workspace.worktrees(cx).next()?.read(cx);
if !worktree.root_entry()?.is_dir() {
return None;
}
Some(worktree.abs_path().to_path_buf())
}
#[cfg(test)]
@ -1353,7 +1316,7 @@ mod tests {
assert!(active_entry.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);
let res = first_project_directory(workspace, cx);
assert_eq!(res, None);
@ -1374,7 +1337,7 @@ mod tests {
assert!(active_entry.is_none());
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);
let res = first_project_directory(workspace, cx);
assert_eq!(res, None);
@ -1394,7 +1357,7 @@ mod tests {
assert!(active_entry.is_none());
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()));
let res = first_project_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
@ -1416,7 +1379,7 @@ mod tests {
assert!(active_entry.is_some());
let res = current_project_directory(workspace, cx);
let res = default_working_directory(workspace, cx);
assert_eq!(res, None);
let res = first_project_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
@ -1438,7 +1401,7 @@ mod tests {
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()));
let res = first_project_directory(workspace, cx);
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>) {
let params = cx.update(AppState::test);
cx.update(|cx| {
terminal::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
language::init(cx);