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, rel_path::rel_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            assert_eq!(rel_path("src/test.js"), project_path.path.as_ref());
 335            assert_eq!(test_file_content, editors[0].read(cx).text(cx));
 336            assert_eq!(
 337                vec![2..3],
 338                editors[0].update(cx, |editor, cx| {
 339                    let snapshot = editor.snapshot(window, cx);
 340
 341                    editor
 342                        .highlighted_rows::<editor::ActiveDebugLine>()
 343                        .map(|(range, _)| {
 344                            let start = range.start.to_point(&snapshot.buffer_snapshot);
 345                            let end = range.end.to_point(&snapshot.buffer_snapshot);
 346                            start.row..end.row
 347                        })
 348                        .collect::<Vec<_>>()
 349                })
 350            );
 351        })
 352        .unwrap();
 353
 354    let stack_frame_list = workspace
 355        .update(cx, |workspace, _window, cx| {
 356            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 357            let active_debug_panel_item = debug_panel
 358                .update(cx, |this, _| this.active_session())
 359                .unwrap();
 360
 361            active_debug_panel_item
 362                .read(cx)
 363                .running_state()
 364                .read(cx)
 365                .stack_frame_list()
 366                .clone()
 367        })
 368        .unwrap();
 369
 370    stack_frame_list.update(cx, |stack_frame_list, cx| {
 371        assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
 372        assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
 373    });
 374
 375    // select second stack frame
 376    stack_frame_list
 377        .update_in(cx, |stack_frame_list, window, cx| {
 378            stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
 379        })
 380        .await
 381        .unwrap();
 382
 383    cx.run_until_parked();
 384
 385    stack_frame_list.update(cx, |stack_frame_list, cx| {
 386        assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
 387        assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
 388    });
 389
 390    let _ = workspace.update(cx, |workspace, window, cx| {
 391        let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
 392        assert_eq!(1, editors.len());
 393
 394        let project_path = editors[0]
 395            .update(cx, |editor, cx| editor.project_path(cx))
 396            .unwrap();
 397        assert_eq!(rel_path("src/module.js"), project_path.path.as_ref());
 398        assert_eq!(module_file_content, editors[0].read(cx).text(cx));
 399        assert_eq!(
 400            vec![0..1],
 401            editors[0].update(cx, |editor, cx| {
 402                let snapshot = editor.snapshot(window, cx);
 403
 404                editor
 405                    .highlighted_rows::<editor::ActiveDebugLine>()
 406                    .map(|(range, _)| {
 407                        let start = range.start.to_point(&snapshot.buffer_snapshot);
 408                        let end = range.end.to_point(&snapshot.buffer_snapshot);
 409                        start.row..end.row
 410                    })
 411                    .collect::<Vec<_>>()
 412            })
 413        );
 414    });
 415}
 416
 417#[gpui::test]
 418async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
 419    init_test(cx);
 420
 421    let fs = FakeFs::new(executor.clone());
 422
 423    let test_file_content = r#"
 424        import { SOME_VALUE } './module.js';
 425
 426        console.log(SOME_VALUE);
 427    "#
 428    .unindent();
 429
 430    let module_file_content = r#"
 431        export SOME_VALUE = 'some value';
 432    "#
 433    .unindent();
 434
 435    fs.insert_tree(
 436        path!("/project"),
 437        json!({
 438           "src": {
 439               "test.js": test_file_content,
 440               "module.js": module_file_content,
 441           }
 442        }),
 443    )
 444    .await;
 445
 446    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 447    let workspace = init_test_workspace(&project, cx).await;
 448    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 449
 450    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 451    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 452
 453    client.on_request::<Threads, _>(move |_, _| {
 454        Ok(dap::ThreadsResponse {
 455            threads: vec![dap::Thread {
 456                id: 1,
 457                name: "Thread 1".into(),
 458            }],
 459        })
 460    });
 461
 462    client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
 463
 464    let stack_frames = vec![
 465        StackFrame {
 466            id: 1,
 467            name: "Stack Frame 1".into(),
 468            source: Some(dap::Source {
 469                name: Some("test.js".into()),
 470                path: Some(path!("/project/src/test.js").into()),
 471                source_reference: None,
 472                presentation_hint: None,
 473                origin: None,
 474                sources: None,
 475                adapter_data: None,
 476                checksums: None,
 477            }),
 478            line: 3,
 479            column: 1,
 480            end_line: None,
 481            end_column: None,
 482            can_restart: None,
 483            instruction_pointer_reference: None,
 484            module_id: None,
 485            presentation_hint: None,
 486        },
 487        StackFrame {
 488            id: 2,
 489            name: "Stack Frame 2".into(),
 490            source: Some(dap::Source {
 491                name: Some("module.js".into()),
 492                path: Some(path!("/project/src/module.js").into()),
 493                source_reference: None,
 494                presentation_hint: None,
 495                origin: Some("ignored".into()),
 496                sources: None,
 497                adapter_data: None,
 498                checksums: None,
 499            }),
 500            line: 1,
 501            column: 1,
 502            end_line: None,
 503            end_column: None,
 504            can_restart: None,
 505            instruction_pointer_reference: None,
 506            module_id: None,
 507            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
 508        },
 509        StackFrame {
 510            id: 3,
 511            name: "Stack Frame 3".into(),
 512            source: Some(dap::Source {
 513                name: Some("module.js".into()),
 514                path: Some(path!("/project/src/module.js").into()),
 515                source_reference: None,
 516                presentation_hint: None,
 517                origin: Some("ignored".into()),
 518                sources: None,
 519                adapter_data: None,
 520                checksums: None,
 521            }),
 522            line: 1,
 523            column: 1,
 524            end_line: None,
 525            end_column: None,
 526            can_restart: None,
 527            instruction_pointer_reference: None,
 528            module_id: None,
 529            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
 530        },
 531        StackFrame {
 532            id: 4,
 533            name: "Stack Frame 4".into(),
 534            source: Some(dap::Source {
 535                name: Some("module.js".into()),
 536                path: Some(path!("/project/src/module.js").into()),
 537                source_reference: None,
 538                presentation_hint: None,
 539                origin: None,
 540                sources: None,
 541                adapter_data: None,
 542                checksums: None,
 543            }),
 544            line: 1,
 545            column: 1,
 546            end_line: None,
 547            end_column: None,
 548            can_restart: None,
 549            instruction_pointer_reference: None,
 550            module_id: None,
 551            presentation_hint: None,
 552        },
 553        StackFrame {
 554            id: 5,
 555            name: "Stack Frame 5".into(),
 556            source: Some(dap::Source {
 557                name: Some("module.js".into()),
 558                path: Some(path!("/project/src/module.js").into()),
 559                source_reference: None,
 560                presentation_hint: None,
 561                origin: None,
 562                sources: None,
 563                adapter_data: None,
 564                checksums: None,
 565            }),
 566            line: 1,
 567            column: 1,
 568            end_line: None,
 569            end_column: None,
 570            can_restart: None,
 571            instruction_pointer_reference: None,
 572            module_id: None,
 573            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
 574        },
 575        StackFrame {
 576            id: 6,
 577            name: "Stack Frame 6".into(),
 578            source: Some(dap::Source {
 579                name: Some("module.js".into()),
 580                path: Some(path!("/project/src/module.js").into()),
 581                source_reference: None,
 582                presentation_hint: None,
 583                origin: None,
 584                sources: None,
 585                adapter_data: None,
 586                checksums: None,
 587            }),
 588            line: 1,
 589            column: 1,
 590            end_line: None,
 591            end_column: None,
 592            can_restart: None,
 593            instruction_pointer_reference: None,
 594            module_id: None,
 595            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
 596        },
 597        StackFrame {
 598            id: 7,
 599            name: "Stack Frame 7".into(),
 600            source: Some(dap::Source {
 601                name: Some("module.js".into()),
 602                path: Some(path!("/project/src/module.js").into()),
 603                source_reference: None,
 604                presentation_hint: None,
 605                origin: None,
 606                sources: None,
 607                adapter_data: None,
 608                checksums: None,
 609            }),
 610            line: 1,
 611            column: 1,
 612            end_line: None,
 613            end_column: None,
 614            can_restart: None,
 615            instruction_pointer_reference: None,
 616            module_id: None,
 617            presentation_hint: None,
 618        },
 619    ];
 620
 621    client.on_request::<StackTrace, _>({
 622        let stack_frames = Arc::new(stack_frames.clone());
 623        move |_, args| {
 624            assert_eq!(1, args.thread_id);
 625
 626            Ok(dap::StackTraceResponse {
 627                stack_frames: (*stack_frames).clone(),
 628                total_frames: None,
 629            })
 630        }
 631    });
 632
 633    client
 634        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 635            reason: dap::StoppedEventReason::Pause,
 636            description: None,
 637            thread_id: Some(1),
 638            preserve_focus_hint: None,
 639            text: None,
 640            all_threads_stopped: None,
 641            hit_breakpoint_ids: None,
 642        }))
 643        .await;
 644
 645    cx.run_until_parked();
 646
 647    // trigger threads to load
 648    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
 649        session.running_state().update(cx, |running_state, cx| {
 650            running_state
 651                .session()
 652                .update(cx, |session, cx| session.threads(cx));
 653        });
 654    });
 655
 656    cx.run_until_parked();
 657
 658    // select first thread
 659    active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
 660        session.running_state().update(cx, |running_state, cx| {
 661            running_state.select_current_thread(
 662                &running_state
 663                    .session()
 664                    .update(cx, |session, cx| session.threads(cx)),
 665                window,
 666                cx,
 667            );
 668        });
 669    });
 670
 671    cx.run_until_parked();
 672
 673    // trigger stack frames to loaded
 674    active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
 675        let stack_frame_list = debug_panel_item
 676            .running_state()
 677            .update(cx, |state, _| state.stack_frame_list().clone());
 678
 679        stack_frame_list.update(cx, |stack_frame_list, cx| {
 680            stack_frame_list.dap_stack_frames(cx);
 681        });
 682    });
 683
 684    cx.run_until_parked();
 685
 686    active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
 687        let stack_frame_list = debug_panel_item
 688            .running_state()
 689            .update(cx, |state, _| state.stack_frame_list().clone());
 690
 691        stack_frame_list.update(cx, |stack_frame_list, cx| {
 692            stack_frame_list.build_entries(true, window, cx);
 693
 694            assert_eq!(
 695                &vec![
 696                    StackFrameEntry::Normal(stack_frames[0].clone()),
 697                    StackFrameEntry::Collapsed(vec![
 698                        stack_frames[1].clone(),
 699                        stack_frames[2].clone()
 700                    ]),
 701                    StackFrameEntry::Normal(stack_frames[3].clone()),
 702                    StackFrameEntry::Collapsed(vec![
 703                        stack_frames[4].clone(),
 704                        stack_frames[5].clone()
 705                    ]),
 706                    StackFrameEntry::Normal(stack_frames[6].clone()),
 707                ],
 708                stack_frame_list.entries()
 709            );
 710
 711            stack_frame_list.expand_collapsed_entry(1, cx);
 712
 713            assert_eq!(
 714                &vec![
 715                    StackFrameEntry::Normal(stack_frames[0].clone()),
 716                    StackFrameEntry::Normal(stack_frames[1].clone()),
 717                    StackFrameEntry::Normal(stack_frames[2].clone()),
 718                    StackFrameEntry::Normal(stack_frames[3].clone()),
 719                    StackFrameEntry::Collapsed(vec![
 720                        stack_frames[4].clone(),
 721                        stack_frames[5].clone()
 722                    ]),
 723                    StackFrameEntry::Normal(stack_frames[6].clone()),
 724                ],
 725                stack_frame_list.entries()
 726            );
 727
 728            stack_frame_list.expand_collapsed_entry(4, cx);
 729
 730            assert_eq!(
 731                &vec![
 732                    StackFrameEntry::Normal(stack_frames[0].clone()),
 733                    StackFrameEntry::Normal(stack_frames[1].clone()),
 734                    StackFrameEntry::Normal(stack_frames[2].clone()),
 735                    StackFrameEntry::Normal(stack_frames[3].clone()),
 736                    StackFrameEntry::Normal(stack_frames[4].clone()),
 737                    StackFrameEntry::Normal(stack_frames[5].clone()),
 738                    StackFrameEntry::Normal(stack_frames[6].clone()),
 739                ],
 740                stack_frame_list.entries()
 741            );
 742        });
 743    });
 744}
 745
 746#[gpui::test]
 747async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) {
 748    init_test(cx);
 749
 750    let fs = FakeFs::new(executor.clone());
 751
 752    let test_file_content = r#"
 753        function main() {
 754            doSomething();
 755        }
 756
 757        function doSomething() {
 758            console.log('doing something');
 759        }
 760    "#
 761    .unindent();
 762
 763    fs.insert_tree(
 764        path!("/project"),
 765        json!({
 766           "src": {
 767               "test.js": test_file_content,
 768           }
 769        }),
 770    )
 771    .await;
 772
 773    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 774    let workspace = init_test_workspace(&project, cx).await;
 775    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 776
 777    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 778    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 779
 780    client.on_request::<Threads, _>(move |_, _| {
 781        Ok(dap::ThreadsResponse {
 782            threads: vec![dap::Thread {
 783                id: 1,
 784                name: "Thread 1".into(),
 785            }],
 786        })
 787    });
 788
 789    client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
 790
 791    let stack_frames = vec![
 792        StackFrame {
 793            id: 1,
 794            name: "main".into(),
 795            source: Some(dap::Source {
 796                name: Some("test.js".into()),
 797                path: Some(path!("/project/src/test.js").into()),
 798                source_reference: None,
 799                presentation_hint: None,
 800                origin: None,
 801                sources: None,
 802                adapter_data: None,
 803                checksums: None,
 804            }),
 805            line: 2,
 806            column: 1,
 807            end_line: None,
 808            end_column: None,
 809            can_restart: None,
 810            instruction_pointer_reference: None,
 811            module_id: None,
 812            presentation_hint: None,
 813        },
 814        StackFrame {
 815            id: 2,
 816            name: "node:internal/modules/cjs/loader".into(),
 817            source: Some(dap::Source {
 818                name: Some("loader.js".into()),
 819                path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()),
 820                source_reference: None,
 821                presentation_hint: None,
 822                origin: None,
 823                sources: None,
 824                adapter_data: None,
 825                checksums: None,
 826            }),
 827            line: 100,
 828            column: 1,
 829            end_line: None,
 830            end_column: None,
 831            can_restart: None,
 832            instruction_pointer_reference: None,
 833            module_id: None,
 834            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
 835        },
 836        StackFrame {
 837            id: 3,
 838            name: "node:internal/modules/run_main".into(),
 839            source: Some(dap::Source {
 840                name: Some("run_main.js".into()),
 841                path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()),
 842                source_reference: None,
 843                presentation_hint: None,
 844                origin: None,
 845                sources: None,
 846                adapter_data: None,
 847                checksums: None,
 848            }),
 849            line: 50,
 850            column: 1,
 851            end_line: None,
 852            end_column: None,
 853            can_restart: None,
 854            instruction_pointer_reference: None,
 855            module_id: None,
 856            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
 857        },
 858        StackFrame {
 859            id: 4,
 860            name: "node:internal/modules/run_main2".into(),
 861            source: Some(dap::Source {
 862                name: Some("run_main.js".into()),
 863                path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()),
 864                source_reference: None,
 865                presentation_hint: None,
 866                origin: None,
 867                sources: None,
 868                adapter_data: None,
 869                checksums: None,
 870            }),
 871            line: 50,
 872            column: 1,
 873            end_line: None,
 874            end_column: None,
 875            can_restart: None,
 876            instruction_pointer_reference: None,
 877            module_id: None,
 878            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
 879        },
 880        StackFrame {
 881            id: 5,
 882            name: "doSomething".into(),
 883            source: Some(dap::Source {
 884                name: Some("test.js".into()),
 885                path: Some(path!("/project/src/test.js").into()),
 886                source_reference: None,
 887                presentation_hint: None,
 888                origin: None,
 889                sources: None,
 890                adapter_data: None,
 891                checksums: None,
 892            }),
 893            line: 3,
 894            column: 1,
 895            end_line: None,
 896            end_column: None,
 897            can_restart: None,
 898            instruction_pointer_reference: None,
 899            module_id: None,
 900            presentation_hint: None,
 901        },
 902    ];
 903
 904    // Store a copy for assertions
 905    let stack_frames_for_assertions = stack_frames.clone();
 906
 907    client.on_request::<StackTrace, _>({
 908        let stack_frames = Arc::new(stack_frames.clone());
 909        move |_, args| {
 910            assert_eq!(1, args.thread_id);
 911
 912            Ok(dap::StackTraceResponse {
 913                stack_frames: (*stack_frames).clone(),
 914                total_frames: None,
 915            })
 916        }
 917    });
 918
 919    client
 920        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 921            reason: dap::StoppedEventReason::Pause,
 922            description: None,
 923            thread_id: Some(1),
 924            preserve_focus_hint: None,
 925            text: None,
 926            all_threads_stopped: None,
 927            hit_breakpoint_ids: None,
 928        }))
 929        .await;
 930
 931    cx.run_until_parked();
 932
 933    // trigger threads to load
 934    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
 935        session.running_state().update(cx, |running_state, cx| {
 936            running_state
 937                .session()
 938                .update(cx, |session, cx| session.threads(cx));
 939        });
 940    });
 941
 942    cx.run_until_parked();
 943
 944    // select first thread
 945    active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
 946        session.running_state().update(cx, |running_state, cx| {
 947            running_state.select_current_thread(
 948                &running_state
 949                    .session()
 950                    .update(cx, |session, cx| session.threads(cx)),
 951                window,
 952                cx,
 953            );
 954        });
 955    });
 956
 957    cx.run_until_parked();
 958
 959    // trigger stack frames to load
 960    active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
 961        let stack_frame_list = debug_panel_item
 962            .running_state()
 963            .update(cx, |state, _| state.stack_frame_list().clone());
 964
 965        stack_frame_list.update(cx, |stack_frame_list, cx| {
 966            stack_frame_list.dap_stack_frames(cx);
 967        });
 968    });
 969
 970    cx.run_until_parked();
 971
 972    let stack_frame_list =
 973        active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
 974            let stack_frame_list = debug_panel_item
 975                .running_state()
 976                .update(cx, |state, _| state.stack_frame_list().clone());
 977
 978            stack_frame_list.update(cx, |stack_frame_list, cx| {
 979                stack_frame_list.build_entries(true, window, cx);
 980
 981                // Verify we have the expected collapsed structure
 982                assert_eq!(
 983                    stack_frame_list.entries(),
 984                    &vec![
 985                        StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
 986                        StackFrameEntry::Collapsed(vec![
 987                            stack_frames_for_assertions[1].clone(),
 988                            stack_frames_for_assertions[2].clone(),
 989                            stack_frames_for_assertions[3].clone()
 990                        ]),
 991                        StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
 992                    ]
 993                );
 994            });
 995
 996            stack_frame_list
 997        });
 998
 999    stack_frame_list.update(cx, |stack_frame_list, cx| {
1000        let all_frames = stack_frame_list.flatten_entries(true, false);
1001        assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially");
1002
1003        stack_frame_list
1004            .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1005        assert_eq!(
1006            stack_frame_list.list_filter(),
1007            StackFrameFilter::OnlyUserFrames
1008        );
1009    });
1010
1011    stack_frame_list.update(cx, |stack_frame_list, cx| {
1012        let user_frames = stack_frame_list.dap_stack_frames(cx);
1013        assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
1014        assert_eq!(user_frames[0].name, "main");
1015        assert_eq!(user_frames[1].name, "doSomething");
1016
1017        // Toggle back to all frames
1018        stack_frame_list
1019            .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1020        assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
1021    });
1022
1023    stack_frame_list.update(cx, |stack_frame_list, cx| {
1024        let all_frames_again = stack_frame_list.flatten_entries(true, false);
1025        assert_eq!(
1026            all_frames_again.len(),
1027            5,
1028            "Should see all 5 frames after toggling back"
1029        );
1030
1031        // Test 3: Verify collapsed entries stay expanded
1032        stack_frame_list.expand_collapsed_entry(1, cx);
1033        assert_eq!(
1034            stack_frame_list.entries(),
1035            &vec![
1036                StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1037                StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1038                StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1039                StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1040                StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
1041            ]
1042        );
1043
1044        stack_frame_list
1045            .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1046        assert_eq!(
1047            stack_frame_list.list_filter(),
1048            StackFrameFilter::OnlyUserFrames
1049        );
1050    });
1051
1052    stack_frame_list.update(cx, |stack_frame_list, cx| {
1053        stack_frame_list
1054            .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
1055        assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
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!(
1062            stack_frame_list.list_filter(),
1063            StackFrameFilter::OnlyUserFrames
1064        );
1065
1066        assert_eq!(
1067            stack_frame_list.dap_stack_frames(cx).as_slice(),
1068            &[
1069                stack_frames_for_assertions[0].clone(),
1070                stack_frames_for_assertions[4].clone()
1071            ]
1072        );
1073
1074        // Verify entries remain expanded
1075        assert_eq!(
1076            stack_frame_list.entries(),
1077            &vec![
1078                StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
1079                StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
1080                StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
1081                StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
1082                StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
1083            ],
1084            "Expanded entries should remain expanded after toggling filter"
1085        );
1086    });
1087}