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