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, LanguageSettings}, 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, PathBuf}, 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())); languages.add(rust_lang()); 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; project_b.update(cx_b, |project, _| project.languages().add(rust_lang())); 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![ 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![ 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| { assert_eq!( LanguageSettings::for_buffer(buffer_b.read(cx), 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::>(); 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::>(); 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_git_worktrees( 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()); remote_fs .insert_tree("/project", json!({ ".git": {}, "file.txt": "content" })) .await; server_cx.update(HeadlessProject::init); 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: Arc::new(BlockedHttpClient), 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, _) = client_a .build_ssh_project("/project", client_ssh, false, cx_a) .await; 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(); let project_b = client_b.join_remote_project(project_id, cx_b).await; executor.run_until_parked(); let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap()); let worktrees = cx_b .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees())) .await .unwrap() .unwrap(); assert_eq!(worktrees.len(), 1); let worktree_directory = PathBuf::from("/worktrees"); cx_b.update(|cx| { repo_b.update(cx, |repo, _| { repo.create_worktree( git::repository::CreateWorktreeTarget::NewBranch { branch_name: "feature-branch".to_string(), base_sha: Some("abc123".to_string()), }, worktree_directory.join("feature-branch"), ) }) }) .await .unwrap() .unwrap(); executor.run_until_parked(); let worktrees = cx_b .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees())) .await .unwrap() .unwrap(); assert_eq!(worktrees.len(), 2); assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch")); assert_eq!( worktrees[1].ref_name, Some("refs/heads/feature-branch".into()) ); assert_eq!(worktrees[1].sha.as_ref(), "abc123"); let server_worktrees = { let server_repo = server_cx.update(|cx| { headless_project.update(cx, |headless_project, cx| { headless_project .git_store .read(cx) .repositories() .values() .next() .unwrap() .clone() }) }); server_cx .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees())) .await .unwrap() .unwrap() }; assert_eq!(server_worktrees.len(), 2); assert_eq!( server_worktrees[1].path, worktree_directory.join("feature-branch") ); // Host (client A) renames the worktree via SSH let repo_a = cx_a.update(|cx| { project_a .read(cx) .repositories(cx) .values() .next() .unwrap() .clone() }); cx_a.update(|cx| { repo_a.update(cx, |repository, _| { repository.rename_worktree( PathBuf::from("/worktrees/feature-branch"), PathBuf::from("/worktrees/renamed-branch"), ) }) }) .await .unwrap() .unwrap(); executor.run_until_parked(); let host_worktrees = cx_a .update(|cx| repo_a.update(cx, |repository, _| repository.worktrees())) .await .unwrap() .unwrap(); assert_eq!( host_worktrees.len(), 2, "Host should still have 2 worktrees after rename" ); assert_eq!( host_worktrees[1].path, PathBuf::from("/worktrees/renamed-branch") ); let server_worktrees = { let server_repo = server_cx.update(|cx| { headless_project.update(cx, |headless_project, cx| { headless_project .git_store .read(cx) .repositories() .values() .next() .unwrap() .clone() }) }); server_cx .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees())) .await .unwrap() .unwrap() }; assert_eq!( server_worktrees.len(), 2, "Server should still have 2 worktrees after rename" ); assert_eq!( server_worktrees[1].path, PathBuf::from("/worktrees/renamed-branch") ); // Host (client A) removes the renamed worktree via SSH cx_a.update(|cx| { repo_a.update(cx, |repository, _| { repository.remove_worktree(PathBuf::from("/worktrees/renamed-branch"), false) }) }) .await .unwrap() .unwrap(); executor.run_until_parked(); let host_worktrees = cx_a .update(|cx| repo_a.update(cx, |repository, _| repository.worktrees())) .await .unwrap() .unwrap(); assert_eq!( host_worktrees.len(), 1, "Host should only have the main worktree after removal" ); let server_worktrees = { let server_repo = server_cx.update(|cx| { headless_project.update(cx, |headless_project, cx| { headless_project .git_store .read(cx) .repositories() .values() .next() .unwrap() .clone() }) }); server_cx .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees())) .await .unwrap() .unwrap() }; assert_eq!( server_worktrees.len(), 1, "Server should only have the main worktree after removal" ); } #[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::(|_, _| 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::(cx)) .unwrap(); let workspace_window = cx_a .window_handle() .downcast::() .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::(cx)) .unwrap(); let workspace_window = cx_a .window_handle() .downcast::() .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::(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::( 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::>() }); 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| { assert_eq!( LanguageSettings::for_buffer(buffer_before_approval.read(cx), 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| { assert_eq!( LanguageSettings::for_buffer(buffer_before_approval.read(cx), 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" ); }