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