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