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