stack_frame_list.rs

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