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