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 extension::ExtensionHostProxy;
8use fs::{FakeFs, Fs as _, RemoveOptions};
9use futures::StreamExt as _;
10use gpui::{
11 AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
12 VisualContext,
13};
14use http_client::BlockedHttpClient;
15use language::{
16 FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
17 language_settings::{Formatter, FormatterList, SelectedFormatter, language_settings},
18 tree_sitter_typescript,
19};
20use node_runtime::NodeRuntime;
21use project::{
22 ProjectPath,
23 debugger::session::ThreadId,
24 lsp_store::{FormatTrigger, LspFormatTarget},
25};
26use remote::RemoteClient;
27use remote_server::{HeadlessAppState, HeadlessProject};
28use rpc::proto;
29use serde_json::json;
30use settings::{PrettierSettingsContent, SettingsStore};
31use std::{
32 path::Path,
33 sync::{Arc, atomic::AtomicUsize},
34};
35use task::TcpArgumentsTemplate;
36use util::{path, rel_path::rel_path};
37
38#[gpui::test(iterations = 10)]
39async fn test_sharing_an_ssh_remote_project(
40 cx_a: &mut TestAppContext,
41 cx_b: &mut TestAppContext,
42 server_cx: &mut TestAppContext,
43) {
44 let executor = cx_a.executor();
45 cx_a.update(|cx| {
46 release_channel::init(SemanticVersion::default(), cx);
47 });
48 server_cx.update(|cx| {
49 release_channel::init(SemanticVersion::default(), cx);
50 });
51 let mut server = TestServer::start(executor.clone()).await;
52 let client_a = server.create_client(cx_a, "user_a").await;
53 let client_b = server.create_client(cx_b, "user_b").await;
54 server
55 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
56 .await;
57
58 // Set up project on remote FS
59 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
60 let remote_fs = FakeFs::new(server_cx.executor());
61 remote_fs
62 .insert_tree(
63 path!("/code"),
64 json!({
65 "project1": {
66 ".zed": {
67 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
68 },
69 "README.md": "# project 1",
70 "src": {
71 "lib.rs": "fn one() -> usize { 1 }"
72 }
73 },
74 "project2": {
75 "README.md": "# project 2",
76 },
77 }),
78 )
79 .await;
80
81 // User A connects to the remote project via SSH.
82 server_cx.update(HeadlessProject::init);
83 let remote_http_client = Arc::new(BlockedHttpClient);
84 let node = NodeRuntime::unavailable();
85 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
86 let _headless_project = server_cx.new(|cx| {
87 client::init_settings(cx);
88 HeadlessProject::new(
89 HeadlessAppState {
90 session: server_ssh,
91 fs: remote_fs.clone(),
92 http_client: remote_http_client,
93 node_runtime: node,
94 languages,
95 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
96 },
97 cx,
98 )
99 });
100
101 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
102 let (project_a, worktree_id) = client_a
103 .build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
104 .await;
105
106 // While the SSH worktree is being scanned, user A shares the remote project.
107 let active_call_a = cx_a.read(ActiveCall::global);
108 let project_id = active_call_a
109 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
110 .await
111 .unwrap();
112
113 // User B joins the project.
114 let project_b = client_b.join_remote_project(project_id, cx_b).await;
115 let worktree_b = project_b
116 .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
117 .unwrap();
118
119 let worktree_a = project_a
120 .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
121 .unwrap();
122
123 executor.run_until_parked();
124
125 worktree_a.update(cx_a, |worktree, _cx| {
126 assert_eq!(
127 worktree.paths().collect::<Vec<_>>(),
128 vec![
129 rel_path(".zed"),
130 rel_path(".zed/settings.json"),
131 rel_path("README.md"),
132 rel_path("src"),
133 rel_path("src/lib.rs"),
134 ]
135 );
136 });
137
138 worktree_b.update(cx_b, |worktree, _cx| {
139 assert_eq!(
140 worktree.paths().collect::<Vec<_>>(),
141 vec![
142 rel_path(".zed"),
143 rel_path(".zed/settings.json"),
144 rel_path("README.md"),
145 rel_path("src"),
146 rel_path("src/lib.rs"),
147 ]
148 );
149 });
150
151 // User B can open buffers in the remote project.
152 let buffer_b = project_b
153 .update(cx_b, |project, cx| {
154 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
155 })
156 .await
157 .unwrap();
158 buffer_b.update(cx_b, |buffer, cx| {
159 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
160 let ix = buffer.text().find('1').unwrap();
161 buffer.edit([(ix..ix + 1, "100")], None, cx);
162 });
163
164 executor.run_until_parked();
165
166 cx_b.read(|cx| {
167 let file = buffer_b.read(cx).file();
168 assert_eq!(
169 language_settings(Some("Rust".into()), file, cx).language_servers,
170 ["override-rust-analyzer".to_string()]
171 )
172 });
173
174 project_b
175 .update(cx_b, |project, cx| {
176 project.save_buffer_as(
177 buffer_b.clone(),
178 ProjectPath {
179 worktree_id: worktree_id.to_owned(),
180 path: rel_path("src/renamed.rs").into(),
181 },
182 cx,
183 )
184 })
185 .await
186 .unwrap();
187 assert_eq!(
188 remote_fs
189 .load(path!("/code/project1/src/renamed.rs").as_ref())
190 .await
191 .unwrap(),
192 "fn one() -> usize { 100 }"
193 );
194 cx_b.run_until_parked();
195 cx_b.update(|cx| {
196 assert_eq!(
197 buffer_b.read(cx).file().unwrap().path().as_ref(),
198 rel_path("src/renamed.rs")
199 );
200 });
201}
202
203#[gpui::test]
204async fn test_ssh_collaboration_git_branches(
205 executor: BackgroundExecutor,
206 cx_a: &mut TestAppContext,
207 cx_b: &mut TestAppContext,
208 server_cx: &mut TestAppContext,
209) {
210 cx_a.set_name("a");
211 cx_b.set_name("b");
212 server_cx.set_name("server");
213
214 cx_a.update(|cx| {
215 release_channel::init(SemanticVersion::default(), cx);
216 });
217 server_cx.update(|cx| {
218 release_channel::init(SemanticVersion::default(), cx);
219 });
220
221 let mut server = TestServer::start(executor.clone()).await;
222 let client_a = server.create_client(cx_a, "user_a").await;
223 let client_b = server.create_client(cx_b, "user_b").await;
224 server
225 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
226 .await;
227
228 // Set up project on remote FS
229 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
230 let remote_fs = FakeFs::new(server_cx.executor());
231 remote_fs
232 .insert_tree("/project", serde_json::json!({ ".git":{} }))
233 .await;
234
235 let branches = ["main", "dev", "feature-1"];
236 let branches_set = branches
237 .iter()
238 .map(ToString::to_string)
239 .collect::<HashSet<_>>();
240 remote_fs.insert_branches(Path::new("/project/.git"), &branches);
241
242 // User A connects to the remote project via SSH.
243 server_cx.update(HeadlessProject::init);
244 let remote_http_client = Arc::new(BlockedHttpClient);
245 let node = NodeRuntime::unavailable();
246 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
247 let headless_project = server_cx.new(|cx| {
248 client::init_settings(cx);
249 HeadlessProject::new(
250 HeadlessAppState {
251 session: server_ssh,
252 fs: remote_fs.clone(),
253 http_client: remote_http_client,
254 node_runtime: node,
255 languages,
256 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
257 },
258 cx,
259 )
260 });
261
262 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
263 let (project_a, _) = client_a
264 .build_ssh_project("/project", client_ssh, cx_a)
265 .await;
266
267 // While the SSH worktree is being scanned, user A shares the remote project.
268 let active_call_a = cx_a.read(ActiveCall::global);
269 let project_id = active_call_a
270 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
271 .await
272 .unwrap();
273
274 // User B joins the project.
275 let project_b = client_b.join_remote_project(project_id, cx_b).await;
276
277 // Give client A sometime to see that B has joined, and that the headless server
278 // has some git repositories
279 executor.run_until_parked();
280
281 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
282
283 let branches_b = cx_b
284 .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
285 .await
286 .unwrap()
287 .unwrap();
288
289 let new_branch = branches[2];
290
291 let branches_b = branches_b
292 .into_iter()
293 .map(|branch| branch.name().to_string())
294 .collect::<HashSet<_>>();
295
296 assert_eq!(&branches_b, &branches_set);
297
298 cx_b.update(|cx| {
299 repo_b.update(cx, |repo_b, _cx| {
300 repo_b.change_branch(new_branch.to_string())
301 })
302 })
303 .await
304 .unwrap()
305 .unwrap();
306
307 executor.run_until_parked();
308
309 let server_branch = server_cx.update(|cx| {
310 headless_project.update(cx, |headless_project, cx| {
311 headless_project.git_store.update(cx, |git_store, cx| {
312 git_store
313 .repositories()
314 .values()
315 .next()
316 .unwrap()
317 .read(cx)
318 .branch
319 .as_ref()
320 .unwrap()
321 .clone()
322 })
323 })
324 });
325
326 assert_eq!(server_branch.name(), branches[2]);
327
328 // Also try creating a new branch
329 cx_b.update(|cx| {
330 repo_b.update(cx, |repo_b, _cx| {
331 repo_b.create_branch("totally-new-branch".to_string())
332 })
333 })
334 .await
335 .unwrap()
336 .unwrap();
337
338 cx_b.update(|cx| {
339 repo_b.update(cx, |repo_b, _cx| {
340 repo_b.change_branch("totally-new-branch".to_string())
341 })
342 })
343 .await
344 .unwrap()
345 .unwrap();
346
347 executor.run_until_parked();
348
349 let server_branch = server_cx.update(|cx| {
350 headless_project.update(cx, |headless_project, cx| {
351 headless_project.git_store.update(cx, |git_store, cx| {
352 git_store
353 .repositories()
354 .values()
355 .next()
356 .unwrap()
357 .read(cx)
358 .branch
359 .as_ref()
360 .unwrap()
361 .clone()
362 })
363 })
364 });
365
366 assert_eq!(server_branch.name(), "totally-new-branch");
367
368 // Remove the git repository and check that all participants get the update.
369 remote_fs
370 .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
371 .await
372 .unwrap();
373 executor.run_until_parked();
374
375 project_a.update(cx_a, |project, cx| {
376 pretty_assertions::assert_eq!(
377 project.git_store().read(cx).repo_snapshots(cx),
378 HashMap::default()
379 );
380 });
381 project_b.update(cx_b, |project, cx| {
382 pretty_assertions::assert_eq!(
383 project.git_store().read(cx).repo_snapshots(cx),
384 HashMap::default()
385 );
386 });
387}
388
389#[gpui::test]
390async fn test_ssh_collaboration_formatting_with_prettier(
391 executor: BackgroundExecutor,
392 cx_a: &mut TestAppContext,
393 cx_b: &mut TestAppContext,
394 server_cx: &mut TestAppContext,
395) {
396 cx_a.set_name("a");
397 cx_b.set_name("b");
398 server_cx.set_name("server");
399
400 cx_a.update(|cx| {
401 release_channel::init(SemanticVersion::default(), cx);
402 });
403 server_cx.update(|cx| {
404 release_channel::init(SemanticVersion::default(), cx);
405 });
406
407 let mut server = TestServer::start(executor.clone()).await;
408 let client_a = server.create_client(cx_a, "user_a").await;
409 let client_b = server.create_client(cx_b, "user_b").await;
410 server
411 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
412 .await;
413
414 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
415 let remote_fs = FakeFs::new(server_cx.executor());
416 let buffer_text = "let one = \"two\"";
417 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
418 remote_fs
419 .insert_tree(
420 path!("/project"),
421 serde_json::json!({ "a.ts": buffer_text }),
422 )
423 .await;
424
425 let test_plugin = "test_plugin";
426 let ts_lang = Arc::new(Language::new(
427 LanguageConfig {
428 name: "TypeScript".into(),
429 matcher: LanguageMatcher {
430 path_suffixes: vec!["ts".to_string()],
431 ..LanguageMatcher::default()
432 },
433 ..LanguageConfig::default()
434 },
435 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
436 ));
437 client_a.language_registry().add(ts_lang.clone());
438 client_b.language_registry().add(ts_lang.clone());
439
440 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
441 let mut fake_language_servers = languages.register_fake_lsp(
442 "TypeScript",
443 FakeLspAdapter {
444 prettier_plugins: vec![test_plugin],
445 ..Default::default()
446 },
447 );
448
449 // User A connects to the remote project via SSH.
450 server_cx.update(HeadlessProject::init);
451 let remote_http_client = Arc::new(BlockedHttpClient);
452 let _headless_project = server_cx.new(|cx| {
453 client::init_settings(cx);
454 HeadlessProject::new(
455 HeadlessAppState {
456 session: server_ssh,
457 fs: remote_fs.clone(),
458 http_client: remote_http_client,
459 node_runtime: NodeRuntime::unavailable(),
460 languages,
461 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
462 },
463 cx,
464 )
465 });
466
467 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
468 let (project_a, worktree_id) = client_a
469 .build_ssh_project(path!("/project"), client_ssh, cx_a)
470 .await;
471
472 // While the SSH worktree is being scanned, user A shares the remote project.
473 let active_call_a = cx_a.read(ActiveCall::global);
474 let project_id = active_call_a
475 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
476 .await
477 .unwrap();
478
479 // User B joins the project.
480 let project_b = client_b.join_remote_project(project_id, cx_b).await;
481 executor.run_until_parked();
482
483 // Opens the buffer and formats it
484 let (buffer_b, _handle) = project_b
485 .update(cx_b, |p, cx| {
486 p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
487 })
488 .await
489 .expect("user B opens buffer for formatting");
490
491 cx_a.update(|cx| {
492 SettingsStore::update_global(cx, |store, cx| {
493 store.update_user_settings(cx, |file| {
494 file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
495 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
496 allowed: Some(true),
497 ..Default::default()
498 });
499 });
500 });
501 });
502 cx_b.update(|cx| {
503 SettingsStore::update_global(cx, |store, cx| {
504 store.update_user_settings(cx, |file| {
505 file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
506 FormatterList::Single(Formatter::LanguageServer { name: None }),
507 ));
508 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
509 allowed: Some(true),
510 ..Default::default()
511 });
512 });
513 });
514 });
515 let fake_language_server = fake_language_servers.next().await.unwrap();
516 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
517 panic!(
518 "Unexpected: prettier should be preferred since it's enabled and language supports it"
519 )
520 });
521
522 project_b
523 .update(cx_b, |project, cx| {
524 project.format(
525 HashSet::from_iter([buffer_b.clone()]),
526 LspFormatTarget::Buffers,
527 true,
528 FormatTrigger::Save,
529 cx,
530 )
531 })
532 .await
533 .unwrap();
534
535 executor.run_until_parked();
536 assert_eq!(
537 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
538 buffer_text.to_string() + "\n" + prettier_format_suffix,
539 "Prettier formatting was not applied to client buffer after client's request"
540 );
541
542 // User A opens and formats the same buffer too
543 let buffer_a = project_a
544 .update(cx_a, |p, cx| {
545 p.open_buffer((worktree_id, rel_path("a.ts")), cx)
546 })
547 .await
548 .expect("user A opens buffer for formatting");
549
550 cx_a.update(|cx| {
551 SettingsStore::update_global(cx, |store, cx| {
552 store.update_user_settings(cx, |file| {
553 file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
554 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
555 allowed: Some(true),
556 ..Default::default()
557 });
558 });
559 });
560 });
561 project_a
562 .update(cx_a, |project, cx| {
563 project.format(
564 HashSet::from_iter([buffer_a.clone()]),
565 LspFormatTarget::Buffers,
566 true,
567 FormatTrigger::Manual,
568 cx,
569 )
570 })
571 .await
572 .unwrap();
573
574 executor.run_until_parked();
575 assert_eq!(
576 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
577 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
578 "Prettier formatting was not applied to client buffer after host's request"
579 );
580}
581
582#[gpui::test]
583async fn test_remote_server_debugger(
584 cx_a: &mut TestAppContext,
585 server_cx: &mut TestAppContext,
586 executor: BackgroundExecutor,
587) {
588 cx_a.update(|cx| {
589 release_channel::init(SemanticVersion::default(), cx);
590 command_palette_hooks::init(cx);
591 zlog::init_test();
592 dap_adapters::init(cx);
593 });
594 server_cx.update(|cx| {
595 release_channel::init(SemanticVersion::default(), cx);
596 dap_adapters::init(cx);
597 });
598 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
599 let remote_fs = FakeFs::new(server_cx.executor());
600 remote_fs
601 .insert_tree(
602 path!("/code"),
603 json!({
604 "lib.rs": "fn one() -> usize { 1 }"
605 }),
606 )
607 .await;
608
609 // User A connects to the remote project via SSH.
610 server_cx.update(HeadlessProject::init);
611 let remote_http_client = Arc::new(BlockedHttpClient);
612 let node = NodeRuntime::unavailable();
613 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
614 let _headless_project = server_cx.new(|cx| {
615 client::init_settings(cx);
616 HeadlessProject::new(
617 HeadlessAppState {
618 session: server_ssh,
619 fs: remote_fs.clone(),
620 http_client: remote_http_client,
621 node_runtime: node,
622 languages,
623 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
624 },
625 cx,
626 )
627 });
628
629 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
630 let mut server = TestServer::start(server_cx.executor()).await;
631 let client_a = server.create_client(cx_a, "user_a").await;
632 cx_a.update(|cx| {
633 debugger_ui::init(cx);
634 command_palette_hooks::init(cx);
635 });
636 let (project_a, _) = client_a
637 .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
638 .await;
639
640 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
641
642 let debugger_panel = workspace
643 .update_in(cx_a, |_workspace, window, cx| {
644 cx.spawn_in(window, DebugPanel::load)
645 })
646 .await
647 .unwrap();
648
649 workspace.update_in(cx_a, |workspace, window, cx| {
650 workspace.add_panel(debugger_panel, window, cx);
651 });
652
653 cx_a.run_until_parked();
654 let debug_panel = workspace
655 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
656 .unwrap();
657
658 let workspace_window = cx_a
659 .window_handle()
660 .downcast::<workspace::Workspace>()
661 .unwrap();
662
663 let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
664 cx_a.run_until_parked();
665 debug_panel.update(cx_a, |debug_panel, cx| {
666 assert_eq!(
667 debug_panel.active_session().unwrap().read(cx).session(cx),
668 session
669 )
670 });
671
672 session.update(cx_a, |session, _| {
673 assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
674 });
675
676 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
677 workspace.project().update(cx, |project, cx| {
678 project.dap_store().update(cx, |dap_store, cx| {
679 dap_store.shutdown_session(session.read(cx).session_id(), cx)
680 })
681 })
682 });
683
684 client_ssh.update(cx_a, |a, _| {
685 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
686 });
687
688 shutdown_session.await.unwrap();
689}
690
691#[gpui::test]
692async fn test_slow_adapter_startup_retries(
693 cx_a: &mut TestAppContext,
694 server_cx: &mut TestAppContext,
695 executor: BackgroundExecutor,
696) {
697 cx_a.update(|cx| {
698 release_channel::init(SemanticVersion::default(), cx);
699 command_palette_hooks::init(cx);
700 zlog::init_test();
701 dap_adapters::init(cx);
702 });
703 server_cx.update(|cx| {
704 release_channel::init(SemanticVersion::default(), cx);
705 dap_adapters::init(cx);
706 });
707 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
708 let remote_fs = FakeFs::new(server_cx.executor());
709 remote_fs
710 .insert_tree(
711 path!("/code"),
712 json!({
713 "lib.rs": "fn one() -> usize { 1 }"
714 }),
715 )
716 .await;
717
718 // User A connects to the remote project via SSH.
719 server_cx.update(HeadlessProject::init);
720 let remote_http_client = Arc::new(BlockedHttpClient);
721 let node = NodeRuntime::unavailable();
722 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
723 let _headless_project = server_cx.new(|cx| {
724 client::init_settings(cx);
725 HeadlessProject::new(
726 HeadlessAppState {
727 session: server_ssh,
728 fs: remote_fs.clone(),
729 http_client: remote_http_client,
730 node_runtime: node,
731 languages,
732 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
733 },
734 cx,
735 )
736 });
737
738 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
739 let mut server = TestServer::start(server_cx.executor()).await;
740 let client_a = server.create_client(cx_a, "user_a").await;
741 cx_a.update(|cx| {
742 debugger_ui::init(cx);
743 command_palette_hooks::init(cx);
744 });
745 let (project_a, _) = client_a
746 .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
747 .await;
748
749 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
750
751 let debugger_panel = workspace
752 .update_in(cx_a, |_workspace, window, cx| {
753 cx.spawn_in(window, DebugPanel::load)
754 })
755 .await
756 .unwrap();
757
758 workspace.update_in(cx_a, |workspace, window, cx| {
759 workspace.add_panel(debugger_panel, window, cx);
760 });
761
762 cx_a.run_until_parked();
763 let debug_panel = workspace
764 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
765 .unwrap();
766
767 let workspace_window = cx_a
768 .window_handle()
769 .downcast::<workspace::Workspace>()
770 .unwrap();
771
772 let count = Arc::new(AtomicUsize::new(0));
773 let session = debugger_ui::tests::start_debug_session_with(
774 &workspace_window,
775 cx_a,
776 DebugTaskDefinition {
777 adapter: "fake-adapter".into(),
778 label: "test".into(),
779 config: json!({
780 "request": "launch"
781 }),
782 tcp_connection: Some(TcpArgumentsTemplate {
783 port: None,
784 host: None,
785 timeout: None,
786 }),
787 },
788 move |client| {
789 let count = count.clone();
790 client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
791 if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
792 return RequestHandling::Exit;
793 }
794 RequestHandling::Respond(Ok(Capabilities::default()))
795 });
796 },
797 )
798 .unwrap();
799 cx_a.run_until_parked();
800
801 let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
802 client
803 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
804 reason: dap::StoppedEventReason::Pause,
805 description: None,
806 thread_id: Some(1),
807 preserve_focus_hint: None,
808 text: None,
809 all_threads_stopped: None,
810 hit_breakpoint_ids: None,
811 }))
812 .await;
813
814 cx_a.run_until_parked();
815
816 let active_session = debug_panel
817 .update(cx_a, |this, _| this.active_session())
818 .unwrap();
819
820 let running_state = active_session.update(cx_a, |active_session, _| {
821 active_session.running_state().clone()
822 });
823
824 assert_eq!(
825 client.id(),
826 running_state.read_with(cx_a, |running_state, _| running_state.session_id())
827 );
828 assert_eq!(
829 ThreadId(1),
830 running_state.read_with(cx_a, |running_state, _| running_state
831 .selected_thread_id()
832 .unwrap())
833 );
834
835 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
836 workspace.project().update(cx, |project, cx| {
837 project.dap_store().update(cx, |dap_store, cx| {
838 dap_store.shutdown_session(session.read(cx).session_id(), cx)
839 })
840 })
841 });
842
843 client_ssh.update(cx_a, |a, _| {
844 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
845 });
846
847 shutdown_session.await.unwrap();
848}