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.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: "doSomething".into(),
871 source: Some(dap::Source {
872 name: Some("test.js".into()),
873 path: Some(path!("/project/src/test.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: 3,
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: None,
889 },
890 ];
891
892 // Store a copy for assertions
893 let stack_frames_for_assertions = stack_frames.clone();
894
895 client.on_request::<StackTrace, _>({
896 let stack_frames = Arc::new(stack_frames.clone());
897 move |_, args| {
898 assert_eq!(1, args.thread_id);
899
900 Ok(dap::StackTraceResponse {
901 stack_frames: (*stack_frames).clone(),
902 total_frames: None,
903 })
904 }
905 });
906
907 client
908 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
909 reason: dap::StoppedEventReason::Pause,
910 description: None,
911 thread_id: Some(1),
912 preserve_focus_hint: None,
913 text: None,
914 all_threads_stopped: None,
915 hit_breakpoint_ids: None,
916 }))
917 .await;
918
919 cx.run_until_parked();
920
921 // trigger threads to load
922 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
923 session.running_state().update(cx, |running_state, cx| {
924 running_state
925 .session()
926 .update(cx, |session, cx| session.threads(cx));
927 });
928 });
929
930 cx.run_until_parked();
931
932 // select first thread
933 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
934 session.running_state().update(cx, |running_state, cx| {
935 running_state.select_current_thread(
936 &running_state
937 .session()
938 .update(cx, |session, cx| session.threads(cx)),
939 window,
940 cx,
941 );
942 });
943 });
944
945 cx.run_until_parked();
946
947 // trigger stack frames to load
948 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
949 let stack_frame_list = debug_panel_item
950 .running_state()
951 .update(cx, |state, _| state.stack_frame_list().clone());
952
953 stack_frame_list.update(cx, |stack_frame_list, cx| {
954 stack_frame_list.dap_stack_frames(cx);
955 });
956 });
957
958 cx.run_until_parked();
959
960 active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, 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.build_entries(true, window, cx);
967
968 // Verify we have the expected collapsed structure
969 assert_eq!(
970 stack_frame_list.entries(),
971 &vec![
972 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
973 StackFrameEntry::Collapsed(vec![
974 stack_frames_for_assertions[1].clone(),
975 stack_frames_for_assertions[2].clone()
976 ]),
977 StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
978 ]
979 );
980
981 // Test 1: Verify filtering works
982 let all_frames = stack_frame_list.flatten_entries(true, false);
983 assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially");
984
985 // Toggle to user frames only
986 stack_frame_list
987 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
988
989 let user_frames = stack_frame_list.dap_stack_frames(cx);
990 assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
991 assert_eq!(user_frames[0].name, "main");
992 assert_eq!(user_frames[1].name, "doSomething");
993
994 // Test 2: Verify filtering toggles correctly
995 // Check we can toggle back and see all frames again
996
997 // Toggle back to all frames
998 stack_frame_list
999 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1000
1001 let all_frames_again = stack_frame_list.flatten_entries(true, false);
1002 assert_eq!(
1003 all_frames_again.len(),
1004 4,
1005 "Should see all 4 frames after toggling back"
1006 );
1007
1008 // Test 3: Verify collapsed entries stay expanded
1009 stack_frame_list.expand_collapsed_entry(1, cx);
1010 assert_eq!(
1011 stack_frame_list.entries(),
1012 &vec![
1013 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1014 StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1015 StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1016 StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1017 ]
1018 );
1019
1020 // Toggle filter twice
1021 stack_frame_list
1022 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1023 stack_frame_list
1024 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1025
1026 // Verify entries remain expanded
1027 assert_eq!(
1028 stack_frame_list.entries(),
1029 &vec![
1030 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1031 StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1032 StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1033 StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1034 ],
1035 "Expanded entries should remain expanded after toggling filter"
1036 );
1037 });
1038 });
1039}