1use crate::{
2 debugger_panel::DebugPanel,
3 session::running::stack_frame_list::{
4 StackFrameEntry, StackFrameFilter, stack_frame_filter_key,
5 },
6 tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
7};
8use dap::{
9 StackFrame,
10 requests::{Scopes, StackTrace, Threads},
11};
12use db::kvp::KEY_VALUE_STORE;
13use editor::{Editor, ToPoint as _};
14use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
15use project::{FakeFs, Project};
16use serde_json::json;
17use std::sync::Arc;
18use unindent::Unindent as _;
19use util::{path, rel_path::rel_path};
20
21#[gpui::test]
22async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
23 executor: BackgroundExecutor,
24 cx: &mut TestAppContext,
25) {
26 init_test(cx);
27
28 let fs = FakeFs::new(executor.clone());
29
30 let test_file_content = r#"
31 import { SOME_VALUE } './module.js';
32
33 console.log(SOME_VALUE);
34 "#
35 .unindent();
36
37 let module_file_content = r#"
38 export SOME_VALUE = 'some value';
39 "#
40 .unindent();
41
42 fs.insert_tree(
43 path!("/project"),
44 json!({
45 "src": {
46 "test.js": test_file_content,
47 "module.js": module_file_content,
48 }
49 }),
50 )
51 .await;
52
53 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
54 let workspace = init_test_workspace(&project, cx).await;
55 let cx = &mut VisualTestContext::from_window(*workspace, cx);
56 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
57 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
58 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
59
60 client.on_request::<Threads, _>(move |_, _| {
61 Ok(dap::ThreadsResponse {
62 threads: vec![dap::Thread {
63 id: 1,
64 name: "Thread 1".into(),
65 }],
66 })
67 });
68
69 let stack_frames = vec![
70 StackFrame {
71 id: 1,
72 name: "Stack Frame 1".into(),
73 source: Some(dap::Source {
74 name: Some("test.js".into()),
75 path: Some(path!("/project/src/test.js").into()),
76 source_reference: None,
77 presentation_hint: None,
78 origin: None,
79 sources: None,
80 adapter_data: None,
81 checksums: None,
82 }),
83 line: 3,
84 column: 1,
85 end_line: None,
86 end_column: None,
87 can_restart: None,
88 instruction_pointer_reference: None,
89 module_id: None,
90 presentation_hint: None,
91 },
92 StackFrame {
93 id: 2,
94 name: "Stack Frame 2".into(),
95 source: Some(dap::Source {
96 name: Some("module.js".into()),
97 path: Some(path!("/project/src/module.js").into()),
98 source_reference: None,
99 presentation_hint: None,
100 origin: None,
101 sources: None,
102 adapter_data: None,
103 checksums: None,
104 }),
105 line: 1,
106 column: 1,
107 end_line: None,
108 end_column: None,
109 can_restart: None,
110 instruction_pointer_reference: None,
111 module_id: None,
112 presentation_hint: None,
113 },
114 ];
115
116 client.on_request::<StackTrace, _>({
117 let stack_frames = Arc::new(stack_frames.clone());
118 move |_, args| {
119 assert_eq!(1, args.thread_id);
120
121 Ok(dap::StackTraceResponse {
122 stack_frames: (*stack_frames).clone(),
123 total_frames: None,
124 })
125 }
126 });
127
128 client
129 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
130 reason: dap::StoppedEventReason::Pause,
131 description: None,
132 thread_id: Some(1),
133 preserve_focus_hint: None,
134 text: None,
135 all_threads_stopped: None,
136 hit_breakpoint_ids: None,
137 }))
138 .await;
139
140 cx.run_until_parked();
141
142 // trigger to load threads
143 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
144 session.running_state().update(cx, |running_state, cx| {
145 running_state
146 .session()
147 .update(cx, |session, cx| session.threads(cx));
148 });
149 });
150
151 cx.run_until_parked();
152
153 // select first thread
154 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
155 session.running_state().update(cx, |running_state, cx| {
156 running_state.select_current_thread(
157 &running_state
158 .session()
159 .update(cx, |session, cx| session.threads(cx)),
160 window,
161 cx,
162 );
163 });
164 });
165
166 cx.run_until_parked();
167
168 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
169 let stack_frame_list = session
170 .running_state()
171 .update(cx, |state, _| state.stack_frame_list().clone());
172
173 stack_frame_list.update(cx, |stack_frame_list, cx| {
174 assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
175 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
176 });
177 });
178}
179
180#[gpui::test]
181async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
182 cx.executor().allow_parking();
183 init_test(cx);
184
185 let fs = FakeFs::new(executor.clone());
186
187 let test_file_content = r#"
188 import { SOME_VALUE } './module.js';
189
190 console.log(SOME_VALUE);
191 "#
192 .unindent();
193
194 let module_file_content = r#"
195 export SOME_VALUE = 'some value';
196 "#
197 .unindent();
198
199 fs.insert_tree(
200 path!("/project"),
201 json!({
202 "src": {
203 "test.js": test_file_content,
204 "module.js": module_file_content,
205 }
206 }),
207 )
208 .await;
209
210 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
211 let workspace = init_test_workspace(&project, cx).await;
212 let _ = workspace.update(cx, |workspace, window, cx| {
213 workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
214 });
215
216 let cx = &mut VisualTestContext::from_window(*workspace, cx);
217 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
218 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
219
220 client.on_request::<Threads, _>(move |_, _| {
221 Ok(dap::ThreadsResponse {
222 threads: vec![dap::Thread {
223 id: 1,
224 name: "Thread 1".into(),
225 }],
226 })
227 });
228
229 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
230
231 let stack_frames = vec![
232 StackFrame {
233 id: 1,
234 name: "Stack Frame 1".into(),
235 source: Some(dap::Source {
236 name: Some("test.js".into()),
237 path: Some(path!("/project/src/test.js").into()),
238 source_reference: None,
239 presentation_hint: None,
240 origin: None,
241 sources: None,
242 adapter_data: None,
243 checksums: None,
244 }),
245 line: 3,
246 column: 1,
247 end_line: None,
248 end_column: None,
249 can_restart: None,
250 instruction_pointer_reference: None,
251 module_id: None,
252 presentation_hint: None,
253 },
254 StackFrame {
255 id: 2,
256 name: "Stack Frame 2".into(),
257 source: Some(dap::Source {
258 name: Some("module.js".into()),
259 path: Some(path!("/project/src/module.js").into()),
260 source_reference: None,
261 presentation_hint: None,
262 origin: None,
263 sources: None,
264 adapter_data: None,
265 checksums: None,
266 }),
267 line: 1,
268 column: 1,
269 end_line: None,
270 end_column: None,
271 can_restart: None,
272 instruction_pointer_reference: None,
273 module_id: None,
274 presentation_hint: None,
275 },
276 ];
277
278 client.on_request::<StackTrace, _>({
279 let stack_frames = Arc::new(stack_frames.clone());
280 move |_, args| {
281 assert_eq!(1, args.thread_id);
282
283 Ok(dap::StackTraceResponse {
284 stack_frames: (*stack_frames).clone(),
285 total_frames: None,
286 })
287 }
288 });
289
290 client
291 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
292 reason: dap::StoppedEventReason::Pause,
293 description: None,
294 thread_id: Some(1),
295 preserve_focus_hint: None,
296 text: None,
297 all_threads_stopped: None,
298 hit_breakpoint_ids: None,
299 }))
300 .await;
301
302 cx.run_until_parked();
303
304 // trigger threads to load
305 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
306 session.running_state().update(cx, |running_state, cx| {
307 running_state
308 .session()
309 .update(cx, |session, cx| session.threads(cx));
310 });
311 });
312
313 cx.run_until_parked();
314
315 // select first thread
316 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
317 session.running_state().update(cx, |running_state, cx| {
318 running_state.select_current_thread(
319 &running_state
320 .session()
321 .update(cx, |session, cx| session.threads(cx)),
322 window,
323 cx,
324 );
325 });
326 });
327
328 cx.run_until_parked();
329
330 workspace
331 .update(cx, |workspace, window, cx| {
332 let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
333 assert_eq!(1, editors.len());
334
335 let project_path = editors[0]
336 .update(cx, |editor, cx| editor.project_path(cx))
337 .unwrap();
338 assert_eq!(rel_path("src/test.js"), project_path.path.as_ref());
339 assert_eq!(test_file_content, editors[0].read(cx).text(cx));
340 assert_eq!(
341 vec![2..3],
342 editors[0].update(cx, |editor, cx| {
343 let snapshot = editor.snapshot(window, cx);
344
345 editor
346 .highlighted_rows::<editor::ActiveDebugLine>()
347 .map(|(range, _)| {
348 let start = range.start.to_point(&snapshot.buffer_snapshot());
349 let end = range.end.to_point(&snapshot.buffer_snapshot());
350 start.row..end.row
351 })
352 .collect::<Vec<_>>()
353 })
354 );
355 })
356 .unwrap();
357
358 let stack_frame_list = workspace
359 .update(cx, |workspace, _window, cx| {
360 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
361 let active_debug_panel_item = debug_panel
362 .update(cx, |this, _| this.active_session())
363 .unwrap();
364
365 active_debug_panel_item
366 .read(cx)
367 .running_state()
368 .read(cx)
369 .stack_frame_list()
370 .clone()
371 })
372 .unwrap();
373
374 stack_frame_list.update(cx, |stack_frame_list, cx| {
375 assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
376 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
377 });
378
379 // select second stack frame
380 stack_frame_list
381 .update_in(cx, |stack_frame_list, window, cx| {
382 stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
383 })
384 .await
385 .unwrap();
386
387 cx.run_until_parked();
388
389 stack_frame_list.update(cx, |stack_frame_list, cx| {
390 assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
391 assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
392 });
393
394 let _ = workspace.update(cx, |workspace, window, cx| {
395 let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
396 assert_eq!(1, editors.len());
397
398 let project_path = editors[0]
399 .update(cx, |editor, cx| editor.project_path(cx))
400 .unwrap();
401 assert_eq!(rel_path("src/module.js"), project_path.path.as_ref());
402 assert_eq!(module_file_content, editors[0].read(cx).text(cx));
403 assert_eq!(
404 vec![0..1],
405 editors[0].update(cx, |editor, cx| {
406 let snapshot = editor.snapshot(window, cx);
407
408 editor
409 .highlighted_rows::<editor::ActiveDebugLine>()
410 .map(|(range, _)| {
411 let start = range.start.to_point(&snapshot.buffer_snapshot());
412 let end = range.end.to_point(&snapshot.buffer_snapshot());
413 start.row..end.row
414 })
415 .collect::<Vec<_>>()
416 })
417 );
418 });
419}
420
421#[gpui::test]
422async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
423 init_test(cx);
424
425 let fs = FakeFs::new(executor.clone());
426
427 let test_file_content = r#"
428 import { SOME_VALUE } './module.js';
429
430 console.log(SOME_VALUE);
431 "#
432 .unindent();
433
434 let module_file_content = r#"
435 export SOME_VALUE = 'some value';
436 "#
437 .unindent();
438
439 fs.insert_tree(
440 path!("/project"),
441 json!({
442 "src": {
443 "test.js": test_file_content,
444 "module.js": module_file_content,
445 }
446 }),
447 )
448 .await;
449
450 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
451 let workspace = init_test_workspace(&project, cx).await;
452 let cx = &mut VisualTestContext::from_window(*workspace, cx);
453
454 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
455 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
456
457 client.on_request::<Threads, _>(move |_, _| {
458 Ok(dap::ThreadsResponse {
459 threads: vec![dap::Thread {
460 id: 1,
461 name: "Thread 1".into(),
462 }],
463 })
464 });
465
466 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
467
468 let stack_frames = vec![
469 StackFrame {
470 id: 1,
471 name: "Stack Frame 1".into(),
472 source: Some(dap::Source {
473 name: Some("test.js".into()),
474 path: Some(path!("/project/src/test.js").into()),
475 source_reference: None,
476 presentation_hint: None,
477 origin: None,
478 sources: None,
479 adapter_data: None,
480 checksums: None,
481 }),
482 line: 3,
483 column: 1,
484 end_line: None,
485 end_column: None,
486 can_restart: None,
487 instruction_pointer_reference: None,
488 module_id: None,
489 presentation_hint: None,
490 },
491 StackFrame {
492 id: 2,
493 name: "Stack Frame 2".into(),
494 source: Some(dap::Source {
495 name: Some("module.js".into()),
496 path: Some(path!("/project/src/module.js").into()),
497 source_reference: None,
498 presentation_hint: None,
499 origin: Some("ignored".into()),
500 sources: None,
501 adapter_data: None,
502 checksums: None,
503 }),
504 line: 1,
505 column: 1,
506 end_line: None,
507 end_column: None,
508 can_restart: None,
509 instruction_pointer_reference: None,
510 module_id: None,
511 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
512 },
513 StackFrame {
514 id: 3,
515 name: "Stack Frame 3".into(),
516 source: Some(dap::Source {
517 name: Some("module.js".into()),
518 path: Some(path!("/project/src/module.js").into()),
519 source_reference: None,
520 presentation_hint: None,
521 origin: Some("ignored".into()),
522 sources: None,
523 adapter_data: None,
524 checksums: None,
525 }),
526 line: 1,
527 column: 1,
528 end_line: None,
529 end_column: None,
530 can_restart: None,
531 instruction_pointer_reference: None,
532 module_id: None,
533 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
534 },
535 StackFrame {
536 id: 4,
537 name: "Stack Frame 4".into(),
538 source: Some(dap::Source {
539 name: Some("module.js".into()),
540 path: Some(path!("/project/src/module.js").into()),
541 source_reference: None,
542 presentation_hint: None,
543 origin: None,
544 sources: None,
545 adapter_data: None,
546 checksums: None,
547 }),
548 line: 1,
549 column: 1,
550 end_line: None,
551 end_column: None,
552 can_restart: None,
553 instruction_pointer_reference: None,
554 module_id: None,
555 presentation_hint: None,
556 },
557 StackFrame {
558 id: 5,
559 name: "Stack Frame 5".into(),
560 source: Some(dap::Source {
561 name: Some("module.js".into()),
562 path: Some(path!("/project/src/module.js").into()),
563 source_reference: None,
564 presentation_hint: None,
565 origin: None,
566 sources: None,
567 adapter_data: None,
568 checksums: None,
569 }),
570 line: 1,
571 column: 1,
572 end_line: None,
573 end_column: None,
574 can_restart: None,
575 instruction_pointer_reference: None,
576 module_id: None,
577 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
578 },
579 StackFrame {
580 id: 6,
581 name: "Stack Frame 6".into(),
582 source: Some(dap::Source {
583 name: Some("module.js".into()),
584 path: Some(path!("/project/src/module.js").into()),
585 source_reference: None,
586 presentation_hint: None,
587 origin: None,
588 sources: None,
589 adapter_data: None,
590 checksums: None,
591 }),
592 line: 1,
593 column: 1,
594 end_line: None,
595 end_column: None,
596 can_restart: None,
597 instruction_pointer_reference: None,
598 module_id: None,
599 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
600 },
601 StackFrame {
602 id: 7,
603 name: "Stack Frame 7".into(),
604 source: Some(dap::Source {
605 name: Some("module.js".into()),
606 path: Some(path!("/project/src/module.js").into()),
607 source_reference: None,
608 presentation_hint: None,
609 origin: None,
610 sources: None,
611 adapter_data: None,
612 checksums: None,
613 }),
614 line: 1,
615 column: 1,
616 end_line: None,
617 end_column: None,
618 can_restart: None,
619 instruction_pointer_reference: None,
620 module_id: None,
621 presentation_hint: None,
622 },
623 ];
624
625 client.on_request::<StackTrace, _>({
626 let stack_frames = Arc::new(stack_frames.clone());
627 move |_, args| {
628 assert_eq!(1, args.thread_id);
629
630 Ok(dap::StackTraceResponse {
631 stack_frames: (*stack_frames).clone(),
632 total_frames: None,
633 })
634 }
635 });
636
637 client
638 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
639 reason: dap::StoppedEventReason::Pause,
640 description: None,
641 thread_id: Some(1),
642 preserve_focus_hint: None,
643 text: None,
644 all_threads_stopped: None,
645 hit_breakpoint_ids: None,
646 }))
647 .await;
648
649 cx.run_until_parked();
650
651 // trigger threads to load
652 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
653 session.running_state().update(cx, |running_state, cx| {
654 running_state
655 .session()
656 .update(cx, |session, cx| session.threads(cx));
657 });
658 });
659
660 cx.run_until_parked();
661
662 // select first thread
663 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
664 session.running_state().update(cx, |running_state, cx| {
665 running_state.select_current_thread(
666 &running_state
667 .session()
668 .update(cx, |session, cx| session.threads(cx)),
669 window,
670 cx,
671 );
672 });
673 });
674
675 cx.run_until_parked();
676
677 // trigger stack frames to loaded
678 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
679 let stack_frame_list = debug_panel_item
680 .running_state()
681 .update(cx, |state, _| state.stack_frame_list().clone());
682
683 stack_frame_list.update(cx, |stack_frame_list, cx| {
684 stack_frame_list.dap_stack_frames(cx);
685 });
686 });
687
688 cx.run_until_parked();
689
690 active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
691 let stack_frame_list = debug_panel_item
692 .running_state()
693 .update(cx, |state, _| state.stack_frame_list().clone());
694
695 stack_frame_list.update(cx, |stack_frame_list, cx| {
696 stack_frame_list.build_entries(true, window, cx);
697
698 assert_eq!(
699 &vec![
700 StackFrameEntry::Normal(stack_frames[0].clone()),
701 StackFrameEntry::Collapsed(vec![
702 stack_frames[1].clone(),
703 stack_frames[2].clone()
704 ]),
705 StackFrameEntry::Normal(stack_frames[3].clone()),
706 StackFrameEntry::Collapsed(vec![
707 stack_frames[4].clone(),
708 stack_frames[5].clone()
709 ]),
710 StackFrameEntry::Normal(stack_frames[6].clone()),
711 ],
712 stack_frame_list.entries()
713 );
714
715 stack_frame_list.expand_collapsed_entry(1, cx);
716
717 assert_eq!(
718 &vec![
719 StackFrameEntry::Normal(stack_frames[0].clone()),
720 StackFrameEntry::Normal(stack_frames[1].clone()),
721 StackFrameEntry::Normal(stack_frames[2].clone()),
722 StackFrameEntry::Normal(stack_frames[3].clone()),
723 StackFrameEntry::Collapsed(vec![
724 stack_frames[4].clone(),
725 stack_frames[5].clone()
726 ]),
727 StackFrameEntry::Normal(stack_frames[6].clone()),
728 ],
729 stack_frame_list.entries()
730 );
731
732 stack_frame_list.expand_collapsed_entry(4, cx);
733
734 assert_eq!(
735 &vec![
736 StackFrameEntry::Normal(stack_frames[0].clone()),
737 StackFrameEntry::Normal(stack_frames[1].clone()),
738 StackFrameEntry::Normal(stack_frames[2].clone()),
739 StackFrameEntry::Normal(stack_frames[3].clone()),
740 StackFrameEntry::Normal(stack_frames[4].clone()),
741 StackFrameEntry::Normal(stack_frames[5].clone()),
742 StackFrameEntry::Normal(stack_frames[6].clone()),
743 ],
744 stack_frame_list.entries()
745 );
746 });
747 });
748}
749
750#[gpui::test]
751async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) {
752 init_test(cx);
753
754 let fs = FakeFs::new(executor.clone());
755
756 let test_file_content = r#"
757 function main() {
758 doSomething();
759 }
760
761 function doSomething() {
762 console.log('doing something');
763 }
764 "#
765 .unindent();
766
767 fs.insert_tree(
768 path!("/project"),
769 json!({
770 "src": {
771 "test.js": test_file_content,
772 }
773 }),
774 )
775 .await;
776
777 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
778 let workspace = init_test_workspace(&project, cx).await;
779 let cx = &mut VisualTestContext::from_window(*workspace, cx);
780
781 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
782 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
783
784 client.on_request::<Threads, _>(move |_, _| {
785 Ok(dap::ThreadsResponse {
786 threads: vec![dap::Thread {
787 id: 1,
788 name: "Thread 1".into(),
789 }],
790 })
791 });
792
793 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
794
795 let stack_frames = vec![
796 StackFrame {
797 id: 1,
798 name: "main".into(),
799 source: Some(dap::Source {
800 name: Some("test.js".into()),
801 path: Some(path!("/project/src/test.js").into()),
802 source_reference: None,
803 presentation_hint: None,
804 origin: None,
805 sources: None,
806 adapter_data: None,
807 checksums: None,
808 }),
809 line: 2,
810 column: 1,
811 end_line: None,
812 end_column: None,
813 can_restart: None,
814 instruction_pointer_reference: None,
815 module_id: None,
816 presentation_hint: None,
817 },
818 StackFrame {
819 id: 2,
820 name: "node:internal/modules/cjs/loader".into(),
821 source: Some(dap::Source {
822 name: Some("loader.js".into()),
823 path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()),
824 source_reference: None,
825 presentation_hint: None,
826 origin: None,
827 sources: None,
828 adapter_data: None,
829 checksums: None,
830 }),
831 line: 100,
832 column: 1,
833 end_line: None,
834 end_column: None,
835 can_restart: None,
836 instruction_pointer_reference: None,
837 module_id: None,
838 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
839 },
840 StackFrame {
841 id: 3,
842 name: "node:internal/modules/run_main".into(),
843 source: Some(dap::Source {
844 name: Some("run_main.js".into()),
845 path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()),
846 source_reference: None,
847 presentation_hint: None,
848 origin: None,
849 sources: None,
850 adapter_data: None,
851 checksums: None,
852 }),
853 line: 50,
854 column: 1,
855 end_line: None,
856 end_column: None,
857 can_restart: None,
858 instruction_pointer_reference: None,
859 module_id: None,
860 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
861 },
862 StackFrame {
863 id: 4,
864 name: "node:internal/modules/run_main2".into(),
865 source: Some(dap::Source {
866 name: Some("run_main.js".into()),
867 path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()),
868 source_reference: None,
869 presentation_hint: None,
870 origin: None,
871 sources: None,
872 adapter_data: None,
873 checksums: None,
874 }),
875 line: 50,
876 column: 1,
877 end_line: None,
878 end_column: None,
879 can_restart: None,
880 instruction_pointer_reference: None,
881 module_id: None,
882 presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
883 },
884 StackFrame {
885 id: 5,
886 name: "doSomething".into(),
887 source: Some(dap::Source {
888 name: Some("test.js".into()),
889 path: Some(path!("/project/src/test.js").into()),
890 source_reference: None,
891 presentation_hint: None,
892 origin: None,
893 sources: None,
894 adapter_data: None,
895 checksums: None,
896 }),
897 line: 3,
898 column: 1,
899 end_line: None,
900 end_column: None,
901 can_restart: None,
902 instruction_pointer_reference: None,
903 module_id: None,
904 presentation_hint: None,
905 },
906 ];
907
908 // Store a copy for assertions
909 let stack_frames_for_assertions = stack_frames.clone();
910
911 client.on_request::<StackTrace, _>({
912 let stack_frames = Arc::new(stack_frames.clone());
913 move |_, args| {
914 assert_eq!(1, args.thread_id);
915
916 Ok(dap::StackTraceResponse {
917 stack_frames: (*stack_frames).clone(),
918 total_frames: None,
919 })
920 }
921 });
922
923 client
924 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
925 reason: dap::StoppedEventReason::Pause,
926 description: None,
927 thread_id: Some(1),
928 preserve_focus_hint: None,
929 text: None,
930 all_threads_stopped: None,
931 hit_breakpoint_ids: None,
932 }))
933 .await;
934
935 cx.run_until_parked();
936
937 // trigger threads to load
938 active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
939 session.running_state().update(cx, |running_state, cx| {
940 running_state
941 .session()
942 .update(cx, |session, cx| session.threads(cx));
943 });
944 });
945
946 cx.run_until_parked();
947
948 // select first thread
949 active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
950 session.running_state().update(cx, |running_state, cx| {
951 running_state.select_current_thread(
952 &running_state
953 .session()
954 .update(cx, |session, cx| session.threads(cx)),
955 window,
956 cx,
957 );
958 });
959 });
960
961 cx.run_until_parked();
962
963 // trigger stack frames to load
964 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
965 let stack_frame_list = debug_panel_item
966 .running_state()
967 .update(cx, |state, _| state.stack_frame_list().clone());
968
969 stack_frame_list.update(cx, |stack_frame_list, cx| {
970 stack_frame_list.dap_stack_frames(cx);
971 });
972 });
973
974 cx.run_until_parked();
975
976 let stack_frame_list =
977 active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
978 let stack_frame_list = debug_panel_item
979 .running_state()
980 .update(cx, |state, _| state.stack_frame_list().clone());
981
982 stack_frame_list.update(cx, |stack_frame_list, cx| {
983 stack_frame_list.build_entries(true, window, cx);
984
985 // Verify we have the expected collapsed structure
986 assert_eq!(
987 stack_frame_list.entries(),
988 &vec![
989 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
990 StackFrameEntry::Collapsed(vec![
991 stack_frames_for_assertions[1].clone(),
992 stack_frames_for_assertions[2].clone(),
993 stack_frames_for_assertions[3].clone()
994 ]),
995 StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
996 ]
997 );
998 });
999
1000 stack_frame_list
1001 });
1002
1003 stack_frame_list.update(cx, |stack_frame_list, cx| {
1004 let all_frames = stack_frame_list.flatten_entries(true, false);
1005 assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially");
1006
1007 stack_frame_list
1008 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1009 assert_eq!(
1010 stack_frame_list.list_filter(),
1011 StackFrameFilter::OnlyUserFrames
1012 );
1013 });
1014
1015 stack_frame_list.update(cx, |stack_frame_list, cx| {
1016 let user_frames = stack_frame_list.dap_stack_frames(cx);
1017 assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
1018 assert_eq!(user_frames[0].name, "main");
1019 assert_eq!(user_frames[1].name, "doSomething");
1020
1021 // Toggle back to all frames
1022 stack_frame_list
1023 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1024 assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
1025 });
1026
1027 stack_frame_list.update(cx, |stack_frame_list, cx| {
1028 let all_frames_again = stack_frame_list.flatten_entries(true, false);
1029 assert_eq!(
1030 all_frames_again.len(),
1031 5,
1032 "Should see all 5 frames after toggling back"
1033 );
1034
1035 // Test 3: Verify collapsed entries stay expanded
1036 stack_frame_list.expand_collapsed_entry(1, cx);
1037 assert_eq!(
1038 stack_frame_list.entries(),
1039 &vec![
1040 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1041 StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1042 StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1043 StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1044 StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
1045 ]
1046 );
1047
1048 stack_frame_list
1049 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1050 assert_eq!(
1051 stack_frame_list.list_filter(),
1052 StackFrameFilter::OnlyUserFrames
1053 );
1054 });
1055
1056 stack_frame_list.update(cx, |stack_frame_list, cx| {
1057 stack_frame_list
1058 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1059 assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
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!(
1066 stack_frame_list.list_filter(),
1067 StackFrameFilter::OnlyUserFrames
1068 );
1069
1070 assert_eq!(
1071 stack_frame_list.dap_stack_frames(cx).as_slice(),
1072 &[
1073 stack_frames_for_assertions[0].clone(),
1074 stack_frames_for_assertions[4].clone()
1075 ]
1076 );
1077
1078 // Verify entries remain expanded
1079 assert_eq!(
1080 stack_frame_list.entries(),
1081 &vec![
1082 StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1083 StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1084 StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1085 StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1086 StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
1087 ],
1088 "Expanded entries should remain expanded after toggling filter"
1089 );
1090 });
1091}
1092
1093#[gpui::test]
1094async fn test_stack_frame_filter_persistence(
1095 executor: BackgroundExecutor,
1096 cx: &mut TestAppContext,
1097) {
1098 init_test(cx);
1099
1100 let fs = FakeFs::new(executor.clone());
1101
1102 fs.insert_tree(
1103 path!("/project"),
1104 json!({
1105 "src": {
1106 "test.js": "function main() { console.log('hello'); }",
1107 }
1108 }),
1109 )
1110 .await;
1111
1112 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
1113 let workspace = init_test_workspace(&project, cx).await;
1114 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1115 workspace
1116 .update(cx, |workspace, _, _| {
1117 workspace.set_random_database_id();
1118 })
1119 .unwrap();
1120
1121 let threads_response = dap::ThreadsResponse {
1122 threads: vec![dap::Thread {
1123 id: 1,
1124 name: "Thread 1".into(),
1125 }],
1126 };
1127
1128 let stack_trace_response = dap::StackTraceResponse {
1129 stack_frames: vec![StackFrame {
1130 id: 1,
1131 name: "main".into(),
1132 source: Some(dap::Source {
1133 name: Some("test.js".into()),
1134 path: Some(path!("/project/src/test.js").into()),
1135 source_reference: None,
1136 presentation_hint: None,
1137 origin: None,
1138 sources: None,
1139 adapter_data: None,
1140 checksums: None,
1141 }),
1142 line: 1,
1143 column: 1,
1144 end_line: None,
1145 end_column: None,
1146 can_restart: None,
1147 instruction_pointer_reference: None,
1148 module_id: None,
1149 presentation_hint: None,
1150 }],
1151 total_frames: None,
1152 };
1153
1154 let stopped_event = dap::StoppedEvent {
1155 reason: dap::StoppedEventReason::Pause,
1156 description: None,
1157 thread_id: Some(1),
1158 preserve_focus_hint: None,
1159 text: None,
1160 all_threads_stopped: None,
1161 hit_breakpoint_ids: None,
1162 };
1163
1164 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1165 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1166 let adapter_name = session.update(cx, |session, _| session.adapter());
1167
1168 client.on_request::<Threads, _>({
1169 let threads_response = threads_response.clone();
1170 move |_, _| Ok(threads_response.clone())
1171 });
1172
1173 client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
1174
1175 client.on_request::<StackTrace, _>({
1176 let stack_trace_response = stack_trace_response.clone();
1177 move |_, _| Ok(stack_trace_response.clone())
1178 });
1179
1180 client
1181 .fake_event(dap::messages::Events::Stopped(stopped_event.clone()))
1182 .await;
1183
1184 cx.run_until_parked();
1185
1186 let stack_frame_list =
1187 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
1188 debug_panel_item
1189 .running_state()
1190 .update(cx, |state, _| state.stack_frame_list().clone())
1191 });
1192
1193 stack_frame_list.update(cx, |stack_frame_list, _cx| {
1194 assert_eq!(
1195 stack_frame_list.list_filter(),
1196 StackFrameFilter::All,
1197 "Initial filter should be All"
1198 );
1199 });
1200
1201 stack_frame_list.update(cx, |stack_frame_list, cx| {
1202 stack_frame_list
1203 .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1204 assert_eq!(
1205 stack_frame_list.list_filter(),
1206 StackFrameFilter::OnlyUserFrames,
1207 "Filter should be OnlyUserFrames after toggle"
1208 );
1209 });
1210
1211 cx.run_until_parked();
1212
1213 let workspace_id = workspace
1214 .update(cx, |workspace, _window, _cx| workspace.database_id())
1215 .ok()
1216 .flatten()
1217 .expect("workspace id has to be some for this test to work properly");
1218
1219 let key = stack_frame_filter_key(&adapter_name, workspace_id);
1220 let stored_value = KEY_VALUE_STORE.read_kvp(&key).unwrap();
1221 assert_eq!(
1222 stored_value,
1223 Some(StackFrameFilter::OnlyUserFrames.into()),
1224 "Filter should be persisted in KVP store with key: {}",
1225 key
1226 );
1227
1228 client
1229 .fake_event(dap::messages::Events::Terminated(None))
1230 .await;
1231 cx.run_until_parked();
1232
1233 let session2 = start_debug_session(&workspace, cx, |_| {}).unwrap();
1234 let client2 = session2.update(cx, |session, _| session.adapter_client().unwrap());
1235
1236 client2.on_request::<Threads, _>({
1237 let threads_response = threads_response.clone();
1238 move |_, _| Ok(threads_response.clone())
1239 });
1240
1241 client2.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
1242
1243 client2.on_request::<StackTrace, _>({
1244 let stack_trace_response = stack_trace_response.clone();
1245 move |_, _| Ok(stack_trace_response.clone())
1246 });
1247
1248 client2
1249 .fake_event(dap::messages::Events::Stopped(stopped_event.clone()))
1250 .await;
1251
1252 cx.run_until_parked();
1253
1254 let stack_frame_list2 =
1255 active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
1256 debug_panel_item
1257 .running_state()
1258 .update(cx, |state, _| state.stack_frame_list().clone())
1259 });
1260
1261 stack_frame_list2.update(cx, |stack_frame_list, _cx| {
1262 assert_eq!(
1263 stack_frame_list.list_filter(),
1264 StackFrameFilter::OnlyUserFrames,
1265 "Filter should be restored from KVP store in new session"
1266 );
1267 });
1268}