1use crate::{
2 debugger_panel::DebugPanel,
3 session::running::stack_frame_list::{StackFrameEntry, StackFrameFilter},
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.running_state().update(cx, |running_state, cx| {
142 running_state
143 .session()
144 .update(cx, |session, cx| session.threads(cx));
145 });
146 });
147
148 cx.run_until_parked();
149
150 // select first thread
151 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
152 session.running_state().update(cx, |running_state, cx| {
153 running_state.select_current_thread(
154 &running_state
155 .session()
156 .update(cx, |session, cx| session.threads(cx)),
157 window,
158 cx,
159 );
160 });
161 });
162
163 cx.run_until_parked();
164
165 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
166 let stack_frame_list = session
167 .running_state()
168 .update(cx, |state, _| state.stack_frame_list().clone());
169
170 stack_frame_list.update(cx, |stack_frame_list, cx| {
171 assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
172 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
173 });
174 });
175}
176
177#[gpui::test]
178async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
179 init_test(cx);
180
181 let fs = FakeFs::new(executor.clone());
182
183 let test_file_content = r#"
184 import { SOME_VALUE } './module.js';
185
186 console.log(SOME_VALUE);
187 "#
188 .unindent();
189
190 let module_file_content = r#"
191 export SOME_VALUE = 'some value';
192 "#
193 .unindent();
194
195 fs.insert_tree(
196 path!("/project"),
197 json!({
198 "src": {
199 "test.js": test_file_content,
200 "module.js": module_file_content,
201 }
202 }),
203 )
204 .await;
205
206 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
207 let workspace = init_test_workspace(&project, cx).await;
208 let _ = workspace.update(cx, |workspace, window, cx| {
209 workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
210 });
211
212 let cx = &mut VisualTestContext::from_window(*workspace, cx);
213 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
214 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
215
216 client.on_request::<Threads, _>(move |_, _| {
217 Ok(dap::ThreadsResponse {
218 threads: vec![dap::Thread {
219 id: 1,
220 name: "Thread 1".into(),
221 }],
222 })
223 });
224
225 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
226
227 let stack_frames = vec![
228 StackFrame {
229 id: 1,
230 name: "Stack Frame 1".into(),
231 source: Some(dap::Source {
232 name: Some("test.js".into()),
233 path: Some(path!("/project/src/test.js").into()),
234 source_reference: None,
235 presentation_hint: None,
236 origin: None,
237 sources: None,
238 adapter_data: None,
239 checksums: None,
240 }),
241 line: 3,
242 column: 1,
243 end_line: None,
244 end_column: None,
245 can_restart: None,
246 instruction_pointer_reference: None,
247 module_id: None,
248 presentation_hint: None,
249 },
250 StackFrame {
251 id: 2,
252 name: "Stack Frame 2".into(),
253 source: Some(dap::Source {
254 name: Some("module.js".into()),
255 path: Some(path!("/project/src/module.js").into()),
256 source_reference: None,
257 presentation_hint: None,
258 origin: None,
259 sources: None,
260 adapter_data: None,
261 checksums: None,
262 }),
263 line: 1,
264 column: 1,
265 end_line: None,
266 end_column: None,
267 can_restart: None,
268 instruction_pointer_reference: None,
269 module_id: None,
270 presentation_hint: None,
271 },
272 ];
273
274 client.on_request::<StackTrace, _>({
275 let stack_frames = Arc::new(stack_frames.clone());
276 move |_, args| {
277 assert_eq!(1, args.thread_id);
278
279 Ok(dap::StackTraceResponse {
280 stack_frames: (*stack_frames).clone(),
281 total_frames: None,
282 })
283 }
284 });
285
286 client
287 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
288 reason: dap::StoppedEventReason::Pause,
289 description: None,
290 thread_id: Some(1),
291 preserve_focus_hint: None,
292 text: None,
293 all_threads_stopped: None,
294 hit_breakpoint_ids: None,
295 }))
296 .await;
297
298 cx.run_until_parked();
299
300 // trigger threads to load
301 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
302 session.running_state().update(cx, |running_state, cx| {
303 running_state
304 .session()
305 .update(cx, |session, cx| session.threads(cx));
306 });
307 });
308
309 cx.run_until_parked();
310
311 // select first thread
312 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
313 session.running_state().update(cx, |running_state, cx| {
314 running_state.select_current_thread(
315 &running_state
316 .session()
317 .update(cx, |session, cx| session.threads(cx)),
318 window,
319 cx,
320 );
321 });
322 });
323
324 cx.run_until_parked();
325
326 workspace
327 .update(cx, |workspace, window, cx| {
328 let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
329 assert_eq!(1, editors.len());
330
331 let project_path = editors[0]
332 .update(cx, |editor, cx| editor.project_path(cx))
333 .unwrap();
334 let expected = if cfg!(target_os = "windows") {
335 "src\\test.js"
336 } else {
337 "src/test.js"
338 };
339 assert_eq!(expected, project_path.path.to_string_lossy());
340 assert_eq!(test_file_content, editors[0].read(cx).text(cx));
341 assert_eq!(
342 vec![2..3],
343 editors[0].update(cx, |editor, cx| {
344 let snapshot = editor.snapshot(window, cx);
345
346 editor
347 .highlighted_rows::<editor::ActiveDebugLine>()
348 .map(|(range, _)| {
349 let start = range.start.to_point(&snapshot.buffer_snapshot);
350 let end = range.end.to_point(&snapshot.buffer_snapshot);
351 start.row..end.row
352 })
353 .collect::<Vec<_>>()
354 })
355 );
356 })
357 .unwrap();
358
359 let stack_frame_list = workspace
360 .update(cx, |workspace, _window, cx| {
361 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
362 let active_debug_panel_item = debug_panel
363 .update(cx, |this, _| this.active_session())
364 .unwrap();
365
366 active_debug_panel_item
367 .read(cx)
368 .running_state()
369 .read(cx)
370 .stack_frame_list()
371 .clone()
372 })
373 .unwrap();
374
375 stack_frame_list.update(cx, |stack_frame_list, cx| {
376 assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
377 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
378 });
379
380 // select second stack frame
381 stack_frame_list
382 .update_in(cx, |stack_frame_list, window, cx| {
383 stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
384 })
385 .await
386 .unwrap();
387
388 cx.run_until_parked();
389
390 stack_frame_list.update(cx, |stack_frame_list, cx| {
391 assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
392 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
393 });
394
395 let _ = workspace.update(cx, |workspace, window, cx| {
396 let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
397 assert_eq!(1, editors.len());
398
399 let project_path = editors[0]
400 .update(cx, |editor, cx| editor.project_path(cx))
401 .unwrap();
402 let expected = if cfg!(target_os = "windows") {
403 "src\\module.js"
404 } else {
405 "src/module.js"
406 };
407 assert_eq!(expected, project_path.path.to_string_lossy());
408 assert_eq!(module_file_content, editors[0].read(cx).text(cx));
409 assert_eq!(
410 vec![0..1],
411 editors[0].update(cx, |editor, cx| {
412 let snapshot = editor.snapshot(window, cx);
413
414 editor
415 .highlighted_rows::<editor::ActiveDebugLine>()
416 .map(|(range, _)| {
417 let start = range.start.to_point(&snapshot.buffer_snapshot);
418 let end = range.end.to_point(&snapshot.buffer_snapshot);
419 start.row..end.row
420 })
421 .collect::<Vec<_>>()
422 })
423 );
424 });
425}
426
427#[gpui::test]
428async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
429 init_test(cx);
430
431 let fs = FakeFs::new(executor.clone());
432
433 let test_file_content = r#"
434 import { SOME_VALUE } './module.js';
435
436 console.log(SOME_VALUE);
437 "#
438 .unindent();
439
440 let module_file_content = r#"
441 export SOME_VALUE = 'some value';
442 "#
443 .unindent();
444
445 fs.insert_tree(
446 path!("/project"),
447 json!({
448 "src": {
449 "test.js": test_file_content,
450 "module.js": module_file_content,
451 }
452 }),
453 )
454 .await;
455
456 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
457 let workspace = init_test_workspace(&project, cx).await;
458 let cx = &mut VisualTestContext::from_window(*workspace, cx);
459
460 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
461 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
462
463 client.on_request::<Threads, _>(move |_, _| {
464 Ok(dap::ThreadsResponse {
465 threads: vec![dap::Thread {
466 id: 1,
467 name: "Thread 1".into(),
468 }],
469 })
470 });
471
472 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
473
474 let stack_frames = vec![
475 StackFrame {
476 id: 1,
477 name: "Stack Frame 1".into(),
478 source: Some(dap::Source {
479 name: Some("test.js".into()),
480 path: Some(path!("/project/src/test.js").into()),
481 source_reference: None,
482 presentation_hint: None,
483 origin: None,
484 sources: None,
485 adapter_data: None,
486 checksums: None,
487 }),
488 line: 3,
489 column: 1,
490 end_line: None,
491 end_column: None,
492 can_restart: None,
493 instruction_pointer_reference: None,
494 module_id: None,
495 presentation_hint: None,
496 },
497 StackFrame {
498 id: 2,
499 name: "Stack Frame 2".into(),
500 source: Some(dap::Source {
501 name: Some("module.js".into()),
502 path: Some(path!("/project/src/module.js").into()),
503 source_reference: None,
504 presentation_hint: None,
505 origin: Some("ignored".into()),
506 sources: None,
507 adapter_data: None,
508 checksums: None,
509 }),
510 line: 1,
511 column: 1,
512 end_line: None,
513 end_column: None,
514 can_restart: None,
515 instruction_pointer_reference: None,
516 module_id: None,
517 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
518 },
519 StackFrame {
520 id: 3,
521 name: "Stack Frame 3".into(),
522 source: Some(dap::Source {
523 name: Some("module.js".into()),
524 path: Some(path!("/project/src/module.js").into()),
525 source_reference: None,
526 presentation_hint: None,
527 origin: Some("ignored".into()),
528 sources: None,
529 adapter_data: None,
530 checksums: None,
531 }),
532 line: 1,
533 column: 1,
534 end_line: None,
535 end_column: None,
536 can_restart: None,
537 instruction_pointer_reference: None,
538 module_id: None,
539 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
540 },
541 StackFrame {
542 id: 4,
543 name: "Stack Frame 4".into(),
544 source: Some(dap::Source {
545 name: Some("module.js".into()),
546 path: Some(path!("/project/src/module.js").into()),
547 source_reference: None,
548 presentation_hint: None,
549 origin: None,
550 sources: None,
551 adapter_data: None,
552 checksums: None,
553 }),
554 line: 1,
555 column: 1,
556 end_line: None,
557 end_column: None,
558 can_restart: None,
559 instruction_pointer_reference: None,
560 module_id: None,
561 presentation_hint: None,
562 },
563 StackFrame {
564 id: 5,
565 name: "Stack Frame 5".into(),
566 source: Some(dap::Source {
567 name: Some("module.js".into()),
568 path: Some(path!("/project/src/module.js").into()),
569 source_reference: None,
570 presentation_hint: None,
571 origin: None,
572 sources: None,
573 adapter_data: None,
574 checksums: None,
575 }),
576 line: 1,
577 column: 1,
578 end_line: None,
579 end_column: None,
580 can_restart: None,
581 instruction_pointer_reference: None,
582 module_id: None,
583 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
584 },
585 StackFrame {
586 id: 6,
587 name: "Stack Frame 6".into(),
588 source: Some(dap::Source {
589 name: Some("module.js".into()),
590 path: Some(path!("/project/src/module.js").into()),
591 source_reference: None,
592 presentation_hint: None,
593 origin: None,
594 sources: None,
595 adapter_data: None,
596 checksums: None,
597 }),
598 line: 1,
599 column: 1,
600 end_line: None,
601 end_column: None,
602 can_restart: None,
603 instruction_pointer_reference: None,
604 module_id: None,
605 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
606 },
607 StackFrame {
608 id: 7,
609 name: "Stack Frame 7".into(),
610 source: Some(dap::Source {
611 name: Some("module.js".into()),
612 path: Some(path!("/project/src/module.js").into()),
613 source_reference: None,
614 presentation_hint: None,
615 origin: None,
616 sources: None,
617 adapter_data: None,
618 checksums: None,
619 }),
620 line: 1,
621 column: 1,
622 end_line: None,
623 end_column: None,
624 can_restart: None,
625 instruction_pointer_reference: None,
626 module_id: None,
627 presentation_hint: None,
628 },
629 ];
630
631 client.on_request::<StackTrace, _>({
632 let stack_frames = Arc::new(stack_frames.clone());
633 move |_, args| {
634 assert_eq!(1, args.thread_id);
635
636 Ok(dap::StackTraceResponse {
637 stack_frames: (*stack_frames).clone(),
638 total_frames: None,
639 })
640 }
641 });
642
643 client
644 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
645 reason: dap::StoppedEventReason::Pause,
646 description: None,
647 thread_id: Some(1),
648 preserve_focus_hint: None,
649 text: None,
650 all_threads_stopped: None,
651 hit_breakpoint_ids: None,
652 }))
653 .await;
654
655 cx.run_until_parked();
656
657 // trigger threads to load
658 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
659 session.running_state().update(cx, |running_state, cx| {
660 running_state
661 .session()
662 .update(cx, |session, cx| session.threads(cx));
663 });
664 });
665
666 cx.run_until_parked();
667
668 // select first thread
669 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
670 session.running_state().update(cx, |running_state, cx| {
671 running_state.select_current_thread(
672 &running_state
673 .session()
674 .update(cx, |session, cx| session.threads(cx)),
675 window,
676 cx,
677 );
678 });
679 });
680
681 cx.run_until_parked();
682
683 // trigger stack frames to loaded
684 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
685 let stack_frame_list = debug_panel_item
686 .running_state()
687 .update(cx, |state, _| state.stack_frame_list().clone());
688
689 stack_frame_list.update(cx, |stack_frame_list, cx| {
690 stack_frame_list.dap_stack_frames(cx);
691 });
692 });
693
694 cx.run_until_parked();
695
696 active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
697 let stack_frame_list = debug_panel_item
698 .running_state()
699 .update(cx, |state, _| state.stack_frame_list().clone());
700
701 stack_frame_list.update(cx, |stack_frame_list, cx| {
702 stack_frame_list.build_entries(true, window, cx);
703
704 assert_eq!(
705 &vec![
706 StackFrameEntry::Normal(stack_frames[0].clone()),
707 StackFrameEntry::Collapsed(vec![
708 stack_frames[1].clone(),
709 stack_frames[2].clone()
710 ]),
711 StackFrameEntry::Normal(stack_frames[3].clone()),
712 StackFrameEntry::Collapsed(vec![
713 stack_frames[4].clone(),
714 stack_frames[5].clone()
715 ]),
716 StackFrameEntry::Normal(stack_frames[6].clone()),
717 ],
718 stack_frame_list.entries()
719 );
720
721 stack_frame_list.expand_collapsed_entry(1, cx);
722
723 assert_eq!(
724 &vec![
725 StackFrameEntry::Normal(stack_frames[0].clone()),
726 StackFrameEntry::Normal(stack_frames[1].clone()),
727 StackFrameEntry::Normal(stack_frames[2].clone()),
728 StackFrameEntry::Normal(stack_frames[3].clone()),
729 StackFrameEntry::Collapsed(vec![
730 stack_frames[4].clone(),
731 stack_frames[5].clone()
732 ]),
733 StackFrameEntry::Normal(stack_frames[6].clone()),
734 ],
735 stack_frame_list.entries()
736 );
737
738 stack_frame_list.expand_collapsed_entry(4, cx);
739
740 assert_eq!(
741 &vec![
742 StackFrameEntry::Normal(stack_frames[0].clone()),
743 StackFrameEntry::Normal(stack_frames[1].clone()),
744 StackFrameEntry::Normal(stack_frames[2].clone()),
745 StackFrameEntry::Normal(stack_frames[3].clone()),
746 StackFrameEntry::Normal(stack_frames[4].clone()),
747 StackFrameEntry::Normal(stack_frames[5].clone()),
748 StackFrameEntry::Normal(stack_frames[6].clone()),
749 ],
750 stack_frame_list.entries()
751 );
752 });
753 });
754}
755
756#[gpui::test]
757async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) {
758 init_test(cx);
759
760 let fs = FakeFs::new(executor.clone());
761
762 let test_file_content = r#"
763 function main() {
764 doSomething();
765 }
766
767 function doSomething() {
768 console.log('doing something');
769 }
770 "#
771 .unindent();
772
773 fs.insert_tree(
774 path!("/project"),
775 json!({
776 "src": {
777 "test.js": test_file_content,
778 }
779 }),
780 )
781 .await;
782
783 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
784 let workspace = init_test_workspace(&project, cx).await;
785 let cx = &mut VisualTestContext::from_window(*workspace, cx);
786
787 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
788 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
789
790 client.on_request::<Threads, _>(move |_, _| {
791 Ok(dap::ThreadsResponse {
792 threads: vec![dap::Thread {
793 id: 1,
794 name: "Thread 1".into(),
795 }],
796 })
797 });
798
799 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
800
801 let stack_frames = vec![
802 StackFrame {
803 id: 1,
804 name: "main".into(),
805 source: Some(dap::Source {
806 name: Some("test.js".into()),
807 path: Some(path!("/project/src/test.js").into()),
808 source_reference: None,
809 presentation_hint: None,
810 origin: None,
811 sources: None,
812 adapter_data: None,
813 checksums: None,
814 }),
815 line: 2,
816 column: 1,
817 end_line: None,
818 end_column: None,
819 can_restart: None,
820 instruction_pointer_reference: None,
821 module_id: None,
822 presentation_hint: None,
823 },
824 StackFrame {
825 id: 2,
826 name: "node:internal/modules/cjs/loader".into(),
827 source: Some(dap::Source {
828 name: Some("loader.js".into()),
829 path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()),
830 source_reference: None,
831 presentation_hint: None,
832 origin: None,
833 sources: None,
834 adapter_data: None,
835 checksums: None,
836 }),
837 line: 100,
838 column: 1,
839 end_line: None,
840 end_column: None,
841 can_restart: None,
842 instruction_pointer_reference: None,
843 module_id: None,
844 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
845 },
846 StackFrame {
847 id: 3,
848 name: "node:internal/modules/run_main".into(),
849 source: Some(dap::Source {
850 name: Some("run_main.js".into()),
851 path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()),
852 source_reference: None,
853 presentation_hint: None,
854 origin: None,
855 sources: None,
856 adapter_data: None,
857 checksums: None,
858 }),
859 line: 50,
860 column: 1,
861 end_line: None,
862 end_column: None,
863 can_restart: None,
864 instruction_pointer_reference: None,
865 module_id: None,
866 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
867 },
868 StackFrame {
869 id: 4,
870 name: "node:internal/modules/run_main2".into(),
871 source: Some(dap::Source {
872 name: Some("run_main.js".into()),
873 path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()),
874 source_reference: None,
875 presentation_hint: None,
876 origin: None,
877 sources: None,
878 adapter_data: None,
879 checksums: None,
880 }),
881 line: 50,
882 column: 1,
883 end_line: None,
884 end_column: None,
885 can_restart: None,
886 instruction_pointer_reference: None,
887 module_id: None,
888 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
889 },
890 StackFrame {
891 id: 5,
892 name: "doSomething".into(),
893 source: Some(dap::Source {
894 name: Some("test.js".into()),
895 path: Some(path!("/project/src/test.js").into()),
896 source_reference: None,
897 presentation_hint: None,
898 origin: None,
899 sources: None,
900 adapter_data: None,
901 checksums: None,
902 }),
903 line: 3,
904 column: 1,
905 end_line: None,
906 end_column: None,
907 can_restart: None,
908 instruction_pointer_reference: None,
909 module_id: None,
910 presentation_hint: None,
911 },
912 ];
913
914 // Store a copy for assertions
915 let stack_frames_for_assertions = stack_frames.clone();
916
917 client.on_request::<StackTrace, _>({
918 let stack_frames = Arc::new(stack_frames.clone());
919 move |_, args| {
920 assert_eq!(1, args.thread_id);
921
922 Ok(dap::StackTraceResponse {
923 stack_frames: (*stack_frames).clone(),
924 total_frames: None,
925 })
926 }
927 });
928
929 client
930 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
931 reason: dap::StoppedEventReason::Pause,
932 description: None,
933 thread_id: Some(1),
934 preserve_focus_hint: None,
935 text: None,
936 all_threads_stopped: None,
937 hit_breakpoint_ids: None,
938 }))
939 .await;
940
941 cx.run_until_parked();
942
943 // trigger threads to load
944 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
945 session.running_state().update(cx, |running_state, cx| {
946 running_state
947 .session()
948 .update(cx, |session, cx| session.threads(cx));
949 });
950 });
951
952 cx.run_until_parked();
953
954 // select first thread
955 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
956 session.running_state().update(cx, |running_state, cx| {
957 running_state.select_current_thread(
958 &running_state
959 .session()
960 .update(cx, |session, cx| session.threads(cx)),
961 window,
962 cx,
963 );
964 });
965 });
966
967 cx.run_until_parked();
968
969 // trigger stack frames to load
970 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
971 let stack_frame_list = debug_panel_item
972 .running_state()
973 .update(cx, |state, _| state.stack_frame_list().clone());
974
975 stack_frame_list.update(cx, |stack_frame_list, cx| {
976 stack_frame_list.dap_stack_frames(cx);
977 });
978 });
979
980 cx.run_until_parked();
981
982 let stack_frame_list =
983 active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
984 let stack_frame_list = debug_panel_item
985 .running_state()
986 .update(cx, |state, _| state.stack_frame_list().clone());
987
988 stack_frame_list.update(cx, |stack_frame_list, cx| {
989 stack_frame_list.build_entries(true, window, cx);
990
991 // Verify we have the expected collapsed structure
992 assert_eq!(
993 stack_frame_list.entries(),
994 &vec![
995 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
996 StackFrameEntry::Collapsed(vec![
997 stack_frames_for_assertions[1].clone(),
998 stack_frames_for_assertions[2].clone(),
999 stack_frames_for_assertions[3].clone()
1000 ]),
1001 StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
1002 ]
1003 );
1004 });
1005
1006 stack_frame_list
1007 });
1008
1009 stack_frame_list.update(cx, |stack_frame_list, cx| {
1010 let all_frames = stack_frame_list.flatten_entries(true, false);
1011 assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially");
1012
1013 stack_frame_list
1014 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1015 assert_eq!(
1016 stack_frame_list.list_filter(),
1017 StackFrameFilter::OnlyUserFrames
1018 );
1019 });
1020
1021 stack_frame_list.update(cx, |stack_frame_list, cx| {
1022 let user_frames = stack_frame_list.dap_stack_frames(cx);
1023 assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
1024 assert_eq!(user_frames[0].name, "main");
1025 assert_eq!(user_frames[1].name, "doSomething");
1026
1027 // Toggle back to all frames
1028 stack_frame_list
1029 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1030 assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
1031 });
1032
1033 stack_frame_list.update(cx, |stack_frame_list, cx| {
1034 let all_frames_again = stack_frame_list.flatten_entries(true, false);
1035 assert_eq!(
1036 all_frames_again.len(),
1037 5,
1038 "Should see all 5 frames after toggling back"
1039 );
1040
1041 // Test 3: Verify collapsed entries stay expanded
1042 stack_frame_list.expand_collapsed_entry(1, cx);
1043 assert_eq!(
1044 stack_frame_list.entries(),
1045 &vec![
1046 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1047 StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1048 StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1049 StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1050 StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
1051 ]
1052 );
1053
1054 stack_frame_list
1055 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1056 assert_eq!(
1057 stack_frame_list.list_filter(),
1058 StackFrameFilter::OnlyUserFrames
1059 );
1060 });
1061
1062 stack_frame_list.update(cx, |stack_frame_list, cx| {
1063 stack_frame_list
1064 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1065 assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
1066 });
1067
1068 stack_frame_list.update(cx, |stack_frame_list, cx| {
1069 stack_frame_list
1070 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1071 assert_eq!(
1072 stack_frame_list.list_filter(),
1073 StackFrameFilter::OnlyUserFrames
1074 );
1075
1076 assert_eq!(
1077 stack_frame_list.dap_stack_frames(cx).as_slice(),
1078 &[
1079 stack_frames_for_assertions[0].clone(),
1080 stack_frames_for_assertions[4].clone()
1081 ]
1082 );
1083
1084 // Verify entries remain expanded
1085 assert_eq!(
1086 stack_frame_list.entries(),
1087 &vec![
1088 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1089 StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1090 StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1091 StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1092 StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
1093 ],
1094 "Expanded entries should remain expanded after toggling filter"
1095 );
1096 });
1097}