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