1use crate::{
2 debugger_panel::DebugPanel,
3 session::running::stack_frame_list::StackFrameEntry,
4 tests::{active_debug_session_panel, init_test, init_test_workspace},
5};
6use dap::{
7 requests::{StackTrace, Threads},
8 StackFrame,
9};
10use editor::{Editor, ToPoint as _};
11use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
12use project::{FakeFs, Project};
13use serde_json::json;
14use std::sync::Arc;
15use unindent::Unindent as _;
16use util::path;
17
18#[gpui::test]
19async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
20 executor: BackgroundExecutor,
21 cx: &mut TestAppContext,
22) {
23 init_test(cx);
24
25 let fs = FakeFs::new(executor.clone());
26
27 let test_file_content = r#"
28 import { SOME_VALUE } './module.js';
29
30 console.log(SOME_VALUE);
31 "#
32 .unindent();
33
34 let module_file_content = r#"
35 export SOME_VALUE = 'some value';
36 "#
37 .unindent();
38
39 fs.insert_tree(
40 path!("/project"),
41 json!({
42 "src": {
43 "test.js": test_file_content,
44 "module.js": module_file_content,
45 }
46 }),
47 )
48 .await;
49
50 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
51 let workspace = init_test_workspace(&project, cx).await;
52 let cx = &mut VisualTestContext::from_window(*workspace, cx);
53
54 let task = project.update(cx, |project, cx| {
55 project.start_debug_session(
56 dap::test_config(dap::DebugRequestType::Launch, None, None),
57 cx,
58 )
59 });
60
61 let session = task.await.unwrap();
62 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
63
64 client
65 .on_request::<Threads, _>(move |_, _| {
66 Ok(dap::ThreadsResponse {
67 threads: vec![dap::Thread {
68 id: 1,
69 name: "Thread 1".into(),
70 }],
71 })
72 })
73 .await;
74
75 let stack_frames = vec![
76 StackFrame {
77 id: 1,
78 name: "Stack Frame 1".into(),
79 source: Some(dap::Source {
80 name: Some("test.js".into()),
81 path: Some(path!("/project/src/test.js").into()),
82 source_reference: None,
83 presentation_hint: None,
84 origin: None,
85 sources: None,
86 adapter_data: None,
87 checksums: None,
88 }),
89 line: 3,
90 column: 1,
91 end_line: None,
92 end_column: None,
93 can_restart: None,
94 instruction_pointer_reference: None,
95 module_id: None,
96 presentation_hint: None,
97 },
98 StackFrame {
99 id: 2,
100 name: "Stack Frame 2".into(),
101 source: Some(dap::Source {
102 name: Some("module.js".into()),
103 path: Some(path!("/project/src/module.js").into()),
104 source_reference: None,
105 presentation_hint: None,
106 origin: None,
107 sources: None,
108 adapter_data: None,
109 checksums: None,
110 }),
111 line: 1,
112 column: 1,
113 end_line: None,
114 end_column: None,
115 can_restart: None,
116 instruction_pointer_reference: None,
117 module_id: None,
118 presentation_hint: None,
119 },
120 ];
121
122 client
123 .on_request::<StackTrace, _>({
124 let stack_frames = Arc::new(stack_frames.clone());
125 move |_, args| {
126 assert_eq!(1, args.thread_id);
127
128 Ok(dap::StackTraceResponse {
129 stack_frames: (*stack_frames).clone(),
130 total_frames: None,
131 })
132 }
133 })
134 .await;
135
136 client
137 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
138 reason: dap::StoppedEventReason::Pause,
139 description: None,
140 thread_id: Some(1),
141 preserve_focus_hint: None,
142 text: None,
143 all_threads_stopped: None,
144 hit_breakpoint_ids: None,
145 }))
146 .await;
147
148 cx.run_until_parked();
149
150 // trigger to load threads
151 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
152 session
153 .mode()
154 .as_running()
155 .unwrap()
156 .update(cx, |running_state, cx| {
157 running_state
158 .session()
159 .update(cx, |session, cx| session.threads(cx));
160 });
161 });
162
163 cx.run_until_parked();
164
165 // select first thread
166 active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
167 session
168 .mode()
169 .as_running()
170 .unwrap()
171 .update(cx, |running_state, cx| {
172 running_state.select_current_thread(
173 &running_state
174 .session()
175 .update(cx, |session, cx| session.threads(cx)),
176 cx,
177 );
178 });
179 });
180
181 cx.run_until_parked();
182
183 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
184 let stack_frame_list = session
185 .mode()
186 .as_running()
187 .unwrap()
188 .update(cx, |state, _| state.stack_frame_list().clone());
189
190 stack_frame_list.update(cx, |stack_frame_list, cx| {
191 assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
192 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
193 });
194 });
195
196 let shutdown_session = project.update(cx, |project, cx| {
197 project.dap_store().update(cx, |dap_store, cx| {
198 dap_store.shutdown_session(session.read(cx).session_id(), cx)
199 })
200 });
201
202 shutdown_session.await.unwrap();
203}
204
205#[gpui::test]
206async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
207 init_test(cx);
208
209 let fs = FakeFs::new(executor.clone());
210
211 let test_file_content = r#"
212 import { SOME_VALUE } './module.js';
213
214 console.log(SOME_VALUE);
215 "#
216 .unindent();
217
218 let module_file_content = r#"
219 export SOME_VALUE = 'some value';
220 "#
221 .unindent();
222
223 fs.insert_tree(
224 path!("/project"),
225 json!({
226 "src": {
227 "test.js": test_file_content,
228 "module.js": module_file_content,
229 }
230 }),
231 )
232 .await;
233
234 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
235 let workspace = init_test_workspace(&project, cx).await;
236 let _ = workspace.update(cx, |workspace, window, cx| {
237 workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
238 });
239
240 let cx = &mut VisualTestContext::from_window(*workspace, cx);
241
242 let task = project.update(cx, |project, cx| {
243 project.start_debug_session(
244 dap::test_config(dap::DebugRequestType::Launch, None, None),
245 cx,
246 )
247 });
248
249 let session = task.await.unwrap();
250 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
251
252 client
253 .on_request::<Threads, _>(move |_, _| {
254 Ok(dap::ThreadsResponse {
255 threads: vec![dap::Thread {
256 id: 1,
257 name: "Thread 1".into(),
258 }],
259 })
260 })
261 .await;
262
263 let stack_frames = vec![
264 StackFrame {
265 id: 1,
266 name: "Stack Frame 1".into(),
267 source: Some(dap::Source {
268 name: Some("test.js".into()),
269 path: Some(path!("/project/src/test.js").into()),
270 source_reference: None,
271 presentation_hint: None,
272 origin: None,
273 sources: None,
274 adapter_data: None,
275 checksums: None,
276 }),
277 line: 3,
278 column: 1,
279 end_line: None,
280 end_column: None,
281 can_restart: None,
282 instruction_pointer_reference: None,
283 module_id: None,
284 presentation_hint: None,
285 },
286 StackFrame {
287 id: 2,
288 name: "Stack Frame 2".into(),
289 source: Some(dap::Source {
290 name: Some("module.js".into()),
291 path: Some(path!("/project/src/module.js").into()),
292 source_reference: None,
293 presentation_hint: None,
294 origin: None,
295 sources: None,
296 adapter_data: None,
297 checksums: None,
298 }),
299 line: 1,
300 column: 1,
301 end_line: None,
302 end_column: None,
303 can_restart: None,
304 instruction_pointer_reference: None,
305 module_id: None,
306 presentation_hint: None,
307 },
308 ];
309
310 client
311 .on_request::<StackTrace, _>({
312 let stack_frames = Arc::new(stack_frames.clone());
313 move |_, args| {
314 assert_eq!(1, args.thread_id);
315
316 Ok(dap::StackTraceResponse {
317 stack_frames: (*stack_frames).clone(),
318 total_frames: None,
319 })
320 }
321 })
322 .await;
323
324 client
325 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
326 reason: dap::StoppedEventReason::Pause,
327 description: None,
328 thread_id: Some(1),
329 preserve_focus_hint: None,
330 text: None,
331 all_threads_stopped: None,
332 hit_breakpoint_ids: None,
333 }))
334 .await;
335
336 cx.run_until_parked();
337
338 // trigger threads to load
339 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
340 session
341 .mode()
342 .as_running()
343 .unwrap()
344 .update(cx, |running_state, cx| {
345 running_state
346 .session()
347 .update(cx, |session, cx| session.threads(cx));
348 });
349 });
350
351 cx.run_until_parked();
352
353 // select first thread
354 active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
355 session
356 .mode()
357 .as_running()
358 .unwrap()
359 .update(cx, |running_state, cx| {
360 running_state.select_current_thread(
361 &running_state
362 .session()
363 .update(cx, |session, cx| session.threads(cx)),
364 cx,
365 );
366 });
367 });
368
369 cx.run_until_parked();
370
371 workspace
372 .update(cx, |workspace, window, cx| {
373 let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
374 assert_eq!(1, editors.len());
375
376 let project_path = editors[0]
377 .update(cx, |editor, cx| editor.project_path(cx))
378 .unwrap();
379 let expected = if cfg!(target_os = "windows") {
380 "src\\test.js"
381 } else {
382 "src/test.js"
383 };
384 assert_eq!(expected, project_path.path.to_string_lossy());
385 assert_eq!(test_file_content, editors[0].read(cx).text(cx));
386 assert_eq!(
387 vec![2..3],
388 editors[0].update(cx, |editor, cx| {
389 let snapshot = editor.snapshot(window, cx);
390
391 editor
392 .highlighted_rows::<editor::DebugCurrentRowHighlight>()
393 .map(|(range, _)| {
394 let start = range.start.to_point(&snapshot.buffer_snapshot);
395 let end = range.end.to_point(&snapshot.buffer_snapshot);
396 start.row..end.row
397 })
398 .collect::<Vec<_>>()
399 })
400 );
401 })
402 .unwrap();
403
404 let stack_frame_list = workspace
405 .update(cx, |workspace, _window, cx| {
406 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
407 let active_debug_panel_item = debug_panel
408 .update(cx, |this, cx| this.active_session(cx))
409 .unwrap();
410
411 active_debug_panel_item
412 .read(cx)
413 .mode()
414 .as_running()
415 .unwrap()
416 .read(cx)
417 .stack_frame_list()
418 .clone()
419 })
420 .unwrap();
421
422 stack_frame_list.update(cx, |stack_frame_list, cx| {
423 assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
424 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
425 });
426
427 // select second stack frame
428 stack_frame_list
429 .update_in(cx, |stack_frame_list, window, cx| {
430 stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
431 })
432 .await
433 .unwrap();
434
435 cx.run_until_parked();
436
437 stack_frame_list.update(cx, |stack_frame_list, cx| {
438 assert_eq!(Some(2), stack_frame_list.current_stack_frame_id());
439 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
440 });
441
442 let _ = workspace.update(cx, |workspace, window, cx| {
443 let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
444 assert_eq!(1, editors.len());
445
446 let project_path = editors[0]
447 .update(cx, |editor, cx| editor.project_path(cx))
448 .unwrap();
449 let expected = if cfg!(target_os = "windows") {
450 "src\\module.js"
451 } else {
452 "src/module.js"
453 };
454 assert_eq!(expected, project_path.path.to_string_lossy());
455 assert_eq!(module_file_content, editors[0].read(cx).text(cx));
456 assert_eq!(
457 vec![0..1],
458 editors[0].update(cx, |editor, cx| {
459 let snapshot = editor.snapshot(window, cx);
460
461 editor
462 .highlighted_rows::<editor::DebugCurrentRowHighlight>()
463 .map(|(range, _)| {
464 let start = range.start.to_point(&snapshot.buffer_snapshot);
465 let end = range.end.to_point(&snapshot.buffer_snapshot);
466 start.row..end.row
467 })
468 .collect::<Vec<_>>()
469 })
470 );
471 });
472
473 let shutdown_session = project.update(cx, |project, cx| {
474 project.dap_store().update(cx, |dap_store, cx| {
475 dap_store.shutdown_session(session.read(cx).session_id(), cx)
476 })
477 });
478
479 shutdown_session.await.unwrap();
480}
481
482#[gpui::test]
483async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
484 init_test(cx);
485
486 let fs = FakeFs::new(executor.clone());
487
488 let test_file_content = r#"
489 import { SOME_VALUE } './module.js';
490
491 console.log(SOME_VALUE);
492 "#
493 .unindent();
494
495 let module_file_content = r#"
496 export SOME_VALUE = 'some value';
497 "#
498 .unindent();
499
500 fs.insert_tree(
501 path!("/project"),
502 json!({
503 "src": {
504 "test.js": test_file_content,
505 "module.js": module_file_content,
506 }
507 }),
508 )
509 .await;
510
511 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
512 let workspace = init_test_workspace(&project, cx).await;
513 let cx = &mut VisualTestContext::from_window(*workspace, cx);
514
515 let task = project.update(cx, |project, cx| {
516 project.start_debug_session(
517 dap::test_config(dap::DebugRequestType::Launch, None, None),
518 cx,
519 )
520 });
521
522 let session = task.await.unwrap();
523 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
524
525 client
526 .on_request::<Threads, _>(move |_, _| {
527 Ok(dap::ThreadsResponse {
528 threads: vec![dap::Thread {
529 id: 1,
530 name: "Thread 1".into(),
531 }],
532 })
533 })
534 .await;
535
536 let stack_frames = vec![
537 StackFrame {
538 id: 1,
539 name: "Stack Frame 1".into(),
540 source: Some(dap::Source {
541 name: Some("test.js".into()),
542 path: Some(path!("/project/src/test.js").into()),
543 source_reference: None,
544 presentation_hint: None,
545 origin: None,
546 sources: None,
547 adapter_data: None,
548 checksums: None,
549 }),
550 line: 3,
551 column: 1,
552 end_line: None,
553 end_column: None,
554 can_restart: None,
555 instruction_pointer_reference: None,
556 module_id: None,
557 presentation_hint: None,
558 },
559 StackFrame {
560 id: 2,
561 name: "Stack Frame 2".into(),
562 source: Some(dap::Source {
563 name: Some("module.js".into()),
564 path: Some(path!("/project/src/module.js").into()),
565 source_reference: None,
566 presentation_hint: None,
567 origin: Some("ignored".into()),
568 sources: None,
569 adapter_data: None,
570 checksums: None,
571 }),
572 line: 1,
573 column: 1,
574 end_line: None,
575 end_column: None,
576 can_restart: None,
577 instruction_pointer_reference: None,
578 module_id: None,
579 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
580 },
581 StackFrame {
582 id: 3,
583 name: "Stack Frame 3".into(),
584 source: Some(dap::Source {
585 name: Some("module.js".into()),
586 path: Some(path!("/project/src/module.js").into()),
587 source_reference: None,
588 presentation_hint: None,
589 origin: Some("ignored".into()),
590 sources: None,
591 adapter_data: None,
592 checksums: None,
593 }),
594 line: 1,
595 column: 1,
596 end_line: None,
597 end_column: None,
598 can_restart: None,
599 instruction_pointer_reference: None,
600 module_id: None,
601 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
602 },
603 StackFrame {
604 id: 4,
605 name: "Stack Frame 4".into(),
606 source: Some(dap::Source {
607 name: Some("module.js".into()),
608 path: Some(path!("/project/src/module.js").into()),
609 source_reference: None,
610 presentation_hint: None,
611 origin: None,
612 sources: None,
613 adapter_data: None,
614 checksums: None,
615 }),
616 line: 1,
617 column: 1,
618 end_line: None,
619 end_column: None,
620 can_restart: None,
621 instruction_pointer_reference: None,
622 module_id: None,
623 presentation_hint: None,
624 },
625 StackFrame {
626 id: 5,
627 name: "Stack Frame 5".into(),
628 source: Some(dap::Source {
629 name: Some("module.js".into()),
630 path: Some(path!("/project/src/module.js").into()),
631 source_reference: None,
632 presentation_hint: None,
633 origin: None,
634 sources: None,
635 adapter_data: None,
636 checksums: None,
637 }),
638 line: 1,
639 column: 1,
640 end_line: None,
641 end_column: None,
642 can_restart: None,
643 instruction_pointer_reference: None,
644 module_id: None,
645 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
646 },
647 StackFrame {
648 id: 6,
649 name: "Stack Frame 6".into(),
650 source: Some(dap::Source {
651 name: Some("module.js".into()),
652 path: Some(path!("/project/src/module.js").into()),
653 source_reference: None,
654 presentation_hint: None,
655 origin: None,
656 sources: None,
657 adapter_data: None,
658 checksums: None,
659 }),
660 line: 1,
661 column: 1,
662 end_line: None,
663 end_column: None,
664 can_restart: None,
665 instruction_pointer_reference: None,
666 module_id: None,
667 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
668 },
669 StackFrame {
670 id: 7,
671 name: "Stack Frame 7".into(),
672 source: Some(dap::Source {
673 name: Some("module.js".into()),
674 path: Some(path!("/project/src/module.js").into()),
675 source_reference: None,
676 presentation_hint: None,
677 origin: None,
678 sources: None,
679 adapter_data: None,
680 checksums: None,
681 }),
682 line: 1,
683 column: 1,
684 end_line: None,
685 end_column: None,
686 can_restart: None,
687 instruction_pointer_reference: None,
688 module_id: None,
689 presentation_hint: None,
690 },
691 ];
692
693 client
694 .on_request::<StackTrace, _>({
695 let stack_frames = Arc::new(stack_frames.clone());
696 move |_, args| {
697 assert_eq!(1, args.thread_id);
698
699 Ok(dap::StackTraceResponse {
700 stack_frames: (*stack_frames).clone(),
701 total_frames: None,
702 })
703 }
704 })
705 .await;
706
707 client
708 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
709 reason: dap::StoppedEventReason::Pause,
710 description: None,
711 thread_id: Some(1),
712 preserve_focus_hint: None,
713 text: None,
714 all_threads_stopped: None,
715 hit_breakpoint_ids: None,
716 }))
717 .await;
718
719 cx.run_until_parked();
720
721 // trigger threads to load
722 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
723 session
724 .mode()
725 .as_running()
726 .unwrap()
727 .update(cx, |running_state, cx| {
728 running_state
729 .session()
730 .update(cx, |session, cx| session.threads(cx));
731 });
732 });
733
734 cx.run_until_parked();
735
736 // select first thread
737 active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
738 session
739 .mode()
740 .as_running()
741 .unwrap()
742 .update(cx, |running_state, cx| {
743 running_state.select_current_thread(
744 &running_state
745 .session()
746 .update(cx, |session, cx| session.threads(cx)),
747 cx,
748 );
749 });
750 });
751
752 cx.run_until_parked();
753
754 // trigger stack frames to loaded
755 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
756 let stack_frame_list = debug_panel_item
757 .mode()
758 .as_running()
759 .unwrap()
760 .update(cx, |state, _| state.stack_frame_list().clone());
761
762 stack_frame_list.update(cx, |stack_frame_list, cx| {
763 stack_frame_list.dap_stack_frames(cx);
764 });
765 });
766
767 cx.run_until_parked();
768
769 active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
770 let stack_frame_list = debug_panel_item
771 .mode()
772 .as_running()
773 .unwrap()
774 .update(cx, |state, _| state.stack_frame_list().clone());
775
776 stack_frame_list.update(cx, |stack_frame_list, cx| {
777 stack_frame_list.build_entries(true, window, cx);
778
779 assert_eq!(
780 &vec![
781 StackFrameEntry::Normal(stack_frames[0].clone()),
782 StackFrameEntry::Collapsed(vec![
783 stack_frames[1].clone(),
784 stack_frames[2].clone()
785 ]),
786 StackFrameEntry::Normal(stack_frames[3].clone()),
787 StackFrameEntry::Collapsed(vec![
788 stack_frames[4].clone(),
789 stack_frames[5].clone()
790 ]),
791 StackFrameEntry::Normal(stack_frames[6].clone()),
792 ],
793 stack_frame_list.entries()
794 );
795
796 stack_frame_list.expand_collapsed_entry(
797 1,
798 &vec![stack_frames[1].clone(), stack_frames[2].clone()],
799 cx,
800 );
801
802 assert_eq!(
803 &vec![
804 StackFrameEntry::Normal(stack_frames[0].clone()),
805 StackFrameEntry::Normal(stack_frames[1].clone()),
806 StackFrameEntry::Normal(stack_frames[2].clone()),
807 StackFrameEntry::Normal(stack_frames[3].clone()),
808 StackFrameEntry::Collapsed(vec![
809 stack_frames[4].clone(),
810 stack_frames[5].clone()
811 ]),
812 StackFrameEntry::Normal(stack_frames[6].clone()),
813 ],
814 stack_frame_list.entries()
815 );
816
817 stack_frame_list.expand_collapsed_entry(
818 4,
819 &vec![stack_frames[4].clone(), stack_frames[5].clone()],
820 cx,
821 );
822
823 assert_eq!(
824 &vec![
825 StackFrameEntry::Normal(stack_frames[0].clone()),
826 StackFrameEntry::Normal(stack_frames[1].clone()),
827 StackFrameEntry::Normal(stack_frames[2].clone()),
828 StackFrameEntry::Normal(stack_frames[3].clone()),
829 StackFrameEntry::Normal(stack_frames[4].clone()),
830 StackFrameEntry::Normal(stack_frames[5].clone()),
831 StackFrameEntry::Normal(stack_frames[6].clone()),
832 ],
833 stack_frame_list.entries()
834 );
835 });
836 });
837
838 let shutdown_session = project.update(cx, |project, cx| {
839 project.dap_store().update(cx, |dap_store, cx| {
840 dap_store.shutdown_session(session.read(cx).session_id(), cx)
841 })
842 });
843
844 shutdown_session.await.unwrap();
845}