1use crate::tests::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::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext};
12use http_client::BlockedHttpClient;
13use language::{
14 FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
15 language_settings::{Formatter, FormatterList, language_settings},
16 rust_lang, tree_sitter_typescript,
17};
18use node_runtime::NodeRuntime;
19use project::{
20 ProjectPath,
21 debugger::session::ThreadId,
22 lsp_store::{FormatTrigger, LspFormatTarget},
23 trusted_worktrees::{PathTrust, TrustedWorktrees},
24};
25use remote::RemoteClient;
26use remote_server::{HeadlessAppState, HeadlessProject};
27use rpc::proto;
28use serde_json::json;
29use settings::{
30 InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
31 SettingsStore,
32};
33use std::{
34 path::Path,
35 sync::{
36 Arc,
37 atomic::{AtomicUsize, Ordering},
38 },
39 time::Duration,
40};
41use task::TcpArgumentsTemplate;
42use util::{path, rel_path::rel_path};
43
44#[gpui::test(iterations = 10)]
45async fn test_sharing_an_ssh_remote_project(
46 cx_a: &mut TestAppContext,
47 cx_b: &mut TestAppContext,
48 server_cx: &mut TestAppContext,
49) {
50 let executor = cx_a.executor();
51 cx_a.update(|cx| {
52 release_channel::init(semver::Version::new(0, 0, 0), cx);
53 });
54 server_cx.update(|cx| {
55 release_channel::init(semver::Version::new(0, 0, 0), cx);
56 });
57 let mut server = TestServer::start(executor.clone()).await;
58 let client_a = server.create_client(cx_a, "user_a").await;
59 let client_b = server.create_client(cx_b, "user_b").await;
60 server
61 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
62 .await;
63
64 // Set up project on remote FS
65 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
66 let remote_fs = FakeFs::new(server_cx.executor());
67 remote_fs
68 .insert_tree(
69 path!("/code"),
70 json!({
71 "project1": {
72 ".zed": {
73 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
74 },
75 "README.md": "# project 1",
76 "src": {
77 "lib.rs": "fn one() -> usize { 1 }"
78 }
79 },
80 "project2": {
81 "README.md": "# project 2",
82 },
83 }),
84 )
85 .await;
86
87 // User A connects to the remote project via SSH.
88 server_cx.update(HeadlessProject::init);
89 let remote_http_client = Arc::new(BlockedHttpClient);
90 let node = NodeRuntime::unavailable();
91 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
92 let _headless_project = server_cx.new(|cx| {
93 HeadlessProject::new(
94 HeadlessAppState {
95 session: server_ssh,
96 fs: remote_fs.clone(),
97 http_client: remote_http_client,
98 node_runtime: node,
99 languages,
100 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
101 },
102 false,
103 cx,
104 )
105 });
106
107 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
108 let (project_a, worktree_id) = client_a
109 .build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
110 .await;
111
112 // While the SSH worktree is being scanned, user A shares the remote project.
113 let active_call_a = cx_a.read(ActiveCall::global);
114 let project_id = active_call_a
115 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
116 .await
117 .unwrap();
118
119 // User B joins the project.
120 let project_b = client_b.join_remote_project(project_id, cx_b).await;
121 let worktree_b = project_b
122 .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
123 .unwrap();
124
125 let worktree_a = project_a
126 .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
127 .unwrap();
128
129 executor.run_until_parked();
130
131 worktree_a.update(cx_a, |worktree, _cx| {
132 assert_eq!(
133 worktree.paths().collect::<Vec<_>>(),
134 vec![
135 rel_path(".zed"),
136 rel_path(".zed/settings.json"),
137 rel_path("README.md"),
138 rel_path("src"),
139 rel_path("src/lib.rs"),
140 ]
141 );
142 });
143
144 worktree_b.update(cx_b, |worktree, _cx| {
145 assert_eq!(
146 worktree.paths().collect::<Vec<_>>(),
147 vec![
148 rel_path(".zed"),
149 rel_path(".zed/settings.json"),
150 rel_path("README.md"),
151 rel_path("src"),
152 rel_path("src/lib.rs"),
153 ]
154 );
155 });
156
157 // User B can open buffers in the remote project.
158 let buffer_b = project_b
159 .update(cx_b, |project, cx| {
160 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
161 })
162 .await
163 .unwrap();
164 buffer_b.update(cx_b, |buffer, cx| {
165 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
166 let ix = buffer.text().find('1').unwrap();
167 buffer.edit([(ix..ix + 1, "100")], None, cx);
168 });
169
170 executor.run_until_parked();
171
172 cx_b.read(|cx| {
173 let file = buffer_b.read(cx).file();
174 assert_eq!(
175 language_settings(Some("Rust".into()), file, cx).language_servers,
176 ["override-rust-analyzer".to_string()]
177 )
178 });
179
180 project_b
181 .update(cx_b, |project, cx| {
182 project.save_buffer_as(
183 buffer_b.clone(),
184 ProjectPath {
185 worktree_id: worktree_id.to_owned(),
186 path: rel_path("src/renamed.rs").into(),
187 },
188 cx,
189 )
190 })
191 .await
192 .unwrap();
193 assert_eq!(
194 remote_fs
195 .load(path!("/code/project1/src/renamed.rs").as_ref())
196 .await
197 .unwrap(),
198 "fn one() -> usize { 100 }"
199 );
200 cx_b.run_until_parked();
201 cx_b.update(|cx| {
202 assert_eq!(
203 buffer_b.read(cx).file().unwrap().path().as_ref(),
204 rel_path("src/renamed.rs")
205 );
206 });
207}
208
209#[gpui::test]
210async fn test_ssh_collaboration_git_branches(
211 executor: BackgroundExecutor,
212 cx_a: &mut TestAppContext,
213 cx_b: &mut TestAppContext,
214 server_cx: &mut TestAppContext,
215) {
216 cx_a.set_name("a");
217 cx_b.set_name("b");
218 server_cx.set_name("server");
219
220 cx_a.update(|cx| {
221 release_channel::init(semver::Version::new(0, 0, 0), cx);
222 });
223 server_cx.update(|cx| {
224 release_channel::init(semver::Version::new(0, 0, 0), cx);
225 });
226
227 let mut server = TestServer::start(executor.clone()).await;
228 let client_a = server.create_client(cx_a, "user_a").await;
229 let client_b = server.create_client(cx_b, "user_b").await;
230 server
231 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
232 .await;
233
234 // Set up project on remote FS
235 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
236 let remote_fs = FakeFs::new(server_cx.executor());
237 remote_fs
238 .insert_tree("/project", serde_json::json!({ ".git":{} }))
239 .await;
240
241 let branches = ["main", "dev", "feature-1"];
242 let branches_set = branches
243 .iter()
244 .map(ToString::to_string)
245 .collect::<HashSet<_>>();
246 remote_fs.insert_branches(Path::new("/project/.git"), &branches);
247
248 // User A connects to the remote project via SSH.
249 server_cx.update(HeadlessProject::init);
250 let remote_http_client = Arc::new(BlockedHttpClient);
251 let node = NodeRuntime::unavailable();
252 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
253 let headless_project = server_cx.new(|cx| {
254 HeadlessProject::new(
255 HeadlessAppState {
256 session: server_ssh,
257 fs: remote_fs.clone(),
258 http_client: remote_http_client,
259 node_runtime: node,
260 languages,
261 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
262 },
263 false,
264 cx,
265 )
266 });
267
268 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
269 let (project_a, _) = client_a
270 .build_ssh_project("/project", client_ssh, false, cx_a)
271 .await;
272
273 // While the SSH worktree is being scanned, user A shares the remote project.
274 let active_call_a = cx_a.read(ActiveCall::global);
275 let project_id = active_call_a
276 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
277 .await
278 .unwrap();
279
280 // User B joins the project.
281 let project_b = client_b.join_remote_project(project_id, cx_b).await;
282
283 // Give client A sometime to see that B has joined, and that the headless server
284 // has some git repositories
285 executor.run_until_parked();
286
287 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
288
289 let branches_b = cx_b
290 .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
291 .await
292 .unwrap()
293 .unwrap();
294
295 let new_branch = branches[2];
296
297 let branches_b = branches_b
298 .into_iter()
299 .map(|branch| branch.name().to_string())
300 .collect::<HashSet<_>>();
301
302 assert_eq!(&branches_b, &branches_set);
303
304 cx_b.update(|cx| {
305 repo_b.update(cx, |repo_b, _cx| {
306 repo_b.change_branch(new_branch.to_string())
307 })
308 })
309 .await
310 .unwrap()
311 .unwrap();
312
313 executor.run_until_parked();
314
315 let server_branch = server_cx.update(|cx| {
316 headless_project.update(cx, |headless_project, cx| {
317 headless_project.git_store.update(cx, |git_store, cx| {
318 git_store
319 .repositories()
320 .values()
321 .next()
322 .unwrap()
323 .read(cx)
324 .branch
325 .as_ref()
326 .unwrap()
327 .clone()
328 })
329 })
330 });
331
332 assert_eq!(server_branch.name(), branches[2]);
333
334 // Also try creating a new branch
335 cx_b.update(|cx| {
336 repo_b.update(cx, |repo_b, _cx| {
337 repo_b.create_branch("totally-new-branch".to_string(), None)
338 })
339 })
340 .await
341 .unwrap()
342 .unwrap();
343
344 cx_b.update(|cx| {
345 repo_b.update(cx, |repo_b, _cx| {
346 repo_b.change_branch("totally-new-branch".to_string())
347 })
348 })
349 .await
350 .unwrap()
351 .unwrap();
352
353 executor.run_until_parked();
354
355 let server_branch = server_cx.update(|cx| {
356 headless_project.update(cx, |headless_project, cx| {
357 headless_project.git_store.update(cx, |git_store, cx| {
358 git_store
359 .repositories()
360 .values()
361 .next()
362 .unwrap()
363 .read(cx)
364 .branch
365 .as_ref()
366 .unwrap()
367 .clone()
368 })
369 })
370 });
371
372 assert_eq!(server_branch.name(), "totally-new-branch");
373
374 // Remove the git repository and check that all participants get the update.
375 remote_fs
376 .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
377 .await
378 .unwrap();
379 executor.run_until_parked();
380
381 project_a.update(cx_a, |project, cx| {
382 pretty_assertions::assert_eq!(
383 project.git_store().read(cx).repo_snapshots(cx),
384 HashMap::default()
385 );
386 });
387 project_b.update(cx_b, |project, cx| {
388 pretty_assertions::assert_eq!(
389 project.git_store().read(cx).repo_snapshots(cx),
390 HashMap::default()
391 );
392 });
393}
394
395#[gpui::test]
396async fn test_ssh_collaboration_formatting_with_prettier(
397 executor: BackgroundExecutor,
398 cx_a: &mut TestAppContext,
399 cx_b: &mut TestAppContext,
400 server_cx: &mut TestAppContext,
401) {
402 cx_a.set_name("a");
403 cx_b.set_name("b");
404 server_cx.set_name("server");
405
406 cx_a.update(|cx| {
407 release_channel::init(semver::Version::new(0, 0, 0), cx);
408 });
409 server_cx.update(|cx| {
410 release_channel::init(semver::Version::new(0, 0, 0), cx);
411 });
412
413 let mut server = TestServer::start(executor.clone()).await;
414 let client_a = server.create_client(cx_a, "user_a").await;
415 let client_b = server.create_client(cx_b, "user_b").await;
416 server
417 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
418 .await;
419
420 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
421 let remote_fs = FakeFs::new(server_cx.executor());
422 let buffer_text = "let one = \"two\"";
423 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
424 remote_fs
425 .insert_tree(
426 path!("/project"),
427 serde_json::json!({ "a.ts": buffer_text }),
428 )
429 .await;
430
431 let test_plugin = "test_plugin";
432 let ts_lang = Arc::new(Language::new(
433 LanguageConfig {
434 name: "TypeScript".into(),
435 matcher: LanguageMatcher {
436 path_suffixes: vec!["ts".to_string()],
437 ..LanguageMatcher::default()
438 },
439 ..LanguageConfig::default()
440 },
441 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
442 ));
443 client_a.language_registry().add(ts_lang.clone());
444 client_b.language_registry().add(ts_lang.clone());
445
446 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
447 let mut fake_language_servers = languages.register_fake_lsp(
448 "TypeScript",
449 FakeLspAdapter {
450 prettier_plugins: vec![test_plugin],
451 ..Default::default()
452 },
453 );
454
455 // User A connects to the remote project via SSH.
456 server_cx.update(HeadlessProject::init);
457 let remote_http_client = Arc::new(BlockedHttpClient);
458 let _headless_project = server_cx.new(|cx| {
459 HeadlessProject::new(
460 HeadlessAppState {
461 session: server_ssh,
462 fs: remote_fs.clone(),
463 http_client: remote_http_client,
464 node_runtime: NodeRuntime::unavailable(),
465 languages,
466 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
467 },
468 false,
469 cx,
470 )
471 });
472
473 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
474 let (project_a, worktree_id) = client_a
475 .build_ssh_project(path!("/project"), client_ssh, false, cx_a)
476 .await;
477
478 // While the SSH worktree is being scanned, user A shares the remote project.
479 let active_call_a = cx_a.read(ActiveCall::global);
480 let project_id = active_call_a
481 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
482 .await
483 .unwrap();
484
485 // User B joins the project.
486 let project_b = client_b.join_remote_project(project_id, cx_b).await;
487 executor.run_until_parked();
488
489 // Opens the buffer and formats it
490 let (buffer_b, _handle) = project_b
491 .update(cx_b, |p, cx| {
492 p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
493 })
494 .await
495 .expect("user B opens buffer for formatting");
496
497 cx_a.update(|cx| {
498 SettingsStore::update_global(cx, |store, cx| {
499 store.update_user_settings(cx, |file| {
500 file.project.all_languages.defaults.formatter = Some(FormatterList::default());
501 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
502 allowed: Some(true),
503 ..Default::default()
504 });
505 });
506 });
507 });
508 cx_b.update(|cx| {
509 SettingsStore::update_global(cx, |store, cx| {
510 store.update_user_settings(cx, |file| {
511 file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
512 Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
513 ));
514 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
515 allowed: Some(true),
516 ..Default::default()
517 });
518 });
519 });
520 });
521 let fake_language_server = fake_language_servers.next().await.unwrap();
522 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
523 panic!(
524 "Unexpected: prettier should be preferred since it's enabled and language supports it"
525 )
526 });
527
528 project_b
529 .update(cx_b, |project, cx| {
530 project.format(
531 HashSet::from_iter([buffer_b.clone()]),
532 LspFormatTarget::Buffers,
533 true,
534 FormatTrigger::Save,
535 cx,
536 )
537 })
538 .await
539 .unwrap();
540
541 executor.run_until_parked();
542 assert_eq!(
543 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
544 buffer_text.to_string() + "\n" + prettier_format_suffix,
545 "Prettier formatting was not applied to client buffer after client's request"
546 );
547
548 // User A opens and formats the same buffer too
549 let buffer_a = project_a
550 .update(cx_a, |p, cx| {
551 p.open_buffer((worktree_id, rel_path("a.ts")), cx)
552 })
553 .await
554 .expect("user A opens buffer for formatting");
555
556 cx_a.update(|cx| {
557 SettingsStore::update_global(cx, |store, cx| {
558 store.update_user_settings(cx, |file| {
559 file.project.all_languages.defaults.formatter = Some(FormatterList::default());
560 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
561 allowed: Some(true),
562 ..Default::default()
563 });
564 });
565 });
566 });
567 project_a
568 .update(cx_a, |project, cx| {
569 project.format(
570 HashSet::from_iter([buffer_a.clone()]),
571 LspFormatTarget::Buffers,
572 true,
573 FormatTrigger::Manual,
574 cx,
575 )
576 })
577 .await
578 .unwrap();
579
580 executor.run_until_parked();
581 assert_eq!(
582 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
583 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
584 "Prettier formatting was not applied to client buffer after host's request"
585 );
586}
587
588#[gpui::test]
589async fn test_remote_server_debugger(
590 cx_a: &mut TestAppContext,
591 server_cx: &mut TestAppContext,
592 executor: BackgroundExecutor,
593) {
594 cx_a.update(|cx| {
595 release_channel::init(semver::Version::new(0, 0, 0), cx);
596 command_palette_hooks::init(cx);
597 zlog::init_test();
598 dap_adapters::init(cx);
599 });
600 server_cx.update(|cx| {
601 release_channel::init(semver::Version::new(0, 0, 0), cx);
602 dap_adapters::init(cx);
603 });
604 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
605 let remote_fs = FakeFs::new(server_cx.executor());
606 remote_fs
607 .insert_tree(
608 path!("/code"),
609 json!({
610 "lib.rs": "fn one() -> usize { 1 }"
611 }),
612 )
613 .await;
614
615 // User A connects to the remote project via SSH.
616 server_cx.update(HeadlessProject::init);
617 let remote_http_client = Arc::new(BlockedHttpClient);
618 let node = NodeRuntime::unavailable();
619 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
620 let _headless_project = server_cx.new(|cx| {
621 HeadlessProject::new(
622 HeadlessAppState {
623 session: server_ssh,
624 fs: remote_fs.clone(),
625 http_client: remote_http_client,
626 node_runtime: node,
627 languages,
628 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
629 },
630 false,
631 cx,
632 )
633 });
634
635 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
636 let mut server = TestServer::start(server_cx.executor()).await;
637 let client_a = server.create_client(cx_a, "user_a").await;
638 cx_a.update(|cx| {
639 debugger_ui::init(cx);
640 command_palette_hooks::init(cx);
641 });
642 let (project_a, _) = client_a
643 .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
644 .await;
645
646 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
647
648 let debugger_panel = workspace
649 .update_in(cx_a, |_workspace, window, cx| {
650 cx.spawn_in(window, DebugPanel::load)
651 })
652 .await
653 .unwrap();
654
655 workspace.update_in(cx_a, |workspace, window, cx| {
656 workspace.add_panel(debugger_panel, window, cx);
657 });
658
659 cx_a.run_until_parked();
660 let debug_panel = workspace
661 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
662 .unwrap();
663
664 let workspace_window = cx_a
665 .window_handle()
666 .downcast::<workspace::Workspace>()
667 .unwrap();
668
669 let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
670 cx_a.run_until_parked();
671 debug_panel.update(cx_a, |debug_panel, cx| {
672 assert_eq!(
673 debug_panel.active_session().unwrap().read(cx).session(cx),
674 session
675 )
676 });
677
678 session.update(cx_a, |session, _| {
679 assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
680 });
681
682 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
683 workspace.project().update(cx, |project, cx| {
684 project.dap_store().update(cx, |dap_store, cx| {
685 dap_store.shutdown_session(session.read(cx).session_id(), cx)
686 })
687 })
688 });
689
690 client_ssh.update(cx_a, |a, _| {
691 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
692 });
693
694 shutdown_session.await.unwrap();
695}
696
697#[gpui::test]
698async fn test_slow_adapter_startup_retries(
699 cx_a: &mut TestAppContext,
700 server_cx: &mut TestAppContext,
701 executor: BackgroundExecutor,
702) {
703 cx_a.update(|cx| {
704 release_channel::init(semver::Version::new(0, 0, 0), cx);
705 command_palette_hooks::init(cx);
706 zlog::init_test();
707 dap_adapters::init(cx);
708 });
709 server_cx.update(|cx| {
710 release_channel::init(semver::Version::new(0, 0, 0), cx);
711 dap_adapters::init(cx);
712 });
713 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
714 let remote_fs = FakeFs::new(server_cx.executor());
715 remote_fs
716 .insert_tree(
717 path!("/code"),
718 json!({
719 "lib.rs": "fn one() -> usize { 1 }"
720 }),
721 )
722 .await;
723
724 // User A connects to the remote project via SSH.
725 server_cx.update(HeadlessProject::init);
726 let remote_http_client = Arc::new(BlockedHttpClient);
727 let node = NodeRuntime::unavailable();
728 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
729 let _headless_project = server_cx.new(|cx| {
730 HeadlessProject::new(
731 HeadlessAppState {
732 session: server_ssh,
733 fs: remote_fs.clone(),
734 http_client: remote_http_client,
735 node_runtime: node,
736 languages,
737 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
738 },
739 false,
740 cx,
741 )
742 });
743
744 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
745 let mut server = TestServer::start(server_cx.executor()).await;
746 let client_a = server.create_client(cx_a, "user_a").await;
747 cx_a.update(|cx| {
748 debugger_ui::init(cx);
749 command_palette_hooks::init(cx);
750 });
751 let (project_a, _) = client_a
752 .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
753 .await;
754
755 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
756
757 let debugger_panel = workspace
758 .update_in(cx_a, |_workspace, window, cx| {
759 cx.spawn_in(window, DebugPanel::load)
760 })
761 .await
762 .unwrap();
763
764 workspace.update_in(cx_a, |workspace, window, cx| {
765 workspace.add_panel(debugger_panel, window, cx);
766 });
767
768 cx_a.run_until_parked();
769 let debug_panel = workspace
770 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
771 .unwrap();
772
773 let workspace_window = cx_a
774 .window_handle()
775 .downcast::<workspace::Workspace>()
776 .unwrap();
777
778 let count = Arc::new(AtomicUsize::new(0));
779 let session = debugger_ui::tests::start_debug_session_with(
780 &workspace_window,
781 cx_a,
782 DebugTaskDefinition {
783 adapter: "fake-adapter".into(),
784 label: "test".into(),
785 config: json!({
786 "request": "launch"
787 }),
788 tcp_connection: Some(TcpArgumentsTemplate {
789 port: None,
790 host: None,
791 timeout: None,
792 }),
793 },
794 move |client| {
795 let count = count.clone();
796 client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
797 if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
798 return RequestHandling::Exit;
799 }
800 RequestHandling::Respond(Ok(Capabilities::default()))
801 });
802 },
803 )
804 .unwrap();
805 cx_a.run_until_parked();
806
807 let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
808 client
809 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
810 reason: dap::StoppedEventReason::Pause,
811 description: None,
812 thread_id: Some(1),
813 preserve_focus_hint: None,
814 text: None,
815 all_threads_stopped: None,
816 hit_breakpoint_ids: None,
817 }))
818 .await;
819
820 cx_a.run_until_parked();
821
822 let active_session = debug_panel
823 .update(cx_a, |this, _| this.active_session())
824 .unwrap();
825
826 let running_state = active_session.update(cx_a, |active_session, _| {
827 active_session.running_state().clone()
828 });
829
830 assert_eq!(
831 client.id(),
832 running_state.read_with(cx_a, |running_state, _| running_state.session_id())
833 );
834 assert_eq!(
835 ThreadId(1),
836 running_state.read_with(cx_a, |running_state, _| running_state
837 .selected_thread_id()
838 .unwrap())
839 );
840
841 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
842 workspace.project().update(cx, |project, cx| {
843 project.dap_store().update(cx, |dap_store, cx| {
844 dap_store.shutdown_session(session.read(cx).session_id(), cx)
845 })
846 })
847 });
848
849 client_ssh.update(cx_a, |a, _| {
850 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
851 });
852
853 shutdown_session.await.unwrap();
854}
855
856#[gpui::test]
857async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
858 cx_a.update(|cx| {
859 release_channel::init(semver::Version::new(0, 0, 0), cx);
860 project::trusted_worktrees::init(HashMap::default(), None, None, cx);
861 });
862 server_cx.update(|cx| {
863 release_channel::init(semver::Version::new(0, 0, 0), cx);
864 project::trusted_worktrees::init(HashMap::default(), None, None, cx);
865 });
866
867 let mut server = TestServer::start(cx_a.executor().clone()).await;
868 let client_a = server.create_client(cx_a, "user_a").await;
869
870 let server_name = "override-rust-analyzer";
871 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
872
873 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
874 let remote_fs = FakeFs::new(server_cx.executor());
875 remote_fs
876 .insert_tree(
877 path!("/projects"),
878 json!({
879 "project_a": {
880 ".zed": {
881 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
882 },
883 "main.rs": "fn main() {}"
884 },
885 "project_b": { "lib.rs": "pub fn lib() {}" }
886 }),
887 )
888 .await;
889
890 server_cx.update(HeadlessProject::init);
891 let remote_http_client = Arc::new(BlockedHttpClient);
892 let node = NodeRuntime::unavailable();
893 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
894 languages.add(rust_lang());
895
896 let capabilities = lsp::ServerCapabilities {
897 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
898 ..lsp::ServerCapabilities::default()
899 };
900 let mut fake_language_servers = languages.register_fake_lsp(
901 "Rust",
902 FakeLspAdapter {
903 name: server_name,
904 capabilities: capabilities.clone(),
905 initializer: Some(Box::new({
906 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
907 move |fake_server| {
908 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
909 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
910 move |_params, _| {
911 lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
912 async move {
913 Ok(Some(vec![lsp::InlayHint {
914 position: lsp::Position::new(0, 0),
915 label: lsp::InlayHintLabel::String("hint".to_string()),
916 kind: None,
917 text_edits: None,
918 tooltip: None,
919 padding_left: None,
920 padding_right: None,
921 data: None,
922 }]))
923 }
924 },
925 );
926 }
927 })),
928 ..FakeLspAdapter::default()
929 },
930 );
931
932 let _headless_project = server_cx.new(|cx| {
933 HeadlessProject::new(
934 HeadlessAppState {
935 session: server_ssh,
936 fs: remote_fs.clone(),
937 http_client: remote_http_client,
938 node_runtime: node,
939 languages,
940 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
941 },
942 true,
943 cx,
944 )
945 });
946
947 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
948 let (project_a, worktree_id_a) = client_a
949 .build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
950 .await;
951
952 cx_a.update(|cx| {
953 release_channel::init(semver::Version::new(0, 0, 0), cx);
954
955 SettingsStore::update_global(cx, |store, cx| {
956 store.update_user_settings(cx, |settings| {
957 let language_settings = &mut settings.project.all_languages.defaults;
958 language_settings.inlay_hints = Some(InlayHintSettingsContent {
959 enabled: Some(true),
960 ..InlayHintSettingsContent::default()
961 })
962 });
963 });
964 });
965
966 project_a
967 .update(cx_a, |project, cx| {
968 project.languages().add(rust_lang());
969 project.languages().register_fake_lsp_adapter(
970 "Rust",
971 FakeLspAdapter {
972 name: server_name,
973 capabilities,
974 ..FakeLspAdapter::default()
975 },
976 );
977 project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
978 })
979 .await
980 .unwrap();
981
982 cx_a.run_until_parked();
983
984 let worktree_ids = project_a.read_with(cx_a, |project, cx| {
985 project
986 .worktrees(cx)
987 .map(|wt| wt.read(cx).id())
988 .collect::<Vec<_>>()
989 });
990 assert_eq!(worktree_ids.len(), 2);
991
992 let trusted_worktrees =
993 cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
994 let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
995
996 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
997 store.can_trust(&worktree_store, worktree_ids[0], cx)
998 });
999 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1000 store.can_trust(&worktree_store, worktree_ids[1], cx)
1001 });
1002 assert!(!can_trust_a, "project_a should be restricted initially");
1003 assert!(!can_trust_b, "project_b should be restricted initially");
1004
1005 let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
1006 store.has_restricted_worktrees(&worktree_store, cx)
1007 });
1008 assert!(has_restricted, "should have restricted worktrees");
1009
1010 let buffer_before_approval = project_a
1011 .update(cx_a, |project, cx| {
1012 project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
1013 })
1014 .await
1015 .unwrap();
1016
1017 let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
1018 Editor::new(
1019 EditorMode::full(),
1020 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
1021 Some(project_a.clone()),
1022 window,
1023 cx,
1024 )
1025 });
1026 cx_a.run_until_parked();
1027 let fake_language_server = fake_language_servers.next();
1028
1029 cx_a.read(|cx| {
1030 let file = buffer_before_approval.read(cx).file();
1031 assert_eq!(
1032 language_settings(Some("Rust".into()), file, cx).language_servers,
1033 ["...".to_string()],
1034 "remote .zed/settings.json must not sync before trust approval"
1035 )
1036 });
1037
1038 editor.update_in(cx_a, |editor, window, cx| {
1039 editor.handle_input("1", window, cx);
1040 });
1041 cx_a.run_until_parked();
1042 cx_a.executor().advance_clock(Duration::from_secs(1));
1043 assert_eq!(
1044 lsp_inlay_hint_request_count.load(Ordering::Acquire),
1045 0,
1046 "inlay hints must not be queried before trust approval"
1047 );
1048
1049 trusted_worktrees.update(cx_a, |store, cx| {
1050 store.trust(
1051 &worktree_store,
1052 HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1053 cx,
1054 );
1055 });
1056 cx_a.run_until_parked();
1057
1058 cx_a.read(|cx| {
1059 let file = buffer_before_approval.read(cx).file();
1060 assert_eq!(
1061 language_settings(Some("Rust".into()), file, cx).language_servers,
1062 ["override-rust-analyzer".to_string()],
1063 "remote .zed/settings.json should sync after trust approval"
1064 )
1065 });
1066 let _fake_language_server = fake_language_server.await.unwrap();
1067 editor.update_in(cx_a, |editor, window, cx| {
1068 editor.handle_input("1", window, cx);
1069 });
1070 cx_a.run_until_parked();
1071 cx_a.executor().advance_clock(Duration::from_secs(1));
1072 assert!(
1073 lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
1074 "inlay hints should be queried after trust approval"
1075 );
1076
1077 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1078 store.can_trust(&worktree_store, worktree_ids[0], cx)
1079 });
1080 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1081 store.can_trust(&worktree_store, worktree_ids[1], cx)
1082 });
1083 assert!(can_trust_a, "project_a should be trusted after trust()");
1084 assert!(!can_trust_b, "project_b should still be restricted");
1085
1086 trusted_worktrees.update(cx_a, |store, cx| {
1087 store.trust(
1088 &worktree_store,
1089 HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1090 cx,
1091 );
1092 });
1093
1094 let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1095 store.can_trust(&worktree_store, worktree_ids[0], cx)
1096 });
1097 let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1098 store.can_trust(&worktree_store, worktree_ids[1], cx)
1099 });
1100 assert!(can_trust_a, "project_a should remain trusted");
1101 assert!(can_trust_b, "project_b should now be trusted");
1102
1103 let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
1104 store.has_restricted_worktrees(&worktree_store, cx)
1105 });
1106 assert!(
1107 !has_restricted_after,
1108 "should have no restricted worktrees after trusting both"
1109 );
1110}