mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Release Notes: - The `zed: open performance profiler` action can now display profiling data from the remote server.
1124 lines
37 KiB
Rust
1124 lines
37 KiB
Rust
use crate::TestServer;
|
|
use call::ActiveCall;
|
|
use collections::{HashMap, HashSet};
|
|
|
|
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
|
|
use debugger_ui::debugger_panel::DebugPanel;
|
|
use editor::{Editor, EditorMode, MultiBuffer};
|
|
use extension::ExtensionHostProxy;
|
|
use fs::{FakeFs, Fs as _, RemoveOptions};
|
|
use futures::StreamExt as _;
|
|
use gpui::{
|
|
AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _,
|
|
};
|
|
use http_client::BlockedHttpClient;
|
|
use language::{
|
|
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
|
language_settings::{Formatter, FormatterList, language_settings},
|
|
rust_lang, tree_sitter_typescript,
|
|
};
|
|
use node_runtime::NodeRuntime;
|
|
use project::{
|
|
ProjectPath,
|
|
debugger::session::ThreadId,
|
|
lsp_store::{FormatTrigger, LspFormatTarget},
|
|
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
|
};
|
|
use remote::RemoteClient;
|
|
use remote_server::{HeadlessAppState, HeadlessProject};
|
|
use rpc::proto;
|
|
use serde_json::json;
|
|
use settings::{
|
|
InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
|
|
SettingsStore,
|
|
};
|
|
use std::{
|
|
path::Path,
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicUsize, Ordering},
|
|
},
|
|
time::Duration,
|
|
};
|
|
use task::TcpArgumentsTemplate;
|
|
use util::{path, rel_path::rel_path};
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
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();
|
|
cx_a.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
});
|
|
server_cx.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
});
|
|
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 (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
|
|
let remote_fs = FakeFs::new(server_cx.executor());
|
|
remote_fs
|
|
.insert_tree(
|
|
path!("/code"),
|
|
json!({
|
|
"project1": {
|
|
".zed": {
|
|
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
|
},
|
|
"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 remote_http_client = Arc::new(BlockedHttpClient);
|
|
let node = NodeRuntime::unavailable();
|
|
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
|
let _headless_project = server_cx.new(|cx| {
|
|
HeadlessProject::new(
|
|
HeadlessAppState {
|
|
session: server_ssh,
|
|
fs: remote_fs.clone(),
|
|
http_client: remote_http_client,
|
|
node_runtime: node,
|
|
languages,
|
|
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
|
startup_time: std::time::Instant::now(),
|
|
},
|
|
false,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
|
|
let (project_a, worktree_id) = client_a
|
|
.build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
|
|
.await;
|
|
|
|
// While the SSH worktree is being scanned, 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.join_remote_project(project_id, cx_b).await;
|
|
let worktree_b = project_b
|
|
.update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
|
|
.unwrap();
|
|
|
|
let worktree_a = project_a
|
|
.update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
|
|
.unwrap();
|
|
|
|
executor.run_until_parked();
|
|
|
|
worktree_a.update(cx_a, |worktree, _cx| {
|
|
assert_eq!(
|
|
worktree.paths().collect::<Vec<_>>(),
|
|
vec![
|
|
rel_path(".zed"),
|
|
rel_path(".zed/settings.json"),
|
|
rel_path("README.md"),
|
|
rel_path("src"),
|
|
rel_path("src/lib.rs"),
|
|
]
|
|
);
|
|
});
|
|
|
|
worktree_b.update(cx_b, |worktree, _cx| {
|
|
assert_eq!(
|
|
worktree.paths().collect::<Vec<_>>(),
|
|
vec![
|
|
rel_path(".zed"),
|
|
rel_path(".zed/settings.json"),
|
|
rel_path("README.md"),
|
|
rel_path("src"),
|
|
rel_path("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, rel_path("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);
|
|
});
|
|
|
|
executor.run_until_parked();
|
|
|
|
cx_b.read(|cx| {
|
|
let file = buffer_b.read(cx).file();
|
|
assert_eq!(
|
|
language_settings(Some("Rust".into()), file, cx).language_servers,
|
|
["override-rust-analyzer".to_string()]
|
|
)
|
|
});
|
|
|
|
project_b
|
|
.update(cx_b, |project, cx| {
|
|
project.save_buffer_as(
|
|
buffer_b.clone(),
|
|
ProjectPath {
|
|
worktree_id: worktree_id.to_owned(),
|
|
path: rel_path("src/renamed.rs").into(),
|
|
},
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
remote_fs
|
|
.load(path!("/code/project1/src/renamed.rs").as_ref())
|
|
.await
|
|
.unwrap(),
|
|
"fn one() -> usize { 100 }"
|
|
);
|
|
cx_b.run_until_parked();
|
|
cx_b.update(|cx| {
|
|
assert_eq!(
|
|
buffer_b.read(cx).file().unwrap().path().as_ref(),
|
|
rel_path("src/renamed.rs")
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_ssh_collaboration_git_branches(
|
|
executor: BackgroundExecutor,
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
) {
|
|
cx_a.set_name("a");
|
|
cx_b.set_name("b");
|
|
server_cx.set_name("server");
|
|
|
|
cx_a.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
});
|
|
server_cx.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
});
|
|
|
|
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 (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
|
|
let remote_fs = FakeFs::new(server_cx.executor());
|
|
remote_fs
|
|
.insert_tree("/project", serde_json::json!({ ".git":{} }))
|
|
.await;
|
|
|
|
let branches = ["main", "dev", "feature-1"];
|
|
let branches_set = branches
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect::<HashSet<_>>();
|
|
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
|
|
|
|
// User A connects to the remote project via SSH.
|
|
server_cx.update(HeadlessProject::init);
|
|
let remote_http_client = Arc::new(BlockedHttpClient);
|
|
let node = NodeRuntime::unavailable();
|
|
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
|
let headless_project = server_cx.new(|cx| {
|
|
HeadlessProject::new(
|
|
HeadlessAppState {
|
|
session: server_ssh,
|
|
fs: remote_fs.clone(),
|
|
http_client: remote_http_client,
|
|
node_runtime: node,
|
|
languages,
|
|
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
|
startup_time: std::time::Instant::now(),
|
|
},
|
|
false,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
|
|
let (project_a, _) = client_a
|
|
.build_ssh_project("/project", client_ssh, false, cx_a)
|
|
.await;
|
|
|
|
// While the SSH worktree is being scanned, 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.join_remote_project(project_id, cx_b).await;
|
|
|
|
// Give client A sometime to see that B has joined, and that the headless server
|
|
// has some git repositories
|
|
executor.run_until_parked();
|
|
|
|
let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
|
|
|
|
let branches_b = cx_b
|
|
.update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
let new_branch = branches[2];
|
|
|
|
let branches_b = branches_b
|
|
.into_iter()
|
|
.map(|branch| branch.name().to_string())
|
|
.collect::<HashSet<_>>();
|
|
|
|
assert_eq!(&branches_b, &branches_set);
|
|
|
|
cx_b.update(|cx| {
|
|
repo_b.update(cx, |repo_b, _cx| {
|
|
repo_b.change_branch(new_branch.to_string())
|
|
})
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
executor.run_until_parked();
|
|
|
|
let server_branch = server_cx.update(|cx| {
|
|
headless_project.update(cx, |headless_project, cx| {
|
|
headless_project.git_store.update(cx, |git_store, cx| {
|
|
git_store
|
|
.repositories()
|
|
.values()
|
|
.next()
|
|
.unwrap()
|
|
.read(cx)
|
|
.branch
|
|
.as_ref()
|
|
.unwrap()
|
|
.clone()
|
|
})
|
|
})
|
|
});
|
|
|
|
assert_eq!(server_branch.name(), branches[2]);
|
|
|
|
// Also try creating a new branch
|
|
cx_b.update(|cx| {
|
|
repo_b.update(cx, |repo_b, _cx| {
|
|
repo_b.create_branch("totally-new-branch".to_string(), None)
|
|
})
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
cx_b.update(|cx| {
|
|
repo_b.update(cx, |repo_b, _cx| {
|
|
repo_b.change_branch("totally-new-branch".to_string())
|
|
})
|
|
})
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
executor.run_until_parked();
|
|
|
|
let server_branch = server_cx.update(|cx| {
|
|
headless_project.update(cx, |headless_project, cx| {
|
|
headless_project.git_store.update(cx, |git_store, cx| {
|
|
git_store
|
|
.repositories()
|
|
.values()
|
|
.next()
|
|
.unwrap()
|
|
.read(cx)
|
|
.branch
|
|
.as_ref()
|
|
.unwrap()
|
|
.clone()
|
|
})
|
|
})
|
|
});
|
|
|
|
assert_eq!(server_branch.name(), "totally-new-branch");
|
|
|
|
// Remove the git repository and check that all participants get the update.
|
|
remote_fs
|
|
.remove_dir("/project/.git".as_ref(), RemoveOptions::default())
|
|
.await
|
|
.unwrap();
|
|
executor.run_until_parked();
|
|
|
|
project_a.update(cx_a, |project, cx| {
|
|
pretty_assertions::assert_eq!(
|
|
project.git_store().read(cx).repo_snapshots(cx),
|
|
HashMap::default()
|
|
);
|
|
});
|
|
project_b.update(cx_b, |project, cx| {
|
|
pretty_assertions::assert_eq!(
|
|
project.git_store().read(cx).repo_snapshots(cx),
|
|
HashMap::default()
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_ssh_collaboration_formatting_with_prettier(
|
|
executor: BackgroundExecutor,
|
|
cx_a: &mut TestAppContext,
|
|
cx_b: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
) {
|
|
cx_a.set_name("a");
|
|
cx_b.set_name("b");
|
|
server_cx.set_name("server");
|
|
|
|
cx_a.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
});
|
|
server_cx.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
});
|
|
|
|
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;
|
|
|
|
let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
|
|
let remote_fs = FakeFs::new(server_cx.executor());
|
|
let buffer_text = "let one = \"two\"";
|
|
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
|
|
remote_fs
|
|
.insert_tree(
|
|
path!("/project"),
|
|
serde_json::json!({ "a.ts": buffer_text }),
|
|
)
|
|
.await;
|
|
|
|
let test_plugin = "test_plugin";
|
|
let ts_lang = Arc::new(Language::new(
|
|
LanguageConfig {
|
|
name: "TypeScript".into(),
|
|
matcher: LanguageMatcher {
|
|
path_suffixes: vec!["ts".to_string()],
|
|
..LanguageMatcher::default()
|
|
},
|
|
..LanguageConfig::default()
|
|
},
|
|
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
|
|
));
|
|
client_a.language_registry().add(ts_lang.clone());
|
|
client_b.language_registry().add(ts_lang.clone());
|
|
|
|
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
|
let mut fake_language_servers = languages.register_fake_lsp(
|
|
"TypeScript",
|
|
FakeLspAdapter {
|
|
prettier_plugins: vec![test_plugin],
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
// User A connects to the remote project via SSH.
|
|
server_cx.update(HeadlessProject::init);
|
|
let remote_http_client = Arc::new(BlockedHttpClient);
|
|
let _headless_project = server_cx.new(|cx| {
|
|
HeadlessProject::new(
|
|
HeadlessAppState {
|
|
session: server_ssh,
|
|
fs: remote_fs.clone(),
|
|
http_client: remote_http_client,
|
|
node_runtime: NodeRuntime::unavailable(),
|
|
languages,
|
|
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
|
startup_time: std::time::Instant::now(),
|
|
},
|
|
false,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
|
|
let (project_a, worktree_id) = client_a
|
|
.build_ssh_project(path!("/project"), client_ssh, false, cx_a)
|
|
.await;
|
|
|
|
// While the SSH worktree is being scanned, 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.join_remote_project(project_id, cx_b).await;
|
|
executor.run_until_parked();
|
|
|
|
// Opens the buffer and formats it
|
|
let (buffer_b, _handle) = project_b
|
|
.update(cx_b, |p, cx| {
|
|
p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
|
|
})
|
|
.await
|
|
.expect("user B opens buffer for formatting");
|
|
|
|
cx_a.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings(cx, |file| {
|
|
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
|
|
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
|
allowed: Some(true),
|
|
..Default::default()
|
|
});
|
|
});
|
|
});
|
|
});
|
|
cx_b.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings(cx, |file| {
|
|
file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
|
|
Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
|
|
));
|
|
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
|
allowed: Some(true),
|
|
..Default::default()
|
|
});
|
|
});
|
|
});
|
|
});
|
|
let fake_language_server = fake_language_servers.next().await.unwrap();
|
|
fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
|
|
panic!(
|
|
"Unexpected: prettier should be preferred since it's enabled and language supports it"
|
|
)
|
|
});
|
|
|
|
project_b
|
|
.update(cx_b, |project, cx| {
|
|
project.format(
|
|
HashSet::from_iter([buffer_b.clone()]),
|
|
LspFormatTarget::Buffers,
|
|
true,
|
|
FormatTrigger::Save,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
|
buffer_text.to_string() + "\n" + prettier_format_suffix,
|
|
"Prettier formatting was not applied to client buffer after client's request"
|
|
);
|
|
|
|
// User A opens and formats the same buffer too
|
|
let buffer_a = project_a
|
|
.update(cx_a, |p, cx| {
|
|
p.open_buffer((worktree_id, rel_path("a.ts")), cx)
|
|
})
|
|
.await
|
|
.expect("user A opens buffer for formatting");
|
|
|
|
cx_a.update(|cx| {
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings(cx, |file| {
|
|
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
|
|
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
|
allowed: Some(true),
|
|
..Default::default()
|
|
});
|
|
});
|
|
});
|
|
});
|
|
project_a
|
|
.update(cx_a, |project, cx| {
|
|
project.format(
|
|
HashSet::from_iter([buffer_a.clone()]),
|
|
LspFormatTarget::Buffers,
|
|
true,
|
|
FormatTrigger::Manual,
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
executor.run_until_parked();
|
|
assert_eq!(
|
|
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
|
buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
|
|
"Prettier formatting was not applied to client buffer after host's request"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_remote_server_debugger(
|
|
cx_a: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
executor: BackgroundExecutor,
|
|
) {
|
|
cx_a.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
command_palette_hooks::init(cx);
|
|
zlog::init_test();
|
|
dap_adapters::init(cx);
|
|
});
|
|
server_cx.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
dap_adapters::init(cx);
|
|
});
|
|
let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
|
|
let remote_fs = FakeFs::new(server_cx.executor());
|
|
remote_fs
|
|
.insert_tree(
|
|
path!("/code"),
|
|
json!({
|
|
"lib.rs": "fn one() -> usize { 1 }"
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// User A connects to the remote project via SSH.
|
|
server_cx.update(HeadlessProject::init);
|
|
let remote_http_client = Arc::new(BlockedHttpClient);
|
|
let node = NodeRuntime::unavailable();
|
|
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
|
let _headless_project = server_cx.new(|cx| {
|
|
HeadlessProject::new(
|
|
HeadlessAppState {
|
|
session: server_ssh,
|
|
fs: remote_fs.clone(),
|
|
http_client: remote_http_client,
|
|
node_runtime: node,
|
|
languages,
|
|
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
|
startup_time: std::time::Instant::now(),
|
|
},
|
|
false,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
|
|
let mut server = TestServer::start(server_cx.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
cx_a.update(|cx| {
|
|
debugger_ui::init(cx);
|
|
command_palette_hooks::init(cx);
|
|
});
|
|
let (project_a, _) = client_a
|
|
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
|
.await;
|
|
|
|
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
|
|
let debugger_panel = workspace
|
|
.update_in(cx_a, |_workspace, window, cx| {
|
|
cx.spawn_in(window, DebugPanel::load)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
workspace.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.add_panel(debugger_panel, window, cx);
|
|
});
|
|
|
|
cx_a.run_until_parked();
|
|
let debug_panel = workspace
|
|
.update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
|
.unwrap();
|
|
|
|
let workspace_window = cx_a
|
|
.window_handle()
|
|
.downcast::<workspace::MultiWorkspace>()
|
|
.unwrap();
|
|
|
|
let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
|
|
cx_a.run_until_parked();
|
|
debug_panel.update(cx_a, |debug_panel, cx| {
|
|
assert_eq!(
|
|
debug_panel.active_session().unwrap().read(cx).session(cx),
|
|
session.clone()
|
|
)
|
|
});
|
|
|
|
session.update(
|
|
cx_a,
|
|
|session: &mut project::debugger::session::Session, _| {
|
|
assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
|
|
},
|
|
);
|
|
|
|
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
|
|
workspace.project().update(cx, |project, cx| {
|
|
project.dap_store().update(cx, |dap_store, cx| {
|
|
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
|
})
|
|
})
|
|
});
|
|
|
|
client_ssh.update(cx_a, |a, _| {
|
|
a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
|
|
});
|
|
|
|
shutdown_session.await.unwrap();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_slow_adapter_startup_retries(
|
|
cx_a: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
executor: BackgroundExecutor,
|
|
) {
|
|
cx_a.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
command_palette_hooks::init(cx);
|
|
zlog::init_test();
|
|
dap_adapters::init(cx);
|
|
});
|
|
server_cx.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
dap_adapters::init(cx);
|
|
});
|
|
let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
|
|
let remote_fs = FakeFs::new(server_cx.executor());
|
|
remote_fs
|
|
.insert_tree(
|
|
path!("/code"),
|
|
json!({
|
|
"lib.rs": "fn one() -> usize { 1 }"
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// User A connects to the remote project via SSH.
|
|
server_cx.update(HeadlessProject::init);
|
|
let remote_http_client = Arc::new(BlockedHttpClient);
|
|
let node = NodeRuntime::unavailable();
|
|
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
|
let _headless_project = server_cx.new(|cx| {
|
|
HeadlessProject::new(
|
|
HeadlessAppState {
|
|
session: server_ssh,
|
|
fs: remote_fs.clone(),
|
|
http_client: remote_http_client,
|
|
node_runtime: node,
|
|
languages,
|
|
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
|
startup_time: std::time::Instant::now(),
|
|
},
|
|
false,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
|
|
let mut server = TestServer::start(server_cx.executor()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
cx_a.update(|cx| {
|
|
debugger_ui::init(cx);
|
|
command_palette_hooks::init(cx);
|
|
});
|
|
let (project_a, _) = client_a
|
|
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
|
.await;
|
|
|
|
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
|
|
|
let debugger_panel = workspace
|
|
.update_in(cx_a, |_workspace, window, cx| {
|
|
cx.spawn_in(window, DebugPanel::load)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
workspace.update_in(cx_a, |workspace, window, cx| {
|
|
workspace.add_panel(debugger_panel, window, cx);
|
|
});
|
|
|
|
cx_a.run_until_parked();
|
|
let debug_panel = workspace
|
|
.update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
|
|
.unwrap();
|
|
|
|
let workspace_window = cx_a
|
|
.window_handle()
|
|
.downcast::<workspace::MultiWorkspace>()
|
|
.unwrap();
|
|
|
|
let count = Arc::new(AtomicUsize::new(0));
|
|
let session = debugger_ui::tests::start_debug_session_with(
|
|
&workspace_window,
|
|
cx_a,
|
|
DebugTaskDefinition {
|
|
adapter: "fake-adapter".into(),
|
|
label: "test".into(),
|
|
config: json!({
|
|
"request": "launch"
|
|
}),
|
|
tcp_connection: Some(TcpArgumentsTemplate {
|
|
port: None,
|
|
host: None,
|
|
timeout: None,
|
|
}),
|
|
},
|
|
move |client| {
|
|
let count = count.clone();
|
|
client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
|
|
if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
|
|
return RequestHandling::Exit;
|
|
}
|
|
RequestHandling::Respond(Ok(Capabilities::default()))
|
|
});
|
|
},
|
|
)
|
|
.unwrap();
|
|
cx_a.run_until_parked();
|
|
|
|
let client = session.update(
|
|
cx_a,
|
|
|session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
|
|
);
|
|
client
|
|
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
|
reason: dap::StoppedEventReason::Pause,
|
|
description: None,
|
|
thread_id: Some(1),
|
|
preserve_focus_hint: None,
|
|
text: None,
|
|
all_threads_stopped: None,
|
|
hit_breakpoint_ids: None,
|
|
}))
|
|
.await;
|
|
|
|
cx_a.run_until_parked();
|
|
|
|
let active_session = debug_panel
|
|
.update(cx_a, |this, _| this.active_session())
|
|
.unwrap();
|
|
|
|
let running_state = active_session.update(cx_a, |active_session, _| {
|
|
active_session.running_state().clone()
|
|
});
|
|
|
|
assert_eq!(
|
|
client.id(),
|
|
running_state.read_with(cx_a, |running_state, _| running_state.session_id())
|
|
);
|
|
assert_eq!(
|
|
ThreadId(1),
|
|
running_state.read_with(cx_a, |running_state, _| running_state
|
|
.selected_thread_id()
|
|
.unwrap())
|
|
);
|
|
|
|
let shutdown_session = workspace.update(cx_a, |workspace, cx| {
|
|
workspace.project().update(cx, |project, cx| {
|
|
project.dap_store().update(cx, |dap_store, cx| {
|
|
dap_store.shutdown_session(session.read(cx).session_id(), cx)
|
|
})
|
|
})
|
|
});
|
|
|
|
client_ssh.update(cx_a, |a, _| {
|
|
a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
|
|
});
|
|
|
|
shutdown_session.await.unwrap();
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
|
cx_a.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
project::trusted_worktrees::init(HashMap::default(), cx);
|
|
});
|
|
server_cx.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
project::trusted_worktrees::init(HashMap::default(), cx);
|
|
});
|
|
|
|
let mut server = TestServer::start(cx_a.executor().clone()).await;
|
|
let client_a = server.create_client(cx_a, "user_a").await;
|
|
|
|
let server_name = "override-rust-analyzer";
|
|
let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
|
|
|
|
let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
|
|
let remote_fs = FakeFs::new(server_cx.executor());
|
|
remote_fs
|
|
.insert_tree(
|
|
path!("/projects"),
|
|
json!({
|
|
"project_a": {
|
|
".zed": {
|
|
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
|
},
|
|
"main.rs": "fn main() {}"
|
|
},
|
|
"project_b": { "lib.rs": "pub fn lib() {}" }
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
server_cx.update(HeadlessProject::init);
|
|
let remote_http_client = Arc::new(BlockedHttpClient);
|
|
let node = NodeRuntime::unavailable();
|
|
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
|
languages.add(rust_lang());
|
|
|
|
let capabilities = lsp::ServerCapabilities {
|
|
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
|
..lsp::ServerCapabilities::default()
|
|
};
|
|
let mut fake_language_servers = languages.register_fake_lsp(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
name: server_name,
|
|
capabilities: capabilities.clone(),
|
|
initializer: Some(Box::new({
|
|
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
|
move |fake_server| {
|
|
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
|
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
|
move |_params, _| {
|
|
lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
|
|
async move {
|
|
Ok(Some(vec![lsp::InlayHint {
|
|
position: lsp::Position::new(0, 0),
|
|
label: lsp::InlayHintLabel::String("hint".to_string()),
|
|
kind: None,
|
|
text_edits: None,
|
|
tooltip: None,
|
|
padding_left: None,
|
|
padding_right: None,
|
|
data: None,
|
|
}]))
|
|
}
|
|
},
|
|
);
|
|
}
|
|
})),
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
|
|
let _headless_project = server_cx.new(|cx| {
|
|
HeadlessProject::new(
|
|
HeadlessAppState {
|
|
session: server_ssh,
|
|
fs: remote_fs.clone(),
|
|
http_client: remote_http_client,
|
|
node_runtime: node,
|
|
languages,
|
|
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
|
startup_time: std::time::Instant::now(),
|
|
},
|
|
true,
|
|
cx,
|
|
)
|
|
});
|
|
|
|
let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
|
|
let (project_a, worktree_id_a) = client_a
|
|
.build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
|
|
.await;
|
|
|
|
cx_a.update(|cx| {
|
|
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
|
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings(cx, |settings| {
|
|
let language_settings = &mut settings.project.all_languages.defaults;
|
|
language_settings.inlay_hints = Some(InlayHintSettingsContent {
|
|
enabled: Some(true),
|
|
..InlayHintSettingsContent::default()
|
|
})
|
|
});
|
|
});
|
|
});
|
|
|
|
project_a
|
|
.update(cx_a, |project, cx| {
|
|
project.languages().add(rust_lang());
|
|
project.languages().register_fake_lsp_adapter(
|
|
"Rust",
|
|
FakeLspAdapter {
|
|
name: server_name,
|
|
capabilities,
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
cx_a.run_until_parked();
|
|
|
|
let worktree_ids = project_a.read_with(cx_a, |project, cx| {
|
|
project
|
|
.worktrees(cx)
|
|
.map(|wt| wt.read(cx).id())
|
|
.collect::<Vec<_>>()
|
|
});
|
|
assert_eq!(worktree_ids.len(), 2);
|
|
|
|
let trusted_worktrees =
|
|
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
|
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
|
|
|
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
|
});
|
|
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
|
});
|
|
assert!(!can_trust_a, "project_a should be restricted initially");
|
|
assert!(!can_trust_b, "project_b should be restricted initially");
|
|
|
|
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
|
|
store.has_restricted_worktrees(&worktree_store, cx)
|
|
});
|
|
assert!(has_restricted, "should have restricted worktrees");
|
|
|
|
let buffer_before_approval = project_a
|
|
.update(cx_a, |project, cx| {
|
|
project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
|
|
Editor::new(
|
|
EditorMode::full(),
|
|
cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
|
|
Some(project_a.clone()),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
cx_a.run_until_parked();
|
|
let fake_language_server = fake_language_servers.next();
|
|
|
|
cx_a.read(|cx| {
|
|
let file = buffer_before_approval.read(cx).file();
|
|
assert_eq!(
|
|
language_settings(Some("Rust".into()), file, cx).language_servers,
|
|
["...".to_string()],
|
|
"remote .zed/settings.json must not sync before trust approval"
|
|
)
|
|
});
|
|
|
|
editor.update_in(cx_a, |editor, window, cx| {
|
|
editor.handle_input("1", window, cx);
|
|
});
|
|
cx_a.run_until_parked();
|
|
cx_a.executor().advance_clock(Duration::from_secs(1));
|
|
assert_eq!(
|
|
lsp_inlay_hint_request_count.load(Ordering::Acquire),
|
|
0,
|
|
"inlay hints must not be queried before trust approval"
|
|
);
|
|
|
|
trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.trust(
|
|
&worktree_store,
|
|
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
|
cx,
|
|
);
|
|
});
|
|
cx_a.run_until_parked();
|
|
|
|
cx_a.read(|cx| {
|
|
let file = buffer_before_approval.read(cx).file();
|
|
assert_eq!(
|
|
language_settings(Some("Rust".into()), file, cx).language_servers,
|
|
["override-rust-analyzer".to_string()],
|
|
"remote .zed/settings.json should sync after trust approval"
|
|
)
|
|
});
|
|
let _fake_language_server = fake_language_server.await.unwrap();
|
|
editor.update_in(cx_a, |editor, window, cx| {
|
|
editor.handle_input("1", window, cx);
|
|
});
|
|
cx_a.run_until_parked();
|
|
cx_a.executor().advance_clock(Duration::from_secs(1));
|
|
assert!(
|
|
lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
|
|
"inlay hints should be queried after trust approval"
|
|
);
|
|
|
|
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
|
});
|
|
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
|
});
|
|
assert!(can_trust_a, "project_a should be trusted after trust()");
|
|
assert!(!can_trust_b, "project_b should still be restricted");
|
|
|
|
trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.trust(
|
|
&worktree_store,
|
|
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
|
});
|
|
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
|
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
|
});
|
|
assert!(can_trust_a, "project_a should remain trusted");
|
|
assert!(can_trust_b, "project_b should now be trusted");
|
|
|
|
let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
|
|
store.has_restricted_worktrees(&worktree_store, cx)
|
|
});
|
|
assert!(
|
|
!has_restricted_after,
|
|
"should have no restricted worktrees after trusting both"
|
|
);
|
|
}
|