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