debugger_panel.rs

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