1use crate::TestServer;
2use call::ActiveCall;
3use collections::{HashMap, HashSet};
4
5use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
6use debugger_ui::debugger_panel::DebugPanel;
7use editor::{Editor, EditorMode, MultiBuffer};
8use extension::ExtensionHostProxy;
9use fs::{FakeFs, Fs as _, RemoveOptions};
10use futures::StreamExt as _;
11use gpui::{
12 AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _,
13};
14use http_client::BlockedHttpClient;
15use language::{
16 FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
17 language_settings::{Formatter, FormatterList, language_settings},
18 rust_lang, tree_sitter_typescript,
19};
20use node_runtime::NodeRuntime;
21use project::{
22 ProjectPath,
23 debugger::session::ThreadId,
24 lsp_store::{FormatTrigger, LspFormatTarget},
25 trusted_worktrees::{PathTrust, TrustedWorktrees},
26};
27use remote::RemoteClient;
28use remote_server::{HeadlessAppState, HeadlessProject};
29use rpc::proto;
30use serde_json::json;
31use settings::{
32 InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
33 SettingsStore,
34};
35use std::{
36 path::Path,
37 sync::{
38 Arc,
39 atomic::{AtomicUsize, Ordering},
40 },
41 time::Duration,
42};
43use task::TcpArgumentsTemplate;
44use util::{path, rel_path::rel_path};
45
46#[gpui::test(iterations = 10)]
47async fn test_sharing_an_ssh_remote_project(
48 cx_a: &mut TestAppContext,
49 cx_b: &mut TestAppContext,
50 server_cx: &mut TestAppContext,
51) {
52 let executor = cx_a.executor();
53 cx_a.update(|cx| {
54 release_channel::init(semver::Version::new(0, 0, 0), cx);
55 });
56 server_cx.update(|cx| {
57 release_channel::init(semver::Version::new(0, 0, 0), cx);
58 });
59 let mut server = TestServer::start(executor.clone()).await;
60 let client_a = server.create_client(cx_a, "user_a").await;
61 let client_b = server.create_client(cx_b, "user_b").await;
62 server
63 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
64 .await;
65
66 // Set up project on remote FS
67 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
68 let remote_fs = FakeFs::new(server_cx.executor());
69 remote_fs
70 .insert_tree(
71 path!("/code"),
72 json!({
73 "project1": {
74 ".zed": {
75 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
76 },
77 "README.md": "# project 1",
78 "src": {
79 "lib.rs": "fn one() -> usize { 1 }"
80 }
81 },
82 "project2": {
83 "README.md": "# project 2",
84 },
85 }),
86 )
87 .await;
88
89 // User A connects to the remote project via SSH.
90 server_cx.update(HeadlessProject::init);
91 let remote_http_client = Arc::new(BlockedHttpClient);
92 let node = NodeRuntime::unavailable();
93 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
94 let _headless_project = server_cx.new(|cx| {
95 HeadlessProject::new(
96 HeadlessAppState {
97 session: server_ssh,
98 fs: remote_fs.clone(),
99 http_client: remote_http_client,
100 node_runtime: node,
101 languages,
102 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
103 startup_time: std::time::Instant::now(),
104 },
105 false,
106 cx,
107 )
108 });
109
110 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
111 let (project_a, worktree_id) = client_a
112 .build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
113 .await;
114
115 // While the SSH worktree is being scanned, user A shares the remote project.
116 let active_call_a = cx_a.read(ActiveCall::global);
117 let project_id = active_call_a
118 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
119 .await
120 .unwrap();
121
122 // User B joins the project.
123 let project_b = client_b.join_remote_project(project_id, cx_b).await;
124 let worktree_b = project_b
125 .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
126 .unwrap();
127
128 let worktree_a = project_a
129 .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
130 .unwrap();
131
132 executor.run_until_parked();
133
134 worktree_a.update(cx_a, |worktree, _cx| {
135 assert_eq!(
136 worktree.paths().collect::<Vec<_>>(),
137 vec![
138 rel_path(".zed"),
139 rel_path(".zed/settings.json"),
140 rel_path("README.md"),
141 rel_path("src"),
142 rel_path("src/lib.rs"),
143 ]
144 );
145 });
146
147 worktree_b.update(cx_b, |worktree, _cx| {
148 assert_eq!(
149 worktree.paths().collect::<Vec<_>>(),
150 vec![
151 rel_path(".zed"),
152 rel_path(".zed/settings.json"),
153 rel_path("README.md"),
154 rel_path("src"),
155 rel_path("src/lib.rs"),
156 ]
157 );
158 });
159
160 // User B can open buffers in the remote project.
161 let buffer_b = project_b
162 .update(cx_b, |project, cx| {
163 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
164 })
165 .await
166 .unwrap();
167 buffer_b.update(cx_b, |buffer, cx| {
168 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
169 let ix = buffer.text().find('1').unwrap();
170 buffer.edit([(ix..ix + 1, "100")], None, cx);
171 });
172
173 executor.run_until_parked();
174
175 cx_b.read(|cx| {
176 let file = buffer_b.read(cx).file();
177 assert_eq!(
178 language_settings(Some("Rust".into()), file, cx).language_servers,
179 ["override-rust-analyzer".to_string()]
180 )
181 });
182
183 project_b
184 .update(cx_b, |project, cx| {
185 project.save_buffer_as(
186 buffer_b.clone(),
187 ProjectPath {
188 worktree_id: worktree_id.to_owned(),
189 path: rel_path("src/renamed.rs").into(),
190 },
191 cx,
192 )
193 })
194 .await
195 .unwrap();
196 assert_eq!(
197 remote_fs
198 .load(path!("/code/project1/src/renamed.rs").as_ref())
199 .await
200 .unwrap(),
201 "fn one() -> usize { 100 }"
202 );
203 cx_b.run_until_parked();
204 cx_b.update(|cx| {
205 assert_eq!(
206 buffer_b.read(cx).file().unwrap().path().as_ref(),
207 rel_path("src/renamed.rs")
208 );
209 });
210}
211
212#[gpui::test]
213async fn test_ssh_collaboration_git_branches(
214 executor: BackgroundExecutor,
215 cx_a: &mut TestAppContext,
216 cx_b: &mut TestAppContext,
217 server_cx: &mut TestAppContext,
218) {
219 cx_a.set_name("a");
220 cx_b.set_name("b");
221 server_cx.set_name("server");
222
223 cx_a.update(|cx| {
224 release_channel::init(semver::Version::new(0, 0, 0), cx);
225 });
226 server_cx.update(|cx| {
227 release_channel::init(semver::Version::new(0, 0, 0), cx);
228 });
229
230 let mut server = TestServer::start(executor.clone()).await;
231 let client_a = server.create_client(cx_a, "user_a").await;
232 let client_b = server.create_client(cx_b, "user_b").await;
233 server
234 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
235 .await;
236
237 // Set up project on remote FS
238 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
239 let remote_fs = FakeFs::new(server_cx.executor());
240 remote_fs
241 .insert_tree("/project", serde_json::json!({ ".git":{} }))
242 .await;
243
244 let branches = ["main", "dev", "feature-1"];
245 let branches_set = branches
246 .iter()
247 .map(ToString::to_string)
248 .collect::<HashSet<_>>();
249 remote_fs.insert_branches(Path::new("/project/.git"), &branches);
250
251 // User A connects to the remote project via SSH.
252 server_cx.update(HeadlessProject::init);
253 let remote_http_client = Arc::new(BlockedHttpClient);
254 let node = NodeRuntime::unavailable();
255 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
256 let headless_project = server_cx.new(|cx| {
257 HeadlessProject::new(
258 HeadlessAppState {
259 session: server_ssh,
260 fs: remote_fs.clone(),
261 http_client: remote_http_client,
262 node_runtime: node,
263 languages,
264 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
265 startup_time: std::time::Instant::now(),
266 },
267 false,
268 cx,
269 )
270 });
271
272 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
273 let (project_a, _) = client_a
274 .build_ssh_project("/project", client_ssh, false, cx_a)
275 .await;
276
277 // While the SSH worktree is being scanned, user A shares the remote project.
278 let active_call_a = cx_a.read(ActiveCall::global);
279 let project_id = active_call_a
280 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
281 .await
282 .unwrap();
283
284 // User B joins the project.
285 let project_b = client_b.join_remote_project(project_id, cx_b).await;
286
287 // Give client A sometime to see that B has joined, and that the headless server
288 // has some git repositories
289 executor.run_until_parked();
290
291 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
292
293 let branches_b = cx_b
294 .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
295 .await
296 .unwrap()
297 .unwrap();
298
299 let new_branch = branches[2];
300
301 let branches_b = branches_b
302 .into_iter()
303 .map(|branch| branch.name().to_string())
304 .collect::<HashSet<_>>();
305
306 assert_eq!(&branches_b, &branches_set);
307
308 cx_b.update(|cx| {
309 repo_b.update(cx, |repo_b, _cx| {
310 repo_b.change_branch(new_branch.to_string())
311 })
312 })
313 .await
314 .unwrap()
315 .unwrap();
316
317 executor.run_until_parked();
318
319 let server_branch = server_cx.update(|cx| {
320 headless_project.update(cx, |headless_project, cx| {
321 headless_project.git_store.update(cx, |git_store, cx| {
322 git_store
323 .repositories()
324 .values()
325 .next()
326 .unwrap()
327 .read(cx)
328 .branch
329 .as_ref()
330 .unwrap()
331 .clone()
332 })
333 })
334 });
335
336 assert_eq!(server_branch.name(), branches[2]);
337
338 // Also try creating a new branch
339 cx_b.update(|cx| {
340 repo_b.update(cx, |repo_b, _cx| {
341 repo_b.create_branch("totally-new-branch".to_string(), None)
342 })
343 })
344 .await
345 .unwrap()
346 .unwrap();
347
348 cx_b.update(|cx| {
349 repo_b.update(cx, |repo_b, _cx| {
350 repo_b.change_branch("totally-new-branch".to_string())
351 })
352 })
353 .await
354 .unwrap()
355 .unwrap();
356
357 executor.run_until_parked();
358
359 let server_branch = server_cx.update(|cx| {
360 headless_project.update(cx, |headless_project, cx| {
361 headless_project.git_store.update(cx, |git_store, cx| {
362 git_store
363 .repositories()
364 .values()
365 .next()
366 .unwrap()
367 .read(cx)
368 .branch
369 .as_ref()
370 .unwrap()
371 .clone()
372 })
373 })
374 });
375
376 assert_eq!(server_branch.name(), "totally-new-branch");
377
378 // Remove the git repository and check that all participants get the update.
379 remote_fs
380 .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
381 .await
382 .unwrap();
383 executor.run_until_parked();
384
385 project_a.update(cx_a, |project, cx| {
386 pretty_assertions::assert_eq!(
387 project.git_store().read(cx).repo_snapshots(cx),
388 HashMap::default()
389 );
390 });
391 project_b.update(cx_b, |project, cx| {
392 pretty_assertions::assert_eq!(
393 project.git_store().read(cx).repo_snapshots(cx),
394 HashMap::default()
395 );
396 });
397}
398
399#[gpui::test]
400async fn test_ssh_collaboration_formatting_with_prettier(
401 executor: BackgroundExecutor,
402 cx_a: &mut TestAppContext,
403 cx_b: &mut TestAppContext,
404 server_cx: &mut TestAppContext,
405) {
406 cx_a.set_name("a");
407 cx_b.set_name("b");
408 server_cx.set_name("server");
409
410 cx_a.update(|cx| {
411 release_channel::init(semver::Version::new(0, 0, 0), cx);
412 });
413 server_cx.update(|cx| {
414 release_channel::init(semver::Version::new(0, 0, 0), cx);
415 });
416
417 let mut server = TestServer::start(executor.clone()).await;
418 let client_a = server.create_client(cx_a, "user_a").await;
419 let client_b = server.create_client(cx_b, "user_b").await;
420 server
421 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
422 .await;
423
424 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
425 let remote_fs = FakeFs::new(server_cx.executor());
426 let buffer_text = "let one = \"two\"";
427 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
428 remote_fs
429 .insert_tree(
430 path!("/project"),
431 serde_json::json!({ "a.ts": buffer_text }),
432 )
433 .await;
434
435 let test_plugin = "test_plugin";
436 let ts_lang = Arc::new(Language::new(
437 LanguageConfig {
438 name: "TypeScript".into(),
439 matcher: LanguageMatcher {
440 path_suffixes: vec!["ts".to_string()],
441 ..LanguageMatcher::default()
442 },
443 ..LanguageConfig::default()
444 },
445 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
446 ));
447 client_a.language_registry().add(ts_lang.clone());
448 client_b.language_registry().add(ts_lang.clone());
449
450 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
451 let mut fake_language_servers = languages.register_fake_lsp(
452 "TypeScript",
453 FakeLspAdapter {
454 prettier_plugins: vec![test_plugin],
455 ..Default::default()
456 },
457 );
458
459 // User A connects to the remote project via SSH.
460 server_cx.update(HeadlessProject::init);
461 let remote_http_client = Arc::new(BlockedHttpClient);
462 let _headless_project = server_cx.new(|cx| {
463 HeadlessProject::new(
464 HeadlessAppState {
465 session: server_ssh,
466 fs: remote_fs.clone(),
467 http_client: remote_http_client,
468 node_runtime: NodeRuntime::unavailable(),
469 languages,
470 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
471 startup_time: std::time::Instant::now(),
472 },
473 false,
474 cx,
475 )
476 });
477
478 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
479 let (project_a, worktree_id) = client_a
480 .build_ssh_project(path!("/project"), client_ssh, false, cx_a)
481 .await;
482
483 // While the SSH worktree is being scanned, user A shares the remote project.
484 let active_call_a = cx_a.read(ActiveCall::global);
485 let project_id = active_call_a
486 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
487 .await
488 .unwrap();
489
490 // User B joins the project.
491 let project_b = client_b.join_remote_project(project_id, cx_b).await;
492 executor.run_until_parked();
493
494 // Opens the buffer and formats it
495 let (buffer_b, _handle) = project_b
496 .update(cx_b, |p, cx| {
497 p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
498 })
499 .await
500 .expect("user B opens buffer for formatting");
501
502 cx_a.update(|cx| {
503 SettingsStore::update_global(cx, |store, cx| {
504 store.update_user_settings(cx, |file| {
505 file.project.all_languages.defaults.formatter = Some(FormatterList::default());
506 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
507 allowed: Some(true),
508 ..Default::default()
509 });
510 });
511 });
512 });
513 cx_b.update(|cx| {
514 SettingsStore::update_global(cx, |store, cx| {
515 store.update_user_settings(cx, |file| {
516 file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
517 Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
518 ));
519 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
520 allowed: Some(true),
521 ..Default::default()
522 });
523 });
524 });
525 });
526 let fake_language_server = fake_language_servers.next().await.unwrap();
527 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
528 panic!(
529 "Unexpected: prettier should be preferred since it's enabled and language supports it"
530 )
531 });
532
533 project_b
534 .update(cx_b, |project, cx| {
535 project.format(
536 HashSet::from_iter([buffer_b.clone()]),
537 LspFormatTarget::Buffers,
538 true,
539 FormatTrigger::Save,
540 cx,
541 )
542 })
543 .await
544 .unwrap();
545
546 executor.run_until_parked();
547 assert_eq!(
548 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
549 buffer_text.to_string() + "\n" + prettier_format_suffix,
550 "Prettier formatting was not applied to client buffer after client's request"
551 );
552
553 // User A opens and formats the same buffer too
554 let buffer_a = project_a
555 .update(cx_a, |p, cx| {
556 p.open_buffer((worktree_id, rel_path("a.ts")), cx)
557 })
558 .await
559 .expect("user A opens buffer for formatting");
560
561 cx_a.update(|cx| {
562 SettingsStore::update_global(cx, |store, cx| {
563 store.update_user_settings(cx, |file| {
564 file.project.all_languages.defaults.formatter = Some(FormatterList::default());
565 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
566 allowed: Some(true),
567 ..Default::default()
568 });
569 });
570 });
571 });
572 project_a
573 .update(cx_a, |project, cx| {
574 project.format(
575 HashSet::from_iter([buffer_a.clone()]),
576 LspFormatTarget::Buffers,
577 true,
578 FormatTrigger::Manual,
579 cx,
580 )
581 })
582 .await
583 .unwrap();
584
585 executor.run_until_parked();
586 assert_eq!(
587 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
588 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
589 "Prettier formatting was not applied to client buffer after host's request"
590 );
591}
592
593#[gpui::test]
594async fn test_remote_server_debugger(
595 cx_a: &mut TestAppContext,
596 server_cx: &mut TestAppContext,
597 executor: BackgroundExecutor,
598) {
599 cx_a.update(|cx| {
600 release_channel::init(semver::Version::new(0, 0, 0), cx);
601 command_palette_hooks::init(cx);
602 zlog::init_test();
603 dap_adapters::init(cx);
604 });
605 server_cx.update(|cx| {
606 release_channel::init(semver::Version::new(0, 0, 0), cx);
607 dap_adapters::init(cx);
608 });
609 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
610 let remote_fs = FakeFs::new(server_cx.executor());
611 remote_fs
612 .insert_tree(
613 path!("/code"),
614 json!({
615 "lib.rs": "fn one() -> usize { 1 }"
616 }),
617 )
618 .await;
619
620 // User A connects to the remote project via SSH.
621 server_cx.update(HeadlessProject::init);
622 let remote_http_client = Arc::new(BlockedHttpClient);
623 let node = NodeRuntime::unavailable();
624 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
625 let _headless_project = server_cx.new(|cx| {
626 HeadlessProject::new(
627 HeadlessAppState {
628 session: server_ssh,
629 fs: remote_fs.clone(),
630 http_client: remote_http_client,
631 node_runtime: node,
632 languages,
633 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
634 startup_time: std::time::Instant::now(),
635 },
636 false,
637 cx,
638 )
639 });
640
641 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
642 let mut server = TestServer::start(server_cx.executor()).await;
643 let client_a = server.create_client(cx_a, "user_a").await;
644 cx_a.update(|cx| {
645 debugger_ui::init(cx);
646 command_palette_hooks::init(cx);
647 });
648 let (project_a, _) = client_a
649 .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
650 .await;
651
652 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
653
654 let debugger_panel = workspace
655 .update_in(cx_a, |_workspace, window, cx| {
656 cx.spawn_in(window, DebugPanel::load)
657 })
658 .await
659 .unwrap();
660
661 workspace.update_in(cx_a, |workspace, window, cx| {
662 workspace.add_panel(debugger_panel, window, cx);
663 });
664
665 cx_a.run_until_parked();
666 let debug_panel = workspace
667 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
668 .unwrap();
669
670 let workspace_window = cx_a
671 .window_handle()
672 .downcast::<workspace::MultiWorkspace>()
673 .unwrap();
674
675 let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
676 cx_a.run_until_parked();
677 debug_panel.update(cx_a, |debug_panel, cx| {
678 assert_eq!(
679 debug_panel.active_session().unwrap().read(cx).session(cx),
680 session.clone()
681 )
682 });
683
684 session.update(
685 cx_a,
686 |session: &mut project::debugger::session::Session, _| {
687 assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
688 },
689 );
690
691 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
692 workspace.project().update(cx, |project, cx| {
693 project.dap_store().update(cx, |dap_store, cx| {
694 dap_store.shutdown_session(session.read(cx).session_id(), cx)
695 })
696 })
697 });
698
699 client_ssh.update(cx_a, |a, _| {
700 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
701 });
702
703 shutdown_session.await.unwrap();
704}
705
706#[gpui::test]
707async fn test_slow_adapter_startup_retries(
708 cx_a: &mut TestAppContext,
709 server_cx: &mut TestAppContext,
710 executor: BackgroundExecutor,
711) {
712 cx_a.update(|cx| {
713 release_channel::init(semver::Version::new(0, 0, 0), cx);
714 command_palette_hooks::init(cx);
715 zlog::init_test();
716 dap_adapters::init(cx);
717 });
718 server_cx.update(|cx| {
719 release_channel::init(semver::Version::new(0, 0, 0), cx);
720 dap_adapters::init(cx);
721 });
722 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
723 let remote_fs = FakeFs::new(server_cx.executor());
724 remote_fs
725 .insert_tree(
726 path!("/code"),
727 json!({
728 "lib.rs": "fn one() -> usize { 1 }"
729 }),
730 )
731 .await;
732
733 // User A connects to the remote project via SSH.
734 server_cx.update(HeadlessProject::init);
735 let remote_http_client = Arc::new(BlockedHttpClient);
736 let node = NodeRuntime::unavailable();
737 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
738 let _headless_project = server_cx.new(|cx| {
739 HeadlessProject::new(
740 HeadlessAppState {
741 session: server_ssh,
742 fs: remote_fs.clone(),
743 http_client: remote_http_client,
744 node_runtime: node,
745 languages,
746 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
747 startup_time: std::time::Instant::now(),
748 },
749 false,
750 cx,
751 )
752 });
753
754 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
755 let mut server = TestServer::start(server_cx.executor()).await;
756 let client_a = server.create_client(cx_a, "user_a").await;
757 cx_a.update(|cx| {
758 debugger_ui::init(cx);
759 command_palette_hooks::init(cx);
760 });
761 let (project_a, _) = client_a
762 .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
763 .await;
764
765 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
766
767 let debugger_panel = workspace
768 .update_in(cx_a, |_workspace, window, cx| {
769 cx.spawn_in(window, DebugPanel::load)
770 })
771 .await
772 .unwrap();
773
774 workspace.update_in(cx_a, |workspace, window, cx| {
775 workspace.add_panel(debugger_panel, window, cx);
776 });
777
778 cx_a.run_until_parked();
779 let debug_panel = workspace
780 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
781 .unwrap();
782
783 let workspace_window = cx_a
784 .window_handle()
785 .downcast::<workspace::MultiWorkspace>()
786 .unwrap();
787
788 let count = Arc::new(AtomicUsize::new(0));
789 let session = debugger_ui::tests::start_debug_session_with(
790 &workspace_window,
791 cx_a,
792 DebugTaskDefinition {
793 adapter: "fake-adapter".into(),
794 label: "test".into(),
795 config: json!({
796 "request": "launch"
797 }),
798 tcp_connection: Some(TcpArgumentsTemplate {
799 port: None,
800 host: None,
801 timeout: None,
802 }),
803 },
804 move |client| {
805 let count = count.clone();
806 client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
807 if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
808 return RequestHandling::Exit;
809 }
810 RequestHandling::Respond(Ok(Capabilities::default()))
811 });
812 },
813 )
814 .unwrap();
815 cx_a.run_until_parked();
816
817 let client = session.update(
818 cx_a,
819 |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
820 );
821 client
822 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
823 reason: dap::StoppedEventReason::Pause,
824 description: None,
825 thread_id: Some(1),
826 preserve_focus_hint: None,
827 text: None,
828 all_threads_stopped: None,
829 hit_breakpoint_ids: None,
830 }))
831 .await;
832
833 cx_a.run_until_parked();
834
835 let active_session = debug_panel
836 .update(cx_a, |this, _| this.active_session())
837 .unwrap();
838
839 let running_state = active_session.update(cx_a, |active_session, _| {
840 active_session.running_state().clone()
841 });
842
843 assert_eq!(
844 client.id(),
845 running_state.read_with(cx_a, |running_state, _| running_state.session_id())
846 );
847 assert_eq!(
848 ThreadId(1),
849 running_state.read_with(cx_a, |running_state, _| running_state
850 .selected_thread_id()
851 .unwrap())
852 );
853
854 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
855 workspace.project().update(cx, |project, cx| {
856 project.dap_store().update(cx, |dap_store, cx| {
857 dap_store.shutdown_session(session.read(cx).session_id(), cx)
858 })
859 })
860 });
861
862 client_ssh.update(cx_a, |a, _| {
863 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
864 });
865
866 shutdown_session.await.unwrap();
867}
868
869#[gpui::test]
870async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
871 cx_a.update(|cx| {
872 release_channel::init(semver::Version::new(0, 0, 0), cx);
873 project::trusted_worktrees::init(HashMap::default(), cx);
874 });
875 server_cx.update(|cx| {
876 release_channel::init(semver::Version::new(0, 0, 0), cx);
877 project::trusted_worktrees::init(HashMap::default(), cx);
878 });
879
880 let mut server = TestServer::start(cx_a.executor().clone()).await;
881 let client_a = server.create_client(cx_a, "user_a").await;
882
883 let server_name = "override-rust-analyzer";
884 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
885
886 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
887 let remote_fs = FakeFs::new(server_cx.executor());
888 remote_fs
889 .insert_tree(
890 path!("/projects"),
891 json!({
892 "project_a": {
893 ".zed": {
894 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
895 },
896 "main.rs": "fn main() {}"
897 },
898 "project_b": { "lib.rs": "pub fn lib() {}" }
899 }),
900 )
901 .await;
902
903 server_cx.update(HeadlessProject::init);
904 let remote_http_client = Arc::new(BlockedHttpClient);
905 let node = NodeRuntime::unavailable();
906 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
907 languages.add(rust_lang());
908
909 let capabilities = lsp::ServerCapabilities {
910 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
911 ..lsp::ServerCapabilities::default()
912 };
913 let mut fake_language_servers = languages.register_fake_lsp(
914 "Rust",
915 FakeLspAdapter {
916 name: server_name,
917 capabilities: capabilities.clone(),
918 initializer: Some(Box::new({
919 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
920 move |fake_server| {
921 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
922 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
923 move |_params, _| {
924 lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
925 async move {
926 Ok(Some(vec![lsp::InlayHint {
927 position: lsp::Position::new(0, 0),
928 label: lsp::InlayHintLabel::String("hint".to_string()),
929 kind: None,
930 text_edits: None,
931 tooltip: None,
932 padding_left: None,
933 padding_right: None,
934 data: None,
935 }]))
936 }
937 },
938 );
939 }
940 })),
941 ..FakeLspAdapter::default()
942 },
943 );
944
945 let _headless_project = server_cx.new(|cx| {
946 HeadlessProject::new(
947 HeadlessAppState {
948 session: server_ssh,
949 fs: remote_fs.clone(),
950 http_client: remote_http_client,
951 node_runtime: node,
952 languages,
953 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
954 startup_time: std::time::Instant::now(),
955 },
956 true,
957 cx,
958 )
959 });
960
961 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
962 let (project_a, worktree_id_a) = client_a
963 .build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
964 .await;
965
966 cx_a.update(|cx| {
967 release_channel::init(semver::Version::new(0, 0, 0), cx);
968
969 SettingsStore::update_global(cx, |store, cx| {
970 store.update_user_settings(cx, |settings| {
971 let language_settings = &mut settings.project.all_languages.defaults;
972 language_settings.inlay_hints = Some(InlayHintSettingsContent {
973 enabled: Some(true),
974 ..InlayHintSettingsContent::default()
975 })
976 });
977 });
978 });
979
980 project_a
981 .update(cx_a, |project, cx| {
982 project.languages().add(rust_lang());
983 project.languages().register_fake_lsp_adapter(
984 "Rust",
985 FakeLspAdapter {
986 name: server_name,
987 capabilities,
988 ..FakeLspAdapter::default()
989 },
990 );
991 project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
992 })
993 .await
994 .unwrap();
995
996 cx_a.run_until_parked();
997
998 let worktree_ids = project_a.read_with(cx_a, |project, cx| {
999 project
1000 .worktrees(cx)
1001 .map(|wt| wt.read(cx).id())
1002 .collect::<Vec<_>>()
1003 });
1004 assert_eq!(worktree_ids.len(), 2);
1005
1006 let trusted_worktrees =
1007 cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
1008 let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
1009
1010 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1011 store.can_trust(&worktree_store, worktree_ids[0], cx)
1012 });
1013 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1014 store.can_trust(&worktree_store, worktree_ids[1], cx)
1015 });
1016 assert!(!can_trust_a, "project_a should be restricted initially");
1017 assert!(!can_trust_b, "project_b should be restricted initially");
1018
1019 let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
1020 store.has_restricted_worktrees(&worktree_store, cx)
1021 });
1022 assert!(has_restricted, "should have restricted worktrees");
1023
1024 let buffer_before_approval = project_a
1025 .update(cx_a, |project, cx| {
1026 project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
1027 })
1028 .await
1029 .unwrap();
1030
1031 let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
1032 Editor::new(
1033 EditorMode::full(),
1034 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
1035 Some(project_a.clone()),
1036 window,
1037 cx,
1038 )
1039 });
1040 cx_a.run_until_parked();
1041 let fake_language_server = fake_language_servers.next();
1042
1043 cx_a.read(|cx| {
1044 let file = buffer_before_approval.read(cx).file();
1045 assert_eq!(
1046 language_settings(Some("Rust".into()), file, cx).language_servers,
1047 ["...".to_string()],
1048 "remote .zed/settings.json must not sync before trust approval"
1049 )
1050 });
1051
1052 editor.update_in(cx_a, |editor, window, cx| {
1053 editor.handle_input("1", window, cx);
1054 });
1055 cx_a.run_until_parked();
1056 cx_a.executor().advance_clock(Duration::from_secs(1));
1057 assert_eq!(
1058 lsp_inlay_hint_request_count.load(Ordering::Acquire),
1059 0,
1060 "inlay hints must not be queried before trust approval"
1061 );
1062
1063 trusted_worktrees.update(cx_a, |store, cx| {
1064 store.trust(
1065 &worktree_store,
1066 HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1067 cx,
1068 );
1069 });
1070 cx_a.run_until_parked();
1071
1072 cx_a.read(|cx| {
1073 let file = buffer_before_approval.read(cx).file();
1074 assert_eq!(
1075 language_settings(Some("Rust".into()), file, cx).language_servers,
1076 ["override-rust-analyzer".to_string()],
1077 "remote .zed/settings.json should sync after trust approval"
1078 )
1079 });
1080 let _fake_language_server = fake_language_server.await.unwrap();
1081 editor.update_in(cx_a, |editor, window, cx| {
1082 editor.handle_input("1", window, cx);
1083 });
1084 cx_a.run_until_parked();
1085 cx_a.executor().advance_clock(Duration::from_secs(1));
1086 assert!(
1087 lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
1088 "inlay hints should be queried after trust approval"
1089 );
1090
1091 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1092 store.can_trust(&worktree_store, worktree_ids[0], cx)
1093 });
1094 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1095 store.can_trust(&worktree_store, worktree_ids[1], cx)
1096 });
1097 assert!(can_trust_a, "project_a should be trusted after trust()");
1098 assert!(!can_trust_b, "project_b should still be restricted");
1099
1100 trusted_worktrees.update(cx_a, |store, cx| {
1101 store.trust(
1102 &worktree_store,
1103 HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1104 cx,
1105 );
1106 });
1107
1108 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1109 store.can_trust(&worktree_store, worktree_ids[0], cx)
1110 });
1111 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1112 store.can_trust(&worktree_store, worktree_ids[1], cx)
1113 });
1114 assert!(can_trust_a, "project_a should remain trusted");
1115 assert!(can_trust_b, "project_b should now be trusted");
1116
1117 let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
1118 store.has_restricted_worktrees(&worktree_store, cx)
1119 });
1120 assert!(
1121 !has_restricted_after,
1122 "should have no restricted worktrees after trusting both"
1123 );
1124}