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