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