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 changes on spawn, as the parent has never stopped.
447 let active_session = debug_panel
448 .read(cx)
449 .active_session()
450 .unwrap()
451 .read(cx)
452 .session(cx);
453 let current_sessions = debug_panel.read(cx).sessions();
454 assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
455 assert_eq!(
456 active_session.read(cx).parent_session(),
457 Some(¤t_sessions[0].read(cx).session(cx))
458 );
459
460 assert_eq!(current_sessions.len(), 2);
461 assert_eq!(current_sessions[0], sessions[0]);
462
463 let parent_session = current_sessions[1]
464 .read(cx)
465 .session(cx)
466 .read(cx)
467 .parent_session()
468 .unwrap();
469 assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
470
471 // We should preserve the original binary (params to spawn process etc.) except for launch params
472 // (as they come from reverse spawn request).
473 let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
474 original_binary.request_args = StartDebuggingRequestArguments {
475 request: StartDebuggingRequestArgumentsRequest::Launch,
476 configuration: fake_config.clone(),
477 };
478
479 assert_eq!(
480 current_sessions[1]
481 .read(cx)
482 .session(cx)
483 .read(cx)
484 .binary()
485 .unwrap(),
486 &original_binary
487 );
488 })
489 .unwrap();
490
491 assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
492}
493
494// // covers that we always send a response back, if something when wrong,
495// // while spawning the terminal
496#[gpui::test]
497async fn test_handle_error_run_in_terminal_reverse_request(
498 executor: BackgroundExecutor,
499 cx: &mut TestAppContext,
500) {
501 init_test(cx);
502
503 let send_response = Arc::new(AtomicBool::new(false));
504
505 let fs = FakeFs::new(executor.clone());
506
507 fs.insert_tree(
508 path!("/project"),
509 json!({
510 "main.rs": "First line\nSecond line\nThird line\nFourth line",
511 }),
512 )
513 .await;
514
515 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
516 let workspace = init_test_workspace(&project, cx).await;
517 let cx = &mut VisualTestContext::from_window(*workspace, cx);
518
519 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
520 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
521
522 client
523 .on_response::<RunInTerminal, _>({
524 let send_response = send_response.clone();
525 move |response| {
526 send_response.store(true, Ordering::SeqCst);
527
528 assert!(!response.success);
529 assert!(response.body.is_some());
530 }
531 })
532 .await;
533
534 client
535 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
536 kind: None,
537 title: None,
538 cwd: "".into(),
539 args: vec!["oops".into(), "oops".into()],
540 env: None,
541 args_can_be_interpreted_by_shell: None,
542 })
543 .await;
544
545 cx.run_until_parked();
546
547 assert!(
548 send_response.load(std::sync::atomic::Ordering::SeqCst),
549 "Expected to receive response from reverse request"
550 );
551
552 workspace
553 .update(cx, |workspace, _window, cx| {
554 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
555
556 assert_eq!(
557 0,
558 terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
559 );
560 })
561 .unwrap();
562}
563
564#[gpui::test]
565async fn test_handle_start_debugging_reverse_request(
566 executor: BackgroundExecutor,
567 cx: &mut TestAppContext,
568) {
569 init_test(cx);
570
571 let send_response = Arc::new(AtomicBool::new(false));
572
573 let fs = FakeFs::new(executor.clone());
574
575 fs.insert_tree(
576 path!("/project"),
577 json!({
578 "main.rs": "First line\nSecond line\nThird line\nFourth line",
579 }),
580 )
581 .await;
582
583 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
584 let workspace = init_test_workspace(&project, cx).await;
585 let cx = &mut VisualTestContext::from_window(*workspace, cx);
586
587 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
588 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
589
590 client.on_request::<dap::requests::Threads, _>(move |_, _| {
591 Ok(dap::ThreadsResponse {
592 threads: vec![dap::Thread {
593 id: 1,
594 name: "Thread 1".into(),
595 }],
596 })
597 });
598
599 client
600 .on_response::<StartDebugging, _>({
601 let send_response = send_response.clone();
602 move |response| {
603 send_response.store(true, Ordering::SeqCst);
604
605 assert!(response.success);
606 assert!(response.body.is_some());
607 }
608 })
609 .await;
610 // Set up handlers for sessions spawned with reverse request too.
611 let _reverse_request_subscription =
612 project::debugger::test::intercept_debug_sessions(cx, |_| {});
613 client
614 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
615 configuration: json!({}),
616 request: StartDebuggingRequestArgumentsRequest::Launch,
617 })
618 .await;
619
620 cx.run_until_parked();
621
622 let child_session = project.update(cx, |project, cx| {
623 project
624 .dap_store()
625 .read(cx)
626 .session_by_id(SessionId(1))
627 .unwrap()
628 });
629 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
630
631 child_client.on_request::<dap::requests::Threads, _>(move |_, _| {
632 Ok(dap::ThreadsResponse {
633 threads: vec![dap::Thread {
634 id: 1,
635 name: "Thread 1".into(),
636 }],
637 })
638 });
639
640 child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
641
642 child_client
643 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
644 reason: dap::StoppedEventReason::Pause,
645 description: None,
646 thread_id: Some(2),
647 preserve_focus_hint: None,
648 text: None,
649 all_threads_stopped: None,
650 hit_breakpoint_ids: None,
651 }))
652 .await;
653
654 cx.run_until_parked();
655
656 assert!(
657 send_response.load(std::sync::atomic::Ordering::SeqCst),
658 "Expected to receive response from reverse request"
659 );
660}
661
662#[gpui::test]
663async fn test_shutdown_children_when_parent_session_shutdown(
664 executor: BackgroundExecutor,
665 cx: &mut TestAppContext,
666) {
667 init_test(cx);
668
669 let fs = FakeFs::new(executor.clone());
670
671 fs.insert_tree(
672 path!("/project"),
673 json!({
674 "main.rs": "First line\nSecond line\nThird line\nFourth line",
675 }),
676 )
677 .await;
678
679 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
680 let dap_store = project.update(cx, |project, _| project.dap_store());
681 let workspace = init_test_workspace(&project, cx).await;
682 let cx = &mut VisualTestContext::from_window(*workspace, cx);
683
684 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
685 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
686
687 client.on_request::<dap::requests::Threads, _>(move |_, _| {
688 Ok(dap::ThreadsResponse {
689 threads: vec![dap::Thread {
690 id: 1,
691 name: "Thread 1".into(),
692 }],
693 })
694 });
695
696 client.on_response::<StartDebugging, _>(move |_| {}).await;
697 // Set up handlers for sessions spawned with reverse request too.
698 let _reverse_request_subscription =
699 project::debugger::test::intercept_debug_sessions(cx, |_| {});
700 // start first child session
701 client
702 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
703 configuration: json!({}),
704 request: StartDebuggingRequestArgumentsRequest::Launch,
705 })
706 .await;
707
708 cx.run_until_parked();
709
710 // start second child session
711 client
712 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
713 configuration: json!({}),
714 request: StartDebuggingRequestArgumentsRequest::Launch,
715 })
716 .await;
717
718 cx.run_until_parked();
719
720 // configure first child session
721 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
722 dap_store.session_by_id(SessionId(1)).unwrap()
723 });
724 let first_child_client =
725 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
726
727 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
728
729 // configure second child session
730 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
731 dap_store.session_by_id(SessionId(2)).unwrap()
732 });
733 let second_child_client =
734 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
735
736 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
737
738 cx.run_until_parked();
739
740 // shutdown parent session
741 dap_store
742 .update(cx, |dap_store, cx| {
743 dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
744 })
745 .await
746 .unwrap();
747
748 // assert parent session and all children sessions are shutdown
749 dap_store.update(cx, |dap_store, cx| {
750 assert!(
751 dap_store
752 .session_by_id(parent_session.read(cx).session_id())
753 .is_none()
754 );
755 assert!(
756 dap_store
757 .session_by_id(first_child_session.read(cx).session_id())
758 .is_none()
759 );
760 assert!(
761 dap_store
762 .session_by_id(second_child_session.read(cx).session_id())
763 .is_none()
764 );
765 });
766}
767
768#[gpui::test]
769async fn test_shutdown_parent_session_if_all_children_are_shutdown(
770 executor: BackgroundExecutor,
771 cx: &mut TestAppContext,
772) {
773 init_test(cx);
774
775 let fs = FakeFs::new(executor.clone());
776
777 fs.insert_tree(
778 path!("/project"),
779 json!({
780 "main.rs": "First line\nSecond line\nThird line\nFourth line",
781 }),
782 )
783 .await;
784
785 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
786 let dap_store = project.update(cx, |project, _| project.dap_store());
787 let workspace = init_test_workspace(&project, cx).await;
788 let cx = &mut VisualTestContext::from_window(*workspace, cx);
789
790 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
791 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
792
793 client.on_response::<StartDebugging, _>(move |_| {}).await;
794 // Set up handlers for sessions spawned with reverse request too.
795 let _reverse_request_subscription =
796 project::debugger::test::intercept_debug_sessions(cx, |_| {});
797 // start first child session
798 client
799 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
800 configuration: json!({}),
801 request: StartDebuggingRequestArgumentsRequest::Launch,
802 })
803 .await;
804
805 cx.run_until_parked();
806
807 // start second child session
808 client
809 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
810 configuration: json!({}),
811 request: StartDebuggingRequestArgumentsRequest::Launch,
812 })
813 .await;
814
815 cx.run_until_parked();
816
817 // configure first child session
818 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
819 dap_store.session_by_id(SessionId(1)).unwrap()
820 });
821 let first_child_client =
822 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
823
824 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
825
826 // configure second child session
827 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
828 dap_store.session_by_id(SessionId(2)).unwrap()
829 });
830 let second_child_client =
831 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
832
833 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
834
835 cx.run_until_parked();
836
837 // shutdown first child session
838 dap_store
839 .update(cx, |dap_store, cx| {
840 dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
841 })
842 .await
843 .unwrap();
844
845 // assert parent session and second child session still exist
846 dap_store.update(cx, |dap_store, cx| {
847 assert!(
848 dap_store
849 .session_by_id(parent_session.read(cx).session_id())
850 .is_some()
851 );
852 assert!(
853 dap_store
854 .session_by_id(first_child_session.read(cx).session_id())
855 .is_none()
856 );
857 assert!(
858 dap_store
859 .session_by_id(second_child_session.read(cx).session_id())
860 .is_some()
861 );
862 });
863
864 // shutdown first child session
865 dap_store
866 .update(cx, |dap_store, cx| {
867 dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
868 })
869 .await
870 .unwrap();
871
872 // assert parent session got shutdown by second child session
873 // because it was the last child
874 dap_store.update(cx, |dap_store, cx| {
875 assert!(
876 dap_store
877 .session_by_id(parent_session.read(cx).session_id())
878 .is_none()
879 );
880 assert!(
881 dap_store
882 .session_by_id(second_child_session.read(cx).session_id())
883 .is_none()
884 );
885 });
886}
887
888#[gpui::test]
889async fn test_debug_panel_item_thread_status_reset_on_failure(
890 executor: BackgroundExecutor,
891 cx: &mut TestAppContext,
892) {
893 init_test(cx);
894
895 let fs = FakeFs::new(executor.clone());
896
897 fs.insert_tree(
898 path!("/project"),
899 json!({
900 "main.rs": "First line\nSecond line\nThird line\nFourth line",
901 }),
902 )
903 .await;
904
905 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
906 let workspace = init_test_workspace(&project, cx).await;
907 let cx = &mut VisualTestContext::from_window(*workspace, cx);
908
909 let session = start_debug_session(&workspace, cx, |client| {
910 client.on_request::<dap::requests::Initialize, _>(move |_, _| {
911 Ok(dap::Capabilities {
912 supports_step_back: Some(true),
913 ..Default::default()
914 })
915 });
916 })
917 .unwrap();
918
919 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
920 const THREAD_ID_NUM: u64 = 1;
921
922 client.on_request::<dap::requests::Threads, _>(move |_, _| {
923 Ok(dap::ThreadsResponse {
924 threads: vec![dap::Thread {
925 id: THREAD_ID_NUM,
926 name: "Thread 1".into(),
927 }],
928 })
929 });
930
931 client.on_request::<Launch, _>(move |_, _| Ok(()));
932
933 client.on_request::<StackTrace, _>(move |_, _| {
934 Ok(dap::StackTraceResponse {
935 stack_frames: Vec::default(),
936 total_frames: None,
937 })
938 });
939
940 client.on_request::<Next, _>(move |_, _| {
941 Err(ErrorResponse {
942 error: Some(dap::Message {
943 id: 1,
944 format: "error".into(),
945 variables: None,
946 send_telemetry: None,
947 show_user: None,
948 url: None,
949 url_label: None,
950 }),
951 })
952 });
953
954 client.on_request::<StepOut, _>(move |_, _| {
955 Err(ErrorResponse {
956 error: Some(dap::Message {
957 id: 1,
958 format: "error".into(),
959 variables: None,
960 send_telemetry: None,
961 show_user: None,
962 url: None,
963 url_label: None,
964 }),
965 })
966 });
967
968 client.on_request::<StepIn, _>(move |_, _| {
969 Err(ErrorResponse {
970 error: Some(dap::Message {
971 id: 1,
972 format: "error".into(),
973 variables: None,
974 send_telemetry: None,
975 show_user: None,
976 url: None,
977 url_label: None,
978 }),
979 })
980 });
981
982 client.on_request::<StepBack, _>(move |_, _| {
983 Err(ErrorResponse {
984 error: Some(dap::Message {
985 id: 1,
986 format: "error".into(),
987 variables: None,
988 send_telemetry: None,
989 show_user: None,
990 url: None,
991 url_label: None,
992 }),
993 })
994 });
995
996 client.on_request::<Continue, _>(move |_, _| {
997 Err(ErrorResponse {
998 error: Some(dap::Message {
999 id: 1,
1000 format: "error".into(),
1001 variables: None,
1002 send_telemetry: None,
1003 show_user: None,
1004 url: None,
1005 url_label: None,
1006 }),
1007 })
1008 });
1009
1010 client
1011 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1012 reason: dap::StoppedEventReason::Pause,
1013 description: None,
1014 thread_id: Some(1),
1015 preserve_focus_hint: None,
1016 text: None,
1017 all_threads_stopped: None,
1018 hit_breakpoint_ids: None,
1019 }))
1020 .await;
1021
1022 cx.run_until_parked();
1023
1024 let running_state = active_debug_session_panel(workspace, cx)
1025 .read_with(cx, |item, _| item.running_state().clone());
1026
1027 cx.run_until_parked();
1028 let thread_id = ThreadId(1);
1029
1030 for operation in &[
1031 "step_over",
1032 "continue_thread",
1033 "step_back",
1034 "step_in",
1035 "step_out",
1036 ] {
1037 running_state.update(cx, |running_state, cx| match *operation {
1038 "step_over" => running_state.step_over(cx),
1039 "continue_thread" => running_state.continue_thread(cx),
1040 "step_back" => running_state.step_back(cx),
1041 "step_in" => running_state.step_in(cx),
1042 "step_out" => running_state.step_out(cx),
1043 _ => unreachable!(),
1044 });
1045
1046 // Check that we step the thread status to the correct intermediate state
1047 running_state.update(cx, |running_state, cx| {
1048 assert_eq!(
1049 running_state
1050 .thread_status(cx)
1051 .expect("There should be an active thread selected"),
1052 match *operation {
1053 "continue_thread" => ThreadStatus::Running,
1054 _ => ThreadStatus::Stepping,
1055 },
1056 "Thread status was not set to correct intermediate state after {} request",
1057 operation
1058 );
1059 });
1060
1061 cx.run_until_parked();
1062
1063 running_state.update(cx, |running_state, cx| {
1064 assert_eq!(
1065 running_state
1066 .thread_status(cx)
1067 .expect("There should be an active thread selected"),
1068 ThreadStatus::Stopped,
1069 "Thread status not reset to Stopped after failed {}",
1070 operation
1071 );
1072
1073 // update state to running, so we can test it actually changes the status back to stopped
1074 running_state
1075 .session()
1076 .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1077 });
1078 }
1079}
1080
1081#[gpui::test]
1082async fn test_send_breakpoints_when_editor_has_been_saved(
1083 executor: BackgroundExecutor,
1084 cx: &mut TestAppContext,
1085) {
1086 init_test(cx);
1087
1088 let fs = FakeFs::new(executor.clone());
1089
1090 fs.insert_tree(
1091 path!("/project"),
1092 json!({
1093 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1094 }),
1095 )
1096 .await;
1097
1098 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1099 let workspace = init_test_workspace(&project, cx).await;
1100 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1101 let project_path = Path::new(path!("/project"));
1102 let worktree = project
1103 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1104 .expect("This worktree should exist in project")
1105 .0;
1106
1107 let worktree_id = workspace
1108 .update(cx, |_, _, cx| worktree.read(cx).id())
1109 .unwrap();
1110
1111 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1112 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1113
1114 let buffer = project
1115 .update(cx, |project, cx| {
1116 project.open_buffer((worktree_id, "main.rs"), cx)
1117 })
1118 .await
1119 .unwrap();
1120
1121 let (editor, cx) = cx.add_window_view(|window, cx| {
1122 Editor::new(
1123 EditorMode::full(),
1124 MultiBuffer::build_from_buffer(buffer, cx),
1125 Some(project.clone()),
1126 window,
1127 cx,
1128 )
1129 });
1130
1131 client.on_request::<Launch, _>(move |_, _| Ok(()));
1132
1133 client.on_request::<StackTrace, _>(move |_, _| {
1134 Ok(dap::StackTraceResponse {
1135 stack_frames: Vec::default(),
1136 total_frames: None,
1137 })
1138 });
1139
1140 client
1141 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1142 reason: dap::StoppedEventReason::Pause,
1143 description: None,
1144 thread_id: Some(1),
1145 preserve_focus_hint: None,
1146 text: None,
1147 all_threads_stopped: None,
1148 hit_breakpoint_ids: None,
1149 }))
1150 .await;
1151
1152 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1153 client.on_request::<SetBreakpoints, _>({
1154 let called_set_breakpoints = called_set_breakpoints.clone();
1155 move |_, args| {
1156 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1157 assert_eq!(
1158 vec![SourceBreakpoint {
1159 line: 2,
1160 column: None,
1161 condition: None,
1162 hit_condition: None,
1163 log_message: None,
1164 mode: None
1165 }],
1166 args.breakpoints.unwrap()
1167 );
1168 assert!(!args.source_modified.unwrap());
1169
1170 called_set_breakpoints.store(true, Ordering::SeqCst);
1171
1172 Ok(dap::SetBreakpointsResponse {
1173 breakpoints: Vec::default(),
1174 })
1175 }
1176 });
1177
1178 editor.update_in(cx, |editor, window, cx| {
1179 editor.move_down(&actions::MoveDown, window, cx);
1180 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1181 });
1182
1183 cx.run_until_parked();
1184
1185 assert!(
1186 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1187 "SetBreakpoint request must be called"
1188 );
1189
1190 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1191 client.on_request::<SetBreakpoints, _>({
1192 let called_set_breakpoints = called_set_breakpoints.clone();
1193 move |_, args| {
1194 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1195 assert_eq!(
1196 vec![SourceBreakpoint {
1197 line: 3,
1198 column: None,
1199 condition: None,
1200 hit_condition: None,
1201 log_message: None,
1202 mode: None
1203 }],
1204 args.breakpoints.unwrap()
1205 );
1206 assert!(args.source_modified.unwrap());
1207
1208 called_set_breakpoints.store(true, Ordering::SeqCst);
1209
1210 Ok(dap::SetBreakpointsResponse {
1211 breakpoints: Vec::default(),
1212 })
1213 }
1214 });
1215
1216 editor.update_in(cx, |editor, window, cx| {
1217 editor.move_up(&actions::MoveUp, window, cx);
1218 editor.insert("new text\n", window, cx);
1219 });
1220
1221 editor
1222 .update_in(cx, |editor, window, cx| {
1223 editor.save(true, project.clone(), window, cx)
1224 })
1225 .await
1226 .unwrap();
1227
1228 cx.run_until_parked();
1229
1230 assert!(
1231 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1232 "SetBreakpoint request must be called after editor is saved"
1233 );
1234}
1235
1236#[gpui::test]
1237async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1238 executor: BackgroundExecutor,
1239 cx: &mut TestAppContext,
1240) {
1241 init_test(cx);
1242
1243 let fs = FakeFs::new(executor.clone());
1244
1245 fs.insert_tree(
1246 path!("/project"),
1247 json!({
1248 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1249 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1250 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1251 }),
1252 )
1253 .await;
1254
1255 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1256 let workspace = init_test_workspace(&project, cx).await;
1257 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1258 let project_path = Path::new(path!("/project"));
1259 let worktree = project
1260 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1261 .expect("This worktree should exist in project")
1262 .0;
1263
1264 let worktree_id = workspace
1265 .update(cx, |_, _, cx| worktree.read(cx).id())
1266 .unwrap();
1267
1268 let first = project
1269 .update(cx, |project, cx| {
1270 project.open_buffer((worktree_id, "main.rs"), cx)
1271 })
1272 .await
1273 .unwrap();
1274
1275 let second = project
1276 .update(cx, |project, cx| {
1277 project.open_buffer((worktree_id, "second.rs"), cx)
1278 })
1279 .await
1280 .unwrap();
1281
1282 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1283 Editor::new(
1284 EditorMode::full(),
1285 MultiBuffer::build_from_buffer(first, cx),
1286 Some(project.clone()),
1287 window,
1288 cx,
1289 )
1290 });
1291
1292 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1293 Editor::new(
1294 EditorMode::full(),
1295 MultiBuffer::build_from_buffer(second, cx),
1296 Some(project.clone()),
1297 window,
1298 cx,
1299 )
1300 });
1301
1302 first_editor.update_in(cx, |editor, window, cx| {
1303 editor.move_down(&actions::MoveDown, window, cx);
1304 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1305 editor.move_down(&actions::MoveDown, window, cx);
1306 editor.move_down(&actions::MoveDown, window, cx);
1307 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1308 });
1309
1310 second_editor.update_in(cx, |editor, 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.move_down(&actions::MoveDown, window, cx);
1315 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1316 });
1317
1318 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1319 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1320
1321 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1322
1323 client.on_request::<SetBreakpoints, _>({
1324 let called_set_breakpoints = called_set_breakpoints.clone();
1325 move |_, args| {
1326 assert!(
1327 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1328 "Send empty breakpoint sets to clear them from DAP servers"
1329 );
1330
1331 match args
1332 .source
1333 .path
1334 .expect("We should always send a breakpoint's path")
1335 .as_str()
1336 {
1337 path!("/project/main.rs") | path!("/project/second.rs") => {}
1338 _ => {
1339 panic!("Unset breakpoints for path that doesn't have any")
1340 }
1341 }
1342
1343 called_set_breakpoints.store(true, Ordering::SeqCst);
1344
1345 Ok(dap::SetBreakpointsResponse {
1346 breakpoints: Vec::default(),
1347 })
1348 }
1349 });
1350
1351 cx.dispatch_action(crate::ClearAllBreakpoints);
1352 cx.run_until_parked();
1353}
1354
1355#[gpui::test]
1356async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1357 executor: BackgroundExecutor,
1358 cx: &mut TestAppContext,
1359) {
1360 init_test(cx);
1361
1362 let fs = FakeFs::new(executor.clone());
1363
1364 fs.insert_tree(
1365 path!("/project"),
1366 json!({
1367 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1368 }),
1369 )
1370 .await;
1371
1372 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1373 let workspace = init_test_workspace(&project, cx).await;
1374 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1375
1376 start_debug_session(&workspace, cx, |client| {
1377 client.on_request::<dap::requests::Initialize, _>(|_, _| {
1378 Err(ErrorResponse {
1379 error: Some(Message {
1380 format: "failed to launch".to_string(),
1381 id: 1,
1382 variables: None,
1383 send_telemetry: None,
1384 show_user: None,
1385 url: None,
1386 url_label: None,
1387 }),
1388 })
1389 });
1390 })
1391 .ok();
1392
1393 cx.run_until_parked();
1394
1395 project.update(cx, |project, cx| {
1396 assert!(
1397 project.dap_store().read(cx).sessions().count() == 0,
1398 "Session wouldn't exist if it was shutdown"
1399 );
1400 });
1401}
1402
1403#[gpui::test]
1404async fn test_we_send_arguments_from_user_config(
1405 executor: BackgroundExecutor,
1406 cx: &mut TestAppContext,
1407) {
1408 init_test(cx);
1409
1410 let fs = FakeFs::new(executor.clone());
1411
1412 fs.insert_tree(
1413 path!("/project"),
1414 json!({
1415 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1416 }),
1417 )
1418 .await;
1419
1420 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1421 let workspace = init_test_workspace(&project, cx).await;
1422 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1423 let debug_definition = DebugTaskDefinition {
1424 adapter: "fake-adapter".into(),
1425 config: json!({
1426 "request": "launch",
1427 "program": "main.rs".to_owned(),
1428 "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1429 "cwd": path!("/Random_path"),
1430 "env": json!({ "KEY": "VALUE" }),
1431 }),
1432 label: "test".into(),
1433 tcp_connection: None,
1434 };
1435
1436 let launch_handler_called = Arc::new(AtomicBool::new(false));
1437
1438 start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1439 let debug_definition = debug_definition.clone();
1440 let launch_handler_called = launch_handler_called.clone();
1441
1442 move |client| {
1443 let debug_definition = debug_definition.clone();
1444 let launch_handler_called = launch_handler_called.clone();
1445
1446 client.on_request::<dap::requests::Launch, _>(move |_, args| {
1447 launch_handler_called.store(true, Ordering::SeqCst);
1448
1449 assert_eq!(args.raw, debug_definition.config);
1450
1451 Ok(())
1452 });
1453 }
1454 })
1455 .ok();
1456
1457 cx.run_until_parked();
1458
1459 assert!(
1460 launch_handler_called.load(Ordering::SeqCst),
1461 "Launch request handler was not called"
1462 );
1463}
1464
1465#[gpui::test]
1466async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1467 init_test(cx);
1468
1469 let fs = FakeFs::new(executor.clone());
1470
1471 fs.insert_tree(
1472 path!("/project"),
1473 json!({
1474 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1475 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1476 }),
1477 )
1478 .await;
1479
1480 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1481 let workspace = init_test_workspace(&project, cx).await;
1482 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1483 let project_path = Path::new(path!("/project"));
1484 let worktree = project
1485 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1486 .expect("This worktree should exist in project")
1487 .0;
1488
1489 let worktree_id = workspace
1490 .update(cx, |_, _, cx| worktree.read(cx).id())
1491 .unwrap();
1492
1493 let main_buffer = project
1494 .update(cx, |project, cx| {
1495 project.open_buffer((worktree_id, "main.rs"), cx)
1496 })
1497 .await
1498 .unwrap();
1499
1500 let second_buffer = project
1501 .update(cx, |project, cx| {
1502 project.open_buffer((worktree_id, "second.rs"), cx)
1503 })
1504 .await
1505 .unwrap();
1506
1507 let (main_editor, cx) = cx.add_window_view(|window, cx| {
1508 Editor::new(
1509 EditorMode::full(),
1510 MultiBuffer::build_from_buffer(main_buffer, cx),
1511 Some(project.clone()),
1512 window,
1513 cx,
1514 )
1515 });
1516
1517 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1518 Editor::new(
1519 EditorMode::full(),
1520 MultiBuffer::build_from_buffer(second_buffer, cx),
1521 Some(project.clone()),
1522 window,
1523 cx,
1524 )
1525 });
1526
1527 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1528 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1529
1530 client.on_request::<dap::requests::Threads, _>(move |_, _| {
1531 Ok(dap::ThreadsResponse {
1532 threads: vec![dap::Thread {
1533 id: 1,
1534 name: "Thread 1".into(),
1535 }],
1536 })
1537 });
1538
1539 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1540 Ok(dap::ScopesResponse {
1541 scopes: Vec::default(),
1542 })
1543 });
1544
1545 client.on_request::<StackTrace, _>(move |_, args| {
1546 assert_eq!(args.thread_id, 1);
1547
1548 Ok(dap::StackTraceResponse {
1549 stack_frames: vec![dap::StackFrame {
1550 id: 1,
1551 name: "frame 1".into(),
1552 source: Some(dap::Source {
1553 name: Some("main.rs".into()),
1554 path: Some(path!("/project/main.rs").into()),
1555 source_reference: None,
1556 presentation_hint: None,
1557 origin: None,
1558 sources: None,
1559 adapter_data: None,
1560 checksums: None,
1561 }),
1562 line: 2,
1563 column: 0,
1564 end_line: None,
1565 end_column: None,
1566 can_restart: None,
1567 instruction_pointer_reference: None,
1568 module_id: None,
1569 presentation_hint: None,
1570 }],
1571 total_frames: None,
1572 })
1573 });
1574
1575 client
1576 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1577 reason: dap::StoppedEventReason::Breakpoint,
1578 description: None,
1579 thread_id: Some(1),
1580 preserve_focus_hint: None,
1581 text: None,
1582 all_threads_stopped: None,
1583 hit_breakpoint_ids: None,
1584 }))
1585 .await;
1586
1587 cx.run_until_parked();
1588
1589 main_editor.update_in(cx, |editor, window, cx| {
1590 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1591
1592 assert_eq!(
1593 active_debug_lines.len(),
1594 1,
1595 "There should be only one active debug line"
1596 );
1597
1598 let point = editor
1599 .snapshot(window, cx)
1600 .buffer_snapshot
1601 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1602
1603 assert_eq!(point.row, 1);
1604 });
1605
1606 second_editor.update(cx, |editor, _| {
1607 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1608
1609 assert!(
1610 active_debug_lines.is_empty(),
1611 "There shouldn't be any active debug lines"
1612 );
1613 });
1614
1615 let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1616 client.on_request::<StackTrace, _>({
1617 let handled_second_stacktrace = handled_second_stacktrace.clone();
1618 move |_, args| {
1619 handled_second_stacktrace.store(true, Ordering::SeqCst);
1620 assert_eq!(args.thread_id, 1);
1621
1622 Ok(dap::StackTraceResponse {
1623 stack_frames: vec![dap::StackFrame {
1624 id: 2,
1625 name: "frame 2".into(),
1626 source: Some(dap::Source {
1627 name: Some("second.rs".into()),
1628 path: Some(path!("/project/second.rs").into()),
1629 source_reference: None,
1630 presentation_hint: None,
1631 origin: None,
1632 sources: None,
1633 adapter_data: None,
1634 checksums: None,
1635 }),
1636 line: 3,
1637 column: 0,
1638 end_line: None,
1639 end_column: None,
1640 can_restart: None,
1641 instruction_pointer_reference: None,
1642 module_id: None,
1643 presentation_hint: None,
1644 }],
1645 total_frames: None,
1646 })
1647 }
1648 });
1649
1650 client
1651 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1652 reason: dap::StoppedEventReason::Breakpoint,
1653 description: None,
1654 thread_id: Some(1),
1655 preserve_focus_hint: None,
1656 text: None,
1657 all_threads_stopped: None,
1658 hit_breakpoint_ids: None,
1659 }))
1660 .await;
1661
1662 cx.run_until_parked();
1663
1664 second_editor.update_in(cx, |editor, window, cx| {
1665 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1666
1667 assert_eq!(
1668 active_debug_lines.len(),
1669 1,
1670 "There should be only one active debug line"
1671 );
1672
1673 let point = editor
1674 .snapshot(window, cx)
1675 .buffer_snapshot
1676 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1677
1678 assert_eq!(point.row, 2);
1679 });
1680
1681 main_editor.update(cx, |editor, _| {
1682 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1683
1684 assert!(
1685 active_debug_lines.is_empty(),
1686 "There shouldn't be any active debug lines"
1687 );
1688 });
1689
1690 assert!(
1691 handled_second_stacktrace.load(Ordering::SeqCst),
1692 "Second stacktrace request handler was not called"
1693 );
1694
1695 client
1696 .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1697 thread_id: 0,
1698 all_threads_continued: Some(true),
1699 }))
1700 .await;
1701
1702 cx.run_until_parked();
1703
1704 second_editor.update(cx, |editor, _| {
1705 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1706
1707 assert!(
1708 active_debug_lines.is_empty(),
1709 "There shouldn't be any active debug lines"
1710 );
1711 });
1712
1713 main_editor.update(cx, |editor, _| {
1714 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1715
1716 assert!(
1717 active_debug_lines.is_empty(),
1718 "There shouldn't be any active debug lines"
1719 );
1720 });
1721
1722 // Clean up
1723 let shutdown_session = project.update(cx, |project, cx| {
1724 project.dap_store().update(cx, |dap_store, cx| {
1725 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1726 })
1727 });
1728
1729 shutdown_session.await.unwrap();
1730
1731 main_editor.update(cx, |editor, _| {
1732 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1733
1734 assert!(
1735 active_debug_lines.is_empty(),
1736 "There shouldn't be any active debug lines after session shutdown"
1737 );
1738 });
1739
1740 second_editor.update(cx, |editor, _| {
1741 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1742
1743 assert!(
1744 active_debug_lines.is_empty(),
1745 "There shouldn't be any active debug lines after session shutdown"
1746 );
1747 });
1748}