1use crate::{
2 persistence::DebuggerPaneItem,
3 tests::{start_debug_session, start_debug_session_with},
4 *,
5};
6use dap::{
7 ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
8 StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
9 adapters::DebugTaskDefinition,
10 client::SessionId,
11 requests::{
12 Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
13 StartDebugging, StepBack, StepIn, StepOut, Threads,
14 },
15};
16use editor::{
17 ActiveDebugLine, Editor, EditorMode, MultiBuffer,
18 actions::{self},
19};
20use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
21use project::{
22 FakeFs, Project,
23 debugger::session::{ThreadId, ThreadStatus},
24};
25use serde_json::json;
26use std::{
27 path::Path,
28 sync::{
29 Arc,
30 atomic::{AtomicBool, Ordering},
31 },
32};
33use terminal_view::terminal_panel::TerminalPanel;
34use tests::{active_debug_session_panel, init_test, init_test_workspace};
35use util::path;
36use workspace::item::SaveOptions;
37use workspace::{Item, dock::Panel};
38
39#[gpui::test]
40async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
41 init_test(cx);
42
43 let fs = FakeFs::new(executor.clone());
44
45 fs.insert_tree(
46 path!("/project"),
47 json!({
48 "main.rs": "First line\nSecond line\nThird line\nFourth line",
49 }),
50 )
51 .await;
52
53 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
54 let workspace = init_test_workspace(&project, cx).await;
55 let cx = &mut VisualTestContext::from_window(*workspace, cx);
56
57 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
58 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
59
60 client.on_request::<Threads, _>(move |_, _| {
61 Ok(dap::ThreadsResponse {
62 threads: vec![dap::Thread {
63 id: 1,
64 name: "Thread 1".into(),
65 }],
66 })
67 });
68
69 client.on_request::<StackTrace, _>(move |_, _| {
70 Ok(dap::StackTraceResponse {
71 stack_frames: Vec::default(),
72 total_frames: None,
73 })
74 });
75
76 cx.run_until_parked();
77
78 // assert we have a debug panel item before the session has stopped
79 workspace
80 .update(cx, |workspace, _window, cx| {
81 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
82 let active_session =
83 debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
84
85 let running_state = active_session.update(cx, |active_session, _| {
86 active_session.running_state().clone()
87 });
88
89 debug_panel.update(cx, |this, cx| {
90 assert!(this.active_session().is_some());
91 assert!(running_state.read(cx).selected_thread_id().is_none());
92 });
93 })
94 .unwrap();
95
96 client
97 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
98 reason: dap::StoppedEventReason::Pause,
99 description: None,
100 thread_id: Some(1),
101 preserve_focus_hint: None,
102 text: None,
103 all_threads_stopped: None,
104 hit_breakpoint_ids: None,
105 }))
106 .await;
107
108 cx.run_until_parked();
109
110 workspace
111 .update(cx, |workspace, _window, cx| {
112 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
113 let active_session = debug_panel
114 .update(cx, |this, _| this.active_session())
115 .unwrap();
116
117 let running_state = active_session.update(cx, |active_session, _| {
118 active_session.running_state().clone()
119 });
120
121 assert_eq!(client.id(), running_state.read(cx).session_id());
122 assert_eq!(
123 ThreadId(1),
124 running_state.read(cx).selected_thread_id().unwrap()
125 );
126 })
127 .unwrap();
128
129 let shutdown_session = project.update(cx, |project, cx| {
130 project.dap_store().update(cx, |dap_store, cx| {
131 dap_store.shutdown_session(session.read(cx).session_id(), cx)
132 })
133 });
134
135 shutdown_session.await.unwrap();
136
137 // assert we still have a debug panel item after the client shutdown
138 workspace
139 .update(cx, |workspace, _window, cx| {
140 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
141
142 let active_session = debug_panel
143 .update(cx, |this, _| this.active_session())
144 .unwrap();
145
146 let running_state = active_session.update(cx, |active_session, _| {
147 active_session.running_state().clone()
148 });
149
150 debug_panel.update(cx, |this, cx| {
151 assert!(this.active_session().is_some());
152 assert_eq!(
153 ThreadId(1),
154 running_state.read(cx).selected_thread_id().unwrap()
155 );
156 });
157 })
158 .unwrap();
159}
160
161#[gpui::test]
162async fn test_we_can_only_have_one_panel_per_debug_session(
163 executor: BackgroundExecutor,
164 cx: &mut TestAppContext,
165) {
166 init_test(cx);
167
168 let fs = FakeFs::new(executor.clone());
169
170 fs.insert_tree(
171 path!("/project"),
172 json!({
173 "main.rs": "First line\nSecond line\nThird line\nFourth line",
174 }),
175 )
176 .await;
177
178 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
179 let workspace = init_test_workspace(&project, cx).await;
180 let cx = &mut VisualTestContext::from_window(*workspace, cx);
181
182 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
183 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
184
185 client.on_request::<Threads, _>(move |_, _| {
186 Ok(dap::ThreadsResponse {
187 threads: vec![dap::Thread {
188 id: 1,
189 name: "Thread 1".into(),
190 }],
191 })
192 });
193
194 client.on_request::<StackTrace, _>(move |_, _| {
195 Ok(dap::StackTraceResponse {
196 stack_frames: Vec::default(),
197 total_frames: None,
198 })
199 });
200
201 cx.run_until_parked();
202
203 // assert we have a debug panel item before the session has stopped
204 workspace
205 .update(cx, |workspace, _window, cx| {
206 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
207
208 debug_panel.update(cx, |this, _| {
209 assert!(this.active_session().is_some());
210 });
211 })
212 .unwrap();
213
214 client
215 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
216 reason: dap::StoppedEventReason::Pause,
217 description: None,
218 thread_id: Some(1),
219 preserve_focus_hint: None,
220 text: None,
221 all_threads_stopped: None,
222 hit_breakpoint_ids: None,
223 }))
224 .await;
225
226 cx.run_until_parked();
227
228 // assert we added a debug panel item
229 workspace
230 .update(cx, |workspace, _window, cx| {
231 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
232 let active_session = debug_panel
233 .update(cx, |this, _| this.active_session())
234 .unwrap();
235
236 let running_state = active_session.update(cx, |active_session, _| {
237 active_session.running_state().clone()
238 });
239
240 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
241 assert_eq!(
242 ThreadId(1),
243 running_state.read(cx).selected_thread_id().unwrap()
244 );
245 })
246 .unwrap();
247
248 client
249 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
250 reason: dap::StoppedEventReason::Pause,
251 description: None,
252 thread_id: Some(2),
253 preserve_focus_hint: None,
254 text: None,
255 all_threads_stopped: None,
256 hit_breakpoint_ids: None,
257 }))
258 .await;
259
260 cx.run_until_parked();
261
262 workspace
263 .update(cx, |workspace, _window, cx| {
264 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
265 let active_session = debug_panel
266 .update(cx, |this, _| this.active_session())
267 .unwrap();
268
269 let running_state = active_session.update(cx, |active_session, _| {
270 active_session.running_state().clone()
271 });
272
273 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
274 assert_eq!(
275 ThreadId(1),
276 running_state.read(cx).selected_thread_id().unwrap()
277 );
278 })
279 .unwrap();
280
281 let shutdown_session = project.update(cx, |project, cx| {
282 project.dap_store().update(cx, |dap_store, cx| {
283 dap_store.shutdown_session(session.read(cx).session_id(), cx)
284 })
285 });
286
287 shutdown_session.await.unwrap();
288
289 // assert we still have a debug panel item after the client shutdown
290 workspace
291 .update(cx, |workspace, _window, cx| {
292 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
293 let active_session = debug_panel
294 .update(cx, |this, _| this.active_session())
295 .unwrap();
296
297 let running_state = active_session.update(cx, |active_session, _| {
298 active_session.running_state().clone()
299 });
300
301 debug_panel.update(cx, |this, cx| {
302 assert!(this.active_session().is_some());
303 assert_eq!(
304 ThreadId(1),
305 running_state.read(cx).selected_thread_id().unwrap()
306 );
307 });
308 })
309 .unwrap();
310}
311
312#[gpui::test]
313async fn test_handle_successful_run_in_terminal_reverse_request(
314 executor: BackgroundExecutor,
315 cx: &mut TestAppContext,
316) {
317 init_test(cx);
318
319 let send_response = Arc::new(AtomicBool::new(false));
320
321 let fs = FakeFs::new(executor.clone());
322
323 fs.insert_tree(
324 path!("/project"),
325 json!({
326 "main.rs": "First line\nSecond line\nThird line\nFourth line",
327 }),
328 )
329 .await;
330
331 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
332 let workspace = init_test_workspace(&project, cx).await;
333 let cx = &mut VisualTestContext::from_window(*workspace, cx);
334
335 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
336 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
337
338 client
339 .on_response::<RunInTerminal, _>({
340 let send_response = send_response.clone();
341 move |response| {
342 send_response.store(true, Ordering::SeqCst);
343
344 assert!(response.success);
345 assert!(response.body.is_some());
346 }
347 })
348 .await;
349
350 client
351 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
352 kind: None,
353 title: None,
354 cwd: std::env::temp_dir().to_string_lossy().to_string(),
355 args: vec![],
356 env: None,
357 args_can_be_interpreted_by_shell: None,
358 })
359 .await;
360
361 cx.run_until_parked();
362
363 assert!(
364 send_response.load(std::sync::atomic::Ordering::SeqCst),
365 "Expected to receive response from reverse request"
366 );
367
368 workspace
369 .update(cx, |workspace, _window, cx| {
370 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
371 let session = debug_panel.read(cx).active_session().unwrap();
372 let running = session.read(cx).running_state();
373 assert_eq!(
374 running
375 .read(cx)
376 .pane_items_status(cx)
377 .get(&DebuggerPaneItem::Terminal),
378 Some(&true)
379 );
380 assert!(running.read(cx).debug_terminal.read(cx).terminal.is_some());
381 })
382 .unwrap();
383}
384
385#[gpui::test]
386async fn test_handle_start_debugging_request(
387 executor: BackgroundExecutor,
388 cx: &mut TestAppContext,
389) {
390 init_test(cx);
391
392 let fs = FakeFs::new(executor.clone());
393
394 fs.insert_tree(
395 path!("/project"),
396 json!({
397 "main.rs": "First line\nSecond line\nThird line\nFourth line",
398 }),
399 )
400 .await;
401
402 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
403 let workspace = init_test_workspace(&project, cx).await;
404 let cx = &mut VisualTestContext::from_window(*workspace, cx);
405
406 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
407 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
408
409 let fake_config = json!({"one": "two"});
410 let launched_with = Arc::new(parking_lot::Mutex::new(None));
411
412 let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
413 let launched_with = launched_with.clone();
414 move |client| {
415 let launched_with = launched_with.clone();
416 client.on_request::<dap::requests::Launch, _>(move |_, args| {
417 launched_with.lock().replace(args.raw);
418 Ok(())
419 });
420 client.on_request::<dap::requests::Attach, _>(move |_, _| {
421 assert!(false, "should not get attach request");
422 Ok(())
423 });
424 }
425 });
426
427 let sessions = workspace
428 .update(cx, |workspace, _window, cx| {
429 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
430 debug_panel.read(cx).sessions()
431 })
432 .unwrap();
433 assert_eq!(sessions.len(), 1);
434 client
435 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
436 request: StartDebuggingRequestArgumentsRequest::Launch,
437 configuration: fake_config.clone(),
438 })
439 .await;
440
441 cx.run_until_parked();
442
443 workspace
444 .update(cx, |workspace, _window, cx| {
445 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
446
447 // Active session does not change on spawn.
448 let active_session = debug_panel
449 .read(cx)
450 .active_session()
451 .unwrap()
452 .read(cx)
453 .session(cx);
454
455 assert_eq!(active_session, sessions[0].read(cx).session(cx));
456 assert!(active_session.read(cx).parent_session().is_none());
457
458 let current_sessions = debug_panel.read(cx).sessions();
459 assert_eq!(current_sessions.len(), 2);
460 assert_eq!(current_sessions[0], sessions[0]);
461
462 let parent_session = current_sessions[1]
463 .read(cx)
464 .session(cx)
465 .read(cx)
466 .parent_session()
467 .unwrap();
468 assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
469
470 // We should preserve the original binary (params to spawn process etc.) except for launch params
471 // (as they come from reverse spawn request).
472 let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
473 original_binary.request_args = StartDebuggingRequestArguments {
474 request: StartDebuggingRequestArgumentsRequest::Launch,
475 configuration: fake_config.clone(),
476 };
477
478 assert_eq!(
479 current_sessions[1]
480 .read(cx)
481 .session(cx)
482 .read(cx)
483 .binary()
484 .unwrap(),
485 &original_binary
486 );
487 })
488 .unwrap();
489
490 assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
491}
492
493// // covers that we always send a response back, if something when wrong,
494// // while spawning the terminal
495#[gpui::test]
496async fn test_handle_error_run_in_terminal_reverse_request(
497 executor: BackgroundExecutor,
498 cx: &mut TestAppContext,
499) {
500 init_test(cx);
501
502 let send_response = Arc::new(AtomicBool::new(false));
503
504 let fs = FakeFs::new(executor.clone());
505
506 fs.insert_tree(
507 path!("/project"),
508 json!({
509 "main.rs": "First line\nSecond line\nThird line\nFourth line",
510 }),
511 )
512 .await;
513
514 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
515 let workspace = init_test_workspace(&project, cx).await;
516 let cx = &mut VisualTestContext::from_window(*workspace, cx);
517
518 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
519 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
520
521 client
522 .on_response::<RunInTerminal, _>({
523 let send_response = send_response.clone();
524 move |response| {
525 send_response.store(true, Ordering::SeqCst);
526
527 assert!(!response.success);
528 assert!(response.body.is_some());
529 }
530 })
531 .await;
532
533 client
534 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
535 kind: None,
536 title: None,
537 cwd: "".into(),
538 args: vec!["oops".into(), "oops".into()],
539 env: None,
540 args_can_be_interpreted_by_shell: None,
541 })
542 .await;
543
544 cx.run_until_parked();
545
546 assert!(
547 send_response.load(std::sync::atomic::Ordering::SeqCst),
548 "Expected to receive response from reverse request"
549 );
550
551 workspace
552 .update(cx, |workspace, _window, cx| {
553 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
554
555 assert_eq!(
556 0,
557 terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
558 );
559 })
560 .unwrap();
561}
562
563#[gpui::test]
564async fn test_handle_start_debugging_reverse_request(
565 executor: BackgroundExecutor,
566 cx: &mut TestAppContext,
567) {
568 init_test(cx);
569
570 let send_response = Arc::new(AtomicBool::new(false));
571
572 let fs = FakeFs::new(executor.clone());
573
574 fs.insert_tree(
575 path!("/project"),
576 json!({
577 "main.rs": "First line\nSecond line\nThird line\nFourth line",
578 }),
579 )
580 .await;
581
582 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
583 let workspace = init_test_workspace(&project, cx).await;
584 let cx = &mut VisualTestContext::from_window(*workspace, cx);
585
586 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
587 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
588
589 client.on_request::<dap::requests::Threads, _>(move |_, _| {
590 Ok(dap::ThreadsResponse {
591 threads: vec![dap::Thread {
592 id: 1,
593 name: "Thread 1".into(),
594 }],
595 })
596 });
597
598 client
599 .on_response::<StartDebugging, _>({
600 let send_response = send_response.clone();
601 move |response| {
602 send_response.store(true, Ordering::SeqCst);
603
604 assert!(response.success);
605 assert!(response.body.is_some());
606 }
607 })
608 .await;
609 // Set up handlers for sessions spawned with reverse request too.
610 let _reverse_request_subscription =
611 project::debugger::test::intercept_debug_sessions(cx, |_| {});
612 client
613 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
614 configuration: json!({}),
615 request: StartDebuggingRequestArgumentsRequest::Launch,
616 })
617 .await;
618
619 cx.run_until_parked();
620
621 let child_session = project.update(cx, |project, cx| {
622 project
623 .dap_store()
624 .read(cx)
625 .session_by_id(SessionId(1))
626 .unwrap()
627 });
628 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
629
630 child_client.on_request::<dap::requests::Threads, _>(move |_, _| {
631 Ok(dap::ThreadsResponse {
632 threads: vec![dap::Thread {
633 id: 1,
634 name: "Thread 1".into(),
635 }],
636 })
637 });
638
639 child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
640
641 child_client
642 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
643 reason: dap::StoppedEventReason::Pause,
644 description: None,
645 thread_id: Some(2),
646 preserve_focus_hint: None,
647 text: None,
648 all_threads_stopped: None,
649 hit_breakpoint_ids: None,
650 }))
651 .await;
652
653 cx.run_until_parked();
654
655 assert!(
656 send_response.load(std::sync::atomic::Ordering::SeqCst),
657 "Expected to receive response from reverse request"
658 );
659}
660
661#[gpui::test]
662async fn test_shutdown_children_when_parent_session_shutdown(
663 executor: BackgroundExecutor,
664 cx: &mut TestAppContext,
665) {
666 init_test(cx);
667
668 let fs = FakeFs::new(executor.clone());
669
670 fs.insert_tree(
671 path!("/project"),
672 json!({
673 "main.rs": "First line\nSecond line\nThird line\nFourth line",
674 }),
675 )
676 .await;
677
678 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
679 let dap_store = project.update(cx, |project, _| project.dap_store());
680 let workspace = init_test_workspace(&project, cx).await;
681 let cx = &mut VisualTestContext::from_window(*workspace, cx);
682
683 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
684 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
685
686 client.on_request::<dap::requests::Threads, _>(move |_, _| {
687 Ok(dap::ThreadsResponse {
688 threads: vec![dap::Thread {
689 id: 1,
690 name: "Thread 1".into(),
691 }],
692 })
693 });
694
695 client.on_response::<StartDebugging, _>(move |_| {}).await;
696 // Set up handlers for sessions spawned with reverse request too.
697 let _reverse_request_subscription =
698 project::debugger::test::intercept_debug_sessions(cx, |_| {});
699 // start first child session
700 client
701 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
702 configuration: json!({}),
703 request: StartDebuggingRequestArgumentsRequest::Launch,
704 })
705 .await;
706
707 cx.run_until_parked();
708
709 // start second child session
710 client
711 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
712 configuration: json!({}),
713 request: StartDebuggingRequestArgumentsRequest::Launch,
714 })
715 .await;
716
717 cx.run_until_parked();
718
719 // configure first child session
720 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
721 dap_store.session_by_id(SessionId(1)).unwrap()
722 });
723 let first_child_client =
724 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
725
726 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
727
728 // configure second child session
729 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
730 dap_store.session_by_id(SessionId(2)).unwrap()
731 });
732 let second_child_client =
733 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
734
735 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
736
737 cx.run_until_parked();
738
739 // shutdown parent session
740 dap_store
741 .update(cx, |dap_store, cx| {
742 dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
743 })
744 .await
745 .unwrap();
746
747 // assert parent session and all children sessions are shutdown
748 dap_store.update(cx, |dap_store, cx| {
749 assert!(
750 dap_store
751 .session_by_id(parent_session.read(cx).session_id())
752 .is_none()
753 );
754 assert!(
755 dap_store
756 .session_by_id(first_child_session.read(cx).session_id())
757 .is_none()
758 );
759 assert!(
760 dap_store
761 .session_by_id(second_child_session.read(cx).session_id())
762 .is_none()
763 );
764 });
765}
766
767#[gpui::test]
768async fn test_shutdown_parent_session_if_all_children_are_shutdown(
769 executor: BackgroundExecutor,
770 cx: &mut TestAppContext,
771) {
772 init_test(cx);
773
774 let fs = FakeFs::new(executor.clone());
775
776 fs.insert_tree(
777 path!("/project"),
778 json!({
779 "main.rs": "First line\nSecond line\nThird line\nFourth line",
780 }),
781 )
782 .await;
783
784 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
785 let dap_store = project.update(cx, |project, _| project.dap_store());
786 let workspace = init_test_workspace(&project, cx).await;
787 let cx = &mut VisualTestContext::from_window(*workspace, cx);
788
789 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
790 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
791
792 client.on_response::<StartDebugging, _>(move |_| {}).await;
793 // Set up handlers for sessions spawned with reverse request too.
794 let _reverse_request_subscription =
795 project::debugger::test::intercept_debug_sessions(cx, |_| {});
796 // start first child session
797 client
798 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
799 configuration: json!({}),
800 request: StartDebuggingRequestArgumentsRequest::Launch,
801 })
802 .await;
803
804 cx.run_until_parked();
805
806 // start second child session
807 client
808 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
809 configuration: json!({}),
810 request: StartDebuggingRequestArgumentsRequest::Launch,
811 })
812 .await;
813
814 cx.run_until_parked();
815
816 // configure first child session
817 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
818 dap_store.session_by_id(SessionId(1)).unwrap()
819 });
820 let first_child_client =
821 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
822
823 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
824
825 // configure second child session
826 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
827 dap_store.session_by_id(SessionId(2)).unwrap()
828 });
829 let second_child_client =
830 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
831
832 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
833
834 cx.run_until_parked();
835
836 // shutdown first child session
837 dap_store
838 .update(cx, |dap_store, cx| {
839 dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
840 })
841 .await
842 .unwrap();
843
844 // assert parent session and second child session still exist
845 dap_store.update(cx, |dap_store, cx| {
846 assert!(
847 dap_store
848 .session_by_id(parent_session.read(cx).session_id())
849 .is_some()
850 );
851 assert!(
852 dap_store
853 .session_by_id(first_child_session.read(cx).session_id())
854 .is_none()
855 );
856 assert!(
857 dap_store
858 .session_by_id(second_child_session.read(cx).session_id())
859 .is_some()
860 );
861 });
862
863 // shutdown first child session
864 dap_store
865 .update(cx, |dap_store, cx| {
866 dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
867 })
868 .await
869 .unwrap();
870
871 // assert parent session got shutdown by second child session
872 // because it was the last child
873 dap_store.update(cx, |dap_store, cx| {
874 assert!(
875 dap_store
876 .session_by_id(parent_session.read(cx).session_id())
877 .is_none()
878 );
879 assert!(
880 dap_store
881 .session_by_id(second_child_session.read(cx).session_id())
882 .is_none()
883 );
884 });
885}
886
887#[gpui::test]
888async fn test_debug_panel_item_thread_status_reset_on_failure(
889 executor: BackgroundExecutor,
890 cx: &mut TestAppContext,
891) {
892 init_test(cx);
893
894 let fs = FakeFs::new(executor.clone());
895
896 fs.insert_tree(
897 path!("/project"),
898 json!({
899 "main.rs": "First line\nSecond line\nThird line\nFourth line",
900 }),
901 )
902 .await;
903
904 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
905 let workspace = init_test_workspace(&project, cx).await;
906 let cx = &mut VisualTestContext::from_window(*workspace, cx);
907
908 let session = start_debug_session(&workspace, cx, |client| {
909 client.on_request::<dap::requests::Initialize, _>(move |_, _| {
910 Ok(dap::Capabilities {
911 supports_step_back: Some(true),
912 ..Default::default()
913 })
914 });
915 })
916 .unwrap();
917
918 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
919 const THREAD_ID_NUM: u64 = 1;
920
921 client.on_request::<dap::requests::Threads, _>(move |_, _| {
922 Ok(dap::ThreadsResponse {
923 threads: vec![dap::Thread {
924 id: THREAD_ID_NUM,
925 name: "Thread 1".into(),
926 }],
927 })
928 });
929
930 client.on_request::<Launch, _>(move |_, _| Ok(()));
931
932 client.on_request::<StackTrace, _>(move |_, _| {
933 Ok(dap::StackTraceResponse {
934 stack_frames: Vec::default(),
935 total_frames: None,
936 })
937 });
938
939 client.on_request::<Next, _>(move |_, _| {
940 Err(ErrorResponse {
941 error: Some(dap::Message {
942 id: 1,
943 format: "error".into(),
944 variables: None,
945 send_telemetry: None,
946 show_user: None,
947 url: None,
948 url_label: None,
949 }),
950 })
951 });
952
953 client.on_request::<StepOut, _>(move |_, _| {
954 Err(ErrorResponse {
955 error: Some(dap::Message {
956 id: 1,
957 format: "error".into(),
958 variables: None,
959 send_telemetry: None,
960 show_user: None,
961 url: None,
962 url_label: None,
963 }),
964 })
965 });
966
967 client.on_request::<StepIn, _>(move |_, _| {
968 Err(ErrorResponse {
969 error: Some(dap::Message {
970 id: 1,
971 format: "error".into(),
972 variables: None,
973 send_telemetry: None,
974 show_user: None,
975 url: None,
976 url_label: None,
977 }),
978 })
979 });
980
981 client.on_request::<StepBack, _>(move |_, _| {
982 Err(ErrorResponse {
983 error: Some(dap::Message {
984 id: 1,
985 format: "error".into(),
986 variables: None,
987 send_telemetry: None,
988 show_user: None,
989 url: None,
990 url_label: None,
991 }),
992 })
993 });
994
995 client.on_request::<Continue, _>(move |_, _| {
996 Err(ErrorResponse {
997 error: Some(dap::Message {
998 id: 1,
999 format: "error".into(),
1000 variables: None,
1001 send_telemetry: None,
1002 show_user: None,
1003 url: None,
1004 url_label: None,
1005 }),
1006 })
1007 });
1008
1009 client
1010 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1011 reason: dap::StoppedEventReason::Pause,
1012 description: None,
1013 thread_id: Some(1),
1014 preserve_focus_hint: None,
1015 text: None,
1016 all_threads_stopped: None,
1017 hit_breakpoint_ids: None,
1018 }))
1019 .await;
1020
1021 cx.run_until_parked();
1022
1023 let running_state = active_debug_session_panel(workspace, cx)
1024 .read_with(cx, |item, _| item.running_state().clone());
1025
1026 cx.run_until_parked();
1027 let thread_id = ThreadId(1);
1028
1029 for operation in &[
1030 "step_over",
1031 "continue_thread",
1032 "step_back",
1033 "step_in",
1034 "step_out",
1035 ] {
1036 running_state.update(cx, |running_state, cx| match *operation {
1037 "step_over" => running_state.step_over(cx),
1038 "continue_thread" => running_state.continue_thread(cx),
1039 "step_back" => running_state.step_back(cx),
1040 "step_in" => running_state.step_in(cx),
1041 "step_out" => running_state.step_out(cx),
1042 _ => unreachable!(),
1043 });
1044
1045 // Check that we step the thread status to the correct intermediate state
1046 running_state.update(cx, |running_state, cx| {
1047 assert_eq!(
1048 running_state
1049 .thread_status(cx)
1050 .expect("There should be an active thread selected"),
1051 match *operation {
1052 "continue_thread" => ThreadStatus::Running,
1053 _ => ThreadStatus::Stepping,
1054 },
1055 "Thread status was not set to correct intermediate state after {} request",
1056 operation
1057 );
1058 });
1059
1060 cx.run_until_parked();
1061
1062 running_state.update(cx, |running_state, cx| {
1063 assert_eq!(
1064 running_state
1065 .thread_status(cx)
1066 .expect("There should be an active thread selected"),
1067 ThreadStatus::Stopped,
1068 "Thread status not reset to Stopped after failed {}",
1069 operation
1070 );
1071
1072 // update state to running, so we can test it actually changes the status back to stopped
1073 running_state
1074 .session()
1075 .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1076 });
1077 }
1078}
1079
1080#[gpui::test]
1081async fn test_send_breakpoints_when_editor_has_been_saved(
1082 executor: BackgroundExecutor,
1083 cx: &mut TestAppContext,
1084) {
1085 init_test(cx);
1086
1087 let fs = FakeFs::new(executor.clone());
1088
1089 fs.insert_tree(
1090 path!("/project"),
1091 json!({
1092 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1093 }),
1094 )
1095 .await;
1096
1097 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1098 let workspace = init_test_workspace(&project, cx).await;
1099 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1100 let project_path = Path::new(path!("/project"));
1101 let worktree = project
1102 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1103 .expect("This worktree should exist in project")
1104 .0;
1105
1106 let worktree_id = workspace
1107 .update(cx, |_, _, cx| worktree.read(cx).id())
1108 .unwrap();
1109
1110 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1111 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1112
1113 let buffer = project
1114 .update(cx, |project, cx| {
1115 project.open_buffer((worktree_id, "main.rs"), cx)
1116 })
1117 .await
1118 .unwrap();
1119
1120 let (editor, cx) = cx.add_window_view(|window, cx| {
1121 Editor::new(
1122 EditorMode::full(),
1123 MultiBuffer::build_from_buffer(buffer, cx),
1124 Some(project.clone()),
1125 window,
1126 cx,
1127 )
1128 });
1129
1130 client.on_request::<Launch, _>(move |_, _| Ok(()));
1131
1132 client.on_request::<StackTrace, _>(move |_, _| {
1133 Ok(dap::StackTraceResponse {
1134 stack_frames: Vec::default(),
1135 total_frames: None,
1136 })
1137 });
1138
1139 client
1140 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1141 reason: dap::StoppedEventReason::Pause,
1142 description: None,
1143 thread_id: Some(1),
1144 preserve_focus_hint: None,
1145 text: None,
1146 all_threads_stopped: None,
1147 hit_breakpoint_ids: None,
1148 }))
1149 .await;
1150
1151 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1152 client.on_request::<SetBreakpoints, _>({
1153 let called_set_breakpoints = called_set_breakpoints.clone();
1154 move |_, args| {
1155 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1156 assert_eq!(
1157 vec![SourceBreakpoint {
1158 line: 2,
1159 column: None,
1160 condition: None,
1161 hit_condition: None,
1162 log_message: None,
1163 mode: None
1164 }],
1165 args.breakpoints.unwrap()
1166 );
1167 assert!(!args.source_modified.unwrap());
1168
1169 called_set_breakpoints.store(true, Ordering::SeqCst);
1170
1171 Ok(dap::SetBreakpointsResponse {
1172 breakpoints: Vec::default(),
1173 })
1174 }
1175 });
1176
1177 editor.update_in(cx, |editor, window, cx| {
1178 editor.move_down(&actions::MoveDown, window, cx);
1179 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1180 });
1181
1182 cx.run_until_parked();
1183
1184 assert!(
1185 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1186 "SetBreakpoint request must be called"
1187 );
1188
1189 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1190 client.on_request::<SetBreakpoints, _>({
1191 let called_set_breakpoints = called_set_breakpoints.clone();
1192 move |_, args| {
1193 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1194 assert_eq!(
1195 vec![SourceBreakpoint {
1196 line: 3,
1197 column: None,
1198 condition: None,
1199 hit_condition: None,
1200 log_message: None,
1201 mode: None
1202 }],
1203 args.breakpoints.unwrap()
1204 );
1205 assert!(args.source_modified.unwrap());
1206
1207 called_set_breakpoints.store(true, Ordering::SeqCst);
1208
1209 Ok(dap::SetBreakpointsResponse {
1210 breakpoints: Vec::default(),
1211 })
1212 }
1213 });
1214
1215 editor.update_in(cx, |editor, window, cx| {
1216 editor.move_up(&actions::MoveUp, window, cx);
1217 editor.insert("new text\n", window, cx);
1218 });
1219
1220 editor
1221 .update_in(cx, |editor, window, cx| {
1222 editor.save(
1223 SaveOptions {
1224 format: true,
1225 autosave: false,
1226 },
1227 project.clone(),
1228 window,
1229 cx,
1230 )
1231 })
1232 .await
1233 .unwrap();
1234
1235 cx.run_until_parked();
1236
1237 assert!(
1238 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1239 "SetBreakpoint request must be called after editor is saved"
1240 );
1241}
1242
1243#[gpui::test]
1244async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1245 executor: BackgroundExecutor,
1246 cx: &mut TestAppContext,
1247) {
1248 init_test(cx);
1249
1250 let fs = FakeFs::new(executor.clone());
1251
1252 fs.insert_tree(
1253 path!("/project"),
1254 json!({
1255 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1256 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1257 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1258 }),
1259 )
1260 .await;
1261
1262 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1263 let workspace = init_test_workspace(&project, cx).await;
1264 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1265 let project_path = Path::new(path!("/project"));
1266 let worktree = project
1267 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1268 .expect("This worktree should exist in project")
1269 .0;
1270
1271 let worktree_id = workspace
1272 .update(cx, |_, _, cx| worktree.read(cx).id())
1273 .unwrap();
1274
1275 let first = project
1276 .update(cx, |project, cx| {
1277 project.open_buffer((worktree_id, "main.rs"), cx)
1278 })
1279 .await
1280 .unwrap();
1281
1282 let second = project
1283 .update(cx, |project, cx| {
1284 project.open_buffer((worktree_id, "second.rs"), cx)
1285 })
1286 .await
1287 .unwrap();
1288
1289 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1290 Editor::new(
1291 EditorMode::full(),
1292 MultiBuffer::build_from_buffer(first, cx),
1293 Some(project.clone()),
1294 window,
1295 cx,
1296 )
1297 });
1298
1299 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1300 Editor::new(
1301 EditorMode::full(),
1302 MultiBuffer::build_from_buffer(second, cx),
1303 Some(project.clone()),
1304 window,
1305 cx,
1306 )
1307 });
1308
1309 first_editor.update_in(cx, |editor, window, cx| {
1310 editor.move_down(&actions::MoveDown, window, cx);
1311 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1312 editor.move_down(&actions::MoveDown, window, cx);
1313 editor.move_down(&actions::MoveDown, window, cx);
1314 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1315 });
1316
1317 second_editor.update_in(cx, |editor, window, cx| {
1318 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1319 editor.move_down(&actions::MoveDown, window, cx);
1320 editor.move_down(&actions::MoveDown, window, cx);
1321 editor.move_down(&actions::MoveDown, window, cx);
1322 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1323 });
1324
1325 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1326 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1327
1328 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1329
1330 client.on_request::<SetBreakpoints, _>({
1331 let called_set_breakpoints = called_set_breakpoints.clone();
1332 move |_, args| {
1333 assert!(
1334 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1335 "Send empty breakpoint sets to clear them from DAP servers"
1336 );
1337
1338 match args
1339 .source
1340 .path
1341 .expect("We should always send a breakpoint's path")
1342 .as_str()
1343 {
1344 path!("/project/main.rs") | path!("/project/second.rs") => {}
1345 _ => {
1346 panic!("Unset breakpoints for path that doesn't have any")
1347 }
1348 }
1349
1350 called_set_breakpoints.store(true, Ordering::SeqCst);
1351
1352 Ok(dap::SetBreakpointsResponse {
1353 breakpoints: Vec::default(),
1354 })
1355 }
1356 });
1357
1358 cx.dispatch_action(crate::ClearAllBreakpoints);
1359 cx.run_until_parked();
1360}
1361
1362#[gpui::test]
1363async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1364 executor: BackgroundExecutor,
1365 cx: &mut TestAppContext,
1366) {
1367 init_test(cx);
1368
1369 let fs = FakeFs::new(executor.clone());
1370
1371 fs.insert_tree(
1372 path!("/project"),
1373 json!({
1374 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1375 }),
1376 )
1377 .await;
1378
1379 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1380 let workspace = init_test_workspace(&project, cx).await;
1381 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1382
1383 start_debug_session(&workspace, cx, |client| {
1384 client.on_request::<dap::requests::Initialize, _>(|_, _| {
1385 Err(ErrorResponse {
1386 error: Some(Message {
1387 format: "failed to launch".to_string(),
1388 id: 1,
1389 variables: None,
1390 send_telemetry: None,
1391 show_user: None,
1392 url: None,
1393 url_label: None,
1394 }),
1395 })
1396 });
1397 })
1398 .ok();
1399
1400 cx.run_until_parked();
1401
1402 project.update(cx, |project, cx| {
1403 assert!(
1404 project.dap_store().read(cx).sessions().count() == 0,
1405 "Session wouldn't exist if it was shutdown"
1406 );
1407 });
1408}
1409
1410#[gpui::test]
1411async fn test_we_send_arguments_from_user_config(
1412 executor: BackgroundExecutor,
1413 cx: &mut TestAppContext,
1414) {
1415 init_test(cx);
1416
1417 let fs = FakeFs::new(executor.clone());
1418
1419 fs.insert_tree(
1420 path!("/project"),
1421 json!({
1422 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1423 }),
1424 )
1425 .await;
1426
1427 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1428 let workspace = init_test_workspace(&project, cx).await;
1429 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1430 let debug_definition = DebugTaskDefinition {
1431 adapter: "fake-adapter".into(),
1432 config: json!({
1433 "request": "launch",
1434 "program": "main.rs".to_owned(),
1435 "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1436 "cwd": path!("/Random_path"),
1437 "env": json!({ "KEY": "VALUE" }),
1438 }),
1439 label: "test".into(),
1440 tcp_connection: None,
1441 };
1442
1443 let launch_handler_called = Arc::new(AtomicBool::new(false));
1444
1445 start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1446 let debug_definition = debug_definition.clone();
1447 let launch_handler_called = launch_handler_called.clone();
1448
1449 move |client| {
1450 let debug_definition = debug_definition.clone();
1451 let launch_handler_called = launch_handler_called.clone();
1452
1453 client.on_request::<dap::requests::Launch, _>(move |_, args| {
1454 launch_handler_called.store(true, Ordering::SeqCst);
1455
1456 assert_eq!(args.raw, debug_definition.config);
1457
1458 Ok(())
1459 });
1460 }
1461 })
1462 .ok();
1463
1464 cx.run_until_parked();
1465
1466 assert!(
1467 launch_handler_called.load(Ordering::SeqCst),
1468 "Launch request handler was not called"
1469 );
1470}
1471
1472#[gpui::test]
1473async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1474 init_test(cx);
1475
1476 let fs = FakeFs::new(executor.clone());
1477
1478 fs.insert_tree(
1479 path!("/project"),
1480 json!({
1481 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1482 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1483 }),
1484 )
1485 .await;
1486
1487 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1488 let workspace = init_test_workspace(&project, cx).await;
1489 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1490 let project_path = Path::new(path!("/project"));
1491 let worktree = project
1492 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1493 .expect("This worktree should exist in project")
1494 .0;
1495
1496 let worktree_id = workspace
1497 .update(cx, |_, _, cx| worktree.read(cx).id())
1498 .unwrap();
1499
1500 let main_buffer = project
1501 .update(cx, |project, cx| {
1502 project.open_buffer((worktree_id, "main.rs"), cx)
1503 })
1504 .await
1505 .unwrap();
1506
1507 let second_buffer = project
1508 .update(cx, |project, cx| {
1509 project.open_buffer((worktree_id, "second.rs"), cx)
1510 })
1511 .await
1512 .unwrap();
1513
1514 let (main_editor, cx) = cx.add_window_view(|window, cx| {
1515 Editor::new(
1516 EditorMode::full(),
1517 MultiBuffer::build_from_buffer(main_buffer, cx),
1518 Some(project.clone()),
1519 window,
1520 cx,
1521 )
1522 });
1523
1524 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1525 Editor::new(
1526 EditorMode::full(),
1527 MultiBuffer::build_from_buffer(second_buffer, cx),
1528 Some(project.clone()),
1529 window,
1530 cx,
1531 )
1532 });
1533
1534 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1535 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1536
1537 client.on_request::<dap::requests::Threads, _>(move |_, _| {
1538 Ok(dap::ThreadsResponse {
1539 threads: vec![dap::Thread {
1540 id: 1,
1541 name: "Thread 1".into(),
1542 }],
1543 })
1544 });
1545
1546 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1547 Ok(dap::ScopesResponse {
1548 scopes: Vec::default(),
1549 })
1550 });
1551
1552 client.on_request::<StackTrace, _>(move |_, args| {
1553 assert_eq!(args.thread_id, 1);
1554
1555 Ok(dap::StackTraceResponse {
1556 stack_frames: vec![dap::StackFrame {
1557 id: 1,
1558 name: "frame 1".into(),
1559 source: Some(dap::Source {
1560 name: Some("main.rs".into()),
1561 path: Some(path!("/project/main.rs").into()),
1562 source_reference: None,
1563 presentation_hint: None,
1564 origin: None,
1565 sources: None,
1566 adapter_data: None,
1567 checksums: None,
1568 }),
1569 line: 2,
1570 column: 0,
1571 end_line: None,
1572 end_column: None,
1573 can_restart: None,
1574 instruction_pointer_reference: None,
1575 module_id: None,
1576 presentation_hint: None,
1577 }],
1578 total_frames: None,
1579 })
1580 });
1581
1582 client
1583 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1584 reason: dap::StoppedEventReason::Breakpoint,
1585 description: None,
1586 thread_id: Some(1),
1587 preserve_focus_hint: None,
1588 text: None,
1589 all_threads_stopped: None,
1590 hit_breakpoint_ids: None,
1591 }))
1592 .await;
1593
1594 cx.run_until_parked();
1595
1596 main_editor.update_in(cx, |editor, window, cx| {
1597 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1598
1599 assert_eq!(
1600 active_debug_lines.len(),
1601 1,
1602 "There should be only one active debug line"
1603 );
1604
1605 let point = editor
1606 .snapshot(window, cx)
1607 .buffer_snapshot
1608 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1609
1610 assert_eq!(point.row, 1);
1611 });
1612
1613 second_editor.update(cx, |editor, _| {
1614 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1615
1616 assert!(
1617 active_debug_lines.is_empty(),
1618 "There shouldn't be any active debug lines"
1619 );
1620 });
1621
1622 let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1623 client.on_request::<StackTrace, _>({
1624 let handled_second_stacktrace = handled_second_stacktrace.clone();
1625 move |_, args| {
1626 handled_second_stacktrace.store(true, Ordering::SeqCst);
1627 assert_eq!(args.thread_id, 1);
1628
1629 Ok(dap::StackTraceResponse {
1630 stack_frames: vec![dap::StackFrame {
1631 id: 2,
1632 name: "frame 2".into(),
1633 source: Some(dap::Source {
1634 name: Some("second.rs".into()),
1635 path: Some(path!("/project/second.rs").into()),
1636 source_reference: None,
1637 presentation_hint: None,
1638 origin: None,
1639 sources: None,
1640 adapter_data: None,
1641 checksums: None,
1642 }),
1643 line: 3,
1644 column: 0,
1645 end_line: None,
1646 end_column: None,
1647 can_restart: None,
1648 instruction_pointer_reference: None,
1649 module_id: None,
1650 presentation_hint: None,
1651 }],
1652 total_frames: None,
1653 })
1654 }
1655 });
1656
1657 client
1658 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1659 reason: dap::StoppedEventReason::Breakpoint,
1660 description: None,
1661 thread_id: Some(1),
1662 preserve_focus_hint: None,
1663 text: None,
1664 all_threads_stopped: None,
1665 hit_breakpoint_ids: None,
1666 }))
1667 .await;
1668
1669 cx.run_until_parked();
1670
1671 second_editor.update_in(cx, |editor, window, cx| {
1672 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1673
1674 assert_eq!(
1675 active_debug_lines.len(),
1676 1,
1677 "There should be only one active debug line"
1678 );
1679
1680 let point = editor
1681 .snapshot(window, cx)
1682 .buffer_snapshot
1683 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1684
1685 assert_eq!(point.row, 2);
1686 });
1687
1688 main_editor.update(cx, |editor, _| {
1689 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1690
1691 assert!(
1692 active_debug_lines.is_empty(),
1693 "There shouldn't be any active debug lines"
1694 );
1695 });
1696
1697 assert!(
1698 handled_second_stacktrace.load(Ordering::SeqCst),
1699 "Second stacktrace request handler was not called"
1700 );
1701
1702 client
1703 .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1704 thread_id: 0,
1705 all_threads_continued: Some(true),
1706 }))
1707 .await;
1708
1709 cx.run_until_parked();
1710
1711 second_editor.update(cx, |editor, _| {
1712 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1713
1714 assert!(
1715 active_debug_lines.is_empty(),
1716 "There shouldn't be any active debug lines"
1717 );
1718 });
1719
1720 main_editor.update(cx, |editor, _| {
1721 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1722
1723 assert!(
1724 active_debug_lines.is_empty(),
1725 "There shouldn't be any active debug lines"
1726 );
1727 });
1728
1729 // Clean up
1730 let shutdown_session = project.update(cx, |project, cx| {
1731 project.dap_store().update(cx, |dap_store, cx| {
1732 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1733 })
1734 });
1735
1736 shutdown_session.await.unwrap();
1737
1738 main_editor.update(cx, |editor, _| {
1739 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1740
1741 assert!(
1742 active_debug_lines.is_empty(),
1743 "There shouldn't be any active debug lines after session shutdown"
1744 );
1745 });
1746
1747 second_editor.update(cx, |editor, _| {
1748 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1749
1750 assert!(
1751 active_debug_lines.is_empty(),
1752 "There shouldn't be any active debug lines after session shutdown"
1753 );
1754 });
1755}