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