debugger_panel.rs

   1use crate::*;
   2use dap::{
   3    ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint, StartDebuggingRequestArguments,
   4    StartDebuggingRequestArgumentsRequest,
   5    client::SessionId,
   6    requests::{
   7        Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
   8        StartDebugging, StepBack, StepIn, StepOut, Threads,
   9    },
  10};
  11use editor::{
  12    Editor, EditorMode, MultiBuffer,
  13    actions::{self},
  14};
  15use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
  16use project::{
  17    FakeFs, Project,
  18    debugger::session::{ThreadId, ThreadStatus},
  19};
  20use serde_json::json;
  21use std::{
  22    path::Path,
  23    sync::{
  24        Arc,
  25        atomic::{AtomicBool, Ordering},
  26    },
  27};
  28use task::LaunchConfig;
  29use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
  30use tests::{active_debug_session_panel, init_test, init_test_workspace};
  31use util::path;
  32use workspace::{Item, dock::Panel};
  33
  34#[gpui::test]
  35async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
  36    init_test(cx);
  37
  38    let fs = FakeFs::new(executor.clone());
  39
  40    fs.insert_tree(
  41        "/project",
  42        json!({
  43            "main.rs": "First line\nSecond line\nThird line\nFourth line",
  44        }),
  45    )
  46    .await;
  47
  48    let project = Project::test(fs, ["/project".as_ref()], cx).await;
  49    let workspace = init_test_workspace(&project, cx).await;
  50    let cx = &mut VisualTestContext::from_window(*workspace, cx);
  51
  52    let task = project.update(cx, |project, cx| {
  53        project.fake_debug_session(
  54            dap::DebugRequestType::Launch(LaunchConfig::default()),
  55            None,
  56            false,
  57            cx,
  58        )
  59    });
  60
  61    let session = task.await.unwrap();
  62    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
  63
  64    client
  65        .on_request::<Threads, _>(move |_, _| {
  66            Ok(dap::ThreadsResponse {
  67                threads: vec![dap::Thread {
  68                    id: 1,
  69                    name: "Thread 1".into(),
  70                }],
  71            })
  72        })
  73        .await;
  74
  75    client
  76        .on_request::<StackTrace, _>(move |_, _| {
  77            Ok(dap::StackTraceResponse {
  78                stack_frames: Vec::default(),
  79                total_frames: None,
  80            })
  81        })
  82        .await;
  83
  84    cx.run_until_parked();
  85
  86    // assert we have a debug panel item before the session has stopped
  87    workspace
  88        .update(cx, |workspace, _window, cx| {
  89            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
  90            let active_session =
  91                debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
  92
  93            let running_state = active_session.update(cx, |active_session, _| {
  94                active_session
  95                    .mode()
  96                    .as_running()
  97                    .expect("Session should be running by this point")
  98                    .clone()
  99            });
 100
 101            debug_panel.update(cx, |this, cx| {
 102                assert!(this.active_session().is_some());
 103                assert!(running_state.read(cx).selected_thread_id().is_none());
 104            });
 105        })
 106        .unwrap();
 107
 108    client
 109        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 110            reason: dap::StoppedEventReason::Pause,
 111            description: None,
 112            thread_id: Some(1),
 113            preserve_focus_hint: None,
 114            text: None,
 115            all_threads_stopped: None,
 116            hit_breakpoint_ids: None,
 117        }))
 118        .await;
 119
 120    cx.run_until_parked();
 121
 122    workspace
 123        .update(cx, |workspace, _window, cx| {
 124            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 125            let active_session = debug_panel
 126                .update(cx, |this, _| this.active_session())
 127                .unwrap();
 128
 129            let running_state = active_session.update(cx, |active_session, _| {
 130                active_session
 131                    .mode()
 132                    .as_running()
 133                    .expect("Session should be running by this point")
 134                    .clone()
 135            });
 136
 137            assert_eq!(client.id(), running_state.read(cx).session_id());
 138            assert_eq!(
 139                ThreadId(1),
 140                running_state.read(cx).selected_thread_id().unwrap()
 141            );
 142        })
 143        .unwrap();
 144
 145    let shutdown_session = project.update(cx, |project, cx| {
 146        project.dap_store().update(cx, |dap_store, cx| {
 147            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 148        })
 149    });
 150
 151    shutdown_session.await.unwrap();
 152
 153    // assert we still have a debug panel item after the client shutdown
 154    workspace
 155        .update(cx, |workspace, _window, cx| {
 156            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 157
 158            let active_session = debug_panel
 159                .update(cx, |this, _| this.active_session())
 160                .unwrap();
 161
 162            let running_state = active_session.update(cx, |active_session, _| {
 163                active_session
 164                    .mode()
 165                    .as_running()
 166                    .expect("Session should be running by this point")
 167                    .clone()
 168            });
 169
 170            debug_panel.update(cx, |this, cx| {
 171                assert!(this.active_session().is_some());
 172                assert_eq!(
 173                    ThreadId(1),
 174                    running_state.read(cx).selected_thread_id().unwrap()
 175                );
 176            });
 177        })
 178        .unwrap();
 179}
 180
 181#[gpui::test]
 182async fn test_we_can_only_have_one_panel_per_debug_session(
 183    executor: BackgroundExecutor,
 184    cx: &mut TestAppContext,
 185) {
 186    init_test(cx);
 187
 188    let fs = FakeFs::new(executor.clone());
 189
 190    fs.insert_tree(
 191        "/project",
 192        json!({
 193            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 194        }),
 195    )
 196    .await;
 197
 198    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 199    let workspace = init_test_workspace(&project, cx).await;
 200    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 201
 202    let task = project.update(cx, |project, cx| {
 203        project.fake_debug_session(
 204            dap::DebugRequestType::Launch(LaunchConfig::default()),
 205            None,
 206            false,
 207            cx,
 208        )
 209    });
 210
 211    let session = task.await.unwrap();
 212    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 213
 214    client
 215        .on_request::<Threads, _>(move |_, _| {
 216            Ok(dap::ThreadsResponse {
 217                threads: vec![dap::Thread {
 218                    id: 1,
 219                    name: "Thread 1".into(),
 220                }],
 221            })
 222        })
 223        .await;
 224
 225    client
 226        .on_request::<StackTrace, _>(move |_, _| {
 227            Ok(dap::StackTraceResponse {
 228                stack_frames: Vec::default(),
 229                total_frames: None,
 230            })
 231        })
 232        .await;
 233
 234    cx.run_until_parked();
 235
 236    // assert we have a debug panel item before the session has stopped
 237    workspace
 238        .update(cx, |workspace, _window, cx| {
 239            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 240
 241            debug_panel.update(cx, |this, _| {
 242                assert!(this.active_session().is_some());
 243            });
 244        })
 245        .unwrap();
 246
 247    client
 248        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 249            reason: dap::StoppedEventReason::Pause,
 250            description: None,
 251            thread_id: Some(1),
 252            preserve_focus_hint: None,
 253            text: None,
 254            all_threads_stopped: None,
 255            hit_breakpoint_ids: None,
 256        }))
 257        .await;
 258
 259    cx.run_until_parked();
 260
 261    // assert we added a debug panel item
 262    workspace
 263        .update(cx, |workspace, _window, cx| {
 264            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 265            let active_session = debug_panel
 266                .update(cx, |this, _| this.active_session())
 267                .unwrap();
 268
 269            let running_state = active_session.update(cx, |active_session, _| {
 270                active_session
 271                    .mode()
 272                    .as_running()
 273                    .expect("Session should be running by this point")
 274                    .clone()
 275            });
 276
 277            assert_eq!(client.id(), active_session.read(cx).session_id(cx));
 278            assert_eq!(
 279                ThreadId(1),
 280                running_state.read(cx).selected_thread_id().unwrap()
 281            );
 282        })
 283        .unwrap();
 284
 285    client
 286        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 287            reason: dap::StoppedEventReason::Pause,
 288            description: None,
 289            thread_id: Some(2),
 290            preserve_focus_hint: None,
 291            text: None,
 292            all_threads_stopped: None,
 293            hit_breakpoint_ids: None,
 294        }))
 295        .await;
 296
 297    cx.run_until_parked();
 298
 299    workspace
 300        .update(cx, |workspace, _window, cx| {
 301            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 302            let active_session = debug_panel
 303                .update(cx, |this, _| this.active_session())
 304                .unwrap();
 305
 306            let running_state = active_session.update(cx, |active_session, _| {
 307                active_session
 308                    .mode()
 309                    .as_running()
 310                    .expect("Session should be running by this point")
 311                    .clone()
 312            });
 313
 314            assert_eq!(client.id(), active_session.read(cx).session_id(cx));
 315            assert_eq!(
 316                ThreadId(1),
 317                running_state.read(cx).selected_thread_id().unwrap()
 318            );
 319        })
 320        .unwrap();
 321
 322    let shutdown_session = project.update(cx, |project, cx| {
 323        project.dap_store().update(cx, |dap_store, cx| {
 324            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 325        })
 326    });
 327
 328    shutdown_session.await.unwrap();
 329
 330    // assert we still have a debug panel item after the client shutdown
 331    workspace
 332        .update(cx, |workspace, _window, cx| {
 333            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 334            let active_session = debug_panel
 335                .update(cx, |this, _| this.active_session())
 336                .unwrap();
 337
 338            let running_state = active_session.update(cx, |active_session, _| {
 339                active_session
 340                    .mode()
 341                    .as_running()
 342                    .expect("Session should be running by this point")
 343                    .clone()
 344            });
 345
 346            debug_panel.update(cx, |this, cx| {
 347                assert!(this.active_session().is_some());
 348                assert_eq!(
 349                    ThreadId(1),
 350                    running_state.read(cx).selected_thread_id().unwrap()
 351                );
 352            });
 353        })
 354        .unwrap();
 355}
 356
 357#[gpui::test]
 358async fn test_handle_successful_run_in_terminal_reverse_request(
 359    executor: BackgroundExecutor,
 360    cx: &mut TestAppContext,
 361) {
 362    init_test(cx);
 363
 364    let send_response = Arc::new(AtomicBool::new(false));
 365
 366    let fs = FakeFs::new(executor.clone());
 367
 368    fs.insert_tree(
 369        "/project",
 370        json!({
 371            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 372        }),
 373    )
 374    .await;
 375
 376    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 377    let workspace = init_test_workspace(&project, cx).await;
 378    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 379
 380    let task = project.update(cx, |project, cx| {
 381        project.fake_debug_session(
 382            dap::DebugRequestType::Launch(LaunchConfig::default()),
 383            None,
 384            false,
 385            cx,
 386        )
 387    });
 388
 389    let session = task.await.unwrap();
 390    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 391
 392    client
 393        .on_response::<RunInTerminal, _>({
 394            let send_response = send_response.clone();
 395            move |response| {
 396                send_response.store(true, Ordering::SeqCst);
 397
 398                assert!(response.success);
 399                assert!(response.body.is_some());
 400            }
 401        })
 402        .await;
 403
 404    client
 405        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 406            kind: None,
 407            title: None,
 408            cwd: std::env::temp_dir().to_string_lossy().to_string(),
 409            args: vec![],
 410            env: None,
 411            args_can_be_interpreted_by_shell: None,
 412        })
 413        .await;
 414
 415    cx.run_until_parked();
 416
 417    assert!(
 418        send_response.load(std::sync::atomic::Ordering::SeqCst),
 419        "Expected to receive response from reverse request"
 420    );
 421
 422    workspace
 423        .update(cx, |workspace, _window, cx| {
 424            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
 425
 426            let panel = terminal_panel.read(cx).pane().unwrap().read(cx);
 427
 428            assert_eq!(1, panel.items_len());
 429            assert!(
 430                panel
 431                    .active_item()
 432                    .unwrap()
 433                    .downcast::<TerminalView>()
 434                    .unwrap()
 435                    .read(cx)
 436                    .terminal()
 437                    .read(cx)
 438                    .debug_terminal()
 439            );
 440        })
 441        .unwrap();
 442
 443    let shutdown_session = project.update(cx, |project, cx| {
 444        project.dap_store().update(cx, |dap_store, cx| {
 445            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 446        })
 447    });
 448
 449    shutdown_session.await.unwrap();
 450}
 451
 452// // covers that we always send a response back, if something when wrong,
 453// // while spawning the terminal
 454#[gpui::test]
 455async fn test_handle_error_run_in_terminal_reverse_request(
 456    executor: BackgroundExecutor,
 457    cx: &mut TestAppContext,
 458) {
 459    init_test(cx);
 460
 461    let send_response = Arc::new(AtomicBool::new(false));
 462
 463    let fs = FakeFs::new(executor.clone());
 464
 465    fs.insert_tree(
 466        "/project",
 467        json!({
 468            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 469        }),
 470    )
 471    .await;
 472
 473    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 474    let workspace = init_test_workspace(&project, cx).await;
 475    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 476
 477    let task = project.update(cx, |project, cx| {
 478        project.fake_debug_session(
 479            dap::DebugRequestType::Launch(LaunchConfig::default()),
 480            None,
 481            false,
 482            cx,
 483        )
 484    });
 485
 486    let session = task.await.unwrap();
 487    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 488
 489    client
 490        .on_response::<RunInTerminal, _>({
 491            let send_response = send_response.clone();
 492            move |response| {
 493                send_response.store(true, Ordering::SeqCst);
 494
 495                assert!(!response.success);
 496                assert!(response.body.is_some());
 497            }
 498        })
 499        .await;
 500
 501    client
 502        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 503            kind: None,
 504            title: None,
 505            cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail
 506            args: vec![],
 507            env: None,
 508            args_can_be_interpreted_by_shell: None,
 509        })
 510        .await;
 511
 512    cx.run_until_parked();
 513
 514    assert!(
 515        send_response.load(std::sync::atomic::Ordering::SeqCst),
 516        "Expected to receive response from reverse request"
 517    );
 518
 519    workspace
 520        .update(cx, |workspace, _window, cx| {
 521            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
 522
 523            assert_eq!(
 524                0,
 525                terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
 526            );
 527        })
 528        .unwrap();
 529
 530    let shutdown_session = project.update(cx, |project, cx| {
 531        project.dap_store().update(cx, |dap_store, cx| {
 532            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 533        })
 534    });
 535
 536    shutdown_session.await.unwrap();
 537}
 538
 539#[gpui::test]
 540async fn test_handle_start_debugging_reverse_request(
 541    executor: BackgroundExecutor,
 542    cx: &mut TestAppContext,
 543) {
 544    init_test(cx);
 545
 546    let send_response = Arc::new(AtomicBool::new(false));
 547
 548    let fs = FakeFs::new(executor.clone());
 549
 550    fs.insert_tree(
 551        "/project",
 552        json!({
 553            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 554        }),
 555    )
 556    .await;
 557
 558    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 559    let workspace = init_test_workspace(&project, cx).await;
 560    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 561
 562    let task = project.update(cx, |project, cx| {
 563        project.fake_debug_session(
 564            dap::DebugRequestType::Launch(LaunchConfig::default()),
 565            None,
 566            false,
 567            cx,
 568        )
 569    });
 570
 571    let session = task.await.unwrap();
 572    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 573
 574    client
 575        .on_request::<dap::requests::Threads, _>(move |_, _| {
 576            Ok(dap::ThreadsResponse {
 577                threads: vec![dap::Thread {
 578                    id: 1,
 579                    name: "Thread 1".into(),
 580                }],
 581            })
 582        })
 583        .await;
 584
 585    client
 586        .on_response::<StartDebugging, _>({
 587            let send_response = send_response.clone();
 588            move |response| {
 589                send_response.store(true, Ordering::SeqCst);
 590
 591                assert!(response.success);
 592                assert!(response.body.is_some());
 593            }
 594        })
 595        .await;
 596
 597    client
 598        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 599            configuration: json!({}),
 600            request: StartDebuggingRequestArgumentsRequest::Launch,
 601        })
 602        .await;
 603
 604    cx.run_until_parked();
 605
 606    let child_session = project.update(cx, |project, cx| {
 607        project
 608            .dap_store()
 609            .read(cx)
 610            .session_by_id(SessionId(1))
 611            .unwrap()
 612    });
 613    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
 614
 615    child_client
 616        .on_request::<dap::requests::Threads, _>(move |_, _| {
 617            Ok(dap::ThreadsResponse {
 618                threads: vec![dap::Thread {
 619                    id: 1,
 620                    name: "Thread 1".into(),
 621                }],
 622            })
 623        })
 624        .await;
 625
 626    child_client
 627        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 628        .await;
 629
 630    child_client
 631        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 632            reason: dap::StoppedEventReason::Pause,
 633            description: None,
 634            thread_id: Some(2),
 635            preserve_focus_hint: None,
 636            text: None,
 637            all_threads_stopped: None,
 638            hit_breakpoint_ids: None,
 639        }))
 640        .await;
 641
 642    cx.run_until_parked();
 643
 644    assert!(
 645        send_response.load(std::sync::atomic::Ordering::SeqCst),
 646        "Expected to receive response from reverse request"
 647    );
 648
 649    let shutdown_session = project.update(cx, |project, cx| {
 650        project.dap_store().update(cx, |dap_store, cx| {
 651            dap_store.shutdown_session(child_session.read(cx).session_id(), cx)
 652        })
 653    });
 654
 655    shutdown_session.await.unwrap();
 656}
 657
 658#[gpui::test]
 659async fn test_shutdown_children_when_parent_session_shutdown(
 660    executor: BackgroundExecutor,
 661    cx: &mut TestAppContext,
 662) {
 663    init_test(cx);
 664
 665    let fs = FakeFs::new(executor.clone());
 666
 667    fs.insert_tree(
 668        "/project",
 669        json!({
 670            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 671        }),
 672    )
 673    .await;
 674
 675    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 676    let dap_store = project.update(cx, |project, _| project.dap_store());
 677    let workspace = init_test_workspace(&project, cx).await;
 678    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 679
 680    let task = project.update(cx, |project, cx| {
 681        project.fake_debug_session(
 682            dap::DebugRequestType::Launch(LaunchConfig::default()),
 683            None,
 684            false,
 685            cx,
 686        )
 687    });
 688
 689    let parent_session = task.await.unwrap();
 690    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 691
 692    client
 693        .on_request::<dap::requests::Threads, _>(move |_, _| {
 694            Ok(dap::ThreadsResponse {
 695                threads: vec![dap::Thread {
 696                    id: 1,
 697                    name: "Thread 1".into(),
 698                }],
 699            })
 700        })
 701        .await;
 702
 703    client.on_response::<StartDebugging, _>(move |_| {}).await;
 704
 705    // start first child session
 706    client
 707        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 708            configuration: json!({}),
 709            request: StartDebuggingRequestArgumentsRequest::Launch,
 710        })
 711        .await;
 712
 713    cx.run_until_parked();
 714
 715    // start second child session
 716    client
 717        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 718            configuration: json!({}),
 719            request: StartDebuggingRequestArgumentsRequest::Launch,
 720        })
 721        .await;
 722
 723    cx.run_until_parked();
 724
 725    // configure first child session
 726    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 727        dap_store.session_by_id(SessionId(1)).unwrap()
 728    });
 729    let first_child_client =
 730        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 731
 732    first_child_client
 733        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 734        .await;
 735
 736    // configure second child session
 737    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 738        dap_store.session_by_id(SessionId(2)).unwrap()
 739    });
 740    let second_child_client =
 741        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 742
 743    second_child_client
 744        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 745        .await;
 746
 747    cx.run_until_parked();
 748
 749    // shutdown parent session
 750    dap_store
 751        .update(cx, |dap_store, cx| {
 752            dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
 753        })
 754        .await
 755        .unwrap();
 756
 757    // assert parent session and all children sessions are shutdown
 758    dap_store.update(cx, |dap_store, cx| {
 759        assert!(
 760            dap_store
 761                .session_by_id(parent_session.read(cx).session_id())
 762                .is_none()
 763        );
 764        assert!(
 765            dap_store
 766                .session_by_id(first_child_session.read(cx).session_id())
 767                .is_none()
 768        );
 769        assert!(
 770            dap_store
 771                .session_by_id(second_child_session.read(cx).session_id())
 772                .is_none()
 773        );
 774    });
 775}
 776
 777#[gpui::test]
 778async fn test_shutdown_parent_session_if_all_children_are_shutdown(
 779    executor: BackgroundExecutor,
 780    cx: &mut TestAppContext,
 781) {
 782    init_test(cx);
 783
 784    let fs = FakeFs::new(executor.clone());
 785
 786    fs.insert_tree(
 787        "/project",
 788        json!({
 789            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 790        }),
 791    )
 792    .await;
 793
 794    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 795    let dap_store = project.update(cx, |project, _| project.dap_store());
 796    let workspace = init_test_workspace(&project, cx).await;
 797    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 798
 799    let task = project.update(cx, |project, cx| {
 800        project.fake_debug_session(
 801            dap::DebugRequestType::Launch(LaunchConfig::default()),
 802            None,
 803            false,
 804            cx,
 805        )
 806    });
 807
 808    let parent_session = task.await.unwrap();
 809    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 810
 811    client.on_response::<StartDebugging, _>(move |_| {}).await;
 812
 813    // start first child session
 814    client
 815        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 816            configuration: json!({}),
 817            request: StartDebuggingRequestArgumentsRequest::Launch,
 818        })
 819        .await;
 820
 821    cx.run_until_parked();
 822
 823    // start second child session
 824    client
 825        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 826            configuration: json!({}),
 827            request: StartDebuggingRequestArgumentsRequest::Launch,
 828        })
 829        .await;
 830
 831    cx.run_until_parked();
 832
 833    // configure first child session
 834    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 835        dap_store.session_by_id(SessionId(1)).unwrap()
 836    });
 837    let first_child_client =
 838        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 839
 840    first_child_client
 841        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 842        .await;
 843
 844    // configure second child session
 845    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 846        dap_store.session_by_id(SessionId(2)).unwrap()
 847    });
 848    let second_child_client =
 849        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 850
 851    second_child_client
 852        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 853        .await;
 854
 855    cx.run_until_parked();
 856
 857    // shutdown first child session
 858    dap_store
 859        .update(cx, |dap_store, cx| {
 860            dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
 861        })
 862        .await
 863        .unwrap();
 864
 865    // assert parent session and second child session still exist
 866    dap_store.update(cx, |dap_store, cx| {
 867        assert!(
 868            dap_store
 869                .session_by_id(parent_session.read(cx).session_id())
 870                .is_some()
 871        );
 872        assert!(
 873            dap_store
 874                .session_by_id(first_child_session.read(cx).session_id())
 875                .is_none()
 876        );
 877        assert!(
 878            dap_store
 879                .session_by_id(second_child_session.read(cx).session_id())
 880                .is_some()
 881        );
 882    });
 883
 884    // shutdown first child session
 885    dap_store
 886        .update(cx, |dap_store, cx| {
 887            dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
 888        })
 889        .await
 890        .unwrap();
 891
 892    // assert parent session got shutdown by second child session
 893    // because it was the last child
 894    dap_store.update(cx, |dap_store, cx| {
 895        assert!(
 896            dap_store
 897                .session_by_id(parent_session.read(cx).session_id())
 898                .is_none()
 899        );
 900        assert!(
 901            dap_store
 902                .session_by_id(second_child_session.read(cx).session_id())
 903                .is_none()
 904        );
 905    });
 906}
 907
 908#[gpui::test]
 909async fn test_debug_panel_item_thread_status_reset_on_failure(
 910    executor: BackgroundExecutor,
 911    cx: &mut TestAppContext,
 912) {
 913    init_test(cx);
 914
 915    let fs = FakeFs::new(executor.clone());
 916
 917    fs.insert_tree(
 918        "/project",
 919        json!({
 920            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 921        }),
 922    )
 923    .await;
 924
 925    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 926    let workspace = init_test_workspace(&project, cx).await;
 927    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 928
 929    let task = project.update(cx, |project, cx| {
 930        project.fake_debug_session(
 931            dap::DebugRequestType::Launch(LaunchConfig::default()),
 932            Some(dap::Capabilities {
 933                supports_step_back: Some(true),
 934                ..Default::default()
 935            }),
 936            false,
 937            cx,
 938        )
 939    });
 940
 941    let session = task.await.unwrap();
 942    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 943    const THREAD_ID_NUM: u64 = 1;
 944
 945    client
 946        .on_request::<dap::requests::Threads, _>(move |_, _| {
 947            Ok(dap::ThreadsResponse {
 948                threads: vec![dap::Thread {
 949                    id: THREAD_ID_NUM,
 950                    name: "Thread 1".into(),
 951                }],
 952            })
 953        })
 954        .await;
 955
 956    client.on_request::<Launch, _>(move |_, _| Ok(())).await;
 957
 958    client
 959        .on_request::<StackTrace, _>(move |_, _| {
 960            Ok(dap::StackTraceResponse {
 961                stack_frames: Vec::default(),
 962                total_frames: None,
 963            })
 964        })
 965        .await;
 966
 967    client
 968        .on_request::<Next, _>(move |_, _| {
 969            Err(ErrorResponse {
 970                error: Some(dap::Message {
 971                    id: 1,
 972                    format: "error".into(),
 973                    variables: None,
 974                    send_telemetry: None,
 975                    show_user: None,
 976                    url: None,
 977                    url_label: None,
 978                }),
 979            })
 980        })
 981        .await;
 982
 983    client
 984        .on_request::<StepOut, _>(move |_, _| {
 985            Err(ErrorResponse {
 986                error: Some(dap::Message {
 987                    id: 1,
 988                    format: "error".into(),
 989                    variables: None,
 990                    send_telemetry: None,
 991                    show_user: None,
 992                    url: None,
 993                    url_label: None,
 994                }),
 995            })
 996        })
 997        .await;
 998
 999    client
1000        .on_request::<StepIn, _>(move |_, _| {
1001            Err(ErrorResponse {
1002                error: Some(dap::Message {
1003                    id: 1,
1004                    format: "error".into(),
1005                    variables: None,
1006                    send_telemetry: None,
1007                    show_user: None,
1008                    url: None,
1009                    url_label: None,
1010                }),
1011            })
1012        })
1013        .await;
1014
1015    client
1016        .on_request::<StepBack, _>(move |_, _| {
1017            Err(ErrorResponse {
1018                error: Some(dap::Message {
1019                    id: 1,
1020                    format: "error".into(),
1021                    variables: None,
1022                    send_telemetry: None,
1023                    show_user: None,
1024                    url: None,
1025                    url_label: None,
1026                }),
1027            })
1028        })
1029        .await;
1030
1031    client
1032        .on_request::<Continue, _>(move |_, _| {
1033            Err(ErrorResponse {
1034                error: Some(dap::Message {
1035                    id: 1,
1036                    format: "error".into(),
1037                    variables: None,
1038                    send_telemetry: None,
1039                    show_user: None,
1040                    url: None,
1041                    url_label: None,
1042                }),
1043            })
1044        })
1045        .await;
1046
1047    client
1048        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1049            reason: dap::StoppedEventReason::Pause,
1050            description: None,
1051            thread_id: Some(1),
1052            preserve_focus_hint: None,
1053            text: None,
1054            all_threads_stopped: None,
1055            hit_breakpoint_ids: None,
1056        }))
1057        .await;
1058
1059    cx.run_until_parked();
1060
1061    let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
1062        item.mode()
1063            .as_running()
1064            .expect("Session should be running by this point")
1065            .clone()
1066    });
1067
1068    cx.run_until_parked();
1069    let thread_id = ThreadId(1);
1070
1071    for operation in &[
1072        "step_over",
1073        "continue_thread",
1074        "step_back",
1075        "step_in",
1076        "step_out",
1077    ] {
1078        running_state.update(cx, |running_state, cx| match *operation {
1079            "step_over" => running_state.step_over(cx),
1080            "continue_thread" => running_state.continue_thread(cx),
1081            "step_back" => running_state.step_back(cx),
1082            "step_in" => running_state.step_in(cx),
1083            "step_out" => running_state.step_out(cx),
1084            _ => unreachable!(),
1085        });
1086
1087        // Check that we step the thread status to the correct intermediate state
1088        running_state.update(cx, |running_state, cx| {
1089            assert_eq!(
1090                running_state
1091                    .thread_status(cx)
1092                    .expect("There should be an active thread selected"),
1093                match *operation {
1094                    "continue_thread" => ThreadStatus::Running,
1095                    _ => ThreadStatus::Stepping,
1096                },
1097                "Thread status was not set to correct intermediate state after {} request",
1098                operation
1099            );
1100        });
1101
1102        cx.run_until_parked();
1103
1104        running_state.update(cx, |running_state, cx| {
1105            assert_eq!(
1106                running_state
1107                    .thread_status(cx)
1108                    .expect("There should be an active thread selected"),
1109                ThreadStatus::Stopped,
1110                "Thread status not reset to Stopped after failed {}",
1111                operation
1112            );
1113
1114            // update state to running, so we can test it actually changes the status back to stopped
1115            running_state
1116                .session()
1117                .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1118        });
1119    }
1120
1121    let shutdown_session = project.update(cx, |project, cx| {
1122        project.dap_store().update(cx, |dap_store, cx| {
1123            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1124        })
1125    });
1126
1127    shutdown_session.await.unwrap();
1128}
1129
1130#[gpui::test]
1131async fn test_send_breakpoints_when_editor_has_been_saved(
1132    executor: BackgroundExecutor,
1133    cx: &mut TestAppContext,
1134) {
1135    init_test(cx);
1136
1137    let fs = FakeFs::new(executor.clone());
1138
1139    fs.insert_tree(
1140        path!("/project"),
1141        json!({
1142            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1143        }),
1144    )
1145    .await;
1146
1147    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1148    let workspace = init_test_workspace(&project, cx).await;
1149    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1150    let project_path = Path::new(path!("/project"));
1151    let worktree = project
1152        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1153        .expect("This worktree should exist in project")
1154        .0;
1155
1156    let worktree_id = workspace
1157        .update(cx, |_, _, cx| worktree.read(cx).id())
1158        .unwrap();
1159
1160    let task = project.update(cx, |project, cx| {
1161        project.fake_debug_session(
1162            dap::DebugRequestType::Launch(LaunchConfig::default()),
1163            None,
1164            false,
1165            cx,
1166        )
1167    });
1168
1169    let session = task.await.unwrap();
1170    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1171
1172    let buffer = project
1173        .update(cx, |project, cx| {
1174            project.open_buffer((worktree_id, "main.rs"), cx)
1175        })
1176        .await
1177        .unwrap();
1178
1179    let (editor, cx) = cx.add_window_view(|window, cx| {
1180        Editor::new(
1181            EditorMode::full(),
1182            MultiBuffer::build_from_buffer(buffer, cx),
1183            Some(project.clone()),
1184            window,
1185            cx,
1186        )
1187    });
1188
1189    client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1190
1191    client
1192        .on_request::<StackTrace, _>(move |_, _| {
1193            Ok(dap::StackTraceResponse {
1194                stack_frames: Vec::default(),
1195                total_frames: None,
1196            })
1197        })
1198        .await;
1199
1200    client
1201        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1202            reason: dap::StoppedEventReason::Pause,
1203            description: None,
1204            thread_id: Some(1),
1205            preserve_focus_hint: None,
1206            text: None,
1207            all_threads_stopped: None,
1208            hit_breakpoint_ids: None,
1209        }))
1210        .await;
1211
1212    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1213    client
1214        .on_request::<SetBreakpoints, _>({
1215            let called_set_breakpoints = called_set_breakpoints.clone();
1216            move |_, args| {
1217                assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1218                assert_eq!(
1219                    vec![SourceBreakpoint {
1220                        line: 2,
1221                        column: None,
1222                        condition: None,
1223                        hit_condition: None,
1224                        log_message: None,
1225                        mode: None
1226                    }],
1227                    args.breakpoints.unwrap()
1228                );
1229                assert!(!args.source_modified.unwrap());
1230
1231                called_set_breakpoints.store(true, Ordering::SeqCst);
1232
1233                Ok(dap::SetBreakpointsResponse {
1234                    breakpoints: Vec::default(),
1235                })
1236            }
1237        })
1238        .await;
1239
1240    editor.update_in(cx, |editor, window, cx| {
1241        editor.move_down(&actions::MoveDown, window, cx);
1242        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1243    });
1244
1245    cx.run_until_parked();
1246
1247    assert!(
1248        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1249        "SetBreakpoint request must be called"
1250    );
1251
1252    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1253    client
1254        .on_request::<SetBreakpoints, _>({
1255            let called_set_breakpoints = called_set_breakpoints.clone();
1256            move |_, args| {
1257                assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1258                assert_eq!(
1259                    vec![SourceBreakpoint {
1260                        line: 3,
1261                        column: None,
1262                        condition: None,
1263                        hit_condition: None,
1264                        log_message: None,
1265                        mode: None
1266                    }],
1267                    args.breakpoints.unwrap()
1268                );
1269                assert!(args.source_modified.unwrap());
1270
1271                called_set_breakpoints.store(true, Ordering::SeqCst);
1272
1273                Ok(dap::SetBreakpointsResponse {
1274                    breakpoints: Vec::default(),
1275                })
1276            }
1277        })
1278        .await;
1279
1280    editor.update_in(cx, |editor, window, cx| {
1281        editor.move_up(&actions::MoveUp, window, cx);
1282        editor.insert("new text\n", window, cx);
1283    });
1284
1285    editor
1286        .update_in(cx, |editor, window, cx| {
1287            editor.save(true, project.clone(), window, cx)
1288        })
1289        .await
1290        .unwrap();
1291
1292    cx.run_until_parked();
1293
1294    assert!(
1295        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1296        "SetBreakpoint request must be called after editor is saved"
1297    );
1298
1299    let shutdown_session = project.update(cx, |project, cx| {
1300        project.dap_store().update(cx, |dap_store, cx| {
1301            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1302        })
1303    });
1304
1305    shutdown_session.await.unwrap();
1306}
1307
1308#[gpui::test]
1309async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1310    executor: BackgroundExecutor,
1311    cx: &mut TestAppContext,
1312) {
1313    init_test(cx);
1314
1315    let fs = FakeFs::new(executor.clone());
1316
1317    fs.insert_tree(
1318        path!("/project"),
1319        json!({
1320            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1321            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1322            "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1323        }),
1324    )
1325    .await;
1326
1327    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1328    let workspace = init_test_workspace(&project, cx).await;
1329    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1330    let project_path = Path::new(path!("/project"));
1331    let worktree = project
1332        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1333        .expect("This worktree should exist in project")
1334        .0;
1335
1336    let worktree_id = workspace
1337        .update(cx, |_, _, cx| worktree.read(cx).id())
1338        .unwrap();
1339
1340    let first = project
1341        .update(cx, |project, cx| {
1342            project.open_buffer((worktree_id, "main.rs"), cx)
1343        })
1344        .await
1345        .unwrap();
1346
1347    let second = project
1348        .update(cx, |project, cx| {
1349            project.open_buffer((worktree_id, "second.rs"), cx)
1350        })
1351        .await
1352        .unwrap();
1353
1354    let (first_editor, cx) = cx.add_window_view(|window, cx| {
1355        Editor::new(
1356            EditorMode::full(),
1357            MultiBuffer::build_from_buffer(first, cx),
1358            Some(project.clone()),
1359            window,
1360            cx,
1361        )
1362    });
1363
1364    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1365        Editor::new(
1366            EditorMode::full(),
1367            MultiBuffer::build_from_buffer(second, cx),
1368            Some(project.clone()),
1369            window,
1370            cx,
1371        )
1372    });
1373
1374    first_editor.update_in(cx, |editor, window, cx| {
1375        editor.move_down(&actions::MoveDown, window, cx);
1376        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1377        editor.move_down(&actions::MoveDown, window, cx);
1378        editor.move_down(&actions::MoveDown, window, cx);
1379        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1380    });
1381
1382    second_editor.update_in(cx, |editor, window, cx| {
1383        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1384        editor.move_down(&actions::MoveDown, window, cx);
1385        editor.move_down(&actions::MoveDown, window, cx);
1386        editor.move_down(&actions::MoveDown, window, cx);
1387        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1388    });
1389
1390    let task = project.update(cx, |project, cx| {
1391        project.fake_debug_session(
1392            dap::DebugRequestType::Launch(LaunchConfig::default()),
1393            None,
1394            false,
1395            cx,
1396        )
1397    });
1398
1399    let session = task.await.unwrap();
1400    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1401
1402    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1403
1404    client
1405        .on_request::<SetBreakpoints, _>({
1406            let called_set_breakpoints = called_set_breakpoints.clone();
1407            move |_, args| {
1408                assert!(
1409                    args.breakpoints.is_none_or(|bps| bps.is_empty()),
1410                    "Send empty breakpoint sets to clear them from DAP servers"
1411                );
1412
1413                match args
1414                    .source
1415                    .path
1416                    .expect("We should always send a breakpoint's path")
1417                    .as_str()
1418                {
1419                    "/project/main.rs" | "/project/second.rs" => {}
1420                    _ => {
1421                        panic!("Unset breakpoints for path that doesn't have any")
1422                    }
1423                }
1424
1425                called_set_breakpoints.store(true, Ordering::SeqCst);
1426
1427                Ok(dap::SetBreakpointsResponse {
1428                    breakpoints: Vec::default(),
1429                })
1430            }
1431        })
1432        .await;
1433
1434    cx.dispatch_action(crate::ClearAllBreakpoints);
1435    cx.run_until_parked();
1436
1437    let shutdown_session = project.update(cx, |project, cx| {
1438        project.dap_store().update(cx, |dap_store, cx| {
1439            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1440        })
1441    });
1442
1443    shutdown_session.await.unwrap();
1444}
1445
1446#[gpui::test]
1447async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1448    executor: BackgroundExecutor,
1449    cx: &mut TestAppContext,
1450) {
1451    init_test(cx);
1452
1453    let fs = FakeFs::new(executor.clone());
1454
1455    fs.insert_tree(
1456        "/project",
1457        json!({
1458            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1459        }),
1460    )
1461    .await;
1462
1463    let project = Project::test(fs, ["/project".as_ref()], cx).await;
1464    let workspace = init_test_workspace(&project, cx).await;
1465    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1466
1467    let task = project.update(cx, |project, cx| {
1468        project.fake_debug_session(
1469            dap::DebugRequestType::Launch(LaunchConfig::default()),
1470            None,
1471            true,
1472            cx,
1473        )
1474    });
1475
1476    assert!(
1477        task.await.is_err(),
1478        "Session should failed to start if launch request fails"
1479    );
1480
1481    cx.run_until_parked();
1482
1483    project.update(cx, |project, cx| {
1484        assert!(
1485            project.dap_store().read(cx).sessions().count() == 0,
1486            "Session wouldn't exist if it was shutdown"
1487        );
1488    });
1489}