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