debugger_panel.rs

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