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