stack_frame_list.rs

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