mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
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:
parent
7733bf686b
commit
b9a53ffa0b
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -251,6 +251,8 @@ jobs:
|
|||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||||
files: |
|
files: |
|
||||||
|
target/zed-remote-server-mac-x86_64.gz
|
||||||
|
target/zed-remote-server-mac-aarch64.gz
|
||||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||||
target/release/Zed.dmg
|
target/release/Zed.dmg
|
||||||
@ -322,7 +324,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||||
files: target/release/zed-linux-x86_64.tar.gz
|
files: |
|
||||||
|
target/zed-remote-server-linux-x86_64.gz
|
||||||
|
target/release/zed-linux-x86_64.tar.gz
|
||||||
body: ""
|
body: ""
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@ -405,7 +409,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||||
files: target/release/zed-linux-aarch64.tar.gz
|
files: |
|
||||||
|
target/zed-remote-server-linux-aarch64.gz
|
||||||
|
target/release/zed-linux-aarch64.tar.gz
|
||||||
body: ""
|
body: ""
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
4
.github/workflows/release_nightly.yml
vendored
4
.github/workflows/release_nightly.yml
vendored
@ -33,6 +33,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: ./script/clippy
|
run: ./script/clippy
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
name: Run tests
|
name: Run tests
|
||||||
@ -90,6 +91,9 @@ jobs:
|
|||||||
- name: Create macOS app bundle
|
- name: Create macOS app bundle
|
||||||
run: script/bundle-mac
|
run: script/bundle-mac
|
||||||
|
|
||||||
|
- name: Build macOS remote server binaries
|
||||||
|
run: script/build-remote-server x86_64 aarch64
|
||||||
|
|
||||||
- name: Upload Zed Nightly
|
- name: Upload Zed Nightly
|
||||||
run: script/upload-nightly macos
|
run: script/upload-nightly macos
|
||||||
|
|
||||||
|
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -984,6 +984,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"markdown_preview",
|
"markdown_preview",
|
||||||
"menu",
|
"menu",
|
||||||
|
"paths",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2526,6 +2527,8 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"recent_projects",
|
"recent_projects",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
|
"remote",
|
||||||
|
"remote_server",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rpc",
|
"rpc",
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
@ -8076,6 +8079,7 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
|
"remote",
|
||||||
"rpc",
|
"rpc",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
@ -8673,6 +8677,50 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "remote"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"collections",
|
||||||
|
"fs",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"parking_lot",
|
||||||
|
"prost",
|
||||||
|
"rpc",
|
||||||
|
"smol",
|
||||||
|
"tempfile",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "remote_server"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"cargo_toml",
|
||||||
|
"client",
|
||||||
|
"clock",
|
||||||
|
"env_logger",
|
||||||
|
"fs",
|
||||||
|
"futures 0.3.28",
|
||||||
|
"gpui",
|
||||||
|
"http 0.1.0",
|
||||||
|
"language",
|
||||||
|
"log",
|
||||||
|
"node_runtime",
|
||||||
|
"project",
|
||||||
|
"remote",
|
||||||
|
"rpc",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
|
"smol",
|
||||||
|
"toml 0.8.10",
|
||||||
|
"worktree",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rend"
|
name = "rend"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -13700,6 +13748,7 @@ dependencies = [
|
|||||||
"quick_action_bar",
|
"quick_action_bar",
|
||||||
"recent_projects",
|
"recent_projects",
|
||||||
"release_channel",
|
"release_channel",
|
||||||
|
"remote",
|
||||||
"repl",
|
"repl",
|
||||||
"rope",
|
"rope",
|
||||||
"search",
|
"search",
|
||||||
@ -13720,6 +13769,7 @@ dependencies = [
|
|||||||
"tree-sitter-markdown",
|
"tree-sitter-markdown",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"ui",
|
"ui",
|
||||||
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
@ -79,6 +79,8 @@ members = [
|
|||||||
"crates/refineable",
|
"crates/refineable",
|
||||||
"crates/refineable/derive_refineable",
|
"crates/refineable/derive_refineable",
|
||||||
"crates/release_channel",
|
"crates/release_channel",
|
||||||
|
"crates/remote",
|
||||||
|
"crates/remote_server",
|
||||||
"crates/repl",
|
"crates/repl",
|
||||||
"crates/rich_text",
|
"crates/rich_text",
|
||||||
"crates/rope",
|
"crates/rope",
|
||||||
@ -160,6 +162,7 @@ assets = { path = "crates/assets" }
|
|||||||
assistant = { path = "crates/assistant" }
|
assistant = { path = "crates/assistant" }
|
||||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||||
assistant_tooling = { path = "crates/assistant_tooling" }
|
assistant_tooling = { path = "crates/assistant_tooling" }
|
||||||
|
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||||
audio = { path = "crates/audio" }
|
audio = { path = "crates/audio" }
|
||||||
auto_update = { path = "crates/auto_update" }
|
auto_update = { path = "crates/auto_update" }
|
||||||
breadcrumbs = { path = "crates/breadcrumbs" }
|
breadcrumbs = { path = "crates/breadcrumbs" }
|
||||||
@ -231,6 +234,8 @@ proto = { path = "crates/proto" }
|
|||||||
quick_action_bar = { path = "crates/quick_action_bar" }
|
quick_action_bar = { path = "crates/quick_action_bar" }
|
||||||
recent_projects = { path = "crates/recent_projects" }
|
recent_projects = { path = "crates/recent_projects" }
|
||||||
release_channel = { path = "crates/release_channel" }
|
release_channel = { path = "crates/release_channel" }
|
||||||
|
remote = { path = "crates/remote" }
|
||||||
|
remote_server = { path = "crates/remote_server" }
|
||||||
repl = { path = "crates/repl" }
|
repl = { path = "crates/repl" }
|
||||||
rich_text = { path = "crates/rich_text" }
|
rich_text = { path = "crates/rich_text" }
|
||||||
rope = { path = "crates/rope" }
|
rope = { path = "crates/rope" }
|
||||||
@ -381,6 +386,7 @@ time = { version = "0.3", features = [
|
|||||||
"serde-well-known",
|
"serde-well-known",
|
||||||
"formatting",
|
"formatting",
|
||||||
] }
|
] }
|
||||||
|
tiny_http = "0.8"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower-http = "0.4.4"
|
tower-http = "0.4.4"
|
||||||
|
@ -23,6 +23,7 @@ isahc.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
markdown_preview.workspace = true
|
markdown_preview.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
|
paths.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
@ -359,7 +359,6 @@ impl AutoUpdater {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.status = AutoUpdateStatus::Checking;
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
|
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
|
||||||
@ -385,20 +384,51 @@ impl AutoUpdater {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
pub async fn get_latest_remote_server_release(
|
||||||
let (client, current_version) = this.read_with(&cx, |this, _| {
|
os: &str,
|
||||||
(this.http_client.clone(), this.current_version)
|
arch: &str,
|
||||||
})?;
|
mut release_channel: ReleaseChannel,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
let this = cx.update(|cx| {
|
||||||
|
cx.default_global::<GlobalAutoUpdate>()
|
||||||
|
.0
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow!("auto-update not initialized"))
|
||||||
|
})??;
|
||||||
|
|
||||||
let asset = match OS {
|
if release_channel == ReleaseChannel::Dev {
|
||||||
"linux" => format!("zed-linux-{}.tar.gz", ARCH),
|
release_channel = ReleaseChannel::Nightly;
|
||||||
"macos" => "Zed.dmg".into(),
|
}
|
||||||
_ => return Err(anyhow!("auto-update not supported for OS {:?}", OS)),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let release = Self::get_latest_release(&this, "zed-remote-server", os, arch, cx).await?;
|
||||||
|
|
||||||
|
let servers_dir = paths::remote_servers_dir();
|
||||||
|
let channel_dir = servers_dir.join(release_channel.dev_name());
|
||||||
|
let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
|
||||||
|
let version_path = platform_dir.join(format!("{}.gz", release.version));
|
||||||
|
smol::fs::create_dir_all(&platform_dir).await.ok();
|
||||||
|
|
||||||
|
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
|
||||||
|
if smol::fs::metadata(&version_path).await.is_err() {
|
||||||
|
log::info!("downloading zed-remote-server {os} {arch}");
|
||||||
|
download_remote_server_binary(&version_path, release, client, cx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(version_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_latest_release(
|
||||||
|
this: &Model<Self>,
|
||||||
|
asset: &str,
|
||||||
|
os: &str,
|
||||||
|
arch: &str,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<JsonRelease> {
|
||||||
|
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
|
||||||
let mut url_string = client.build_url(&format!(
|
let mut url_string = client.build_url(&format!(
|
||||||
"/api/releases/latest?asset={}&os={}&arch={}",
|
"/api/releases/latest?asset={}&os={}&arch={}",
|
||||||
asset, OS, ARCH
|
asset, os, arch
|
||||||
));
|
));
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
if let Some(param) = ReleaseChannel::try_global(cx)
|
if let Some(param) = ReleaseChannel::try_global(cx)
|
||||||
@ -418,8 +448,17 @@ impl AutoUpdater {
|
|||||||
.await
|
.await
|
||||||
.context("error reading release")?;
|
.context("error reading release")?;
|
||||||
|
|
||||||
let release: JsonRelease =
|
serde_json::from_slice(body.as_slice()).context("error deserializing release")
|
||||||
serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
|
}
|
||||||
|
|
||||||
|
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||||
|
let (client, current_version) = this.update(&mut cx, |this, cx| {
|
||||||
|
this.status = AutoUpdateStatus::Checking;
|
||||||
|
cx.notify();
|
||||||
|
(this.http_client.clone(), this.current_version)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let release = Self::get_latest_release(&this, "zed", OS, ARCH, &mut cx).await?;
|
||||||
|
|
||||||
let should_download = match *RELEASE_CHANNEL {
|
let should_download = match *RELEASE_CHANNEL {
|
||||||
ReleaseChannel::Nightly => cx
|
ReleaseChannel::Nightly => cx
|
||||||
@ -446,7 +485,7 @@ impl AutoUpdater {
|
|||||||
let temp_dir = tempfile::Builder::new()
|
let temp_dir = tempfile::Builder::new()
|
||||||
.prefix("zed-auto-update")
|
.prefix("zed-auto-update")
|
||||||
.tempdir()?;
|
.tempdir()?;
|
||||||
let downloaded_asset = download_release(&temp_dir, release, &asset, client, &cx).await?;
|
let downloaded_asset = download_release(&temp_dir, release, "zed", client, &cx).await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.status = AutoUpdateStatus::Installing;
|
this.status = AutoUpdateStatus::Installing;
|
||||||
@ -500,6 +539,32 @@ impl AutoUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn download_remote_server_binary(
|
||||||
|
target_path: &PathBuf,
|
||||||
|
release: JsonRelease,
|
||||||
|
client: Arc<HttpClientWithUrl>,
|
||||||
|
cx: &AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut target_file = File::create(&target_path).await?;
|
||||||
|
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||||
|
let installation_id = Client::global(cx).telemetry().installation_id();
|
||||||
|
let release_channel =
|
||||||
|
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||||
|
let telemetry = TelemetrySettings::get_global(cx).metrics;
|
||||||
|
|
||||||
|
(installation_id, release_channel, telemetry)
|
||||||
|
})?;
|
||||||
|
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||||
|
installation_id,
|
||||||
|
release_channel,
|
||||||
|
telemetry,
|
||||||
|
})?);
|
||||||
|
|
||||||
|
let mut response = client.get(&release.url, request_body, true).await?;
|
||||||
|
smol::io::copy(response.body_mut(), &mut target_file).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn download_release(
|
async fn download_release(
|
||||||
temp_dir: &tempfile::TempDir,
|
temp_dir: &tempfile::TempDir,
|
||||||
release: JsonRelease,
|
release: JsonRelease,
|
||||||
|
@ -129,6 +129,7 @@ fn main() -> Result<()> {
|
|||||||
|| path.starts_with("http://")
|
|| path.starts_with("http://")
|
||||||
|| path.starts_with("https://")
|
|| path.starts_with("https://")
|
||||||
|| path.starts_with("file://")
|
|| path.starts_with("file://")
|
||||||
|
|| path.starts_with("ssh://")
|
||||||
{
|
{
|
||||||
urls.push(path.to_string());
|
urls.push(path.to_string());
|
||||||
} else {
|
} else {
|
||||||
|
@ -99,6 +99,8 @@ pretty_assertions.workspace = true
|
|||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
recent_projects = { workspace = true }
|
recent_projects = { workspace = true }
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
|
remote = { workspace = true, features = ["test-support"] }
|
||||||
|
remote_server.workspace = true
|
||||||
dev_server_projects.workspace = true
|
dev_server_projects.workspace = true
|
||||||
rpc = { workspace = true, features = ["test-support"] }
|
rpc = { workspace = true, features = ["test-support"] }
|
||||||
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||||
|
@ -16,6 +16,7 @@ mod notification_tests;
|
|||||||
mod random_channel_buffer_tests;
|
mod random_channel_buffer_tests;
|
||||||
mod random_project_collaboration_tests;
|
mod random_project_collaboration_tests;
|
||||||
mod randomized_test_helpers;
|
mod randomized_test_helpers;
|
||||||
|
mod remote_editing_collaboration_tests;
|
||||||
mod test_server;
|
mod test_server;
|
||||||
|
|
||||||
use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
|
use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
|
||||||
|
102
crates/collab/src/tests/remote_editing_collaboration_tests.rs
Normal file
102
crates/collab/src/tests/remote_editing_collaboration_tests.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
use crate::tests::TestServer;
|
||||||
|
use call::ActiveCall;
|
||||||
|
use fs::{FakeFs, Fs as _};
|
||||||
|
use gpui::{Context as _, TestAppContext};
|
||||||
|
use remote::SshSession;
|
||||||
|
use remote_server::HeadlessProject;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_sharing_an_ssh_remote_project(
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
server_cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
let executor = cx_a.executor();
|
||||||
|
let mut server = TestServer::start(executor.clone()).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Set up project on remote FS
|
||||||
|
let (client_ssh, server_ssh) = SshSession::fake(cx_a, server_cx);
|
||||||
|
let remote_fs = FakeFs::new(server_cx.executor());
|
||||||
|
remote_fs
|
||||||
|
.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project2": {
|
||||||
|
"README.md": "# project 2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// User A connects to the remote project via SSH.
|
||||||
|
server_cx.update(HeadlessProject::init);
|
||||||
|
let _headless_project =
|
||||||
|
server_cx.new_model(|cx| HeadlessProject::new(server_ssh, remote_fs.clone(), cx));
|
||||||
|
|
||||||
|
let (project_a, worktree_id) = client_a
|
||||||
|
.build_ssh_project("/code/project1", client_ssh, cx_a)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// User A shares the remote project.
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// User B joins the project.
|
||||||
|
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||||
|
let worktree_b = project_b
|
||||||
|
.update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
executor.run_until_parked();
|
||||||
|
worktree_b.update(cx_b, |worktree, _cx| {
|
||||||
|
assert_eq!(
|
||||||
|
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
Path::new("README.md"),
|
||||||
|
Path::new("src"),
|
||||||
|
Path::new("src/lib.rs"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// User B can open buffers in the remote project.
|
||||||
|
let buffer_b = project_b
|
||||||
|
.update(cx_b, |project, cx| {
|
||||||
|
project.open_buffer((worktree_id, "src/lib.rs"), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
buffer_b.update(cx_b, |buffer, cx| {
|
||||||
|
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
|
||||||
|
let ix = buffer.text().find('1').unwrap();
|
||||||
|
buffer.edit([(ix..ix + 1, "100")], None, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
project_b
|
||||||
|
.update(cx_b, |project, cx| project.save_buffer(buffer_b, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
remote_fs
|
||||||
|
.load("/code/project1/src/lib.rs".as_ref())
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
"fn one() -> usize { 100 }"
|
||||||
|
);
|
||||||
|
}
|
@ -25,6 +25,7 @@ use node_runtime::FakeNodeRuntime;
|
|||||||
use notifications::NotificationStore;
|
use notifications::NotificationStore;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, WorktreeId};
|
use project::{Project, WorktreeId};
|
||||||
|
use remote::SshSession;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelRole},
|
proto::{self, ChannelRole},
|
||||||
RECEIVE_TIMEOUT,
|
RECEIVE_TIMEOUT,
|
||||||
@ -814,6 +815,30 @@ impl TestClient {
|
|||||||
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn build_ssh_project(
|
||||||
|
&self,
|
||||||
|
root_path: impl AsRef<Path>,
|
||||||
|
ssh: Arc<SshSession>,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> (Model<Project>, WorktreeId) {
|
||||||
|
let project = cx.update(|cx| {
|
||||||
|
Project::ssh(
|
||||||
|
ssh,
|
||||||
|
self.client().clone(),
|
||||||
|
self.app_state.node_runtime.clone(),
|
||||||
|
self.app_state.user_store.clone(),
|
||||||
|
self.app_state.languages.clone(),
|
||||||
|
self.app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let (worktree, _) = project
|
||||||
|
.update(cx, |p, cx| p.find_or_create_worktree(root_path, true, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
self.fs()
|
self.fs()
|
||||||
.insert_tree(
|
.insert_tree(
|
||||||
|
@ -1236,7 +1236,7 @@ mod tests {
|
|||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto(&self) -> rpc::proto::File {
|
fn to_proto(&self, _: &AppContext) -> rpc::proto::File {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -488,6 +488,7 @@ pub struct Editor {
|
|||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
show_breadcrumbs: bool,
|
show_breadcrumbs: bool,
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
|
redact_all: bool,
|
||||||
show_line_numbers: Option<bool>,
|
show_line_numbers: Option<bool>,
|
||||||
show_git_diff_gutter: Option<bool>,
|
show_git_diff_gutter: Option<bool>,
|
||||||
show_code_actions: Option<bool>,
|
show_code_actions: Option<bool>,
|
||||||
@ -1822,6 +1823,7 @@ impl Editor {
|
|||||||
show_code_actions: None,
|
show_code_actions: None,
|
||||||
show_runnables: None,
|
show_runnables: None,
|
||||||
show_wrap_guides: None,
|
show_wrap_guides: None,
|
||||||
|
redact_all: false,
|
||||||
show_indent_guides,
|
show_indent_guides,
|
||||||
placeholder_text: None,
|
placeholder_text: None,
|
||||||
highlight_order: 0,
|
highlight_order: 0,
|
||||||
@ -10414,6 +10416,11 @@ impl Editor {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_redact_all(&mut self, redact_all: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
self.redact_all = redact_all;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
||||||
self.show_wrap_guides = Some(show_wrap_guides);
|
self.show_wrap_guides = Some(show_wrap_guides);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@ -11108,6 +11115,10 @@ impl Editor {
|
|||||||
display_snapshot: &DisplaySnapshot,
|
display_snapshot: &DisplaySnapshot,
|
||||||
cx: &WindowContext,
|
cx: &WindowContext,
|
||||||
) -> Vec<Range<DisplayPoint>> {
|
) -> Vec<Range<DisplayPoint>> {
|
||||||
|
if self.redact_all {
|
||||||
|
return vec![DisplayPoint::zero()..display_snapshot.max_point()];
|
||||||
|
}
|
||||||
|
|
||||||
display_snapshot
|
display_snapshot
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.redacted_ranges(search_range, |file| {
|
.redacted_ranges(search_range, |file| {
|
||||||
|
@ -95,8 +95,11 @@ pub trait Fs: Send + Sync {
|
|||||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
|
||||||
fn is_fake(&self) -> bool;
|
fn is_fake(&self) -> bool;
|
||||||
async fn is_case_sensitive(&self) -> Result<bool>;
|
async fn is_case_sensitive(&self) -> Result<bool>;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fn as_fake(&self) -> &FakeFs;
|
fn as_fake(&self) -> &FakeFs {
|
||||||
|
panic!("called as_fake on a real fs");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
@ -602,11 +605,6 @@ impl Fs for RealFs {
|
|||||||
temp_dir.close()?;
|
temp_dir.close()?;
|
||||||
case_sensitive
|
case_sensitive
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
fn as_fake(&self) -> &FakeFs {
|
|
||||||
panic!("called `RealFs::as_fake`")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
@ -112,7 +112,18 @@ impl App {
|
|||||||
log::info!("GPUI was compiled in test mode");
|
log::info!("GPUI was compiled in test mode");
|
||||||
|
|
||||||
Self(AppContext::new(
|
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(()),
|
Arc::new(()),
|
||||||
http::client(None),
|
http::client(None),
|
||||||
))
|
))
|
||||||
|
@ -64,11 +64,16 @@ pub(crate) use test::*;
|
|||||||
pub(crate) use windows::*;
|
pub(crate) use windows::*;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub(crate) fn current_platform() -> Rc<dyn Platform> {
|
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
||||||
Rc::new(MacPlatform::new())
|
Rc::new(MacPlatform::new(headless))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[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() {
|
match guess_compositor() {
|
||||||
"Wayland" => Rc::new(WaylandClient::new()),
|
"Wayland" => Rc::new(WaylandClient::new()),
|
||||||
"X11" => Rc::new(X11Client::new()),
|
"X11" => Rc::new(X11Client::new()),
|
||||||
@ -99,7 +104,7 @@ pub fn guess_compositor() -> &'static str {
|
|||||||
|
|
||||||
// todo("windows")
|
// todo("windows")
|
||||||
#[cfg(target_os = "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())
|
Rc::new(WindowsPlatform::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,10 +12,9 @@ use core_graphics::{
|
|||||||
event::{CGEvent, CGEventFlags, CGKeyCode},
|
event::{CGEvent, CGEventFlags, CGKeyCode},
|
||||||
event_source::{CGEventSource, CGEventSourceStateID},
|
event_source::{CGEventSource, CGEventSourceStateID},
|
||||||
};
|
};
|
||||||
use ctor::ctor;
|
|
||||||
use metal::foreign_types::ForeignType as _;
|
use metal::foreign_types::ForeignType as _;
|
||||||
use objc::{class, msg_send, sel, sel_impl};
|
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 BACKSPACE_KEY: u16 = 0x7f;
|
||||||
const SPACE_KEY: u16 = b' ' as u16;
|
const SPACE_KEY: u16 = b' ' as u16;
|
||||||
@ -25,13 +24,22 @@ const ESCAPE_KEY: u16 = 0x1b;
|
|||||||
const TAB_KEY: u16 = 0x09;
|
const TAB_KEY: u16 = 0x09;
|
||||||
const SHIFT_TAB_KEY: u16 = 0x19;
|
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]
|
INIT_EVENT_SOURCE.call_once(|| {
|
||||||
unsafe fn build_event_source() {
|
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
|
||||||
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
|
unsafe {
|
||||||
EVENT_SOURCE = source.as_ptr();
|
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);
|
mem::forget(source);
|
||||||
|
event
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn key_to_native(key: &str) -> Cow<str> {
|
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
|
// 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
|
// an event with the given flags instead lets us access `characters`, which always
|
||||||
// returns a valid string.
|
// returns a valid string.
|
||||||
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
|
let event = synthesize_keyboard_event(code);
|
||||||
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
|
|
||||||
mem::forget(source);
|
|
||||||
|
|
||||||
let mut flags = CGEventFlags::empty();
|
let mut flags = CGEventFlags::empty();
|
||||||
if cmd {
|
if cmd {
|
||||||
|
@ -24,6 +24,7 @@ use core_foundation::{
|
|||||||
boolean::CFBoolean,
|
boolean::CFBoolean,
|
||||||
data::CFData,
|
data::CFData,
|
||||||
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
|
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
|
||||||
|
runloop::CFRunLoopRun,
|
||||||
string::{CFString, CFStringRef},
|
string::{CFString, CFStringRef},
|
||||||
};
|
};
|
||||||
use ctor::ctor;
|
use ctor::ctor;
|
||||||
@ -139,6 +140,7 @@ pub(crate) struct MacPlatformState {
|
|||||||
foreground_executor: ForegroundExecutor,
|
foreground_executor: ForegroundExecutor,
|
||||||
text_system: Arc<MacTextSystem>,
|
text_system: Arc<MacTextSystem>,
|
||||||
renderer_context: renderer::Context,
|
renderer_context: renderer::Context,
|
||||||
|
headless: bool,
|
||||||
pasteboard: id,
|
pasteboard: id,
|
||||||
text_hash_pasteboard_type: id,
|
text_hash_pasteboard_type: id,
|
||||||
metadata_pasteboard_type: id,
|
metadata_pasteboard_type: id,
|
||||||
@ -155,15 +157,16 @@ pub(crate) struct MacPlatformState {
|
|||||||
|
|
||||||
impl Default for MacPlatform {
|
impl Default for MacPlatform {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MacPlatform {
|
impl MacPlatform {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new(headless: bool) -> Self {
|
||||||
let dispatcher = Arc::new(MacDispatcher::new());
|
let dispatcher = Arc::new(MacDispatcher::new());
|
||||||
Self(Mutex::new(MacPlatformState {
|
Self(Mutex::new(MacPlatformState {
|
||||||
background_executor: BackgroundExecutor::new(dispatcher.clone()),
|
background_executor: BackgroundExecutor::new(dispatcher.clone()),
|
||||||
|
headless,
|
||||||
foreground_executor: ForegroundExecutor::new(dispatcher),
|
foreground_executor: ForegroundExecutor::new(dispatcher),
|
||||||
text_system: Arc::new(MacTextSystem::new()),
|
text_system: Arc::new(MacTextSystem::new()),
|
||||||
renderer_context: renderer::Context::default(),
|
renderer_context: renderer::Context::default(),
|
||||||
@ -394,7 +397,15 @@ impl Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
|
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 {
|
unsafe {
|
||||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||||
@ -1238,7 +1249,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_platform() -> MacPlatform {
|
fn build_platform() -> MacPlatform {
|
||||||
let platform = MacPlatform::new();
|
let platform = MacPlatform::new(false);
|
||||||
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
||||||
platform
|
platform
|
||||||
}
|
}
|
||||||
|
@ -372,7 +372,7 @@ pub trait File: Send + Sync {
|
|||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
|
||||||
/// Converts this file into a protobuf message.
|
/// Converts this file into a protobuf message.
|
||||||
fn to_proto(&self) -> rpc::proto::File;
|
fn to_proto(&self, cx: &AppContext) -> rpc::proto::File;
|
||||||
|
|
||||||
/// Return whether Zed considers this to be a private file.
|
/// Return whether Zed considers this to be a private file.
|
||||||
fn is_private(&self) -> bool;
|
fn is_private(&self) -> bool;
|
||||||
@ -612,10 +612,10 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize the buffer's state to a protobuf message.
|
/// Serialize the buffer's state to a protobuf message.
|
||||||
pub fn to_proto(&self) -> proto::BufferState {
|
pub fn to_proto(&self, cx: &AppContext) -> proto::BufferState {
|
||||||
proto::BufferState {
|
proto::BufferState {
|
||||||
id: self.remote_id().into(),
|
id: self.remote_id().into(),
|
||||||
file: self.file.as_ref().map(|f| f.to_proto()),
|
file: self.file.as_ref().map(|f| f.to_proto(cx)),
|
||||||
base_text: self.base_text().to_string(),
|
base_text: self.base_text().to_string(),
|
||||||
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
|
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
|
||||||
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||||
@ -3940,7 +3940,7 @@ impl File for TestFile {
|
|||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto(&self) -> rpc::proto::File {
|
fn to_proto(&self, _: &AppContext) -> rpc::proto::File {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2038,7 +2038,7 @@ fn test_serialization(cx: &mut gpui::AppContext) {
|
|||||||
});
|
});
|
||||||
assert_eq!(buffer1.read(cx).text(), "abcDF");
|
assert_eq!(buffer1.read(cx).text(), "abcDF");
|
||||||
|
|
||||||
let state = buffer1.read(cx).to_proto();
|
let state = buffer1.read(cx).to_proto(cx);
|
||||||
let ops = cx
|
let ops = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.block(buffer1.read(cx).serialize_ops(None, cx));
|
.block(buffer1.read(cx).serialize_ops(None, cx));
|
||||||
@ -2165,7 +2165,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
|||||||
|
|
||||||
for i in 0..rng.gen_range(min_peers..=max_peers) {
|
for i in 0..rng.gen_range(min_peers..=max_peers) {
|
||||||
let buffer = cx.new_model(|cx| {
|
let buffer = cx.new_model(|cx| {
|
||||||
let state = base_buffer.read(cx).to_proto();
|
let state = base_buffer.read(cx).to_proto(cx);
|
||||||
let ops = cx
|
let ops = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.block(base_buffer.read(cx).serialize_ops(None, cx));
|
.block(base_buffer.read(cx).serialize_ops(None, cx));
|
||||||
@ -2272,7 +2272,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
|||||||
mutation_count -= 1;
|
mutation_count -= 1;
|
||||||
}
|
}
|
||||||
50..=59 if replica_ids.len() < max_peers => {
|
50..=59 if replica_ids.len() < max_peers => {
|
||||||
let old_buffer_state = buffer.read(cx).to_proto();
|
let old_buffer_state = buffer.read(cx).to_proto(cx);
|
||||||
let old_buffer_ops = cx
|
let old_buffer_ops = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.block(buffer.read(cx).serialize_ops(None, cx));
|
.block(buffer.read(cx).serialize_ops(None, cx));
|
||||||
|
@ -17,7 +17,7 @@ test-support = ["async-pipe"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
|
async-pipe = { workspace = true, optional = true }
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
@ -35,7 +35,7 @@ release_channel.workspace = true
|
|||||||
windows.workspace = true
|
windows.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
async-pipe.workspace = true
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
@ -4807,7 +4807,7 @@ mod tests {
|
|||||||
fn test_remote(cx: &mut AppContext) {
|
fn test_remote(cx: &mut AppContext) {
|
||||||
let host_buffer = cx.new_model(|cx| Buffer::local("a", cx));
|
let host_buffer = cx.new_model(|cx| Buffer::local("a", cx));
|
||||||
let guest_buffer = cx.new_model(|cx| {
|
let guest_buffer = cx.new_model(|cx| {
|
||||||
let state = host_buffer.read(cx).to_proto();
|
let state = host_buffer.read(cx).to_proto(cx);
|
||||||
let ops = cx
|
let ops = cx
|
||||||
.background_executor()
|
.background_executor()
|
||||||
.block(host_buffer.read(cx).serialize_ops(None, cx));
|
.block(host_buffer.read(cx).serialize_ops(None, cx));
|
||||||
|
@ -224,26 +224,28 @@ pub fn default_prettier_dir() -> &'static PathBuf {
|
|||||||
DEFAULT_PRETTIER_DIR.get_or_init(|| support_dir().join("prettier"))
|
DEFAULT_PRETTIER_DIR.get_or_init(|| support_dir().join("prettier"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the remote server binaries directory.
|
||||||
|
pub fn remote_servers_dir() -> &'static PathBuf {
|
||||||
|
static REMOTE_SERVERS_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
REMOTE_SERVERS_DIR.get_or_init(|| support_dir().join("remote_servers"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the relative path to a `.zed` folder within a project.
|
/// Returns the relative path to a `.zed` folder within a project.
|
||||||
pub fn local_settings_folder_relative_path() -> &'static Path {
|
pub fn local_settings_folder_relative_path() -> &'static Path {
|
||||||
static LOCAL_SETTINGS_FOLDER_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
|
Path::new(".zed")
|
||||||
LOCAL_SETTINGS_FOLDER_RELATIVE_PATH.get_or_init(|| Path::new(".zed"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the relative path to a `settings.json` file within a project.
|
/// Returns the relative path to a `settings.json` file within a project.
|
||||||
pub fn local_settings_file_relative_path() -> &'static Path {
|
pub fn local_settings_file_relative_path() -> &'static Path {
|
||||||
static LOCAL_SETTINGS_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
|
Path::new(".zed/settings.json")
|
||||||
LOCAL_SETTINGS_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".zed/settings.json"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the relative path to a `tasks.json` file within a project.
|
/// Returns the relative path to a `tasks.json` file within a project.
|
||||||
pub fn local_tasks_file_relative_path() -> &'static Path {
|
pub fn local_tasks_file_relative_path() -> &'static Path {
|
||||||
static LOCAL_TASKS_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
|
Path::new(".zed/tasks.json")
|
||||||
LOCAL_TASKS_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".zed/tasks.json"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the relative path to a `.vscode/tasks.json` file within a project.
|
/// Returns the relative path to a `.vscode/tasks.json` file within a project.
|
||||||
pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
|
pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
|
||||||
static LOCAL_VSCODE_TASKS_FILE_RELATIVE_PATH: OnceLock<&Path> = OnceLock::new();
|
Path::new(".vscode/tasks.json")
|
||||||
LOCAL_VSCODE_TASKS_FILE_RELATIVE_PATH.get_or_init(|| Path::new(".vscode/tasks.json"))
|
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ prettier.workspace = true
|
|||||||
worktree.workspace = true
|
worktree.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
remote.workspace = true
|
||||||
rpc.workspace = true
|
rpc.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
|
@ -705,7 +705,7 @@ impl BufferStore {
|
|||||||
|
|
||||||
let operations = buffer.update(cx, |b, cx| b.serialize_ops(None, cx))?;
|
let operations = buffer.update(cx, |b, cx| b.serialize_ops(None, cx))?;
|
||||||
let operations = operations.await;
|
let operations = operations.await;
|
||||||
let state = buffer.update(cx, |buffer, _| buffer.to_proto())?;
|
let state = buffer.update(cx, |buffer, cx| buffer.to_proto(cx))?;
|
||||||
|
|
||||||
let initial_state = proto::CreateBufferForPeer {
|
let initial_state = proto::CreateBufferForPeer {
|
||||||
project_id,
|
project_id,
|
||||||
|
@ -74,15 +74,18 @@ use postage::watch;
|
|||||||
use prettier_support::{DefaultPrettier, PrettierInstance};
|
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||||
use project_settings::{DirenvSettings, LspSettings, ProjectSettings};
|
use project_settings::{DirenvSettings, LspSettings, ProjectSettings};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rpc::ErrorCode;
|
use remote::SshSession;
|
||||||
|
use rpc::{proto::AddWorktree, ErrorCode};
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
use search_history::SearchHistory;
|
use search_history::SearchHistory;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore};
|
use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use smol::channel::{Receiver, Sender};
|
use smol::{
|
||||||
use smol::lock::Semaphore;
|
channel::{Receiver, Sender},
|
||||||
|
lock::Semaphore,
|
||||||
|
};
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
use snippet_provider::SnippetProvider;
|
use snippet_provider::SnippetProvider;
|
||||||
use std::{
|
use std::{
|
||||||
@ -196,6 +199,7 @@ pub struct Project {
|
|||||||
>,
|
>,
|
||||||
user_store: Model<UserStore>,
|
user_store: Model<UserStore>,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
ssh_session: Option<Arc<SshSession>>,
|
||||||
client_state: ProjectClientState,
|
client_state: ProjectClientState,
|
||||||
collaborators: HashMap<proto::PeerId, Collaborator>,
|
collaborators: HashMap<proto::PeerId, Collaborator>,
|
||||||
client_subscriptions: Vec<client::Subscription>,
|
client_subscriptions: Vec<client::Subscription>,
|
||||||
@ -793,6 +797,7 @@ impl Project {
|
|||||||
client,
|
client,
|
||||||
user_store,
|
user_store,
|
||||||
fs,
|
fs,
|
||||||
|
ssh_session: None,
|
||||||
next_entry_id: Default::default(),
|
next_entry_id: Default::default(),
|
||||||
next_diagnostic_group_id: Default::default(),
|
next_diagnostic_group_id: Default::default(),
|
||||||
diagnostics: 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(
|
pub async fn remote(
|
||||||
remote_id: u64,
|
remote_id: u64,
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
@ -924,6 +947,7 @@ impl Project {
|
|||||||
snippets,
|
snippets,
|
||||||
yarn,
|
yarn,
|
||||||
fs,
|
fs,
|
||||||
|
ssh_session: None,
|
||||||
next_entry_id: Default::default(),
|
next_entry_id: Default::default(),
|
||||||
next_diagnostic_group_id: Default::default(),
|
next_diagnostic_group_id: Default::default(),
|
||||||
diagnostic_summaries: Default::default(),
|
diagnostic_summaries: Default::default(),
|
||||||
@ -1628,7 +1652,7 @@ impl Project {
|
|||||||
this.client.send(
|
this.client.send(
|
||||||
proto::UpdateDiagnosticSummary {
|
proto::UpdateDiagnosticSummary {
|
||||||
project_id,
|
project_id,
|
||||||
worktree_id: cx.entity_id().as_u64(),
|
worktree_id: worktree.id().to_proto(),
|
||||||
summary: Some(
|
summary: Some(
|
||||||
summary.to_proto(server_id, path),
|
summary.to_proto(server_id, path),
|
||||||
),
|
),
|
||||||
@ -2442,7 +2466,7 @@ impl Project {
|
|||||||
.send(proto::UpdateBufferFile {
|
.send(proto::UpdateBufferFile {
|
||||||
project_id,
|
project_id,
|
||||||
buffer_id: buffer_id.into(),
|
buffer_id: buffer_id.into(),
|
||||||
file: Some(new_file.to_proto()),
|
file: Some(new_file.to_proto(cx)),
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
@ -2464,11 +2488,23 @@ impl Project {
|
|||||||
self.request_buffer_diff_recalculation(&buffer, cx);
|
self.request_buffer_diff_recalculation(&buffer, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let buffer_id = buffer.read(cx).remote_id();
|
||||||
match event {
|
match event {
|
||||||
BufferEvent::Operation(operation) => {
|
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 {
|
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Operation {
|
||||||
buffer_id: buffer.read(cx).remote_id(),
|
buffer_id,
|
||||||
operation: language::proto::serialize_operation(operation),
|
operation,
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@ -2948,9 +2984,10 @@ impl Project {
|
|||||||
language: Arc<Language>,
|
language: Arc<Language>,
|
||||||
cx: &mut ModelContext<Self>,
|
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);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7632,7 +7669,9 @@ impl Project {
|
|||||||
) -> Task<Result<Model<Worktree>>> {
|
) -> Task<Result<Model<Worktree>>> {
|
||||||
let path: Arc<Path> = abs_path.as_ref().into();
|
let path: Arc<Path> = abs_path.as_ref().into();
|
||||||
if !self.loading_worktrees.contains_key(&path) {
|
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)
|
self.create_local_worktree(abs_path, visible, cx)
|
||||||
} else if self.dev_server_project_id.is_some() {
|
} else if self.dev_server_project_id.is_some() {
|
||||||
self.create_dev_server_worktree(abs_path, cx)
|
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(
|
fn create_local_worktree(
|
||||||
&mut self,
|
&mut self,
|
||||||
abs_path: impl AsRef<Path>,
|
abs_path: impl AsRef<Path>,
|
||||||
@ -7922,7 +7994,7 @@ impl Project {
|
|||||||
.send(proto::UpdateBufferFile {
|
.send(proto::UpdateBufferFile {
|
||||||
project_id,
|
project_id,
|
||||||
buffer_id: buffer.read(cx).remote_id().into(),
|
buffer_id: buffer.read(cx).remote_id().into(),
|
||||||
file: Some(new_file.to_proto()),
|
file: Some(new_file.to_proto(cx)),
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
@ -9073,6 +9145,13 @@ impl Project {
|
|||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<proto::Ack> {
|
) -> Result<proto::Ack> {
|
||||||
this.update(&mut cx, |this, cx| {
|
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| {
|
this.buffer_store.update(cx, |buffer_store, cx| {
|
||||||
buffer_store.handle_update_buffer(envelope, this.is_remote(), cx)
|
buffer_store.handle_update_buffer(envelope, this.is_remote(), cx)
|
||||||
})
|
})
|
||||||
@ -9231,7 +9310,7 @@ impl Project {
|
|||||||
.send(proto::UpdateBufferFile {
|
.send(proto::UpdateBufferFile {
|
||||||
project_id,
|
project_id,
|
||||||
buffer_id: buffer_id.into(),
|
buffer_id: buffer_id.into(),
|
||||||
file: Some(file.to_proto()),
|
file: Some(file.to_proto(cx)),
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
@ -269,6 +269,10 @@ message Envelope {
|
|||||||
ListRemoteDirectory list_remote_directory = 219;
|
ListRemoteDirectory list_remote_directory = 219;
|
||||||
ListRemoteDirectoryResponse list_remote_directory_response = 220;
|
ListRemoteDirectoryResponse list_remote_directory_response = 220;
|
||||||
UpdateDevServerProject update_dev_server_project = 221; // current max
|
UpdateDevServerProject update_dev_server_project = 221; // current max
|
||||||
|
|
||||||
|
// Remote
|
||||||
|
AddWorktree add_worktree = 500;
|
||||||
|
AddWorktreeResponse add_worktree_response = 501;
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
@ -2426,3 +2430,13 @@ message SynchronizeContexts {
|
|||||||
message SynchronizeContextsResponse {
|
message SynchronizeContextsResponse {
|
||||||
repeated ContextVersion contexts = 1;
|
repeated ContextVersion contexts = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remote FS
|
||||||
|
|
||||||
|
message AddWorktree {
|
||||||
|
string path = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddWorktreeResponse {
|
||||||
|
uint64 worktree_id = 1;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! messages {
|
macro_rules! messages {
|
||||||
($(($name:ident, $priority:ident)),* $(,)?) => {
|
($(($name:ident, $priority:ident)),* $(,)?) => {
|
||||||
pub fn build_typed_envelope(sender_id: PeerId, received_at: Instant, envelope: Envelope) -> Option<Box<dyn AnyTypedEnvelope>> {
|
pub fn build_typed_envelope(sender_id: PeerId, received_at: std::time::Instant, envelope: Envelope) -> Option<Box<dyn AnyTypedEnvelope>> {
|
||||||
match envelope.payload {
|
match envelope.payload {
|
||||||
$(Some(envelope::Payload::$name(payload)) => {
|
$(Some(envelope::Payload::$name(payload)) => {
|
||||||
Some(Box::new(TypedEnvelope {
|
Some(Box::new(TypedEnvelope {
|
||||||
|
@ -18,7 +18,7 @@ use std::{
|
|||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
iter, mem,
|
iter, mem,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
|
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
|
||||||
@ -395,6 +395,9 @@ messages!(
|
|||||||
(UpdateContext, Foreground),
|
(UpdateContext, Foreground),
|
||||||
(SynchronizeContexts, Foreground),
|
(SynchronizeContexts, Foreground),
|
||||||
(SynchronizeContextsResponse, Foreground),
|
(SynchronizeContextsResponse, Foreground),
|
||||||
|
// Remote development
|
||||||
|
(AddWorktree, Foreground),
|
||||||
|
(AddWorktreeResponse, Foreground),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
@ -512,6 +515,8 @@ request_messages!(
|
|||||||
(RestartLanguageServers, Ack),
|
(RestartLanguageServers, Ack),
|
||||||
(OpenContext, OpenContextResponse),
|
(OpenContext, OpenContextResponse),
|
||||||
(SynchronizeContexts, SynchronizeContextsResponse),
|
(SynchronizeContexts, SynchronizeContextsResponse),
|
||||||
|
// Remote development
|
||||||
|
(AddWorktree, AddWorktreeResponse),
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(
|
entity_messages!(
|
||||||
|
36
crates/remote/Cargo.toml
Normal file
36
crates/remote/Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "remote"
|
||||||
|
description = "Client-side subsystem for remote editing"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/remote.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
test-support = ["fs/test-support"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
|
fs.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
prost.workspace = true
|
||||||
|
rpc.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
tempfile.workspace = true
|
||||||
|
util.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
fs = { workspace = true, features = ["test-support"] }
|
1
crates/remote/LICENSE-GPL
Symbolic link
1
crates/remote/LICENSE-GPL
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../LICENSE-GPL
|
51
crates/remote/src/protocol.rs
Normal file
51
crates/remote/src/protocol.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
|
use prost::Message as _;
|
||||||
|
use rpc::proto::Envelope;
|
||||||
|
use std::mem::size_of;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||||
|
pub struct MessageId(pub u32);
|
||||||
|
|
||||||
|
pub type MessageLen = u32;
|
||||||
|
pub const MESSAGE_LEN_SIZE: usize = size_of::<MessageLen>();
|
||||||
|
|
||||||
|
pub fn message_len_from_buffer(buffer: &[u8]) -> MessageLen {
|
||||||
|
MessageLen::from_le_bytes(buffer.try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_message_with_len<S: AsyncRead + Unpin>(
|
||||||
|
stream: &mut S,
|
||||||
|
buffer: &mut Vec<u8>,
|
||||||
|
message_len: MessageLen,
|
||||||
|
) -> Result<Envelope> {
|
||||||
|
buffer.resize(message_len as usize, 0);
|
||||||
|
stream.read_exact(buffer).await?;
|
||||||
|
Ok(Envelope::decode(buffer.as_slice())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_message<S: AsyncRead + Unpin>(
|
||||||
|
stream: &mut S,
|
||||||
|
buffer: &mut Vec<u8>,
|
||||||
|
) -> Result<Envelope> {
|
||||||
|
buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||||
|
stream.read_exact(buffer).await?;
|
||||||
|
let len = message_len_from_buffer(buffer);
|
||||||
|
read_message_with_len(stream, buffer, len).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_message<S: AsyncWrite + Unpin>(
|
||||||
|
stream: &mut S,
|
||||||
|
buffer: &mut Vec<u8>,
|
||||||
|
message: Envelope,
|
||||||
|
) -> Result<()> {
|
||||||
|
let message_len = message.encoded_len() as u32;
|
||||||
|
stream
|
||||||
|
.write_all(message_len.to_le_bytes().as_slice())
|
||||||
|
.await?;
|
||||||
|
buffer.clear();
|
||||||
|
buffer.reserve(message_len as usize);
|
||||||
|
message.encode(buffer)?;
|
||||||
|
stream.write_all(buffer).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
4
crates/remote/src/remote.rs
Normal file
4
crates/remote/src/remote.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod protocol;
|
||||||
|
pub mod ssh_session;
|
||||||
|
|
||||||
|
pub use ssh_session::{SshClientDelegate, SshPlatform, SshSession};
|
643
crates/remote/src/ssh_session.rs
Normal file
643
crates/remote/src/ssh_session.rs
Normal file
@ -0,0 +1,643 @@
|
|||||||
|
use crate::protocol::{
|
||||||
|
message_len_from_buffer, read_message_with_len, write_message, MessageId, MESSAGE_LEN_SIZE,
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
|
use collections::HashMap;
|
||||||
|
use futures::{
|
||||||
|
channel::{mpsc, oneshot},
|
||||||
|
future::{BoxFuture, LocalBoxFuture},
|
||||||
|
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, StreamExt as _,
|
||||||
|
};
|
||||||
|
use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, WeakModel};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use rpc::{
|
||||||
|
proto::{
|
||||||
|
self, build_typed_envelope, AnyTypedEnvelope, Envelope, EnvelopedMessage, PeerId,
|
||||||
|
ProtoClient, RequestMessage,
|
||||||
|
},
|
||||||
|
TypedEnvelope,
|
||||||
|
};
|
||||||
|
use smol::{
|
||||||
|
fs,
|
||||||
|
process::{self, Stdio},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
any::TypeId,
|
||||||
|
ffi::OsStr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU32, Ordering::SeqCst},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
pub struct SshSession {
|
||||||
|
next_message_id: AtomicU32,
|
||||||
|
response_channels: ResponseChannels,
|
||||||
|
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||||
|
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
|
||||||
|
message_handlers: Mutex<
|
||||||
|
HashMap<
|
||||||
|
TypeId,
|
||||||
|
Arc<
|
||||||
|
dyn Send
|
||||||
|
+ Sync
|
||||||
|
+ Fn(
|
||||||
|
Box<dyn AnyTypedEnvelope>,
|
||||||
|
Arc<SshSession>,
|
||||||
|
AsyncAppContext,
|
||||||
|
) -> Option<LocalBoxFuture<'static, Result<()>>>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SshClientState {
|
||||||
|
socket_path: PathBuf,
|
||||||
|
port: u16,
|
||||||
|
url: String,
|
||||||
|
_master_process: process::Child,
|
||||||
|
_temp_dir: TempDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SpawnRequest {
|
||||||
|
command: String,
|
||||||
|
process_tx: oneshot::Sender<process::Child>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct SshPlatform {
|
||||||
|
pub os: &'static str,
|
||||||
|
pub arch: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SshClientDelegate {
|
||||||
|
fn ask_password(
|
||||||
|
&self,
|
||||||
|
prompt: String,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> oneshot::Receiver<Result<String>>;
|
||||||
|
fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result<PathBuf>;
|
||||||
|
fn get_server_binary(
|
||||||
|
&self,
|
||||||
|
platform: SshPlatform,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
||||||
|
|
||||||
|
impl SshSession {
|
||||||
|
pub async fn client(
|
||||||
|
user: String,
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<Arc<Self>> {
|
||||||
|
let client_state = SshClientState::new(user, host, port, delegate.clone(), cx).await?;
|
||||||
|
|
||||||
|
let platform = query_platform(&client_state).await?;
|
||||||
|
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
||||||
|
let remote_binary_path = delegate.remote_server_binary_path(cx)?;
|
||||||
|
ensure_server_binary(
|
||||||
|
&client_state,
|
||||||
|
&local_binary_path,
|
||||||
|
&remote_binary_path,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (spawn_process_tx, mut spawn_process_rx) = mpsc::unbounded::<SpawnRequest>();
|
||||||
|
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||||
|
|
||||||
|
let mut remote_server_child = client_state
|
||||||
|
.ssh_command(&remote_binary_path)
|
||||||
|
.arg("run")
|
||||||
|
.spawn()
|
||||||
|
.context("failed to spawn remote server")?;
|
||||||
|
let mut child_stderr = remote_server_child.stderr.take().unwrap();
|
||||||
|
let mut child_stdout = remote_server_child.stdout.take().unwrap();
|
||||||
|
let mut child_stdin = remote_server_child.stdin.take().unwrap();
|
||||||
|
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
executor.clone().spawn(async move {
|
||||||
|
let mut stdin_buffer = Vec::new();
|
||||||
|
let mut stdout_buffer = Vec::new();
|
||||||
|
let mut stderr_buffer = Vec::new();
|
||||||
|
let mut stderr_offset = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||||
|
stderr_buffer.resize(stderr_offset + 1024, 0);
|
||||||
|
|
||||||
|
select_biased! {
|
||||||
|
outgoing = outgoing_rx.next().fuse() => {
|
||||||
|
let Some(outgoing) = outgoing else {
|
||||||
|
return anyhow::Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
request = spawn_process_rx.next().fuse() => {
|
||||||
|
let Some(request) = request else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("spawn process: {:?}", request.command);
|
||||||
|
let child = client_state
|
||||||
|
.ssh_command(&request.command)
|
||||||
|
.spawn()
|
||||||
|
.context("failed to create channel")?;
|
||||||
|
request.process_tx.send(child).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
result = child_stdout.read(&mut stdout_buffer).fuse() => {
|
||||||
|
match result {
|
||||||
|
Ok(len) => {
|
||||||
|
if len == 0 {
|
||||||
|
child_stdin.close().await?;
|
||||||
|
let status = remote_server_child.status().await?;
|
||||||
|
if !status.success() {
|
||||||
|
log::info!("channel exited with status: {status:?}");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if len < stdout_buffer.len() {
|
||||||
|
child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_len = message_len_from_buffer(&stdout_buffer);
|
||||||
|
match read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len).await {
|
||||||
|
Ok(envelope) => {
|
||||||
|
incoming_tx.unbounded_send(envelope).ok();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("error decoding message {error:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
Err(anyhow!("error reading stdout: {error:?}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = child_stderr.read(&mut stderr_buffer[stderr_offset..]).fuse() => {
|
||||||
|
match result {
|
||||||
|
Ok(len) => {
|
||||||
|
stderr_offset += len;
|
||||||
|
let mut start_ix = 0;
|
||||||
|
while let Some(ix) = stderr_buffer[start_ix..stderr_offset].iter().position(|b| b == &b'\n') {
|
||||||
|
let line_ix = start_ix + ix;
|
||||||
|
let content = String::from_utf8_lossy(&stderr_buffer[start_ix..line_ix]);
|
||||||
|
start_ix = line_ix + 1;
|
||||||
|
eprintln!("(remote) {}", content);
|
||||||
|
}
|
||||||
|
stderr_buffer.drain(0..start_ix);
|
||||||
|
stderr_offset -= start_ix;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
Err(anyhow!("error reading stderr: {error:?}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
|
||||||
|
cx.update(|cx| Self::new(incoming_rx, outgoing_tx, spawn_process_tx, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server(
|
||||||
|
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||||
|
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Arc<SshSession> {
|
||||||
|
let (tx, _rx) = mpsc::unbounded();
|
||||||
|
Self::new(incoming_rx, outgoing_tx, tx, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn fake(
|
||||||
|
client_cx: &mut gpui::TestAppContext,
|
||||||
|
server_cx: &mut gpui::TestAppContext,
|
||||||
|
) -> (Arc<Self>, Arc<Self>) {
|
||||||
|
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
|
||||||
|
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
|
||||||
|
let (tx, _rx) = mpsc::unbounded();
|
||||||
|
(
|
||||||
|
client_cx
|
||||||
|
.update(|cx| Self::new(server_to_client_rx, client_to_server_tx, tx.clone(), cx)),
|
||||||
|
server_cx
|
||||||
|
.update(|cx| Self::new(client_to_server_rx, server_to_client_tx, tx.clone(), cx)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||||
|
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||||
|
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Arc<SshSession> {
|
||||||
|
let this = Arc::new(Self {
|
||||||
|
next_message_id: AtomicU32::new(0),
|
||||||
|
response_channels: ResponseChannels::default(),
|
||||||
|
outgoing_tx,
|
||||||
|
spawn_process_tx,
|
||||||
|
message_handlers: Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn(|cx| {
|
||||||
|
let this = this.clone();
|
||||||
|
async move {
|
||||||
|
let peer_id = PeerId { owner_id: 0, id: 0 };
|
||||||
|
while let Some(incoming) = incoming_rx.next().await {
|
||||||
|
if let Some(request_id) = incoming.responding_to {
|
||||||
|
let request_id = MessageId(request_id);
|
||||||
|
let sender = this.response_channels.lock().remove(&request_id);
|
||||||
|
if let Some(sender) = sender {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
if incoming.payload.is_some() {
|
||||||
|
sender.send((incoming, tx)).ok();
|
||||||
|
}
|
||||||
|
rx.await.ok();
|
||||||
|
}
|
||||||
|
} else if let Some(envelope) =
|
||||||
|
build_typed_envelope(peer_id, Instant::now(), incoming)
|
||||||
|
{
|
||||||
|
log::debug!(
|
||||||
|
"ssh message received. name:{}",
|
||||||
|
envelope.payload_type_name()
|
||||||
|
);
|
||||||
|
let type_id = envelope.payload_type_id();
|
||||||
|
let handler = this.message_handlers.lock().get(&type_id).cloned();
|
||||||
|
if let Some(handler) = handler {
|
||||||
|
if let Some(future) = handler(envelope, this.clone(), cx.clone()) {
|
||||||
|
future.await.ok();
|
||||||
|
} else {
|
||||||
|
this.message_handlers.lock().remove(&type_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request<T: RequestMessage>(
|
||||||
|
&self,
|
||||||
|
payload: T,
|
||||||
|
) -> impl 'static + Future<Output = Result<T::Response>> {
|
||||||
|
log::debug!("ssh request start. name:{}", T::NAME);
|
||||||
|
let response = self.request_dynamic(payload.into_envelope(0, None, None), "");
|
||||||
|
async move {
|
||||||
|
let response = response.await?;
|
||||||
|
log::debug!("ssh request finish. name:{}", T::NAME);
|
||||||
|
T::Response::from_envelope(response)
|
||||||
|
.ok_or_else(|| anyhow!("received a response of the wrong type"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send<T: EnvelopedMessage>(&self, payload: T) -> Result<()> {
|
||||||
|
self.send_dynamic(payload.into_envelope(0, None, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_dynamic(
|
||||||
|
&self,
|
||||||
|
mut envelope: proto::Envelope,
|
||||||
|
_request_type: &'static str,
|
||||||
|
) -> impl 'static + Future<Output = Result<proto::Envelope>> {
|
||||||
|
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.response_channels
|
||||||
|
.lock()
|
||||||
|
.insert(MessageId(envelope.id), tx);
|
||||||
|
self.outgoing_tx.unbounded_send(envelope).ok();
|
||||||
|
async move { Ok(rx.await.context("connection lost")?.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||||
|
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||||
|
self.outgoing_tx.unbounded_send(envelope)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn_process(&self, command: String) -> process::Child {
|
||||||
|
let (process_tx, process_rx) = oneshot::channel();
|
||||||
|
self.spawn_process_tx
|
||||||
|
.unbounded_send(SpawnRequest {
|
||||||
|
command,
|
||||||
|
process_tx,
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
process_rx.await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_message_handler<M, E, H, F>(&self, entity: WeakModel<E>, handler: H)
|
||||||
|
where
|
||||||
|
M: EnvelopedMessage,
|
||||||
|
E: 'static,
|
||||||
|
H: 'static + Sync + Send + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F,
|
||||||
|
F: 'static + Future<Output = Result<()>>,
|
||||||
|
{
|
||||||
|
let message_type_id = TypeId::of::<M>();
|
||||||
|
self.message_handlers.lock().insert(
|
||||||
|
message_type_id,
|
||||||
|
Arc::new(move |envelope, _, cx| {
|
||||||
|
let entity = entity.upgrade()?;
|
||||||
|
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||||
|
Some(handler(entity, *envelope, cx).boxed_local())
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_request_handler<M, E, H, F>(&self, entity: WeakModel<E>, handler: H)
|
||||||
|
where
|
||||||
|
M: EnvelopedMessage + RequestMessage,
|
||||||
|
E: 'static,
|
||||||
|
H: 'static + Sync + Send + Fn(Model<E>, TypedEnvelope<M>, AsyncAppContext) -> F,
|
||||||
|
F: 'static + Future<Output = Result<M::Response>>,
|
||||||
|
{
|
||||||
|
let message_type_id = TypeId::of::<M>();
|
||||||
|
self.message_handlers.lock().insert(
|
||||||
|
message_type_id,
|
||||||
|
Arc::new(move |envelope, this, cx| {
|
||||||
|
let entity = entity.upgrade()?;
|
||||||
|
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
|
||||||
|
let request_id = envelope.message_id();
|
||||||
|
Some(
|
||||||
|
handler(entity, *envelope, cx)
|
||||||
|
.then(move |result| async move {
|
||||||
|
this.outgoing_tx.unbounded_send(result?.into_envelope(
|
||||||
|
this.next_message_id.fetch_add(1, SeqCst),
|
||||||
|
Some(request_id),
|
||||||
|
None,
|
||||||
|
))?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.boxed_local(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtoClient for SshSession {
|
||||||
|
fn request(
|
||||||
|
&self,
|
||||||
|
envelope: proto::Envelope,
|
||||||
|
request_type: &'static str,
|
||||||
|
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
||||||
|
self.request_dynamic(envelope, request_type).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(&self, envelope: proto::Envelope) -> Result<()> {
|
||||||
|
self.send_dynamic(envelope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshClientState {
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
async fn new(
|
||||||
|
user: String,
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
|
cx: &AsyncAppContext,
|
||||||
|
) -> Result<Self> {
|
||||||
|
Err(anyhow!("ssh is not supported on this platform"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn new(
|
||||||
|
user: String,
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
|
cx: &AsyncAppContext,
|
||||||
|
) -> Result<Self> {
|
||||||
|
use smol::fs::unix::PermissionsExt as _;
|
||||||
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
let url = format!("{user}@{host}");
|
||||||
|
let temp_dir = tempfile::Builder::new()
|
||||||
|
.prefix("zed-ssh-session")
|
||||||
|
.tempdir()?;
|
||||||
|
|
||||||
|
// Create a TCP listener to handle requests from the askpass program.
|
||||||
|
let listener = smol::net::TcpListener::bind("127.0.0.1:0")
|
||||||
|
.await
|
||||||
|
.expect("failed to find open port");
|
||||||
|
let askpass_port = listener.local_addr().unwrap().port();
|
||||||
|
let askpass_task = cx.spawn(|mut cx| async move {
|
||||||
|
while let Ok((mut stream, _)) = listener.accept().await {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
if stream.read_to_end(&mut buffer).await.is_err() {
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
let password_prompt = String::from_utf8_lossy(&buffer);
|
||||||
|
if let Some(password) = delegate
|
||||||
|
.ask_password(password_prompt.to_string(), &mut cx)
|
||||||
|
.await
|
||||||
|
.context("failed to get ssh password")
|
||||||
|
.and_then(|p| p)
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
stream.write_all(password.as_bytes()).await.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an askpass script that communicates back to this process using TCP.
|
||||||
|
let askpass_script = format!(
|
||||||
|
"{shebang}\n echo \"$@\" | nc 127.0.0.1 {askpass_port} 2> /dev/null",
|
||||||
|
shebang = "#!/bin/sh"
|
||||||
|
);
|
||||||
|
let askpass_script_path = temp_dir.path().join("askpass.sh");
|
||||||
|
fs::write(&askpass_script_path, askpass_script).await?;
|
||||||
|
fs::set_permissions(&askpass_script_path, std::fs::Permissions::from_mode(0o755)).await?;
|
||||||
|
|
||||||
|
// Start the master SSH process, which does not do anything except for establish
|
||||||
|
// the connection and keep it open, allowing other ssh commands to reuse it
|
||||||
|
// via a control socket.
|
||||||
|
let socket_path = temp_dir.path().join("ssh.sock");
|
||||||
|
let mut master_process = process::Command::new("ssh")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.env("SSH_ASKPASS_REQUIRE", "force")
|
||||||
|
.env("SSH_ASKPASS", &askpass_script_path)
|
||||||
|
.args(["-N", "-o", "ControlMaster=yes", "-o"])
|
||||||
|
.arg(format!("ControlPath={}", socket_path.display()))
|
||||||
|
.args(["-p", &port.to_string()])
|
||||||
|
.arg(&url)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
// Wait for this ssh process to close its stdout, indicating that authentication
|
||||||
|
// has completed.
|
||||||
|
let stdout = master_process.stdout.as_mut().unwrap();
|
||||||
|
let mut output = Vec::new();
|
||||||
|
stdout.read_to_end(&mut output).await?;
|
||||||
|
drop(askpass_task);
|
||||||
|
|
||||||
|
if master_process.try_status()?.is_some() {
|
||||||
|
output.clear();
|
||||||
|
let mut stderr = master_process.stderr.take().unwrap();
|
||||||
|
stderr.read_to_end(&mut output).await?;
|
||||||
|
Err(anyhow!(
|
||||||
|
"failed to connect: {}",
|
||||||
|
String::from_utf8_lossy(&output)
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
_master_process: master_process,
|
||||||
|
port,
|
||||||
|
_temp_dir: temp_dir,
|
||||||
|
socket_path,
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> {
|
||||||
|
let mut command = process::Command::new("scp");
|
||||||
|
let output = self
|
||||||
|
.ssh_options(&mut command)
|
||||||
|
.arg("-P")
|
||||||
|
.arg(&self.port.to_string())
|
||||||
|
.arg(&src_path)
|
||||||
|
.arg(&format!("{}:{}", self.url, dest_path.display()))
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"failed to upload file {} -> {}: {}",
|
||||||
|
src_path.display(),
|
||||||
|
dest_path.display(),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
||||||
|
let mut command = process::Command::new("ssh");
|
||||||
|
self.ssh_options(&mut command)
|
||||||
|
.arg("-p")
|
||||||
|
.arg(&self.port.to_string())
|
||||||
|
.arg(&self.url)
|
||||||
|
.arg(program);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
||||||
|
command
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.args(["-o", "ControlMaster=no", "-o"])
|
||||||
|
.arg(format!("ControlPath={}", self.socket_path.display()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||||
|
let output = command.output().await?;
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"failed to run command: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn query_platform(session: &SshClientState) -> Result<SshPlatform> {
|
||||||
|
let os = run_cmd(session.ssh_command("uname").arg("-s")).await?;
|
||||||
|
let arch = run_cmd(session.ssh_command("uname").arg("-m")).await?;
|
||||||
|
|
||||||
|
let os = match os.trim() {
|
||||||
|
"Darwin" => "macos",
|
||||||
|
"Linux" => "linux",
|
||||||
|
_ => Err(anyhow!("unknown uname os {os:?}"))?,
|
||||||
|
};
|
||||||
|
let arch = if arch.starts_with("arm") || arch.starts_with("aarch64") {
|
||||||
|
"aarch64"
|
||||||
|
} else if arch.starts_with("x86") || arch.starts_with("i686") {
|
||||||
|
"x86_64"
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("unknown uname architecture {arch:?}"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SshPlatform { os, arch })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_server_binary(
|
||||||
|
session: &SshClientState,
|
||||||
|
src_path: &Path,
|
||||||
|
dst_path: &Path,
|
||||||
|
version: SemanticVersion,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut dst_path_gz = dst_path.to_path_buf();
|
||||||
|
dst_path_gz.set_extension("gz");
|
||||||
|
|
||||||
|
if let Some(parent) = dst_path.parent() {
|
||||||
|
run_cmd(session.ssh_command("mkdir").arg("-p").arg(parent)).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut server_binary_exists = false;
|
||||||
|
if let Ok(installed_version) = run_cmd(session.ssh_command(&dst_path).arg("version")).await {
|
||||||
|
if installed_version.trim() == version.to_string() {
|
||||||
|
server_binary_exists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if server_binary_exists {
|
||||||
|
log::info!("remote development server already present",);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let src_stat = fs::metadata(src_path).await?;
|
||||||
|
let size = src_stat.len();
|
||||||
|
let server_mode = 0o755;
|
||||||
|
|
||||||
|
let t0 = Instant::now();
|
||||||
|
log::info!("uploading remote development server ({}kb)", size / 1024);
|
||||||
|
session
|
||||||
|
.upload_file(src_path, &dst_path_gz)
|
||||||
|
.await
|
||||||
|
.context("failed to upload server binary")?;
|
||||||
|
log::info!("uploaded remote development server in {:?}", t0.elapsed());
|
||||||
|
|
||||||
|
log::info!("extracting remote development server");
|
||||||
|
run_cmd(
|
||||||
|
session
|
||||||
|
.ssh_command("gunzip")
|
||||||
|
.arg("--force")
|
||||||
|
.arg(&dst_path_gz),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
log::info!("unzipping remote development server");
|
||||||
|
run_cmd(
|
||||||
|
session
|
||||||
|
.ssh_command("chmod")
|
||||||
|
.arg(format!("{:o}", server_mode))
|
||||||
|
.arg(&dst_path),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
51
crates/remote_server/Cargo.toml
Normal file
51
crates/remote_server/Cargo.toml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
[package]
|
||||||
|
name = "remote_server"
|
||||||
|
description = "Daemon used for remote editing"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.1.0"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/remote_server.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "remote_server"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
test-support = ["fs/test-support"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
env_logger.workspace = true
|
||||||
|
fs.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
project.workspace = true
|
||||||
|
remote.workspace = true
|
||||||
|
rpc.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
worktree.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
client = { workspace = true, features = ["test-support"] }
|
||||||
|
clock = { workspace = true, features = ["test-support"] }
|
||||||
|
fs = { workspace = true, features = ["test-support"] }
|
||||||
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
|
http = { workspace = true, features = ["test-support"] }
|
||||||
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
node_runtime = { workspace = true, features = ["test-support"] }
|
||||||
|
remote = { workspace = true, features = ["test-support"] }
|
||||||
|
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cargo_toml.workspace = true
|
||||||
|
toml.workspace = true
|
1
crates/remote_server/LICENSE-GPL
Symbolic link
1
crates/remote_server/LICENSE-GPL
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../LICENSE-GPL
|
10
crates/remote_server/build.rs
Normal file
10
crates/remote_server/build.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const ZED_MANIFEST: &str = include_str!("../zed/Cargo.toml");
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let zed_cargo_toml: cargo_toml::Manifest =
|
||||||
|
toml::from_str(ZED_MANIFEST).expect("failed to parse zed Cargo.toml");
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=ZED_PKG_VERSION={}",
|
||||||
|
zed_cargo_toml.package.unwrap().version.unwrap()
|
||||||
|
);
|
||||||
|
}
|
166
crates/remote_server/src/headless_project.rs
Normal file
166
crates/remote_server/src/headless_project.rs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
|
||||||
|
use project::{buffer_store::BufferStore, ProjectPath, WorktreeId, WorktreeSettings};
|
||||||
|
use remote::SshSession;
|
||||||
|
use rpc::{
|
||||||
|
proto::{self, AnyProtoClient, PeerId},
|
||||||
|
TypedEnvelope,
|
||||||
|
};
|
||||||
|
use settings::{Settings as _, SettingsStore};
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
|
};
|
||||||
|
use worktree::Worktree;
|
||||||
|
|
||||||
|
const PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
|
||||||
|
const PROJECT_ID: u64 = 0;
|
||||||
|
|
||||||
|
pub struct HeadlessProject {
|
||||||
|
pub fs: Arc<dyn Fs>,
|
||||||
|
pub session: AnyProtoClient,
|
||||||
|
pub worktrees: Vec<Model<Worktree>>,
|
||||||
|
pub buffer_store: Model<BufferStore>,
|
||||||
|
pub next_entry_id: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeadlessProject {
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.set_global(SettingsStore::default());
|
||||||
|
WorktreeSettings::register(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
|
||||||
|
let this = cx.weak_model();
|
||||||
|
|
||||||
|
session.add_request_handler(this.clone(), Self::handle_add_worktree);
|
||||||
|
session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
|
||||||
|
session.add_request_handler(this.clone(), Self::handle_update_buffer);
|
||||||
|
session.add_request_handler(this.clone(), Self::handle_save_buffer);
|
||||||
|
|
||||||
|
HeadlessProject {
|
||||||
|
session: session.into(),
|
||||||
|
fs,
|
||||||
|
worktrees: Vec::new(),
|
||||||
|
buffer_store: cx.new_model(|_| BufferStore::new(true)),
|
||||||
|
next_entry_id: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
|
||||||
|
self.worktrees
|
||||||
|
.iter()
|
||||||
|
.find(|worktree| worktree.read(cx).id() == id)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_add_worktree(
|
||||||
|
this: Model<Self>,
|
||||||
|
message: TypedEnvelope<proto::AddWorktree>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::AddWorktreeResponse> {
|
||||||
|
let worktree = this
|
||||||
|
.update(&mut cx.clone(), |this, _| {
|
||||||
|
Worktree::local(
|
||||||
|
Path::new(&message.payload.path),
|
||||||
|
true,
|
||||||
|
this.fs.clone(),
|
||||||
|
this.next_entry_id.clone(),
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let session = this.session.clone();
|
||||||
|
this.worktrees.push(worktree.clone());
|
||||||
|
worktree.update(cx, |worktree, cx| {
|
||||||
|
worktree.observe_updates(0, cx, move |update| {
|
||||||
|
session.send(update).ok();
|
||||||
|
futures::future::ready(true)
|
||||||
|
});
|
||||||
|
proto::AddWorktreeResponse {
|
||||||
|
worktree_id: worktree.id().to_proto(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_update_buffer(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::UpdateBuffer>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::Ack> {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.buffer_store.update(cx, |buffer_store, cx| {
|
||||||
|
buffer_store.handle_update_buffer(envelope, false, cx)
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_save_buffer(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::SaveBuffer>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::BufferSaved> {
|
||||||
|
let (buffer_store, worktree) = this.update(&mut cx, |this, cx| {
|
||||||
|
let buffer_store = this.buffer_store.clone();
|
||||||
|
let worktree = if let Some(path) = &envelope.payload.new_path {
|
||||||
|
Some(
|
||||||
|
this.worktree_for_id(WorktreeId::from_proto(path.worktree_id), cx)
|
||||||
|
.context("worktree does not exist")?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
anyhow::Ok((buffer_store, worktree))
|
||||||
|
})??;
|
||||||
|
BufferStore::handle_save_buffer(buffer_store, PROJECT_ID, worktree, envelope, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_open_buffer_by_path(
|
||||||
|
this: Model<Self>,
|
||||||
|
message: TypedEnvelope<proto::OpenBufferByPath>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::OpenBufferResponse> {
|
||||||
|
let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
|
||||||
|
let (buffer_store, buffer, session) = this.update(&mut cx, |this, cx| {
|
||||||
|
let worktree = this
|
||||||
|
.worktree_for_id(worktree_id, cx)
|
||||||
|
.context("no such worktree")?;
|
||||||
|
let buffer_store = this.buffer_store.clone();
|
||||||
|
let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
|
||||||
|
buffer_store.open_buffer(
|
||||||
|
ProjectPath {
|
||||||
|
worktree_id,
|
||||||
|
path: PathBuf::from(message.payload.path).into(),
|
||||||
|
},
|
||||||
|
worktree,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
anyhow::Ok((buffer_store, buffer, this.session.clone()))
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let buffer = buffer.await?;
|
||||||
|
let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
BufferStore::create_buffer_for_peer(
|
||||||
|
buffer_store,
|
||||||
|
PEER_ID,
|
||||||
|
buffer_id,
|
||||||
|
PROJECT_ID,
|
||||||
|
session,
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Ok(proto::OpenBufferResponse {
|
||||||
|
buffer_id: buffer_id.to_proto(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
78
crates/remote_server/src/main.rs
Normal file
78
crates/remote_server/src/main.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use fs::RealFs;
|
||||||
|
use futures::channel::mpsc;
|
||||||
|
use gpui::Context as _;
|
||||||
|
use remote::{
|
||||||
|
protocol::{read_message, write_message},
|
||||||
|
SshSession,
|
||||||
|
};
|
||||||
|
use remote_server::HeadlessProject;
|
||||||
|
use smol::{io::AsyncWriteExt, stream::StreamExt as _, Async};
|
||||||
|
use std::{env, io, mem, process, sync::Arc};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
env::set_var("RUST_BACKTRACE", "1");
|
||||||
|
env::set_var("RUST_LOG", "remote=trace");
|
||||||
|
|
||||||
|
let subcommand = std::env::args().nth(1);
|
||||||
|
match subcommand.as_deref() {
|
||||||
|
Some("run") => {}
|
||||||
|
Some("version") => {
|
||||||
|
println!("{}", env!("ZED_PKG_VERSION"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("usage: remote <run|version>");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
gpui::App::headless().run(move |cx| {
|
||||||
|
HeadlessProject::init(cx);
|
||||||
|
|
||||||
|
let (incoming_tx, incoming_rx) = mpsc::unbounded();
|
||||||
|
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded();
|
||||||
|
|
||||||
|
let mut stdin = Async::new(io::stdin()).unwrap();
|
||||||
|
let mut stdout = Async::new(io::stdout()).unwrap();
|
||||||
|
|
||||||
|
let session = SshSession::server(incoming_rx, outgoing_tx, cx);
|
||||||
|
let project = cx.new_model(|cx| {
|
||||||
|
HeadlessProject::new(
|
||||||
|
session.clone(),
|
||||||
|
Arc::new(RealFs::new(Default::default(), None)),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let mut output_buffer = Vec::new();
|
||||||
|
while let Some(message) = outgoing_rx.next().await {
|
||||||
|
write_message(&mut stdout, &mut output_buffer, message).await?;
|
||||||
|
stdout.flush().await?;
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let mut input_buffer = Vec::new();
|
||||||
|
loop {
|
||||||
|
let message = match read_message(&mut stdin, &mut input_buffer).await {
|
||||||
|
Ok(message) => message,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("error reading message: {:?}", error);
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
incoming_tx.unbounded_send(message).ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
mem::forget(project);
|
||||||
|
});
|
||||||
|
}
|
134
crates/remote_server/src/remote_editing_tests.rs
Normal file
134
crates/remote_server/src/remote_editing_tests.rs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
use crate::headless_project::HeadlessProject;
|
||||||
|
use client::{Client, UserStore};
|
||||||
|
use clock::FakeSystemClock;
|
||||||
|
use fs::{FakeFs, Fs as _};
|
||||||
|
use gpui::{Context, Model, TestAppContext};
|
||||||
|
use http::FakeHttpClient;
|
||||||
|
use language::LanguageRegistry;
|
||||||
|
use node_runtime::FakeNodeRuntime;
|
||||||
|
use project::Project;
|
||||||
|
use remote::SshSession;
|
||||||
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||||
|
let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(server_cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/code",
|
||||||
|
json!({
|
||||||
|
"project1": {
|
||||||
|
"README.md": "# project 1",
|
||||||
|
"src": {
|
||||||
|
"lib.rs": "fn one() -> usize { 1 }"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project2": {
|
||||||
|
"README.md": "# project 2",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
server_cx.update(HeadlessProject::init);
|
||||||
|
let _headless_project =
|
||||||
|
server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
|
||||||
|
|
||||||
|
let project = build_project(client_ssh, cx);
|
||||||
|
let (worktree, _) = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_worktree("/code/project1", true, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The client sees the worktree's contents.
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
|
||||||
|
worktree.update(cx, |worktree, _cx| {
|
||||||
|
assert_eq!(
|
||||||
|
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
Path::new("README.md"),
|
||||||
|
Path::new("src"),
|
||||||
|
Path::new("src/lib.rs"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The user opens a buffer in the remote worktree. The buffer's
|
||||||
|
// contents are loaded from the remote filesystem.
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
|
||||||
|
let ix = buffer.text().find('1').unwrap();
|
||||||
|
buffer.edit([(ix..ix + 1, "100")], None, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The user saves the buffer. The new contents are written to the
|
||||||
|
// remote filesystem.
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| project.save_buffer(buffer, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
|
||||||
|
"fn one() -> usize { 100 }"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A new file is created in the remote filesystem. The user
|
||||||
|
// sees the new file.
|
||||||
|
fs.save(
|
||||||
|
"/code/project1/src/main.rs".as_ref(),
|
||||||
|
&"fn main() {}".into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
worktree.update(cx, |worktree, _cx| {
|
||||||
|
assert_eq!(
|
||||||
|
worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
Path::new("README.md"),
|
||||||
|
Path::new("src"),
|
||||||
|
Path::new("src/lib.rs"),
|
||||||
|
Path::new("src/main.rs"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = cx.update(|cx| {
|
||||||
|
Client::new(
|
||||||
|
Arc::new(FakeSystemClock::default()),
|
||||||
|
FakeHttpClient::with_404_response(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let node = FakeNodeRuntime::new();
|
||||||
|
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||||
|
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
cx.update(|cx| {
|
||||||
|
Project::init(&client, cx);
|
||||||
|
language::init(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
|
||||||
|
}
|
6
crates/remote_server/src/remote_server.rs
Normal file
6
crates/remote_server/src/remote_server.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
mod headless_project;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod remote_editing_tests;
|
||||||
|
|
||||||
|
pub use headless_project::HeadlessProject;
|
@ -13,7 +13,6 @@ use futures::{
|
|||||||
mpsc::{self, UnboundedSender},
|
mpsc::{self, UnboundedSender},
|
||||||
oneshot,
|
oneshot,
|
||||||
},
|
},
|
||||||
future::BoxFuture,
|
|
||||||
select_biased,
|
select_biased,
|
||||||
stream::select,
|
stream::select,
|
||||||
task::Poll,
|
task::Poll,
|
||||||
@ -129,13 +128,11 @@ struct ScanRequest {
|
|||||||
|
|
||||||
pub struct RemoteWorktree {
|
pub struct RemoteWorktree {
|
||||||
snapshot: Snapshot,
|
snapshot: Snapshot,
|
||||||
background_snapshot: Arc<Mutex<Snapshot>>,
|
background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>,
|
||||||
project_id: u64,
|
project_id: u64,
|
||||||
client: AnyProtoClient,
|
client: AnyProtoClient,
|
||||||
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
|
updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
|
||||||
update_observer: Arc<
|
update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>,
|
||||||
Mutex<Option<Box<dyn Send + FnMut(proto::UpdateWorktree) -> BoxFuture<'static, bool>>>>,
|
|
||||||
>,
|
|
||||||
snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
|
snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
@ -463,10 +460,9 @@ impl Worktree {
|
|||||||
Arc::from(PathBuf::from(worktree.abs_path)),
|
Arc::from(PathBuf::from(worktree.abs_path)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (updates_tx, mut updates_rx) = mpsc::unbounded();
|
let background_snapshot = Arc::new(Mutex::new((snapshot.clone(), Vec::new())));
|
||||||
let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
|
let (background_updates_tx, mut background_updates_rx) = mpsc::unbounded();
|
||||||
let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
|
let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
|
||||||
let update_observer = Arc::new(Mutex::new(None));
|
|
||||||
|
|
||||||
let worktree = RemoteWorktree {
|
let worktree = RemoteWorktree {
|
||||||
client,
|
client,
|
||||||
@ -474,36 +470,45 @@ impl Worktree {
|
|||||||
replica_id,
|
replica_id,
|
||||||
snapshot,
|
snapshot,
|
||||||
background_snapshot: background_snapshot.clone(),
|
background_snapshot: background_snapshot.clone(),
|
||||||
update_observer: update_observer.clone(),
|
updates_tx: Some(background_updates_tx),
|
||||||
updates_tx: Some(updates_tx),
|
update_observer: None,
|
||||||
snapshot_subscriptions: Default::default(),
|
snapshot_subscriptions: Default::default(),
|
||||||
visible: worktree.visible,
|
visible: worktree.visible,
|
||||||
disconnected: false,
|
disconnected: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Apply updates to a separate snapshto in a background task, then
|
||||||
|
// send them to a foreground task which updates the model.
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
while let Some(update) = updates_rx.next().await {
|
while let Some(update) = background_updates_rx.next().await {
|
||||||
let call = update_observer
|
{
|
||||||
.lock()
|
let mut lock = background_snapshot.lock();
|
||||||
.as_mut()
|
if let Err(error) = lock.0.apply_remote_update(update.clone()) {
|
||||||
.map(|observer| (observer)(update.clone()));
|
log::error!("error applying worktree update: {}", error);
|
||||||
if let Some(call) = call {
|
}
|
||||||
call.await;
|
lock.1.push(update);
|
||||||
}
|
|
||||||
if let Err(error) = background_snapshot.lock().apply_remote_update(update) {
|
|
||||||
log::error!("error applying worktree update: {}", error);
|
|
||||||
}
|
}
|
||||||
snapshot_updated_tx.send(()).await.ok();
|
snapshot_updated_tx.send(()).await.ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
// On the foreground task, update to the latest snapshot and notify
|
||||||
|
// any update observer of all updates that led to that snapshot.
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
while (snapshot_updated_rx.recv().await).is_some() {
|
while (snapshot_updated_rx.recv().await).is_some() {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
let this = this.as_remote_mut().unwrap();
|
let this = this.as_remote_mut().unwrap();
|
||||||
this.snapshot = this.background_snapshot.lock().clone();
|
{
|
||||||
|
let mut lock = this.background_snapshot.lock();
|
||||||
|
this.snapshot = lock.0.clone();
|
||||||
|
if let Some(tx) = &this.update_observer {
|
||||||
|
for update in lock.1.drain(..) {
|
||||||
|
tx.unbounded_send(update).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
cx.emit(Event::UpdatedEntries(Arc::from([])));
|
cx.emit(Event::UpdatedEntries(Arc::from([])));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
|
while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
|
||||||
@ -631,11 +636,7 @@ impl Worktree {
|
|||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
Worktree::Local(this) => this.observe_updates(project_id, cx, callback),
|
Worktree::Local(this) => this.observe_updates(project_id, cx, callback),
|
||||||
Worktree::Remote(this) => {
|
Worktree::Remote(this) => this.observe_updates(project_id, cx, callback),
|
||||||
this.update_observer
|
|
||||||
.lock()
|
|
||||||
.replace(Box::new(move |update| callback(update).boxed()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,7 +646,7 @@ impl Worktree {
|
|||||||
this.update_observer.take();
|
this.update_observer.take();
|
||||||
}
|
}
|
||||||
Worktree::Remote(this) => {
|
Worktree::Remote(this) => {
|
||||||
this.update_observer.lock().take();
|
this.update_observer.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -654,7 +655,7 @@ impl Worktree {
|
|||||||
pub fn has_update_observer(&self) -> bool {
|
pub fn has_update_observer(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Worktree::Local(this) => this.update_observer.is_some(),
|
Worktree::Local(this) => this.update_observer.is_some(),
|
||||||
Worktree::Remote(this) => this.update_observer.lock().is_some(),
|
Worktree::Remote(this) => this.update_observer.is_some(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -739,24 +740,7 @@ impl Worktree {
|
|||||||
) -> Option<Task<Result<()>>> {
|
) -> Option<Task<Result<()>>> {
|
||||||
match self {
|
match self {
|
||||||
Worktree::Local(this) => this.delete_entry(entry_id, trash, cx),
|
Worktree::Local(this) => this.delete_entry(entry_id, trash, cx),
|
||||||
Worktree::Remote(this) => {
|
Worktree::Remote(this) => this.delete_entry(entry_id, trash, cx),
|
||||||
let response = this.client.request(proto::DeleteProjectEntry {
|
|
||||||
project_id: this.project_id,
|
|
||||||
entry_id: entry_id.to_proto(),
|
|
||||||
use_trash: trash,
|
|
||||||
});
|
|
||||||
Some(cx.spawn(move |this, mut cx| async move {
|
|
||||||
let response = response.await?;
|
|
||||||
this.update(&mut cx, move |worktree, cx| {
|
|
||||||
worktree.as_remote_mut().unwrap().delete_entry(
|
|
||||||
entry_id,
|
|
||||||
response.worktree_scan_id as usize,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.await
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -769,36 +753,7 @@ impl Worktree {
|
|||||||
let new_path = new_path.into();
|
let new_path = new_path.into();
|
||||||
match self {
|
match self {
|
||||||
Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
|
Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
|
||||||
Worktree::Remote(this) => {
|
Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx),
|
||||||
let response = this.client.request(proto::RenameProjectEntry {
|
|
||||||
project_id: this.project_id,
|
|
||||||
entry_id: entry_id.to_proto(),
|
|
||||||
new_path: new_path.to_string_lossy().into(),
|
|
||||||
});
|
|
||||||
cx.spawn(move |this, mut cx| async move {
|
|
||||||
let response = response.await?;
|
|
||||||
match response.entry {
|
|
||||||
Some(entry) => this
|
|
||||||
.update(&mut cx, |this, cx| {
|
|
||||||
this.as_remote_mut().unwrap().insert_entry(
|
|
||||||
entry,
|
|
||||||
response.worktree_scan_id as usize,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.await
|
|
||||||
.map(CreatedEntry::Included),
|
|
||||||
None => {
|
|
||||||
let abs_path = this.update(&mut cx, |worktree, _| {
|
|
||||||
worktree
|
|
||||||
.absolutize(&new_path)
|
|
||||||
.with_context(|| format!("absolutizing {new_path:?}"))
|
|
||||||
})??;
|
|
||||||
Ok(CreatedEntry::Excluded { abs_path })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1825,6 +1780,40 @@ impl RemoteWorktree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn observe_updates<F, Fut>(
|
||||||
|
&mut self,
|
||||||
|
project_id: u64,
|
||||||
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
callback: F,
|
||||||
|
) where
|
||||||
|
F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
|
||||||
|
Fut: 'static + Send + Future<Output = bool>,
|
||||||
|
{
|
||||||
|
let (tx, mut rx) = mpsc::unbounded();
|
||||||
|
let initial_update = self
|
||||||
|
.snapshot
|
||||||
|
.build_initial_update(project_id, self.id().to_proto());
|
||||||
|
self.updates_tx = Some(tx);
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let mut update = initial_update;
|
||||||
|
loop {
|
||||||
|
if !callback(update).await {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(next_update) = rx.next().await {
|
||||||
|
update = next_update;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
let this = this.as_remote_mut().unwrap();
|
||||||
|
this.updates_tx.take();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn observed_snapshot(&self, scan_id: usize) -> bool {
|
fn observed_snapshot(&self, scan_id: usize) -> bool {
|
||||||
self.completed_scan_id >= scan_id
|
self.completed_scan_id >= scan_id
|
||||||
}
|
}
|
||||||
@ -1861,7 +1850,7 @@ impl RemoteWorktree {
|
|||||||
wait_for_snapshot.await?;
|
wait_for_snapshot.await?;
|
||||||
this.update(&mut cx, |worktree, _| {
|
this.update(&mut cx, |worktree, _| {
|
||||||
let worktree = worktree.as_remote_mut().unwrap();
|
let worktree = worktree.as_remote_mut().unwrap();
|
||||||
let mut snapshot = worktree.background_snapshot.lock();
|
let snapshot = &mut worktree.background_snapshot.lock().0;
|
||||||
let entry = snapshot.insert_entry(entry);
|
let entry = snapshot.insert_entry(entry);
|
||||||
worktree.snapshot = snapshot.clone();
|
worktree.snapshot = snapshot.clone();
|
||||||
entry
|
entry
|
||||||
@ -1871,20 +1860,67 @@ impl RemoteWorktree {
|
|||||||
|
|
||||||
fn delete_entry(
|
fn delete_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
scan_id: usize,
|
trash: bool,
|
||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
) -> Task<Result<()>> {
|
) -> Option<Task<Result<()>>> {
|
||||||
let wait_for_snapshot = self.wait_for_snapshot(scan_id);
|
let response = self.client.request(proto::DeleteProjectEntry {
|
||||||
|
project_id: self.project_id,
|
||||||
|
entry_id: entry_id.to_proto(),
|
||||||
|
use_trash: trash,
|
||||||
|
});
|
||||||
|
Some(cx.spawn(move |this, mut cx| async move {
|
||||||
|
let response = response.await?;
|
||||||
|
let scan_id = response.worktree_scan_id as usize;
|
||||||
|
|
||||||
|
this.update(&mut cx, move |this, _| {
|
||||||
|
this.as_remote_mut().unwrap().wait_for_snapshot(scan_id)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
let this = this.as_remote_mut().unwrap();
|
||||||
|
let snapshot = &mut this.background_snapshot.lock().0;
|
||||||
|
snapshot.delete_entry(entry_id);
|
||||||
|
this.snapshot = snapshot.clone();
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename_entry(
|
||||||
|
&mut self,
|
||||||
|
entry_id: ProjectEntryId,
|
||||||
|
new_path: impl Into<Arc<Path>>,
|
||||||
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
) -> Task<Result<CreatedEntry>> {
|
||||||
|
let new_path = new_path.into();
|
||||||
|
let response = self.client.request(proto::RenameProjectEntry {
|
||||||
|
project_id: self.project_id,
|
||||||
|
entry_id: entry_id.to_proto(),
|
||||||
|
new_path: new_path.to_string_lossy().into(),
|
||||||
|
});
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
wait_for_snapshot.await?;
|
let response = response.await?;
|
||||||
this.update(&mut cx, |worktree, _| {
|
match response.entry {
|
||||||
let worktree = worktree.as_remote_mut().unwrap();
|
Some(entry) => this
|
||||||
let mut snapshot = worktree.background_snapshot.lock();
|
.update(&mut cx, |this, cx| {
|
||||||
snapshot.delete_entry(id);
|
this.as_remote_mut().unwrap().insert_entry(
|
||||||
worktree.snapshot = snapshot.clone();
|
entry,
|
||||||
})?;
|
response.worktree_scan_id as usize,
|
||||||
Ok(())
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
.map(CreatedEntry::Included),
|
||||||
|
None => {
|
||||||
|
let abs_path = this.update(&mut cx, |worktree, _| {
|
||||||
|
worktree
|
||||||
|
.absolutize(&new_path)
|
||||||
|
.with_context(|| format!("absolutizing {new_path:?}"))
|
||||||
|
})??;
|
||||||
|
Ok(CreatedEntry::Excluded { abs_path })
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1912,6 +1948,35 @@ impl Snapshot {
|
|||||||
&self.abs_path
|
&self.abs_path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
|
||||||
|
let mut updated_entries = self
|
||||||
|
.entries_by_path
|
||||||
|
.iter()
|
||||||
|
.map(proto::Entry::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
updated_entries.sort_unstable_by_key(|e| e.id);
|
||||||
|
|
||||||
|
let mut updated_repositories = self
|
||||||
|
.repository_entries
|
||||||
|
.values()
|
||||||
|
.map(proto::RepositoryEntry::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
|
||||||
|
|
||||||
|
proto::UpdateWorktree {
|
||||||
|
project_id,
|
||||||
|
worktree_id,
|
||||||
|
abs_path: self.abs_path().to_string_lossy().into(),
|
||||||
|
root_name: self.root_name().to_string(),
|
||||||
|
updated_entries,
|
||||||
|
removed_entries: Vec::new(),
|
||||||
|
scan_id: self.scan_id as u64,
|
||||||
|
is_last_update: self.completed_scan_id == self.scan_id,
|
||||||
|
updated_repositories,
|
||||||
|
removed_repositories: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn absolutize(&self, path: &Path) -> Result<PathBuf> {
|
pub fn absolutize(&self, path: &Path) -> Result<PathBuf> {
|
||||||
if path
|
if path
|
||||||
.components()
|
.components()
|
||||||
@ -1978,6 +2043,12 @@ impl Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> {
|
pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> {
|
||||||
|
log::trace!(
|
||||||
|
"applying remote worktree update. {} entries updated, {} removed",
|
||||||
|
update.updated_entries.len(),
|
||||||
|
update.removed_entries.len()
|
||||||
|
);
|
||||||
|
|
||||||
let mut entries_by_path_edits = Vec::new();
|
let mut entries_by_path_edits = Vec::new();
|
||||||
let mut entries_by_id_edits = Vec::new();
|
let mut entries_by_id_edits = Vec::new();
|
||||||
|
|
||||||
@ -2372,35 +2443,6 @@ impl LocalSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
|
|
||||||
let mut updated_entries = self
|
|
||||||
.entries_by_path
|
|
||||||
.iter()
|
|
||||||
.map(proto::Entry::from)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
updated_entries.sort_unstable_by_key(|e| e.id);
|
|
||||||
|
|
||||||
let mut updated_repositories = self
|
|
||||||
.repository_entries
|
|
||||||
.values()
|
|
||||||
.map(proto::RepositoryEntry::from)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
updated_repositories.sort_unstable_by_key(|e| e.work_directory_id);
|
|
||||||
|
|
||||||
proto::UpdateWorktree {
|
|
||||||
project_id,
|
|
||||||
worktree_id,
|
|
||||||
abs_path: self.abs_path().to_string_lossy().into(),
|
|
||||||
root_name: self.root_name().to_string(),
|
|
||||||
updated_entries,
|
|
||||||
removed_entries: Vec::new(),
|
|
||||||
scan_id: self.scan_id as u64,
|
|
||||||
is_last_update: self.completed_scan_id == self.scan_id,
|
|
||||||
updated_repositories,
|
|
||||||
removed_repositories: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
||||||
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||||
let abs_path = self.abs_path.join(&entry.path);
|
let abs_path = self.abs_path.join(&entry.path);
|
||||||
@ -2999,9 +3041,9 @@ impl language::File for File {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto(&self) -> rpc::proto::File {
|
fn to_proto(&self, cx: &AppContext) -> rpc::proto::File {
|
||||||
rpc::proto::File {
|
rpc::proto::File {
|
||||||
worktree_id: self.worktree.entity_id().as_u64(),
|
worktree_id: self.worktree.read(cx).id().to_proto(),
|
||||||
entry_id: self.entry_id.map(|id| id.to_proto()),
|
entry_id: self.entry_id.map(|id| id.to_proto()),
|
||||||
path: self.path.to_string_lossy().into(),
|
path: self.path.to_string_lossy().into(),
|
||||||
mtime: self.mtime.map(|time| time.into()),
|
mtime: self.mtime.map(|time| time.into()),
|
||||||
|
@ -79,6 +79,7 @@ quick_action_bar.workspace = true
|
|||||||
recent_projects.workspace = true
|
recent_projects.workspace = true
|
||||||
dev_server_projects.workspace = true
|
dev_server_projects.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
|
remote.workspace = true
|
||||||
repl.workspace = true
|
repl.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
search.workspace = true
|
search.workspace = true
|
||||||
@ -96,6 +97,7 @@ telemetry_events.workspace = true
|
|||||||
terminal_view.workspace = true
|
terminal_view.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
theme_selector.workspace = true
|
theme_selector.workspace = true
|
||||||
|
url.workspace = true
|
||||||
urlencoding = "2.1.2"
|
urlencoding = "2.1.2"
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
@ -46,7 +46,7 @@ use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
|
|||||||
use workspace::{AppState, WorkspaceSettings, WorkspaceStore};
|
use workspace::{AppState, WorkspaceSettings, WorkspaceStore};
|
||||||
use zed::{
|
use zed::{
|
||||||
app_menus, build_window_options, handle_cli_connection, handle_keymap_file_changes,
|
app_menus, build_window_options, handle_cli_connection, handle_keymap_file_changes,
|
||||||
initialize_workspace, open_paths_with_positions, OpenListener, OpenRequest,
|
initialize_workspace, open_paths_with_positions, open_ssh_paths, OpenListener, OpenRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::zed::inline_completion_registry;
|
use crate::zed::inline_completion_registry;
|
||||||
@ -520,6 +520,21 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(connection_info) = request.ssh_connection {
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
open_ssh_paths(
|
||||||
|
connection_info,
|
||||||
|
request.open_paths,
|
||||||
|
app_state,
|
||||||
|
workspace::OpenOptions::default(),
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut task = None;
|
let mut task = None;
|
||||||
if !request.open_paths.is_empty() {
|
if !request.open_paths.is_empty() {
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
@ -890,7 +905,10 @@ fn parse_url_arg(arg: &str, cx: &AppContext) -> Result<String> {
|
|||||||
match std::fs::canonicalize(Path::new(&arg)) {
|
match std::fs::canonicalize(Path::new(&arg)) {
|
||||||
Ok(path) => Ok(format!("file://{}", path.to_string_lossy())),
|
Ok(path) => Ok(format!("file://{}", path.to_string_lossy())),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if arg.starts_with("file://") || arg.starts_with("zed-cli://") {
|
if arg.starts_with("file://")
|
||||||
|
|| arg.starts_with("zed-cli://")
|
||||||
|
|| arg.starts_with("ssh://")
|
||||||
|
{
|
||||||
Ok(arg.into())
|
Ok(arg.into())
|
||||||
} else if let Some(_) = parse_zed_link(&arg, cx) {
|
} else if let Some(_) = parse_zed_link(&arg, cx) {
|
||||||
Ok(arg.into())
|
Ok(arg.into())
|
||||||
|
@ -5,6 +5,7 @@ pub(crate) mod linux_prompts;
|
|||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
pub(crate) mod only_instance;
|
pub(crate) mod only_instance;
|
||||||
mod open_listener;
|
mod open_listener;
|
||||||
|
mod password_prompt;
|
||||||
|
|
||||||
pub use app_menus::*;
|
pub use app_menus::*;
|
||||||
use breadcrumbs::Breadcrumbs;
|
use breadcrumbs::Breadcrumbs;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
use crate::{handle_open_request, init_headless, init_ui, zed::password_prompt::PasswordPrompt};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use auto_update::AutoUpdater;
|
||||||
use cli::{ipc, IpcHandshake};
|
use cli::{ipc, IpcHandshake};
|
||||||
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
||||||
use client::parse_zed_link;
|
use client::parse_zed_link;
|
||||||
@ -9,8 +11,12 @@ use editor::Editor;
|
|||||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::channel::{mpsc, oneshot};
|
||||||
use futures::{FutureExt, SinkExt, StreamExt};
|
use futures::{FutureExt, SinkExt, StreamExt};
|
||||||
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
|
use gpui::{
|
||||||
|
AppContext, AsyncAppContext, Global, SemanticVersion, VisualContext as _, WindowHandle,
|
||||||
|
};
|
||||||
use language::{Bias, Point};
|
use language::{Bias, Point};
|
||||||
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
|
use remote::SshPlatform;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -22,14 +28,21 @@ use welcome::{show_welcome_view, FIRST_OPEN};
|
|||||||
use workspace::item::ItemHandle;
|
use workspace::item::ItemHandle;
|
||||||
use workspace::{AppState, Workspace};
|
use workspace::{AppState, Workspace};
|
||||||
|
|
||||||
use crate::{handle_open_request, init_headless, init_ui};
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct OpenRequest {
|
pub struct OpenRequest {
|
||||||
pub cli_connection: Option<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)>,
|
pub cli_connection: Option<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)>,
|
||||||
pub open_paths: Vec<PathLikeWithPosition<PathBuf>>,
|
pub open_paths: Vec<PathLikeWithPosition<PathBuf>>,
|
||||||
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
||||||
pub join_channel: Option<u64>,
|
pub join_channel: Option<u64>,
|
||||||
|
pub ssh_connection: Option<SshConnectionInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct SshConnectionInfo {
|
||||||
|
pub username: String,
|
||||||
|
pub password: Option<String>,
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenRequest {
|
impl OpenRequest {
|
||||||
@ -42,6 +55,8 @@ impl OpenRequest {
|
|||||||
this.parse_file_path(file)
|
this.parse_file_path(file)
|
||||||
} else if let Some(file) = url.strip_prefix("zed://file") {
|
} else if let Some(file) = url.strip_prefix("zed://file") {
|
||||||
this.parse_file_path(file)
|
this.parse_file_path(file)
|
||||||
|
} else if url.starts_with("ssh://") {
|
||||||
|
this.parse_ssh_file_path(&url)?
|
||||||
} else if let Some(request_path) = parse_zed_link(&url, cx) {
|
} else if let Some(request_path) = parse_zed_link(&url, cx) {
|
||||||
this.parse_request_path(request_path).log_err();
|
this.parse_request_path(request_path).log_err();
|
||||||
} else {
|
} else {
|
||||||
@ -62,6 +77,37 @@ impl OpenRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_ssh_file_path(&mut self, file: &str) -> Result<()> {
|
||||||
|
let url = url::Url::parse(file)?;
|
||||||
|
let host = url
|
||||||
|
.host()
|
||||||
|
.ok_or_else(|| anyhow!("missing host in ssh url: {}", file))?
|
||||||
|
.to_string();
|
||||||
|
let username = url.username().to_string();
|
||||||
|
if username.is_empty() {
|
||||||
|
return Err(anyhow!("missing username in ssh url: {}", file));
|
||||||
|
}
|
||||||
|
let password = url.password().map(|s| s.to_string());
|
||||||
|
let port = url.port().unwrap_or(22);
|
||||||
|
if !self.open_paths.is_empty() {
|
||||||
|
return Err(anyhow!("cannot open both local and ssh paths"));
|
||||||
|
}
|
||||||
|
let connection = SshConnectionInfo {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
if let Some(ssh_connection) = &self.ssh_connection {
|
||||||
|
if *ssh_connection != connection {
|
||||||
|
return Err(anyhow!("cannot open multiple ssh connections"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.ssh_connection = Some(connection);
|
||||||
|
self.parse_file_path(url.path());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_request_path(&mut self, request_path: &str) -> Result<()> {
|
fn parse_request_path(&mut self, request_path: &str) -> Result<()> {
|
||||||
let mut parts = request_path.split('/');
|
let mut parts = request_path.split('/');
|
||||||
if parts.next() == Some("channel") {
|
if parts.next() == Some("channel") {
|
||||||
@ -109,6 +155,95 @@ impl OpenListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SshClientDelegate {
|
||||||
|
window: WindowHandle<Workspace>,
|
||||||
|
known_password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl remote::SshClientDelegate for SshClientDelegate {
|
||||||
|
fn ask_password(
|
||||||
|
&self,
|
||||||
|
prompt: String,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> oneshot::Receiver<Result<String>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let mut known_password = self.known_password.clone();
|
||||||
|
self.window
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
cx.activate_window();
|
||||||
|
if let Some(password) = known_password.take() {
|
||||||
|
tx.send(Ok(password)).ok();
|
||||||
|
} else {
|
||||||
|
workspace.toggle_modal(cx, |cx| PasswordPrompt::new(prompt, tx, cx));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_server_binary(
|
||||||
|
&self,
|
||||||
|
platform: SshPlatform,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
tx.send(get_server_binary(platform, &mut cx).await).ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result<PathBuf> {
|
||||||
|
let release_channel = cx.update(|cx| ReleaseChannel::global(cx))?;
|
||||||
|
Ok(format!(".local/zed-remote-server-{}", release_channel.dev_name()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_server_binary(
|
||||||
|
platform: SshPlatform,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<(PathBuf, SemanticVersion)> {
|
||||||
|
let (version, release_channel) =
|
||||||
|
cx.update(|cx| (AppVersion::global(cx), ReleaseChannel::global(cx)))?;
|
||||||
|
|
||||||
|
// In dev mode, build the remote server binary from source
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if crate::stdout_is_a_pty()
|
||||||
|
&& release_channel == ReleaseChannel::Dev
|
||||||
|
&& platform.arch == std::env::consts::ARCH
|
||||||
|
&& platform.os == std::env::consts::OS
|
||||||
|
{
|
||||||
|
use smol::process::{Command, Stdio};
|
||||||
|
|
||||||
|
log::info!("building remote server binary from source");
|
||||||
|
run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?;
|
||||||
|
run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?;
|
||||||
|
run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?;
|
||||||
|
|
||||||
|
let path = std::env::current_dir()?.join("target/debug/remote_server.gz");
|
||||||
|
return Ok((path, version));
|
||||||
|
|
||||||
|
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||||
|
let output = command.stderr(Stdio::inherit()).output().await?;
|
||||||
|
if !output.status.success() {
|
||||||
|
Err(anyhow!("failed to run command: {:?}", command))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let binary_path = AutoUpdater::get_latest_remote_server_release(
|
||||||
|
platform.os,
|
||||||
|
platform.arch,
|
||||||
|
release_channel,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((binary_path, version))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> {
|
pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> {
|
||||||
use release_channel::RELEASE_CHANNEL_NAME;
|
use release_channel::RELEASE_CHANNEL_NAME;
|
||||||
@ -160,6 +295,72 @@ fn connect_to_cli(
|
|||||||
Ok((async_request_rx, response_tx))
|
Ok((async_request_rx, response_tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn open_ssh_paths(
|
||||||
|
connection_info: SshConnectionInfo,
|
||||||
|
paths: Vec<PathLikeWithPosition<PathBuf>>,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
_open_options: workspace::OpenOptions,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let options = cx.update(|cx| (app_state.build_window_options)(None, cx))?;
|
||||||
|
let window = cx.open_window(options, |cx| {
|
||||||
|
let project = project::Project::local(
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.node_runtime.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let session = remote::SshSession::client(
|
||||||
|
connection_info.username,
|
||||||
|
connection_info.host,
|
||||||
|
connection_info.port,
|
||||||
|
Arc::new(SshClientDelegate {
|
||||||
|
window,
|
||||||
|
known_password: connection_info.password,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if session.is_err() {
|
||||||
|
window.update(cx, |_, cx| cx.remove_window()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = session?;
|
||||||
|
|
||||||
|
let project = cx.update(|cx| {
|
||||||
|
project::Project::ssh(
|
||||||
|
session,
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.node_runtime.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_worktree(&path.path_like, true, cx)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.update(cx, |_, cx| {
|
||||||
|
cx.replace_root_view(|cx| Workspace::new(None, project, app_state, cx))
|
||||||
|
})?;
|
||||||
|
window.update(cx, |_, cx| cx.activate_window())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn open_paths_with_positions(
|
pub async fn open_paths_with_positions(
|
||||||
path_likes: &Vec<PathLikeWithPosition<PathBuf>>,
|
path_likes: &Vec<PathLikeWithPosition<PathBuf>>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
69
crates/zed/src/zed/password_prompt.rs
Normal file
69
crates/zed/src/zed/password_prompt.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use editor::Editor;
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use gpui::{
|
||||||
|
px, DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SharedString, View,
|
||||||
|
};
|
||||||
|
use ui::{v_flex, InteractiveElement, Label, Styled, StyledExt as _, ViewContext, VisualContext};
|
||||||
|
use workspace::ModalView;
|
||||||
|
|
||||||
|
pub struct PasswordPrompt {
|
||||||
|
prompt: SharedString,
|
||||||
|
tx: Option<oneshot::Sender<Result<String>>>,
|
||||||
|
editor: View<Editor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PasswordPrompt {
|
||||||
|
pub fn new(
|
||||||
|
prompt: String,
|
||||||
|
tx: oneshot::Sender<Result<String>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
prompt: SharedString::from(prompt),
|
||||||
|
tx: Some(tx),
|
||||||
|
editor: cx.new_view(|cx| {
|
||||||
|
let mut editor = Editor::single_line(cx);
|
||||||
|
editor.set_redact_all(true, cx);
|
||||||
|
editor
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
let text = self.editor.read(cx).text(cx);
|
||||||
|
if let Some(tx) = self.tx.take() {
|
||||||
|
tx.send(Ok(text)).ok();
|
||||||
|
};
|
||||||
|
cx.emit(DismissEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(DismissEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for PasswordPrompt {
|
||||||
|
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.key_context("PasswordPrompt")
|
||||||
|
.elevation_3(cx)
|
||||||
|
.p_4()
|
||||||
|
.gap_2()
|
||||||
|
.on_action(cx.listener(Self::dismiss))
|
||||||
|
.on_action(cx.listener(Self::confirm))
|
||||||
|
.w(px(400.))
|
||||||
|
.child(Label::new(self.prompt.clone()))
|
||||||
|
.child(self.editor.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusableView for PasswordPrompt {
|
||||||
|
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||||
|
self.editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for PasswordPrompt {}
|
||||||
|
|
||||||
|
impl ModalView for PasswordPrompt {}
|
@ -43,12 +43,13 @@ script/generate-licenses
|
|||||||
|
|
||||||
# Build binary in release mode
|
# Build binary in release mode
|
||||||
export RUSTFLAGS="-C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib"
|
export RUSTFLAGS="-C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib"
|
||||||
cargo build --release --target "${target_triple}" --package zed --package cli
|
cargo build --release --target "${target_triple}" --package zed --package cli --package remote_server
|
||||||
|
|
||||||
# Strip the binary of all debug symbols
|
# Strip the binary of all debug symbols
|
||||||
# Later, we probably want to do something like this: https://github.com/GabrielMajeri/separate-symbols
|
# Later, we probably want to do something like this: https://github.com/GabrielMajeri/separate-symbols
|
||||||
strip --strip-debug "${target_dir}/${target_triple}/release/zed"
|
strip --strip-debug "${target_dir}/${target_triple}/release/zed"
|
||||||
strip --strip-debug "${target_dir}/${target_triple}/release/cli"
|
strip --strip-debug "${target_dir}/${target_triple}/release/cli"
|
||||||
|
strip --strip-debug "${target_dir}/${target_triple}/release/remote_server"
|
||||||
|
|
||||||
suffix=""
|
suffix=""
|
||||||
if [ "$channel" != "stable" ]; then
|
if [ "$channel" != "stable" ]; then
|
||||||
@ -104,7 +105,8 @@ envsubst < "crates/zed/resources/zed.desktop.in" > "${zed_dir}/share/application
|
|||||||
cp "assets/licenses.md" "${zed_dir}/licenses.md"
|
cp "assets/licenses.md" "${zed_dir}/licenses.md"
|
||||||
|
|
||||||
# Create archive out of everything that's in the temp directory
|
# Create archive out of everything that's in the temp directory
|
||||||
target="linux-$(uname -m)"
|
arch=$(uname -m)
|
||||||
|
target="linux-${arch}"
|
||||||
if [[ "$channel" == "dev" ]]; then
|
if [[ "$channel" == "dev" ]]; then
|
||||||
archive="zed-${commit}-${target}.tar.gz"
|
archive="zed-${commit}-${target}.tar.gz"
|
||||||
else
|
else
|
||||||
@ -115,3 +117,5 @@ rm -rf "${archive}"
|
|||||||
remove_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz"
|
remove_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz"
|
||||||
ls "${target_dir}/release" | grep -E ${remove_match} | xargs -d "\n" -I {} rm -f "${target_dir}/release/{}" || true
|
ls "${target_dir}/release" | grep -E ${remove_match} | xargs -d "\n" -I {} rm -f "${target_dir}/release/{}" || true
|
||||||
tar -czvf "${target_dir}/release/$archive" -C ${temp_dir} "zed$suffix.app"
|
tar -czvf "${target_dir}/release/$archive" -C ${temp_dir} "zed$suffix.app"
|
||||||
|
|
||||||
|
gzip --stdout --best "${target_dir}/${target_triple}/release/remote_server" > "${target_dir}/zed-remote-server-linux-${arch}.gz"
|
||||||
|
@ -10,8 +10,10 @@ local_arch=false
|
|||||||
local_only=false
|
local_only=false
|
||||||
local_install=false
|
local_install=false
|
||||||
bundle_name=""
|
bundle_name=""
|
||||||
|
can_code_sign=false
|
||||||
|
|
||||||
# This must match the team in the provisioning profile.
|
# This must match the team in the provisioning profile.
|
||||||
|
IDENTITY="Zed Industries, Inc."
|
||||||
APPLE_NOTORIZATION_TEAM="MQ55VZLNZQ"
|
APPLE_NOTORIZATION_TEAM="MQ55VZLNZQ"
|
||||||
|
|
||||||
# Function for displaying help info
|
# Function for displaying help info
|
||||||
@ -78,10 +80,10 @@ local_target_triple=${host_line#*: }
|
|||||||
|
|
||||||
if [ "$local_arch" = true ]; then
|
if [ "$local_arch" = true ]; then
|
||||||
echo "Building for local target only."
|
echo "Building for local target only."
|
||||||
cargo build ${build_flag} --package zed --package cli
|
cargo build ${build_flag} --package zed --package cli --package remote_server
|
||||||
else
|
else
|
||||||
echo "Compiling zed binaries"
|
echo "Compiling zed binaries"
|
||||||
cargo build ${build_flag} --package zed --package cli --target aarch64-apple-darwin --target x86_64-apple-darwin
|
cargo build ${build_flag} --package zed --package cli --package remote_server --target aarch64-apple-darwin --target x86_64-apple-darwin
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Creating application bundle"
|
echo "Creating application bundle"
|
||||||
@ -108,6 +110,27 @@ mv Cargo.toml.backup Cargo.toml
|
|||||||
popd
|
popd
|
||||||
echo "Bundled ${app_path}"
|
echo "Bundled ${app_path}"
|
||||||
|
|
||||||
|
if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_USERNAME:-}" && -n "${APPLE_NOTARIZATION_PASSWORD:-}" ]]; then
|
||||||
|
can_code_sign=true
|
||||||
|
|
||||||
|
echo "Setting up keychain for code signing..."
|
||||||
|
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
|
||||||
|
security default-keychain -s zed.keychain
|
||||||
|
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
||||||
|
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/zed-certificate.p12
|
||||||
|
security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||||
|
rm /tmp/zed-certificate.p12
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
echo "Cleaning up keychain"
|
||||||
|
security default-keychain -s login.keychain
|
||||||
|
security delete-keychain zed.keychain
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
GIT_VERSION="v2.43.3"
|
GIT_VERSION="v2.43.3"
|
||||||
GIT_VERSION_SHA="fa29823"
|
GIT_VERSION_SHA="fa29823"
|
||||||
|
|
||||||
@ -165,7 +188,7 @@ function prepare_binaries() {
|
|||||||
local architecture=$1
|
local architecture=$1
|
||||||
local app_path=$2
|
local app_path=$2
|
||||||
|
|
||||||
echo "Unpacking dSYMs for $architecture"
|
echo "Unpacking dSYMs for $ architecture"
|
||||||
dsymutil --flat target/${architecture}/${target_dir}/Zed
|
dsymutil --flat target/${architecture}/${target_dir}/Zed
|
||||||
version="$(cargo metadata --no-deps --manifest-path crates/zed/Cargo.toml --offline --format-version=1 | jq -r '.packages | map(select(.name == "zed"))[0].version')"
|
version="$(cargo metadata --no-deps --manifest-path crates/zed/Cargo.toml --offline --format-version=1 | jq -r '.packages | map(select(.name == "zed"))[0].version')"
|
||||||
if [ "$channel" == "nightly" ]; then
|
if [ "$channel" == "nightly" ]; then
|
||||||
@ -188,7 +211,7 @@ function prepare_binaries() {
|
|||||||
cp target/${architecture}/${target_dir}/cli "${app_path}/Contents/MacOS/cli"
|
cp target/${architecture}/${target_dir}/cli "${app_path}/Contents/MacOS/cli"
|
||||||
}
|
}
|
||||||
|
|
||||||
function sign_binaries() {
|
function sign_app_binaries() {
|
||||||
local app_path=$1
|
local app_path=$1
|
||||||
local architecture=$2
|
local architecture=$2
|
||||||
local architecture_dir=$3
|
local architecture_dir=$3
|
||||||
@ -207,24 +230,14 @@ function sign_binaries() {
|
|||||||
# Note: The app identifier for our development builds is the same as the app identifier for nightly.
|
# Note: The app identifier for our development builds is the same as the app identifier for nightly.
|
||||||
cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
|
cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
|
||||||
|
|
||||||
if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_USERNAME:-}" && -n "${APPLE_NOTARIZATION_PASSWORD:-}" ]]; then
|
if [[ $can_code_sign = true ]]; then
|
||||||
echo "Signing bundle with Apple-issued certificate"
|
echo "Code signing binaries"
|
||||||
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
|
|
||||||
security default-keychain -s zed.keychain
|
|
||||||
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
|
||||||
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/zed-certificate.p12
|
|
||||||
security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
|
||||||
rm /tmp/zed-certificate.p12
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
|
||||||
|
|
||||||
# sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514
|
# sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514
|
||||||
/usr/bin/codesign --deep --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks/WebRTC.framework" -v
|
/usr/bin/codesign --deep --force --timestamp --sign "$IDENTITY" "${app_path}/Contents/Frameworks/WebRTC.framework" -v
|
||||||
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v
|
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "${app_path}/Contents/MacOS/cli" -v
|
||||||
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/git" -v
|
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "${app_path}/Contents/MacOS/git" -v
|
||||||
/usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/zed" -v
|
/usr/bin/codesign --deep --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "$IDENTITY" "${app_path}/Contents/MacOS/zed" -v
|
||||||
/usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
|
/usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "$IDENTITY" "${app_path}" -v
|
||||||
|
|
||||||
security default-keychain -s login.keychain
|
|
||||||
else
|
else
|
||||||
echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD"
|
echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD"
|
||||||
if [[ "$local_only" = false ]]; then
|
if [[ "$local_only" = false ]]; then
|
||||||
@ -291,20 +304,13 @@ function sign_binaries() {
|
|||||||
mkdir -p ${dmg_source_directory}
|
mkdir -p ${dmg_source_directory}
|
||||||
mv "${app_path}" "${dmg_source_directory}"
|
mv "${app_path}" "${dmg_source_directory}"
|
||||||
|
|
||||||
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
|
if [[ $can_code_sign = true ]]; then
|
||||||
echo "Creating temporary DMG at ${dmg_file_path} using ${dmg_source_directory} to notarize app bundle"
|
echo "Creating temporary DMG at ${dmg_file_path} using ${dmg_source_directory} to notarize app bundle"
|
||||||
hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}"
|
hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}"
|
||||||
|
|
||||||
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
|
echo "Code-signing DMG"
|
||||||
security default-keychain -s zed.keychain
|
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
|
||||||
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
|
||||||
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/zed-certificate.p12
|
|
||||||
security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
|
||||||
rm /tmp/zed-certificate.p12
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
|
||||||
|
|
||||||
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "$(pwd)/${dmg_file_path}" -v
|
|
||||||
security default-keychain -s login.keychain
|
|
||||||
echo "Notarizing DMG with Apple"
|
echo "Notarizing DMG with Apple"
|
||||||
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
|
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
|
||||||
|
|
||||||
@ -330,17 +336,9 @@ function sign_binaries() {
|
|||||||
npm install --global dmg-license minimist
|
npm install --global dmg-license minimist
|
||||||
dmg-license script/eula/eula.json "${dmg_file_path}"
|
dmg-license script/eula/eula.json "${dmg_file_path}"
|
||||||
|
|
||||||
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
|
if [[ $can_code_sign = true ]]; then
|
||||||
echo "Notarizing DMG with Apple"
|
echo "Notarizing DMG with Apple"
|
||||||
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
|
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "$(pwd)/${dmg_file_path}" -v
|
||||||
security default-keychain -s zed.keychain
|
|
||||||
security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
|
||||||
echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/zed-certificate.p12
|
|
||||||
security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
|
||||||
rm /tmp/zed-certificate.p12
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
|
||||||
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "Zed Industries, Inc." "$(pwd)/${dmg_file_path}" -v
|
|
||||||
security default-keychain -s login.keychain
|
|
||||||
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
|
"${xcode_bin_dir_path}/notarytool" submit --wait --apple-id "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" --team-id "$APPLE_NOTORIZATION_TEAM" "${dmg_file_path}"
|
||||||
"${xcode_bin_dir_path}/stapler" staple "${dmg_file_path}"
|
"${xcode_bin_dir_path}/stapler" staple "${dmg_file_path}"
|
||||||
fi
|
fi
|
||||||
@ -351,8 +349,19 @@ function sign_binaries() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sign_binary() {
|
||||||
|
local binary_path=$1
|
||||||
|
|
||||||
|
if [[ $can_code_sign = true ]]; then
|
||||||
|
echo "Code signing executable $binary_path"
|
||||||
|
/usr/bin/codesign --deep --force --timestamp --options runtime --sign "$IDENTITY" "${app_path}" -v
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$local_arch" = true ]; then
|
if [ "$local_arch" = true ]; then
|
||||||
sign_binaries "$app_path" "$local_target_triple" "$local_target_triple"
|
sign_app_binaries "$app_path" "$local_target_triple" "$local_target_triple"
|
||||||
|
|
||||||
|
sign_binary "target/release/remote_server"
|
||||||
else
|
else
|
||||||
# Create universal binary
|
# Create universal binary
|
||||||
prepare_binaries "aarch64-apple-darwin" "$app_path_aarch64"
|
prepare_binaries "aarch64-apple-darwin" "$app_path_aarch64"
|
||||||
@ -370,8 +379,13 @@ else
|
|||||||
target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \
|
target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \
|
||||||
-output \
|
-output \
|
||||||
"${app_path}/Contents/MacOS/cli"
|
"${app_path}/Contents/MacOS/cli"
|
||||||
sign_binaries "$app_path" "universal" "."
|
|
||||||
|
|
||||||
sign_binaries "$app_path_x64" "x86_64-apple-darwin" "x86_64-apple-darwin"
|
sign_app_binaries "$app_path" "universal" "."
|
||||||
sign_binaries "$app_path_aarch64" "aarch64-apple-darwin" "aarch64-apple-darwin"
|
sign_app_binaries "$app_path_x64" "x86_64-apple-darwin" "x86_64-apple-darwin"
|
||||||
|
sign_app_binaries "$app_path_aarch64" "aarch64-apple-darwin" "aarch64-apple-darwin"
|
||||||
|
|
||||||
|
sign_binary "target/x86_64-apple-darwin/release/remote_server"
|
||||||
|
sign_binary "target/aarch64-apple-darwin/release/remote_server"
|
||||||
|
gzip --stdout --best target/x86_64-apple-darwin/release/remote_server > target/zed-remote-server-mac-x86_64.gz
|
||||||
|
gzip --stdout --best target/aarch64-apple-darwin/release/remote_server > target/zed-remote-server-mac-aarch64.gz
|
||||||
fi
|
fi
|
||||||
|
@ -33,6 +33,12 @@ bucket_name="zed-nightly-host"
|
|||||||
|
|
||||||
sha=$(git rev-parse HEAD)
|
sha=$(git rev-parse HEAD)
|
||||||
echo ${sha} > target/latest-sha
|
echo ${sha} > target/latest-sha
|
||||||
|
|
||||||
|
find target -type f -name "zed-remote-server-*.gz" -print0 | while IFS= read -r -d '' file_to_upload; do
|
||||||
|
upload_to_blob_store $bucket_name "$file_to_upload" "nightly/$(basename "$file_to_upload")"
|
||||||
|
rm -f "$file_to_upload"
|
||||||
|
done
|
||||||
|
|
||||||
case "$target" in
|
case "$target" in
|
||||||
macos)
|
macos)
|
||||||
upload_to_blob_store $bucket_name "target/aarch64-apple-darwin/release/Zed.dmg" "nightly/Zed-aarch64.dmg"
|
upload_to_blob_store $bucket_name "target/aarch64-apple-darwin/release/Zed.dmg" "nightly/Zed-aarch64.dmg"
|
||||||
@ -43,9 +49,9 @@ case "$target" in
|
|||||||
rm -f "target/latest-sha"
|
rm -f "target/latest-sha"
|
||||||
;;
|
;;
|
||||||
linux-targz)
|
linux-targz)
|
||||||
find . -type f -name "zed-*.tar.gz" -print0 | while IFS= read -r -d '' bundle_file; do
|
find . -type f -name "zed-*.tar.gz" -print0 | while IFS= read -r -d '' file_to_upload; do
|
||||||
upload_to_blob_store $bucket_name "$bundle_file" "nightly/$(basename "$bundle_file")"
|
upload_to_blob_store $bucket_name "$file_to_upload" "nightly/$(basename "$file_to_upload")"
|
||||||
rm -f "$bundle_file"
|
rm -f "$file_to_upload"
|
||||||
done
|
done
|
||||||
upload_to_blob_store $bucket_name "target/latest-sha" "nightly/latest-sha-linux-targz"
|
upload_to_blob_store $bucket_name "target/latest-sha" "nightly/latest-sha-linux-targz"
|
||||||
rm -f "target/latest-sha"
|
rm -f "target/latest-sha"
|
||||||
|
Loading…
Reference in New Issue
Block a user