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