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