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