Add the ability to edit remote directories over SSH (#14530)

This is a first step towards allowing you to edit remote projects
directly over SSH. We'll start with a pretty bare-bones feature set, and
incrementally add further features.

### Todo

Distribution
* [x] Build nightly releases of `zed-remote-server` binaries
    * [x] linux (arm + x86)
    * [x] mac (arm + x86)
* [x] Build stable + preview releases of `zed-remote-server`
* [x] download and cache remote server binaries as needed when opening
ssh project
* [x] ensure server has the latest version of the binary


Auth
* [x] allow specifying password at the command line
* [x] auth via ssh keys
* [x] UI password prompt

Features
* [x] upload remote server binary to server automatically
* [x] opening directories
* [x] tracking file system updates
* [x] opening, editing, saving buffers
* [ ] file operations (rename, delete, create)
* [ ] git diffs
* [ ] project search

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Max Brunsfeld 2024-07-19 10:27:26 -07:00 committed by GitHub
parent 7733bf686b
commit b9a53ffa0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 2194 additions and 250 deletions

View file

@ -74,15 +74,18 @@ use postage::watch;
use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{DirenvSettings, LspSettings, ProjectSettings};
use rand::prelude::*;
use rpc::ErrorCode;
use remote::SshSession;
use rpc::{proto::AddWorktree, ErrorCode};
use search::SearchQuery;
use search_history::SearchHistory;
use serde::Serialize;
use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore};
use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff};
use smol::channel::{Receiver, Sender};
use smol::lock::Semaphore;
use smol::{
channel::{Receiver, Sender},
lock::Semaphore,
};
use snippet::Snippet;
use snippet_provider::SnippetProvider;
use std::{
@ -196,6 +199,7 @@ pub struct Project {
>,
user_store: Model<UserStore>,
fs: Arc<dyn Fs>,
ssh_session: Option<Arc<SshSession>>,
client_state: ProjectClientState,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
@ -793,6 +797,7 @@ impl Project {
client,
user_store,
fs,
ssh_session: None,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
diagnostics: Default::default(),
@ -825,6 +830,24 @@ impl Project {
})
}
pub fn ssh(
ssh_session: Arc<SshSession>,
client: Arc<Client>,
node: Arc<dyn NodeRuntime>,
user_store: Model<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut AppContext,
) -> Model<Self> {
let this = Self::local(client, node, user_store, languages, fs, cx);
this.update(cx, |this, cx| {
ssh_session.add_message_handler(cx.weak_model(), Self::handle_update_worktree);
ssh_session.add_message_handler(cx.weak_model(), Self::handle_create_buffer_for_peer);
this.ssh_session = Some(ssh_session);
});
this
}
pub async fn remote(
remote_id: u64,
client: Arc<Client>,
@ -924,6 +947,7 @@ impl Project {
snippets,
yarn,
fs,
ssh_session: None,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
diagnostic_summaries: Default::default(),
@ -1628,7 +1652,7 @@ impl Project {
this.client.send(
proto::UpdateDiagnosticSummary {
project_id,
worktree_id: cx.entity_id().as_u64(),
worktree_id: worktree.id().to_proto(),
summary: Some(
summary.to_proto(server_id, path),
),
@ -2442,7 +2466,7 @@ impl Project {
.send(proto::UpdateBufferFile {
project_id,
buffer_id: buffer_id.into(),
file: Some(new_file.to_proto()),
file: Some(new_file.to_proto(cx)),
})
.log_err();
}
@ -2464,11 +2488,23 @@ impl Project {
self.request_buffer_diff_recalculation(&buffer, cx);
}
let buffer_id = buffer.read(cx).remote_id();
match event {
BufferEvent::Operation(operation) => {
let operation = language::proto::serialize_operation(operation);
if let Some(ssh) = &self.ssh_session {
ssh.send(proto::UpdateBuffer {
project_id: 0,
buffer_id: buffer_id.to_proto(),
operations: vec![operation.clone()],
})
.ok();
}
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Operation {
buffer_id: buffer.read(cx).remote_id(),
operation: language::proto::serialize_operation(operation),
buffer_id,
operation,
})
.ok();
}
@ -2948,9 +2984,10 @@ impl Project {
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx));
let (root_file, is_local) =
worktree.update(cx, |tree, cx| (tree.root_file(cx), tree.is_local()));
let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx);
if !settings.enable_language_server {
if !settings.enable_language_server || !is_local {
return;
}
@ -7632,7 +7669,9 @@ impl Project {
) -> Task<Result<Model<Worktree>>> {
let path: Arc<Path> = abs_path.as_ref().into();
if !self.loading_worktrees.contains_key(&path) {
let task = if self.is_local() {
let task = if self.ssh_session.is_some() {
self.create_ssh_worktree(abs_path, visible, cx)
} else if self.is_local() {
self.create_local_worktree(abs_path, visible, cx)
} else if self.dev_server_project_id.is_some() {
self.create_dev_server_worktree(abs_path, cx)
@ -7651,6 +7690,39 @@ impl Project {
})
}
fn create_ssh_worktree(
&mut self,
abs_path: impl AsRef<Path>,
visible: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
let ssh = self.ssh_session.clone().unwrap();
let abs_path = abs_path.as_ref();
let root_name = abs_path.file_name().unwrap().to_string_lossy().to_string();
let path = abs_path.to_string_lossy().to_string();
cx.spawn(|this, mut cx| async move {
let response = ssh.request(AddWorktree { path: path.clone() }).await?;
let worktree = cx.update(|cx| {
Worktree::remote(
0,
0,
proto::WorktreeMetadata {
id: response.worktree_id,
root_name,
visible,
abs_path: path,
},
ssh.clone().into(),
cx,
)
})?;
this.update(&mut cx, |this, cx| this.add_worktree(&worktree, cx))?;
Ok(worktree)
})
}
fn create_local_worktree(
&mut self,
abs_path: impl AsRef<Path>,
@ -7922,7 +7994,7 @@ impl Project {
.send(proto::UpdateBufferFile {
project_id,
buffer_id: buffer.read(cx).remote_id().into(),
file: Some(new_file.to_proto()),
file: Some(new_file.to_proto(cx)),
})
.log_err();
}
@ -9073,6 +9145,13 @@ impl Project {
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
this.update(&mut cx, |this, cx| {
if let Some(ssh) = &this.ssh_session {
let mut payload = envelope.payload.clone();
payload.project_id = 0;
cx.background_executor()
.spawn(ssh.request(payload))
.detach_and_log_err(cx);
}
this.buffer_store.update(cx, |buffer_store, cx| {
buffer_store.handle_update_buffer(envelope, this.is_remote(), cx)
})
@ -9231,7 +9310,7 @@ impl Project {
.send(proto::UpdateBufferFile {
project_id,
buffer_id: buffer_id.into(),
file: Some(file.to_proto()),
file: Some(file.to_proto(cx)),
})
.log_err();
}