Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:34:21.623Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

stack_frame_list.rs

1274 lines · 41.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.