debugger_panel.rs

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