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