debugger_panel.rs

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