stack_frame_list.rs

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