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