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

@ -112,7 +112,18 @@ impl App {
log::info!("GPUI was compiled in test mode");
Self(AppContext::new(
current_platform(),
current_platform(false),
Arc::new(()),
http::client(None),
))
}
/// Build an app in headless mode. This prevents opening windows,
/// but makes it possible to run an application in an context like
/// SSH, where GUI applications are not allowed.
pub fn headless() -> Self {
Self(AppContext::new(
current_platform(true),
Arc::new(()),
http::client(None),
))

View file

@ -64,11 +64,16 @@ pub(crate) use test::*;
pub(crate) use windows::*;
#[cfg(target_os = "macos")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
Rc::new(MacPlatform::new())
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
Rc::new(MacPlatform::new(headless))
}
#[cfg(target_os = "linux")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
if headless {
return Rc::new(HeadlessClient::new());
}
match guess_compositor() {
"Wayland" => Rc::new(WaylandClient::new()),
"X11" => Rc::new(X11Client::new()),
@ -99,7 +104,7 @@ pub fn guess_compositor() -> &'static str {
// todo("windows")
#[cfg(target_os = "windows")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
Rc::new(WindowsPlatform::new())
}

View file

@ -12,10 +12,9 @@ use core_graphics::{
event::{CGEvent, CGEventFlags, CGKeyCode},
event_source::{CGEventSource, CGEventSourceStateID},
};
use ctor::ctor;
use metal::foreign_types::ForeignType as _;
use objc::{class, msg_send, sel, sel_impl};
use std::{borrow::Cow, mem, ptr};
use std::{borrow::Cow, mem, ptr, sync::Once};
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
@ -25,13 +24,22 @@ const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
fn synthesize_keyboard_event(code: CGKeyCode) -> CGEvent {
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
static INIT_EVENT_SOURCE: Once = Once::new();
#[ctor]
unsafe fn build_event_source() {
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
EVENT_SOURCE = source.as_ptr();
INIT_EVENT_SOURCE.call_once(|| {
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
unsafe {
EVENT_SOURCE = source.as_ptr();
};
mem::forget(source);
});
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
mem::forget(source);
event
}
pub fn key_to_native(key: &str) -> Cow<str> {
@ -335,9 +343,7 @@ fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
// an event with the given flags instead lets us access `characters`, which always
// returns a valid string.
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
mem::forget(source);
let event = synthesize_keyboard_event(code);
let mut flags = CGEventFlags::empty();
if cmd {

View file

@ -24,6 +24,7 @@ use core_foundation::{
boolean::CFBoolean,
data::CFData,
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
runloop::CFRunLoopRun,
string::{CFString, CFStringRef},
};
use ctor::ctor;
@ -139,6 +140,7 @@ pub(crate) struct MacPlatformState {
foreground_executor: ForegroundExecutor,
text_system: Arc<MacTextSystem>,
renderer_context: renderer::Context,
headless: bool,
pasteboard: id,
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
@ -155,15 +157,16 @@ pub(crate) struct MacPlatformState {
impl Default for MacPlatform {
fn default() -> Self {
Self::new()
Self::new(false)
}
}
impl MacPlatform {
pub(crate) fn new() -> Self {
pub(crate) fn new(headless: bool) -> Self {
let dispatcher = Arc::new(MacDispatcher::new());
Self(Mutex::new(MacPlatformState {
background_executor: BackgroundExecutor::new(dispatcher.clone()),
headless,
foreground_executor: ForegroundExecutor::new(dispatcher),
text_system: Arc::new(MacTextSystem::new()),
renderer_context: renderer::Context::default(),
@ -394,7 +397,15 @@ impl Platform for MacPlatform {
}
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
self.0.lock().finish_launching = Some(on_finish_launching);
let mut state = self.0.lock();
if state.headless {
drop(state);
on_finish_launching();
unsafe { CFRunLoopRun() };
} else {
state.finish_launching = Some(on_finish_launching);
drop(state);
}
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
@ -1238,7 +1249,7 @@ mod tests {
}
fn build_platform() -> MacPlatform {
let platform = MacPlatform::new();
let platform = MacPlatform::new(false);
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
platform
}