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