debugger_panel.rs

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