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