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