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};
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_git_worktrees(
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 remote_fs
427 .insert_tree("/project", json!({ ".git": {}, "file.txt": "content" }))
428 .await;
429
430 server_cx.update(HeadlessProject::init);
431 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
432 let headless_project = server_cx.new(|cx| {
433 HeadlessProject::new(
434 HeadlessAppState {
435 session: server_ssh,
436 fs: remote_fs.clone(),
437 http_client: Arc::new(BlockedHttpClient),
438 node_runtime: NodeRuntime::unavailable(),
439 languages,
440 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
441 startup_time: std::time::Instant::now(),
442 },
443 false,
444 cx,
445 )
446 });
447
448 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
449 let (project_a, _) = client_a
450 .build_ssh_project("/project", client_ssh, false, cx_a)
451 .await;
452
453 let active_call_a = cx_a.read(ActiveCall::global);
454 let project_id = active_call_a
455 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
456 .await
457 .unwrap();
458 let project_b = client_b.join_remote_project(project_id, cx_b).await;
459
460 executor.run_until_parked();
461
462 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
463
464 let worktrees = cx_b
465 .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees()))
466 .await
467 .unwrap()
468 .unwrap();
469 assert_eq!(worktrees.len(), 1);
470
471 let worktree_directory = PathBuf::from("/project");
472 cx_b.update(|cx| {
473 repo_b.update(cx, |repo, _| {
474 repo.create_worktree(
475 "feature-branch".to_string(),
476 worktree_directory.clone(),
477 Some("abc123".to_string()),
478 )
479 })
480 })
481 .await
482 .unwrap()
483 .unwrap();
484
485 executor.run_until_parked();
486
487 let worktrees = cx_b
488 .update(|cx| repo_b.update(cx, |repo, _| repo.worktrees()))
489 .await
490 .unwrap()
491 .unwrap();
492 assert_eq!(worktrees.len(), 2);
493 assert_eq!(worktrees[1].path, worktree_directory.join("feature-branch"));
494 assert_eq!(worktrees[1].ref_name.as_ref(), "refs/heads/feature-branch");
495 assert_eq!(worktrees[1].sha.as_ref(), "abc123");
496
497 let server_worktrees = {
498 let server_repo = server_cx.update(|cx| {
499 headless_project.update(cx, |headless_project, cx| {
500 headless_project
501 .git_store
502 .read(cx)
503 .repositories()
504 .values()
505 .next()
506 .unwrap()
507 .clone()
508 })
509 });
510 server_cx
511 .update(|cx| server_repo.update(cx, |repo, _| repo.worktrees()))
512 .await
513 .unwrap()
514 .unwrap()
515 };
516 assert_eq!(server_worktrees.len(), 2);
517 assert_eq!(
518 server_worktrees[1].path,
519 worktree_directory.join("feature-branch")
520 );
521}
522
523#[gpui::test]
524async fn test_ssh_collaboration_formatting_with_prettier(
525 executor: BackgroundExecutor,
526 cx_a: &mut TestAppContext,
527 cx_b: &mut TestAppContext,
528 server_cx: &mut TestAppContext,
529) {
530 cx_a.set_name("a");
531 cx_b.set_name("b");
532 server_cx.set_name("server");
533
534 cx_a.update(|cx| {
535 release_channel::init(semver::Version::new(0, 0, 0), cx);
536 });
537 server_cx.update(|cx| {
538 release_channel::init(semver::Version::new(0, 0, 0), cx);
539 });
540
541 let mut server = TestServer::start(executor.clone()).await;
542 let client_a = server.create_client(cx_a, "user_a").await;
543 let client_b = server.create_client(cx_b, "user_b").await;
544 server
545 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
546 .await;
547
548 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
549 let remote_fs = FakeFs::new(server_cx.executor());
550 let buffer_text = "let one = \"two\"";
551 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
552 remote_fs
553 .insert_tree(
554 path!("/project"),
555 serde_json::json!({ "a.ts": buffer_text }),
556 )
557 .await;
558
559 let test_plugin = "test_plugin";
560 let ts_lang = Arc::new(Language::new(
561 LanguageConfig {
562 name: "TypeScript".into(),
563 matcher: LanguageMatcher {
564 path_suffixes: vec!["ts".to_string()],
565 ..LanguageMatcher::default()
566 },
567 ..LanguageConfig::default()
568 },
569 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
570 ));
571 client_a.language_registry().add(ts_lang.clone());
572 client_b.language_registry().add(ts_lang.clone());
573
574 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
575 let mut fake_language_servers = languages.register_fake_lsp(
576 "TypeScript",
577 FakeLspAdapter {
578 prettier_plugins: vec![test_plugin],
579 ..Default::default()
580 },
581 );
582
583 // User A connects to the remote project via SSH.
584 server_cx.update(HeadlessProject::init);
585 let remote_http_client = Arc::new(BlockedHttpClient);
586 let _headless_project = server_cx.new(|cx| {
587 HeadlessProject::new(
588 HeadlessAppState {
589 session: server_ssh,
590 fs: remote_fs.clone(),
591 http_client: remote_http_client,
592 node_runtime: NodeRuntime::unavailable(),
593 languages,
594 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
595 startup_time: std::time::Instant::now(),
596 },
597 false,
598 cx,
599 )
600 });
601
602 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
603 let (project_a, worktree_id) = client_a
604 .build_ssh_project(path!("/project"), client_ssh, false, cx_a)
605 .await;
606
607 // While the SSH worktree is being scanned, user A shares the remote project.
608 let active_call_a = cx_a.read(ActiveCall::global);
609 let project_id = active_call_a
610 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
611 .await
612 .unwrap();
613
614 // User B joins the project.
615 let project_b = client_b.join_remote_project(project_id, cx_b).await;
616 executor.run_until_parked();
617
618 // Opens the buffer and formats it
619 let (buffer_b, _handle) = project_b
620 .update(cx_b, |p, cx| {
621 p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
622 })
623 .await
624 .expect("user B opens buffer for formatting");
625
626 cx_a.update(|cx| {
627 SettingsStore::update_global(cx, |store, cx| {
628 store.update_user_settings(cx, |file| {
629 file.project.all_languages.defaults.formatter = Some(FormatterList::default());
630 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
631 allowed: Some(true),
632 ..Default::default()
633 });
634 });
635 });
636 });
637 cx_b.update(|cx| {
638 SettingsStore::update_global(cx, |store, cx| {
639 store.update_user_settings(cx, |file| {
640 file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
641 Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
642 ));
643 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
644 allowed: Some(true),
645 ..Default::default()
646 });
647 });
648 });
649 });
650 let fake_language_server = fake_language_servers.next().await.unwrap();
651 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
652 panic!(
653 "Unexpected: prettier should be preferred since it's enabled and language supports it"
654 )
655 });
656
657 project_b
658 .update(cx_b, |project, cx| {
659 project.format(
660 HashSet::from_iter([buffer_b.clone()]),
661 LspFormatTarget::Buffers,
662 true,
663 FormatTrigger::Save,
664 cx,
665 )
666 })
667 .await
668 .unwrap();
669
670 executor.run_until_parked();
671 assert_eq!(
672 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
673 buffer_text.to_string() + "\n" + prettier_format_suffix,
674 "Prettier formatting was not applied to client buffer after client's request"
675 );
676
677 // User A opens and formats the same buffer too
678 let buffer_a = project_a
679 .update(cx_a, |p, cx| {
680 p.open_buffer((worktree_id, rel_path("a.ts")), cx)
681 })
682 .await
683 .expect("user A opens buffer for formatting");
684
685 cx_a.update(|cx| {
686 SettingsStore::update_global(cx, |store, cx| {
687 store.update_user_settings(cx, |file| {
688 file.project.all_languages.defaults.formatter = Some(FormatterList::default());
689 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
690 allowed: Some(true),
691 ..Default::default()
692 });
693 });
694 });
695 });
696 project_a
697 .update(cx_a, |project, cx| {
698 project.format(
699 HashSet::from_iter([buffer_a.clone()]),
700 LspFormatTarget::Buffers,
701 true,
702 FormatTrigger::Manual,
703 cx,
704 )
705 })
706 .await
707 .unwrap();
708
709 executor.run_until_parked();
710 assert_eq!(
711 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
712 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
713 "Prettier formatting was not applied to client buffer after host's request"
714 );
715}
716
717#[gpui::test]
718async fn test_remote_server_debugger(
719 cx_a: &mut TestAppContext,
720 server_cx: &mut TestAppContext,
721 executor: BackgroundExecutor,
722) {
723 cx_a.update(|cx| {
724 release_channel::init(semver::Version::new(0, 0, 0), cx);
725 command_palette_hooks::init(cx);
726 zlog::init_test();
727 dap_adapters::init(cx);
728 });
729 server_cx.update(|cx| {
730 release_channel::init(semver::Version::new(0, 0, 0), cx);
731 dap_adapters::init(cx);
732 });
733 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
734 let remote_fs = FakeFs::new(server_cx.executor());
735 remote_fs
736 .insert_tree(
737 path!("/code"),
738 json!({
739 "lib.rs": "fn one() -> usize { 1 }"
740 }),
741 )
742 .await;
743
744 // User A connects to the remote project via SSH.
745 server_cx.update(HeadlessProject::init);
746 let remote_http_client = Arc::new(BlockedHttpClient);
747 let node = NodeRuntime::unavailable();
748 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
749 let _headless_project = server_cx.new(|cx| {
750 HeadlessProject::new(
751 HeadlessAppState {
752 session: server_ssh,
753 fs: remote_fs.clone(),
754 http_client: remote_http_client,
755 node_runtime: node,
756 languages,
757 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
758 startup_time: std::time::Instant::now(),
759 },
760 false,
761 cx,
762 )
763 });
764
765 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
766 let mut server = TestServer::start(server_cx.executor()).await;
767 let client_a = server.create_client(cx_a, "user_a").await;
768 cx_a.update(|cx| {
769 debugger_ui::init(cx);
770 command_palette_hooks::init(cx);
771 });
772 let (project_a, _) = client_a
773 .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
774 .await;
775
776 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
777
778 let debugger_panel = workspace
779 .update_in(cx_a, |_workspace, window, cx| {
780 cx.spawn_in(window, DebugPanel::load)
781 })
782 .await
783 .unwrap();
784
785 workspace.update_in(cx_a, |workspace, window, cx| {
786 workspace.add_panel(debugger_panel, window, cx);
787 });
788
789 cx_a.run_until_parked();
790 let debug_panel = workspace
791 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
792 .unwrap();
793
794 let workspace_window = cx_a
795 .window_handle()
796 .downcast::<workspace::MultiWorkspace>()
797 .unwrap();
798
799 let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
800 cx_a.run_until_parked();
801 debug_panel.update(cx_a, |debug_panel, cx| {
802 assert_eq!(
803 debug_panel.active_session().unwrap().read(cx).session(cx),
804 session.clone()
805 )
806 });
807
808 session.update(
809 cx_a,
810 |session: &mut project::debugger::session::Session, _| {
811 assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
812 },
813 );
814
815 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
816 workspace.project().update(cx, |project, cx| {
817 project.dap_store().update(cx, |dap_store, cx| {
818 dap_store.shutdown_session(session.read(cx).session_id(), cx)
819 })
820 })
821 });
822
823 client_ssh.update(cx_a, |a, _| {
824 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
825 });
826
827 shutdown_session.await.unwrap();
828}
829
830#[gpui::test]
831async fn test_slow_adapter_startup_retries(
832 cx_a: &mut TestAppContext,
833 server_cx: &mut TestAppContext,
834 executor: BackgroundExecutor,
835) {
836 cx_a.update(|cx| {
837 release_channel::init(semver::Version::new(0, 0, 0), cx);
838 command_palette_hooks::init(cx);
839 zlog::init_test();
840 dap_adapters::init(cx);
841 });
842 server_cx.update(|cx| {
843 release_channel::init(semver::Version::new(0, 0, 0), cx);
844 dap_adapters::init(cx);
845 });
846 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
847 let remote_fs = FakeFs::new(server_cx.executor());
848 remote_fs
849 .insert_tree(
850 path!("/code"),
851 json!({
852 "lib.rs": "fn one() -> usize { 1 }"
853 }),
854 )
855 .await;
856
857 // User A connects to the remote project via SSH.
858 server_cx.update(HeadlessProject::init);
859 let remote_http_client = Arc::new(BlockedHttpClient);
860 let node = NodeRuntime::unavailable();
861 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
862 let _headless_project = server_cx.new(|cx| {
863 HeadlessProject::new(
864 HeadlessAppState {
865 session: server_ssh,
866 fs: remote_fs.clone(),
867 http_client: remote_http_client,
868 node_runtime: node,
869 languages,
870 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
871 startup_time: std::time::Instant::now(),
872 },
873 false,
874 cx,
875 )
876 });
877
878 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
879 let mut server = TestServer::start(server_cx.executor()).await;
880 let client_a = server.create_client(cx_a, "user_a").await;
881 cx_a.update(|cx| {
882 debugger_ui::init(cx);
883 command_palette_hooks::init(cx);
884 });
885 let (project_a, _) = client_a
886 .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
887 .await;
888
889 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
890
891 let debugger_panel = workspace
892 .update_in(cx_a, |_workspace, window, cx| {
893 cx.spawn_in(window, DebugPanel::load)
894 })
895 .await
896 .unwrap();
897
898 workspace.update_in(cx_a, |workspace, window, cx| {
899 workspace.add_panel(debugger_panel, window, cx);
900 });
901
902 cx_a.run_until_parked();
903 let debug_panel = workspace
904 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
905 .unwrap();
906
907 let workspace_window = cx_a
908 .window_handle()
909 .downcast::<workspace::MultiWorkspace>()
910 .unwrap();
911
912 let count = Arc::new(AtomicUsize::new(0));
913 let session = debugger_ui::tests::start_debug_session_with(
914 &workspace_window,
915 cx_a,
916 DebugTaskDefinition {
917 adapter: "fake-adapter".into(),
918 label: "test".into(),
919 config: json!({
920 "request": "launch"
921 }),
922 tcp_connection: Some(TcpArgumentsTemplate {
923 port: None,
924 host: None,
925 timeout: None,
926 }),
927 },
928 move |client| {
929 let count = count.clone();
930 client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
931 if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
932 return RequestHandling::Exit;
933 }
934 RequestHandling::Respond(Ok(Capabilities::default()))
935 });
936 },
937 )
938 .unwrap();
939 cx_a.run_until_parked();
940
941 let client = session.update(
942 cx_a,
943 |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
944 );
945 client
946 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
947 reason: dap::StoppedEventReason::Pause,
948 description: None,
949 thread_id: Some(1),
950 preserve_focus_hint: None,
951 text: None,
952 all_threads_stopped: None,
953 hit_breakpoint_ids: None,
954 }))
955 .await;
956
957 cx_a.run_until_parked();
958
959 let active_session = debug_panel
960 .update(cx_a, |this, _| this.active_session())
961 .unwrap();
962
963 let running_state = active_session.update(cx_a, |active_session, _| {
964 active_session.running_state().clone()
965 });
966
967 assert_eq!(
968 client.id(),
969 running_state.read_with(cx_a, |running_state, _| running_state.session_id())
970 );
971 assert_eq!(
972 ThreadId(1),
973 running_state.read_with(cx_a, |running_state, _| running_state
974 .selected_thread_id()
975 .unwrap())
976 );
977
978 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
979 workspace.project().update(cx, |project, cx| {
980 project.dap_store().update(cx, |dap_store, cx| {
981 dap_store.shutdown_session(session.read(cx).session_id(), cx)
982 })
983 })
984 });
985
986 client_ssh.update(cx_a, |a, _| {
987 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
988 });
989
990 shutdown_session.await.unwrap();
991}
992
993#[gpui::test]
994async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
995 cx_a.update(|cx| {
996 release_channel::init(semver::Version::new(0, 0, 0), cx);
997 project::trusted_worktrees::init(HashMap::default(), cx);
998 });
999 server_cx.update(|cx| {
1000 release_channel::init(semver::Version::new(0, 0, 0), cx);
1001 project::trusted_worktrees::init(HashMap::default(), cx);
1002 });
1003
1004 let mut server = TestServer::start(cx_a.executor().clone()).await;
1005 let client_a = server.create_client(cx_a, "user_a").await;
1006
1007 let server_name = "override-rust-analyzer";
1008 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
1009
1010 let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
1011 let remote_fs = FakeFs::new(server_cx.executor());
1012 remote_fs
1013 .insert_tree(
1014 path!("/projects"),
1015 json!({
1016 "project_a": {
1017 ".zed": {
1018 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
1019 },
1020 "main.rs": "fn main() {}"
1021 },
1022 "project_b": { "lib.rs": "pub fn lib() {}" }
1023 }),
1024 )
1025 .await;
1026
1027 server_cx.update(HeadlessProject::init);
1028 let remote_http_client = Arc::new(BlockedHttpClient);
1029 let node = NodeRuntime::unavailable();
1030 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
1031 languages.add(rust_lang());
1032
1033 let capabilities = lsp::ServerCapabilities {
1034 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
1035 ..lsp::ServerCapabilities::default()
1036 };
1037 let mut fake_language_servers = languages.register_fake_lsp(
1038 "Rust",
1039 FakeLspAdapter {
1040 name: server_name,
1041 capabilities: capabilities.clone(),
1042 initializer: Some(Box::new({
1043 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
1044 move |fake_server| {
1045 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
1046 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
1047 move |_params, _| {
1048 lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
1049 async move {
1050 Ok(Some(vec![lsp::InlayHint {
1051 position: lsp::Position::new(0, 0),
1052 label: lsp::InlayHintLabel::String("hint".to_string()),
1053 kind: None,
1054 text_edits: None,
1055 tooltip: None,
1056 padding_left: None,
1057 padding_right: None,
1058 data: None,
1059 }]))
1060 }
1061 },
1062 );
1063 }
1064 })),
1065 ..FakeLspAdapter::default()
1066 },
1067 );
1068
1069 let _headless_project = server_cx.new(|cx| {
1070 HeadlessProject::new(
1071 HeadlessAppState {
1072 session: server_ssh,
1073 fs: remote_fs.clone(),
1074 http_client: remote_http_client,
1075 node_runtime: node,
1076 languages,
1077 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
1078 startup_time: std::time::Instant::now(),
1079 },
1080 true,
1081 cx,
1082 )
1083 });
1084
1085 let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
1086 let (project_a, worktree_id_a) = client_a
1087 .build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
1088 .await;
1089
1090 cx_a.update(|cx| {
1091 release_channel::init(semver::Version::new(0, 0, 0), cx);
1092
1093 SettingsStore::update_global(cx, |store, cx| {
1094 store.update_user_settings(cx, |settings| {
1095 let language_settings = &mut settings.project.all_languages.defaults;
1096 language_settings.inlay_hints = Some(InlayHintSettingsContent {
1097 enabled: Some(true),
1098 ..InlayHintSettingsContent::default()
1099 })
1100 });
1101 });
1102 });
1103
1104 project_a
1105 .update(cx_a, |project, cx| {
1106 project.languages().add(rust_lang());
1107 project.languages().register_fake_lsp_adapter(
1108 "Rust",
1109 FakeLspAdapter {
1110 name: server_name,
1111 capabilities,
1112 ..FakeLspAdapter::default()
1113 },
1114 );
1115 project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
1116 })
1117 .await
1118 .unwrap();
1119
1120 cx_a.run_until_parked();
1121
1122 let worktree_ids = project_a.read_with(cx_a, |project, cx| {
1123 project
1124 .worktrees(cx)
1125 .map(|wt| wt.read(cx).id())
1126 .collect::<Vec<_>>()
1127 });
1128 assert_eq!(worktree_ids.len(), 2);
1129
1130 let trusted_worktrees =
1131 cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
1132 let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
1133
1134 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1135 store.can_trust(&worktree_store, worktree_ids[0], cx)
1136 });
1137 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1138 store.can_trust(&worktree_store, worktree_ids[1], cx)
1139 });
1140 assert!(!can_trust_a, "project_a should be restricted initially");
1141 assert!(!can_trust_b, "project_b should be restricted initially");
1142
1143 let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
1144 store.has_restricted_worktrees(&worktree_store, cx)
1145 });
1146 assert!(has_restricted, "should have restricted worktrees");
1147
1148 let buffer_before_approval = project_a
1149 .update(cx_a, |project, cx| {
1150 project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
1151 })
1152 .await
1153 .unwrap();
1154
1155 let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
1156 Editor::new(
1157 EditorMode::full(),
1158 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
1159 Some(project_a.clone()),
1160 window,
1161 cx,
1162 )
1163 });
1164 cx_a.run_until_parked();
1165 let fake_language_server = fake_language_servers.next();
1166
1167 cx_a.read(|cx| {
1168 let file = buffer_before_approval.read(cx).file();
1169 assert_eq!(
1170 language_settings(Some("Rust".into()), file, cx).language_servers,
1171 ["...".to_string()],
1172 "remote .zed/settings.json must not sync before trust approval"
1173 )
1174 });
1175
1176 editor.update_in(cx_a, |editor, window, cx| {
1177 editor.handle_input("1", window, cx);
1178 });
1179 cx_a.run_until_parked();
1180 cx_a.executor().advance_clock(Duration::from_secs(1));
1181 assert_eq!(
1182 lsp_inlay_hint_request_count.load(Ordering::Acquire),
1183 0,
1184 "inlay hints must not be queried before trust approval"
1185 );
1186
1187 trusted_worktrees.update(cx_a, |store, cx| {
1188 store.trust(
1189 &worktree_store,
1190 HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1191 cx,
1192 );
1193 });
1194 cx_a.run_until_parked();
1195
1196 cx_a.read(|cx| {
1197 let file = buffer_before_approval.read(cx).file();
1198 assert_eq!(
1199 language_settings(Some("Rust".into()), file, cx).language_servers,
1200 ["override-rust-analyzer".to_string()],
1201 "remote .zed/settings.json should sync after trust approval"
1202 )
1203 });
1204 let _fake_language_server = fake_language_server.await.unwrap();
1205 editor.update_in(cx_a, |editor, window, cx| {
1206 editor.handle_input("1", window, cx);
1207 });
1208 cx_a.run_until_parked();
1209 cx_a.executor().advance_clock(Duration::from_secs(1));
1210 assert!(
1211 lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
1212 "inlay hints should be queried after trust approval"
1213 );
1214
1215 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1216 store.can_trust(&worktree_store, worktree_ids[0], cx)
1217 });
1218 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1219 store.can_trust(&worktree_store, worktree_ids[1], cx)
1220 });
1221 assert!(can_trust_a, "project_a should be trusted after trust()");
1222 assert!(!can_trust_b, "project_b should still be restricted");
1223
1224 trusted_worktrees.update(cx_a, |store, cx| {
1225 store.trust(
1226 &worktree_store,
1227 HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1228 cx,
1229 );
1230 });
1231
1232 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1233 store.can_trust(&worktree_store, worktree_ids[0], cx)
1234 });
1235 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1236 store.can_trust(&worktree_store, worktree_ids[1], cx)
1237 });
1238 assert!(can_trust_a, "project_a should remain trusted");
1239 assert!(can_trust_b, "project_b should now be trusted");
1240
1241 let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
1242 store.has_restricted_worktrees(&worktree_store, cx)
1243 });
1244 assert!(
1245 !has_restricted_after,
1246 "should have no restricted worktrees after trusting both"
1247 );
1248}