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