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