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
 403#[gpui::test]
 404async fn test_handle_start_debugging_request(
 405    executor: BackgroundExecutor,
 406    cx: &mut TestAppContext,
 407) {
 408    init_test(cx);
 409
 410    let fs = FakeFs::new(executor.clone());
 411
 412    fs.insert_tree(
 413        "/project",
 414        json!({
 415            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 416        }),
 417    )
 418    .await;
 419
 420    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 421    let workspace = init_test_workspace(&project, cx).await;
 422    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 423
 424    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 425    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 426
 427    let fake_config = json!({"one": "two"});
 428    let launched_with = Arc::new(parking_lot::Mutex::new(None));
 429
 430    let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
 431        let launched_with = launched_with.clone();
 432        move |client| {
 433            let launched_with = launched_with.clone();
 434            client.on_request::<dap::requests::Launch, _>(move |_, args| {
 435                launched_with.lock().replace(args.raw);
 436                Ok(())
 437            });
 438            client.on_request::<dap::requests::Attach, _>(move |_, _| {
 439                assert!(false, "should not get attach request");
 440                Ok(())
 441            });
 442        }
 443    });
 444
 445    client
 446        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 447            request: StartDebuggingRequestArgumentsRequest::Launch,
 448            configuration: fake_config.clone(),
 449        })
 450        .await;
 451
 452    cx.run_until_parked();
 453
 454    workspace
 455        .update(cx, |workspace, _window, cx| {
 456            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 457            let active_session = debug_panel
 458                .read(cx)
 459                .active_session()
 460                .unwrap()
 461                .read(cx)
 462                .session(cx);
 463            let parent_session = active_session.read(cx).parent_session().unwrap();
 464
 465            assert_eq!(
 466                active_session.read(cx).definition(),
 467                parent_session.read(cx).definition()
 468            );
 469        })
 470        .unwrap();
 471
 472    assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
 473}
 474
 475// // covers that we always send a response back, if something when wrong,
 476// // while spawning the terminal
 477#[gpui::test]
 478async fn test_handle_error_run_in_terminal_reverse_request(
 479    executor: BackgroundExecutor,
 480    cx: &mut TestAppContext,
 481) {
 482    init_test(cx);
 483
 484    let send_response = Arc::new(AtomicBool::new(false));
 485
 486    let fs = FakeFs::new(executor.clone());
 487
 488    fs.insert_tree(
 489        "/project",
 490        json!({
 491            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 492        }),
 493    )
 494    .await;
 495
 496    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 497    let workspace = init_test_workspace(&project, cx).await;
 498    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 499
 500    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 501    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 502
 503    client
 504        .on_response::<RunInTerminal, _>({
 505            let send_response = send_response.clone();
 506            move |response| {
 507                send_response.store(true, Ordering::SeqCst);
 508
 509                assert!(!response.success);
 510                assert!(response.body.is_some());
 511            }
 512        })
 513        .await;
 514
 515    client
 516        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 517            kind: None,
 518            title: None,
 519            cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail
 520            args: vec![],
 521            env: None,
 522            args_can_be_interpreted_by_shell: None,
 523        })
 524        .await;
 525
 526    cx.run_until_parked();
 527
 528    assert!(
 529        send_response.load(std::sync::atomic::Ordering::SeqCst),
 530        "Expected to receive response from reverse request"
 531    );
 532
 533    workspace
 534        .update(cx, |workspace, _window, cx| {
 535            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
 536
 537            assert_eq!(
 538                0,
 539                terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
 540            );
 541        })
 542        .unwrap();
 543}
 544
 545#[gpui::test]
 546async fn test_handle_start_debugging_reverse_request(
 547    executor: BackgroundExecutor,
 548    cx: &mut TestAppContext,
 549) {
 550    init_test(cx);
 551
 552    let send_response = Arc::new(AtomicBool::new(false));
 553
 554    let fs = FakeFs::new(executor.clone());
 555
 556    fs.insert_tree(
 557        "/project",
 558        json!({
 559            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 560        }),
 561    )
 562    .await;
 563
 564    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 565    let workspace = init_test_workspace(&project, cx).await;
 566    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 567
 568    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 569    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 570
 571    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 572        Ok(dap::ThreadsResponse {
 573            threads: vec![dap::Thread {
 574                id: 1,
 575                name: "Thread 1".into(),
 576            }],
 577        })
 578    });
 579
 580    client
 581        .on_response::<StartDebugging, _>({
 582            let send_response = send_response.clone();
 583            move |response| {
 584                send_response.store(true, Ordering::SeqCst);
 585
 586                assert!(response.success);
 587                assert!(response.body.is_some());
 588            }
 589        })
 590        .await;
 591    // Set up handlers for sessions spawned with reverse request too.
 592    let _reverse_request_subscription =
 593        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 594    client
 595        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 596            configuration: json!({}),
 597            request: StartDebuggingRequestArgumentsRequest::Launch,
 598        })
 599        .await;
 600
 601    cx.run_until_parked();
 602
 603    let child_session = project.update(cx, |project, cx| {
 604        project
 605            .dap_store()
 606            .read(cx)
 607            .session_by_id(SessionId(1))
 608            .unwrap()
 609    });
 610    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
 611
 612    child_client.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
 621    child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 622
 623    child_client
 624        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 625            reason: dap::StoppedEventReason::Pause,
 626            description: None,
 627            thread_id: Some(2),
 628            preserve_focus_hint: None,
 629            text: None,
 630            all_threads_stopped: None,
 631            hit_breakpoint_ids: None,
 632        }))
 633        .await;
 634
 635    cx.run_until_parked();
 636
 637    assert!(
 638        send_response.load(std::sync::atomic::Ordering::SeqCst),
 639        "Expected to receive response from reverse request"
 640    );
 641}
 642
 643#[gpui::test]
 644async fn test_shutdown_children_when_parent_session_shutdown(
 645    executor: BackgroundExecutor,
 646    cx: &mut TestAppContext,
 647) {
 648    init_test(cx);
 649
 650    let fs = FakeFs::new(executor.clone());
 651
 652    fs.insert_tree(
 653        "/project",
 654        json!({
 655            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 656        }),
 657    )
 658    .await;
 659
 660    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 661    let dap_store = project.update(cx, |project, _| project.dap_store());
 662    let workspace = init_test_workspace(&project, cx).await;
 663    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 664
 665    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 666    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 667
 668    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 669        Ok(dap::ThreadsResponse {
 670            threads: vec![dap::Thread {
 671                id: 1,
 672                name: "Thread 1".into(),
 673            }],
 674        })
 675    });
 676
 677    client.on_response::<StartDebugging, _>(move |_| {}).await;
 678    // Set up handlers for sessions spawned with reverse request too.
 679    let _reverse_request_subscription =
 680        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 681    // start first child session
 682    client
 683        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 684            configuration: json!({}),
 685            request: StartDebuggingRequestArgumentsRequest::Launch,
 686        })
 687        .await;
 688
 689    cx.run_until_parked();
 690
 691    // start second child session
 692    client
 693        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 694            configuration: json!({}),
 695            request: StartDebuggingRequestArgumentsRequest::Launch,
 696        })
 697        .await;
 698
 699    cx.run_until_parked();
 700
 701    // configure first child session
 702    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 703        dap_store.session_by_id(SessionId(1)).unwrap()
 704    });
 705    let first_child_client =
 706        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 707
 708    first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 709
 710    // configure second child session
 711    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 712        dap_store.session_by_id(SessionId(2)).unwrap()
 713    });
 714    let second_child_client =
 715        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 716
 717    second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 718
 719    cx.run_until_parked();
 720
 721    // shutdown parent session
 722    dap_store
 723        .update(cx, |dap_store, cx| {
 724            dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
 725        })
 726        .await
 727        .unwrap();
 728
 729    // assert parent session and all children sessions are shutdown
 730    dap_store.update(cx, |dap_store, cx| {
 731        assert!(
 732            dap_store
 733                .session_by_id(parent_session.read(cx).session_id())
 734                .is_none()
 735        );
 736        assert!(
 737            dap_store
 738                .session_by_id(first_child_session.read(cx).session_id())
 739                .is_none()
 740        );
 741        assert!(
 742            dap_store
 743                .session_by_id(second_child_session.read(cx).session_id())
 744                .is_none()
 745        );
 746    });
 747}
 748
 749#[gpui::test]
 750async fn test_shutdown_parent_session_if_all_children_are_shutdown(
 751    executor: BackgroundExecutor,
 752    cx: &mut TestAppContext,
 753) {
 754    init_test(cx);
 755
 756    let fs = FakeFs::new(executor.clone());
 757
 758    fs.insert_tree(
 759        "/project",
 760        json!({
 761            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 762        }),
 763    )
 764    .await;
 765
 766    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 767    let dap_store = project.update(cx, |project, _| project.dap_store());
 768    let workspace = init_test_workspace(&project, cx).await;
 769    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 770
 771    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 772    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 773
 774    client.on_response::<StartDebugging, _>(move |_| {}).await;
 775    // Set up handlers for sessions spawned with reverse request too.
 776    let _reverse_request_subscription =
 777        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 778    // start first child session
 779    client
 780        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 781            configuration: json!({}),
 782            request: StartDebuggingRequestArgumentsRequest::Launch,
 783        })
 784        .await;
 785
 786    cx.run_until_parked();
 787
 788    // start second child session
 789    client
 790        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 791            configuration: json!({}),
 792            request: StartDebuggingRequestArgumentsRequest::Launch,
 793        })
 794        .await;
 795
 796    cx.run_until_parked();
 797
 798    // configure first child session
 799    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 800        dap_store.session_by_id(SessionId(1)).unwrap()
 801    });
 802    let first_child_client =
 803        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 804
 805    first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 806
 807    // configure second child session
 808    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 809        dap_store.session_by_id(SessionId(2)).unwrap()
 810    });
 811    let second_child_client =
 812        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 813
 814    second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 815
 816    cx.run_until_parked();
 817
 818    // shutdown first child session
 819    dap_store
 820        .update(cx, |dap_store, cx| {
 821            dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
 822        })
 823        .await
 824        .unwrap();
 825
 826    // assert parent session and second child session still exist
 827    dap_store.update(cx, |dap_store, cx| {
 828        assert!(
 829            dap_store
 830                .session_by_id(parent_session.read(cx).session_id())
 831                .is_some()
 832        );
 833        assert!(
 834            dap_store
 835                .session_by_id(first_child_session.read(cx).session_id())
 836                .is_none()
 837        );
 838        assert!(
 839            dap_store
 840                .session_by_id(second_child_session.read(cx).session_id())
 841                .is_some()
 842        );
 843    });
 844
 845    // shutdown first child session
 846    dap_store
 847        .update(cx, |dap_store, cx| {
 848            dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
 849        })
 850        .await
 851        .unwrap();
 852
 853    // assert parent session got shutdown by second child session
 854    // because it was the last child
 855    dap_store.update(cx, |dap_store, cx| {
 856        assert!(
 857            dap_store
 858                .session_by_id(parent_session.read(cx).session_id())
 859                .is_none()
 860        );
 861        assert!(
 862            dap_store
 863                .session_by_id(second_child_session.read(cx).session_id())
 864                .is_none()
 865        );
 866    });
 867}
 868
 869#[gpui::test]
 870async fn test_debug_panel_item_thread_status_reset_on_failure(
 871    executor: BackgroundExecutor,
 872    cx: &mut TestAppContext,
 873) {
 874    init_test(cx);
 875
 876    let fs = FakeFs::new(executor.clone());
 877
 878    fs.insert_tree(
 879        "/project",
 880        json!({
 881            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 882        }),
 883    )
 884    .await;
 885
 886    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 887    let workspace = init_test_workspace(&project, cx).await;
 888    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 889
 890    let session = start_debug_session(&workspace, cx, |client| {
 891        client.on_request::<dap::requests::Initialize, _>(move |_, _| {
 892            Ok(dap::Capabilities {
 893                supports_step_back: Some(true),
 894                ..Default::default()
 895            })
 896        });
 897    })
 898    .unwrap();
 899
 900    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 901    const THREAD_ID_NUM: u64 = 1;
 902
 903    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 904        Ok(dap::ThreadsResponse {
 905            threads: vec![dap::Thread {
 906                id: THREAD_ID_NUM,
 907                name: "Thread 1".into(),
 908            }],
 909        })
 910    });
 911
 912    client.on_request::<Launch, _>(move |_, _| Ok(()));
 913
 914    client.on_request::<StackTrace, _>(move |_, _| {
 915        Ok(dap::StackTraceResponse {
 916            stack_frames: Vec::default(),
 917            total_frames: None,
 918        })
 919    });
 920
 921    client.on_request::<Next, _>(move |_, _| {
 922        Err(ErrorResponse {
 923            error: Some(dap::Message {
 924                id: 1,
 925                format: "error".into(),
 926                variables: None,
 927                send_telemetry: None,
 928                show_user: None,
 929                url: None,
 930                url_label: None,
 931            }),
 932        })
 933    });
 934
 935    client.on_request::<StepOut, _>(move |_, _| {
 936        Err(ErrorResponse {
 937            error: Some(dap::Message {
 938                id: 1,
 939                format: "error".into(),
 940                variables: None,
 941                send_telemetry: None,
 942                show_user: None,
 943                url: None,
 944                url_label: None,
 945            }),
 946        })
 947    });
 948
 949    client.on_request::<StepIn, _>(move |_, _| {
 950        Err(ErrorResponse {
 951            error: Some(dap::Message {
 952                id: 1,
 953                format: "error".into(),
 954                variables: None,
 955                send_telemetry: None,
 956                show_user: None,
 957                url: None,
 958                url_label: None,
 959            }),
 960        })
 961    });
 962
 963    client.on_request::<StepBack, _>(move |_, _| {
 964        Err(ErrorResponse {
 965            error: Some(dap::Message {
 966                id: 1,
 967                format: "error".into(),
 968                variables: None,
 969                send_telemetry: None,
 970                show_user: None,
 971                url: None,
 972                url_label: None,
 973            }),
 974        })
 975    });
 976
 977    client.on_request::<Continue, _>(move |_, _| {
 978        Err(ErrorResponse {
 979            error: Some(dap::Message {
 980                id: 1,
 981                format: "error".into(),
 982                variables: None,
 983                send_telemetry: None,
 984                show_user: None,
 985                url: None,
 986                url_label: None,
 987            }),
 988        })
 989    });
 990
 991    client
 992        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 993            reason: dap::StoppedEventReason::Pause,
 994            description: None,
 995            thread_id: Some(1),
 996            preserve_focus_hint: None,
 997            text: None,
 998            all_threads_stopped: None,
 999            hit_breakpoint_ids: None,
