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