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