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::{
18 Formatter, FormatterList, PrettierSettings, SelectedFormatter, language_settings,
19 },
20 tree_sitter_typescript,
21};
22use node_runtime::NodeRuntime;
23use project::{
24 ProjectPath,
25 debugger::session::ThreadId,
26 lsp_store::{FormatTrigger, LspFormatTarget},
27};
28use remote::RemoteClient;
29use remote_server::{HeadlessAppState, HeadlessProject};
30use rpc::proto;
31use serde_json::json;
32use settings::{PrettierSettingsContent, SettingsStore};
33use std::{
34 path::Path,
35 sync::{Arc, atomic::AtomicUsize},
36};
37use task::TcpArgumentsTemplate;
38use util::path;
39
40#[gpui::test(iterations = 10)]
41async fn test_sharing_an_ssh_remote_project(
42 cx_a: &mut TestAppContext,
43 cx_b: &mut TestAppContext,
44 server_cx: &mut TestAppContext,
45) {
46 let executor = cx_a.executor();
47 cx_a.update(|cx| {
48 release_channel::init(SemanticVersion::default(), cx);
49 });
50 server_cx.update(|cx| {
51 release_channel::init(SemanticVersion::default(), cx);
52 });
53 let mut server = TestServer::start(executor.clone()).await;
54 let client_a = server.create_client(cx_a, "user_a").await;
55 let client_b = server.create_client(cx_b, "user_b").await;
56 server
57 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
58 .await;
59
60 // Set up project on remote FS
61 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
62 let remote_fs = FakeFs::new(server_cx.executor());
63 remote_fs
64 .insert_tree(
65 path!("/code"),
66 json!({
67 "project1": {
68 ".zed": {
69 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
70 },
71 "README.md": "# project 1",
72 "src": {
73 "lib.rs": "fn one() -> usize { 1 }"
74 }
75 },
76 "project2": {
77 "README.md": "# project 2",
78 },
79 }),
80 )
81 .await;
82
83 // User A connects to the remote project via SSH.
84 server_cx.update(HeadlessProject::init);
85 let remote_http_client = Arc::new(BlockedHttpClient);
86 let node = NodeRuntime::unavailable();
87 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
88 let _headless_project = server_cx.new(|cx| {
89 client::init_settings(cx);
90 HeadlessProject::new(
91 HeadlessAppState {
92 session: server_ssh,
93 fs: remote_fs.clone(),
94 http_client: remote_http_client,
95 node_runtime: node,
96 languages,
97 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
98 },
99 cx,
100 )
101 });
102
103 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
104 let (project_a, worktree_id) = client_a
105 .build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
106 .await;
107
108 // While the SSH worktree is being scanned, user A shares the remote project.
109 let active_call_a = cx_a.read(ActiveCall::global);
110 let project_id = active_call_a
111 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
112 .await
113 .unwrap();
114
115 // User B joins the project.
116 let project_b = client_b.join_remote_project(project_id, cx_b).await;
117 let worktree_b = project_b
118 .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
119 .unwrap();
120
121 let worktree_a = project_a
122 .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
123 .unwrap();
124
125 executor.run_until_parked();
126
127 worktree_a.update(cx_a, |worktree, _cx| {
128 assert_eq!(
129 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
130 vec![
131 Path::new(".zed"),
132 Path::new(".zed/settings.json"),
133 Path::new("README.md"),
134 Path::new("src"),
135 Path::new("src/lib.rs"),
136 ]
137 );
138 });
139
140 worktree_b.update(cx_b, |worktree, _cx| {
141 assert_eq!(
142 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
143 vec![
144 Path::new(".zed"),
145 Path::new(".zed/settings.json"),
146 Path::new("README.md"),
147 Path::new("src"),
148 Path::new("src/lib.rs"),
149 ]
150 );
151 });
152
153 // User B can open buffers in the remote project.
154 let buffer_b = project_b
155 .update(cx_b, |project, cx| {
156 project.open_buffer((worktree_id, "src/lib.rs"), cx)
157 })
158 .await
159 .unwrap();
160 buffer_b.update(cx_b, |buffer, cx| {
161 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
162 let ix = buffer.text().find('1').unwrap();
163 buffer.edit([(ix..ix + 1, "100")], None, cx);
164 });
165
166 executor.run_until_parked();
167
168 cx_b.read(|cx| {
169 let file = buffer_b.read(cx).file();
170 assert_eq!(
171 language_settings(Some("Rust".into()), file, cx).language_servers,
172 ["override-rust-analyzer".to_string()]
173 )
174 });
175
176 project_b
177 .update(cx_b, |project, cx| {
178 project.save_buffer_as(
179 buffer_b.clone(),
180 ProjectPath {
181 worktree_id: worktree_id.to_owned(),
182 path: Arc::from(Path::new("src/renamed.rs")),
183 },
184 cx,
185 )
186 })
187 .await
188 .unwrap();
189 assert_eq!(
190 remote_fs
191 .load(path!("/code/project1/src/renamed.rs").as_ref())
192 .await
193 .unwrap(),
194 "fn one() -> usize { 100 }"
195 );
196 cx_b.run_until_parked();
197 cx_b.update(|cx| {
198 assert_eq!(
199 buffer_b
200 .read(cx)
201 .file()
202 .unwrap()
203 .path()
204 .to_string_lossy()
205 .to_string(),
206 path!("src/renamed.rs").to_string()
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(SemanticVersion::default(), cx);
224 });
225 server_cx.update(|cx| {
226 release_channel::init(SemanticVersion::default(), 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 client::init_settings(cx);
257 HeadlessProject::new(
258 HeadlessAppState {
259 session: server_ssh,
260 fs: remote_fs.clone(),
261 http_client: remote_http_client,
262 node_runtime: node,
263 languages,
264 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
265 },
266 cx,
267 )
268 });
269
270 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
271 let (project_a, _) = client_a
272 .build_ssh_project("/project", client_ssh, 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())
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(SemanticVersion::default(), cx);
410 });
411 server_cx.update(|cx| {
412 release_channel::init(SemanticVersion::default(), 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 client::init_settings(cx);
462 HeadlessProject::new(
463 HeadlessAppState {
464 session: server_ssh,
465 fs: remote_fs.clone(),
466 http_client: remote_http_client,
467 node_runtime: NodeRuntime::unavailable(),
468 languages,
469 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
470 },
471 cx,
472 )
473 });
474
475 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
476 let (project_a, worktree_id) = client_a
477 .build_ssh_project(path!("/project"), client_ssh, 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, "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(SelectedFormatter::Auto);
503 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
504 allowed: 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(SelectedFormatter::List(
514 FormatterList::Single(Formatter::LanguageServer { name: None }),
515 ));
516 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
517 allowed: 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| p.open_buffer((worktree_id, "a.ts"), cx))
553 .await
554 .expect("user A opens buffer for formatting");
555
556 cx_a.update(|cx| {
557 SettingsStore::update_global(cx, |store, cx| {
558 store.update_user_settings(cx, |file| {
559 file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
560 file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
561 allowed: true,
562 ..Default::default()
563 });
564 });
565 });
566 });
567 project_a
568 .update(cx_a, |project, cx| {
569 project.format(
570 HashSet::from_iter([buffer_a.clone()]),
571 LspFormatTarget::Buffers,
572 true,
573 FormatTrigger::Manual,
574 cx,
575 )
576 })
577 .await
578 .unwrap();
579
580 executor.run_until_parked();
581 assert_eq!(
582 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
583 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
584 "Prettier formatting was not applied to client buffer after host's request"
585 );
586}
587
588#[gpui::test]
589async fn test_remote_server_debugger(
590 cx_a: &mut TestAppContext,
591 server_cx: &mut TestAppContext,
592 executor: BackgroundExecutor,
593) {
594 cx_a.update(|cx| {
595 release_channel::init(SemanticVersion::default(), cx);
596 command_palette_hooks::init(cx);
597 zlog::init_test();
598 dap_adapters::init(cx);
599 });
600 server_cx.update(|cx| {
601 release_channel::init(SemanticVersion::default(), cx);
602 dap_adapters::init(cx);
603 });
604 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
605 let remote_fs = FakeFs::new(server_cx.executor());
606 remote_fs
607 .insert_tree(
608 path!("/code"),
609 json!({
610 "lib.rs": "fn one() -> usize { 1 }"
611 }),
612 )
613 .await;
614
615 // User A connects to the remote project via SSH.
616 server_cx.update(HeadlessProject::init);
617 let remote_http_client = Arc::new(BlockedHttpClient);
618 let node = NodeRuntime::unavailable();
619 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
620 let _headless_project = server_cx.new(|cx| {
621 client::init_settings(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 cx,
632 )
633 });
634
635 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
636 let mut server = TestServer::start(server_cx.executor()).await;
637 let client_a = server.create_client(cx_a, "user_a").await;
638 cx_a.update(|cx| {
639 debugger_ui::init(cx);
640 command_palette_hooks::init(cx);
641 });
642 let (project_a, _) = client_a
643 .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
644 .await;
645
646 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
647
648 let debugger_panel = workspace
649 .update_in(cx_a, |_workspace, window, cx| {
650 cx.spawn_in(window, DebugPanel::load)
651 })
652 .await
653 .unwrap();
654
655 workspace.update_in(cx_a, |workspace, window, cx| {
656 workspace.add_panel(debugger_panel, window, cx);
657 });
658
659 cx_a.run_until_parked();
660 let debug_panel = workspace
661 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
662 .unwrap();
663
664 let workspace_window = cx_a
665 .window_handle()
666 .downcast::<workspace::Workspace>()
667 .unwrap();
668
669 let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
670 cx_a.run_until_parked();
671 debug_panel.update(cx_a, |debug_panel, cx| {
672 assert_eq!(
673 debug_panel.active_session().unwrap().read(cx).session(cx),
674 session
675 )
676 });
677
678 session.update(cx_a, |session, _| {
679 assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
680 });
681
682 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
683 workspace.project().update(cx, |project, cx| {
684 project.dap_store().update(cx, |dap_store, cx| {
685 dap_store.shutdown_session(session.read(cx).session_id(), cx)
686 })
687 })
688 });
689
690 client_ssh.update(cx_a, |a, _| {
691 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
692 });
693
694 shutdown_session.await.unwrap();
695}
696
697#[gpui::test]
698async fn test_slow_adapter_startup_retries(
699 cx_a: &mut TestAppContext,
700 server_cx: &mut TestAppContext,
701 executor: BackgroundExecutor,
702) {
703 cx_a.update(|cx| {
704 release_channel::init(SemanticVersion::default(), cx);
705 command_palette_hooks::init(cx);
706 zlog::init_test();
707 dap_adapters::init(cx);
708 });
709 server_cx.update(|cx| {
710 release_channel::init(SemanticVersion::default(), cx);
711 dap_adapters::init(cx);
712 });
713 let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
714 let remote_fs = FakeFs::new(server_cx.executor());
715 remote_fs
716 .insert_tree(
717 path!("/code"),
718 json!({
719 "lib.rs": "fn one() -> usize { 1 }"
720 }),
721 )
722 .await;
723
724 // User A connects to the remote project via SSH.
725 server_cx.update(HeadlessProject::init);
726 let remote_http_client = Arc::new(BlockedHttpClient);
727 let node = NodeRuntime::unavailable();
728 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
729 let _headless_project = server_cx.new(|cx| {
730 client::init_settings(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 cx,
741 )
742 });
743
744 let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
745 let mut server = TestServer::start(server_cx.executor()).await;
746 let client_a = server.create_client(cx_a, "user_a").await;
747 cx_a.update(|cx| {
748 debugger_ui::init(cx);
749 command_palette_hooks::init(cx);
750 });
751 let (project_a, _) = client_a
752 .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
753 .await;
754
755 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
756
757 let debugger_panel = workspace
758 .update_in(cx_a, |_workspace, window, cx| {
759 cx.spawn_in(window, DebugPanel::load)
760 })
761 .await
762 .unwrap();
763
764 workspace.update_in(cx_a, |workspace, window, cx| {
765 workspace.add_panel(debugger_panel, window, cx);
766 });
767
768 cx_a.run_until_parked();
769 let debug_panel = workspace
770 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
771 .unwrap();
772
773 let workspace_window = cx_a
774 .window_handle()
775 .downcast::<workspace::Workspace>()
776 .unwrap();
777
778 let count = Arc::new(AtomicUsize::new(0));
779 let session = debugger_ui::tests::start_debug_session_with(
780 &workspace_window,
781 cx_a,
782 DebugTaskDefinition {
783 adapter: "fake-adapter".into(),
784 label: "test".into(),
785 config: json!({
786 "request": "launch"
787 }),
788 tcp_connection: Some(TcpArgumentsTemplate {
789 port: None,
790 host: None,
791 timeout: None,
792 }),
793 },
794 move |client| {
795 let count = count.clone();
796 client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
797 if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
798 return RequestHandling::Exit;
799 }
800 RequestHandling::Respond(Ok(Capabilities::default()))
801 });
802 },
803 )
804 .unwrap();
805 cx_a.run_until_parked();
806
807 let client = session.update(cx_a, |session, _| session.adapter_client().unwrap());
808 client
809 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
810 reason: dap::StoppedEventReason::Pause,
811 description: None,
812 thread_id: Some(1),
813 preserve_focus_hint: None,
814 text: None,
815 all_threads_stopped: None,
816 hit_breakpoint_ids: None,
817 }))
818 .await;
819
820 cx_a.run_until_parked();
821
822 let active_session = debug_panel
823 .update(cx_a, |this, _| this.active_session())
824 .unwrap();
825
826 let running_state = active_session.update(cx_a, |active_session, _| {
827 active_session.running_state().clone()
828 });
829
830 assert_eq!(
831 client.id(),
832 running_state.read_with(cx_a, |running_state, _| running_state.session_id())
833 );
834 assert_eq!(
835 ThreadId(1),
836 running_state.read_with(cx_a, |running_state, _| running_state
837 .selected_thread_id()
838 .unwrap())
839 );
840
841 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
842 workspace.project().update(cx, |project, cx| {
843 project.dap_store().update(cx, |dap_store, cx| {
844 dap_store.shutdown_session(session.read(cx).session_id(), cx)
845 })
846 })
847 });
848
849 client_ssh.update(cx_a, |a, _| {
850 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
851 });
852
853 shutdown_session.await.unwrap();
854}