1000        }))
1001        .await;
1002
1003    cx.run_until_parked();
1004
1005    let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
1006        item.mode()
1007            .as_running()
1008            .expect("Session should be running by this point")
1009            .clone()
1010    });
1011
1012    cx.run_until_parked();
1013    let thread_id = ThreadId(1);
1014
1015    for operation in &[
1016        "step_over",
1017        "continue_thread",
1018        "step_back",
1019        "step_in",
1020        "step_out",
1021    ] {
1022        running_state.update(cx, |running_state, cx| match *operation {
1023            "step_over" => running_state.step_over(cx),
1024            "continue_thread" => running_state.continue_thread(cx),
1025            "step_back" => running_state.step_back(cx),
1026            "step_in" => running_state.step_in(cx),
1027            "step_out" => running_state.step_out(cx),
1028            _ => unreachable!(),
1029        });
1030
1031        // Check that we step the thread status to the correct intermediate state
1032        running_state.update(cx, |running_state, cx| {
1033            assert_eq!(
1034                running_state
1035                    .thread_status(cx)
1036                    .expect("There should be an active thread selected"),
1037                match *operation {
1038                    "continue_thread" => ThreadStatus::Running,
1039                    _ => ThreadStatus::Stepping,
1040                },
1041                "Thread status was not set to correct intermediate state after {} request",
1042                operation
1043            );
1044        });
1045
1046        cx.run_until_parked();
1047
1048        running_state.update(cx, |running_state, cx| {
1049            assert_eq!(
1050                running_state
1051                    .thread_status(cx)
1052                    .expect("There should be an active thread selected"),
1053                ThreadStatus::Stopped,
1054                "Thread status not reset to Stopped after failed {}",
1055                operation
1056            );
1057
1058            // update state to running, so we can test it actually changes the status back to stopped
1059            running_state
1060                .session()
1061                .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1062        });
1063    }
1064}
1065
1066#[gpui::test]
1067async fn test_send_breakpoints_when_editor_has_been_saved(
1068    executor: BackgroundExecutor,
1069    cx: &mut TestAppContext,
1070) {
1071    init_test(cx);
1072
1073    let fs = FakeFs::new(executor.clone());
1074
1075    fs.insert_tree(
1076        path!("/project"),
1077        json!({
1078            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1079        }),
1080    )
1081    .await;
1082
1083    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1084    let workspace = init_test_workspace(&project, cx).await;
1085    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1086    let project_path = Path::new(path!("/project"));
1087    let worktree = project
1088        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1089        .expect("This worktree should exist in project")
1090        .0;
1091
1092    let worktree_id = workspace
1093        .update(cx, |_, _, cx| worktree.read(cx).id())
1094        .unwrap();
1095
1096    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1097    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1098
1099    let buffer = project
1100        .update(cx, |project, cx| {
1101            project.open_buffer((worktree_id, "main.rs"), cx)
1102        })
1103        .await
1104        .unwrap();
1105
1106    let (editor, cx) = cx.add_window_view(|window, cx| {
1107        Editor::new(
1108            EditorMode::full(),
1109            MultiBuffer::build_from_buffer(buffer, cx),
1110            Some(project.clone()),
1111            window,
1112            cx,
1113        )
1114    });
1115
1116    client.on_request::<Launch, _>(move |_, _| Ok(()));
1117
1118    client.on_request::<StackTrace, _>(move |_, _| {
1119        Ok(dap::StackTraceResponse {
1120            stack_frames: Vec::default(),
1121            total_frames: None,
1122        })
1123    });
1124
1125    client
1126        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1127            reason: dap::StoppedEventReason::Pause,
1128            description: None,
1129            thread_id: Some(1),
1130            preserve_focus_hint: None,
1131            text: None,
1132            all_threads_stopped: None,
1133            hit_breakpoint_ids: None,
1134        }))
1135        .await;
1136
1137    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1138    client.on_request::<SetBreakpoints, _>({
1139        let called_set_breakpoints = called_set_breakpoints.clone();
1140        move |_, args| {
1141            assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1142            assert_eq!(
1143                vec![SourceBreakpoint {
1144                    line: 2,
1145                    column: None,
1146                    condition: None,
1147                    hit_condition: None,
1148                    log_message: None,
1149                    mode: None
1150                }],
1151                args.breakpoints.unwrap()
1152            );
1153            assert!(!args.source_modified.unwrap());
1154
1155            called_set_breakpoints.store(true, Ordering::SeqCst);
1156
1157            Ok(dap::SetBreakpointsResponse {
1158                breakpoints: Vec::default(),
1159            })
1160        }
1161    });
1162
1163    editor.update_in(cx, |editor, window, cx| {
1164        editor.move_down(&actions::MoveDown, window, cx);
1165        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1166    });
1167
1168    cx.run_until_parked();
1169
1170    assert!(
1171        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1172        "SetBreakpoint request must be called"
1173    );
1174
1175    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1176    client.on_request::<SetBreakpoints, _>({
1177        let called_set_breakpoints = called_set_breakpoints.clone();
1178        move |_, args| {
1179            assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1180            assert_eq!(
1181                vec![SourceBreakpoint {
1182                    line: 3,
1183                    column: None,
1184                    condition: None,
1185                    hit_condition: None,
1186                    log_message: None,
1187                    mode: None
1188                }],
1189                args.breakpoints.unwrap()
1190            );
1191            assert!(args.source_modified.unwrap());
1192
1193            called_set_breakpoints.store(true, Ordering::SeqCst);
1194
1195            Ok(dap::SetBreakpointsResponse {
1196                breakpoints: Vec::default(),
1197            })
1198        }
1199    });
1200
1201    editor.update_in(cx, |editor, window, cx| {
1202        editor.move_up(&actions::MoveUp, window, cx);
1203        editor.insert("new text\n", window, cx);
1204    });
1205
1206    editor
1207        .update_in(cx, |editor, window, cx| {
1208            editor.save(true, project.clone(), window, cx)
1209        })
1210        .await
1211        .unwrap();
1212
1213    cx.run_until_parked();
1214
1215    assert!(
1216        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1217        "SetBreakpoint request must be called after editor is saved"
1218    );
1219}
1220
1221#[gpui::test]
1222async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1223    executor: BackgroundExecutor,
1224    cx: &mut TestAppContext,
1225) {
1226    init_test(cx);
1227
1228    let fs = FakeFs::new(executor.clone());
1229
1230    fs.insert_tree(
1231        path!("/project"),
1232        json!({
1233            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1234            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1235            "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1236        }),
1237    )
1238    .await;
1239
1240    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1241    let workspace = init_test_workspace(&project, cx).await;
1242    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1243    let project_path = Path::new(path!("/project"));
1244    let worktree = project
1245        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1246        .expect("This worktree should exist in project")
1247        .0;
1248
1249    let worktree_id = workspace
1250        .update(cx, |_, _, cx| worktree.read(cx).id())
1251        .unwrap();
1252
1253    let first = project
1254        .update(cx, |project, cx| {
1255            project.open_buffer((worktree_id, "main.rs"), cx)
1256        })
1257        .await
1258        .unwrap();
1259
1260    let second = project
1261        .update(cx, |project, cx| {
1262            project.open_buffer((worktree_id, "second.rs"), cx)
1263        })
1264        .await
1265        .unwrap();
1266
1267    let (first_editor, cx) = cx.add_window_view(|window, cx| {
1268        Editor::new(
1269            EditorMode::full(),
1270            MultiBuffer::build_from_buffer(first, cx),
1271            Some(project.clone()),
1272            window,
1273            cx,
1274        )
1275    });
1276
1277    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1278        Editor::new(
1279            EditorMode::full(),
1280            MultiBuffer::build_from_buffer(second, cx),
1281            Some(project.clone()),
1282            window,
1283            cx,
1284        )
1285    });
1286
1287    first_editor.update_in(cx, |editor, window, cx| {
1288        editor.move_down(&actions::MoveDown, window, cx);
1289        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1290        editor.move_down(&actions::MoveDown, window, cx);
1291        editor.move_down(&actions::MoveDown, window, cx);
1292        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1293    });
1294
1295    second_editor.update_in(cx, |editor, window, cx| {
1296        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1297        editor.move_down(&actions::MoveDown, window, cx);
1298        editor.move_down(&actions::MoveDown, window, cx);
1299        editor.move_down(&actions::MoveDown, window, cx);
1300        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1301    });
1302
1303    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1304    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1305
1306    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1307
1308    client.on_request::<SetBreakpoints, _>({
1309        let called_set_breakpoints = called_set_breakpoints.clone();
1310        move |_, args| {
1311            assert!(
1312                args.breakpoints.is_none_or(|bps| bps.is_empty()),
1313                "Send empty breakpoint sets to clear them from DAP servers"
1314            );
1315
1316            match args
1317                .source
1318                .path
1319                .expect("We should always send a breakpoint's path")
1320                .as_str()
1321            {
1322                "/project/main.rs" | "/project/second.rs" => {}
1323                _ => {
1324                    panic!("Unset breakpoints for path that doesn't have any")
1325                }
1326            }
1327
1328            called_set_breakpoints.store(true, Ordering::SeqCst);
1329
1330            Ok(dap::SetBreakpointsResponse {
1331                breakpoints: Vec::default(),
1332            })
1333        }
1334    });
1335
1336    cx.dispatch_action(crate::ClearAllBreakpoints);
1337    cx.run_until_parked();
1338}
1339
1340#[gpui::test]
1341async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1342    executor: BackgroundExecutor,
1343    cx: &mut TestAppContext,
1344) {
1345    init_test(cx);
1346
1347    let fs = FakeFs::new(executor.clone());
1348
1349    fs.insert_tree(
1350        "/project",
1351        json!({
1352            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1353        }),
1354    )
1355    .await;
1356
1357    let project = Project::test(fs, ["/project".as_ref()], cx).await;
1358    let workspace = init_test_workspace(&project, cx).await;
1359    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1360
1361    start_debug_session(&workspace, cx, |client| {
1362        client.on_request::<dap::requests::Initialize, _>(|_, _| {
1363            Err(ErrorResponse {
1364                error: Some(Message {
1365                    format: "failed to launch".to_string(),
1366                    id: 1,
1367                    variables: None,
1368                    send_telemetry: None,
1369                    show_user: None,
1370                    url: None,
1371                    url_label: None,
1372                }),
1373            })
1374        });
1375    })
1376    .ok();
1377
1378    cx.run_until_parked();
1379
1380    project.update(cx, |project, cx| {
1381        assert!(
1382            project.dap_store().read(cx).sessions().count() == 0,
1383            "Session wouldn't exist if it was shutdown"
1384        );
1385    });
1386}