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