debugger_panel.rs

   1#![expect(clippy::result_large_err)]
   2use crate::{
   3    persistence::DebuggerPaneItem,
   4    tests::{start_debug_session, start_debug_session_with},
   5    *,
   6};
   7use dap::{
   8    ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
   9    StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
  10    adapters::DebugTaskDefinition,
  11    client::SessionId,
  12    requests::{
  13        Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
  14        StartDebugging, StepBack, StepIn, StepOut, Threads,
  15    },
  16};
  17use editor::{
  18    ActiveDebugLine, Editor, EditorMode, MultiBuffer,
  19    actions::{self},
  20};
  21use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
  22use project::{
  23    FakeFs, Project,
  24    debugger::session::{ThreadId, ThreadStatus},
  25};
  26use serde_json::json;
  27use std::{
  28    path::Path,
  29    sync::{
  30        Arc,
  31        atomic::{AtomicBool, AtomicUsize, Ordering},
  32    },
  33};
  34use terminal_view::terminal_panel::TerminalPanel;
  35use tests::{active_debug_session_panel, init_test, init_test_workspace};
  36use util::{path, rel_path::rel_path};
  37use workspace::item::SaveOptions;
  38use workspace::pane_group::SplitDirection;
  39use workspace::{Item, dock::Panel, move_active_item};
  40
  41#[gpui::test]
  42async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
  43    init_test(cx);
  44
  45    let fs = FakeFs::new(executor.clone());
  46
  47    fs.insert_tree(
  48        path!("/project"),
  49        json!({
  50            "main.rs": "First line\nSecond line\nThird line\nFourth line",
  51        }),
  52    )
  53    .await;
  54
  55    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
  56    let workspace = init_test_workspace(&project, cx).await;
  57    let cx = &mut VisualTestContext::from_window(*workspace, cx);
  58
  59    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
  60    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
  61
  62    client.on_request::<Threads, _>(move |_, _| {
  63        Ok(dap::ThreadsResponse {
  64            threads: vec![dap::Thread {
  65                id: 1,
  66                name: "Thread 1".into(),
  67            }],
  68        })
  69    });
  70
  71    client.on_request::<StackTrace, _>(move |_, _| {
  72        Ok(dap::StackTraceResponse {
  73            stack_frames: Vec::default(),
  74            total_frames: None,
  75        })
  76    });
  77
  78    cx.run_until_parked();
  79
  80    // assert we have a debug panel item before the session has stopped
  81    workspace
  82        .update(cx, |workspace, _window, cx| {
  83            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
  84            let active_session =
  85                debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
  86
  87            let running_state = active_session.update(cx, |active_session, _| {
  88                active_session.running_state().clone()
  89            });
  90
  91            debug_panel.update(cx, |this, cx| {
  92                assert!(this.active_session().is_some());
  93                assert!(running_state.read(cx).selected_thread_id().is_none());
  94            });
  95        })
  96        .unwrap();
  97
  98    client
  99        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 100            reason: dap::StoppedEventReason::Pause,
 101            description: None,
 102            thread_id: Some(1),
 103            preserve_focus_hint: None,
 104            text: None,
 105            all_threads_stopped: None,
 106            hit_breakpoint_ids: None,
 107        }))
 108        .await;
 109
 110    cx.run_until_parked();
 111
 112    workspace
 113        .update(cx, |workspace, _window, cx| {
 114            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 115            let active_session = debug_panel
 116                .update(cx, |this, _| this.active_session())
 117                .unwrap();
 118
 119            let running_state = active_session.update(cx, |active_session, _| {
 120                active_session.running_state().clone()
 121            });
 122
 123            assert_eq!(client.id(), running_state.read(cx).session_id());
 124            assert_eq!(
 125                ThreadId(1),
 126                running_state.read(cx).selected_thread_id().unwrap()
 127            );
 128        })
 129        .unwrap();
 130
 131    let shutdown_session = project.update(cx, |project, cx| {
 132        project.dap_store().update(cx, |dap_store, cx| {
 133            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 134        })
 135    });
 136
 137    shutdown_session.await.unwrap();
 138
 139    // assert we still have a debug panel item after the client shutdown
 140    workspace
 141        .update(cx, |workspace, _window, cx| {
 142            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 143
 144            let active_session = debug_panel
 145                .update(cx, |this, _| this.active_session())
 146                .unwrap();
 147
 148            let running_state = active_session.update(cx, |active_session, _| {
 149                active_session.running_state().clone()
 150            });
 151
 152            debug_panel.update(cx, |this, cx| {
 153                assert!(this.active_session().is_some());
 154                assert_eq!(
 155                    ThreadId(1),
 156                    running_state.read(cx).selected_thread_id().unwrap()
 157                );
 158            });
 159        })
 160        .unwrap();
 161}
 162
 163#[gpui::test]
 164async fn test_we_can_only_have_one_panel_per_debug_session(
 165    executor: BackgroundExecutor,
 166    cx: &mut TestAppContext,
 167) {
 168    init_test(cx);
 169
 170    let fs = FakeFs::new(executor.clone());
 171
 172    fs.insert_tree(
 173        path!("/project"),
 174        json!({
 175            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 176        }),
 177    )
 178    .await;
 179
 180    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 181    let workspace = init_test_workspace(&project, cx).await;
 182    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 183
 184    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 185    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 186
 187    client.on_request::<Threads, _>(move |_, _| {
 188        Ok(dap::ThreadsResponse {
 189            threads: vec![dap::Thread {
 190                id: 1,
 191                name: "Thread 1".into(),
 192            }],
 193        })
 194    });
 195
 196    client.on_request::<StackTrace, _>(move |_, _| {
 197        Ok(dap::StackTraceResponse {
 198            stack_frames: Vec::default(),
 199            total_frames: None,
 200        })
 201    });
 202
 203    cx.run_until_parked();
 204
 205    // assert we have a debug panel item before the session has stopped
 206    workspace
 207        .update(cx, |workspace, _window, cx| {
 208            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 209
 210            debug_panel.update(cx, |this, _| {
 211                assert!(this.active_session().is_some());
 212            });
 213        })
 214        .unwrap();
 215
 216    client
 217        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 218            reason: dap::StoppedEventReason::Pause,
 219            description: None,
 220            thread_id: Some(1),
 221            preserve_focus_hint: None,
 222            text: None,
 223            all_threads_stopped: None,
 224            hit_breakpoint_ids: None,
 225        }))
 226        .await;
 227
 228    cx.run_until_parked();
 229
 230    // assert we added a debug panel item
 231    workspace
 232        .update(cx, |workspace, _window, cx| {
 233            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 234            let active_session = debug_panel
 235                .update(cx, |this, _| this.active_session())
 236                .unwrap();
 237
 238            let running_state = active_session.update(cx, |active_session, _| {
 239                active_session.running_state().clone()
 240            });
 241
 242            assert_eq!(client.id(), active_session.read(cx).session_id(cx));
 243            assert_eq!(
 244                ThreadId(1),
 245                running_state.read(cx).selected_thread_id().unwrap()
 246            );
 247        })
 248        .unwrap();
 249
 250    client
 251        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 252            reason: dap::StoppedEventReason::Pause,
 253            description: None,
 254            thread_id: Some(2),
 255            preserve_focus_hint: None,
 256            text: None,
 257            all_threads_stopped: None,
 258            hit_breakpoint_ids: None,
 259        }))
 260        .await;
 261
 262    cx.run_until_parked();
 263
 264    workspace
 265        .update(cx, |workspace, _window, cx| {
 266            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 267            let active_session = debug_panel
 268                .update(cx, |this, _| this.active_session())
 269                .unwrap();
 270
 271            let running_state = active_session.update(cx, |active_session, _| {
 272                active_session.running_state().clone()
 273            });
 274
 275            assert_eq!(client.id(), active_session.read(cx).session_id(cx));
 276            assert_eq!(
 277                ThreadId(1),
 278                running_state.read(cx).selected_thread_id().unwrap()
 279            );
 280        })
 281        .unwrap();
 282
 283    let shutdown_session = project.update(cx, |project, cx| {
 284        project.dap_store().update(cx, |dap_store, cx| {
 285            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 286        })
 287    });
 288
 289    shutdown_session.await.unwrap();
 290
 291    // assert we still have a debug panel item after the client shutdown
 292    workspace
 293        .update(cx, |workspace, _window, cx| {
 294            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 295            let active_session = debug_panel
 296                .update(cx, |this, _| this.active_session())
 297                .unwrap();
 298
 299            let running_state = active_session.update(cx, |active_session, _| {
 300                active_session.running_state().clone()
 301            });
 302
 303            debug_panel.update(cx, |this, cx| {
 304                assert!(this.active_session().is_some());
 305                assert_eq!(
 306                    ThreadId(1),
 307                    running_state.read(cx).selected_thread_id().unwrap()
 308                );
 309            });
 310        })
 311        .unwrap();
 312}
 313
 314#[gpui::test]
 315async fn test_handle_successful_run_in_terminal_reverse_request(
 316    executor: BackgroundExecutor,
 317    cx: &mut TestAppContext,
 318) {
 319    // needed because the debugger launches a terminal which starts a background PTY
 320    cx.executor().allow_parking();
 321    init_test(cx);
 322
 323    let send_response = Arc::new(AtomicBool::new(false));
 324
 325    let fs = FakeFs::new(executor.clone());
 326
 327    fs.insert_tree(
 328        path!("/project"),
 329        json!({
 330            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 331        }),
 332    )
 333    .await;
 334
 335    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 336    let workspace = init_test_workspace(&project, cx).await;
 337    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 338
 339    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 340    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 341
 342    client
 343        .on_response::<RunInTerminal, _>({
 344            let send_response = send_response.clone();
 345            move |response| {
 346                send_response.store(true, Ordering::SeqCst);
 347
 348                assert!(response.success);
 349                assert!(response.body.is_some());
 350            }
 351        })
 352        .await;
 353
 354    client
 355        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 356            kind: None,
 357            title: None,
 358            cwd: std::env::temp_dir().to_string_lossy().into_owned(),
 359            args: vec![],
 360            env: None,
 361            args_can_be_interpreted_by_shell: None,
 362        })
 363        .await;
 364
 365    cx.run_until_parked();
 366
 367    assert!(
 368        send_response.load(std::sync::atomic::Ordering::SeqCst),
 369        "Expected to receive response from reverse request"
 370    );
 371
 372    workspace
 373        .update(cx, |workspace, _window, cx| {
 374            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 375            let session = debug_panel.read(cx).active_session().unwrap();
 376            let running = session.read(cx).running_state();
 377            assert_eq!(
 378                running
 379                    .read(cx)
 380                    .pane_items_status(cx)
 381                    .get(&DebuggerPaneItem::Terminal),
 382                Some(&true)
 383            );
 384            assert!(running.read(cx).debug_terminal.read(cx).terminal.is_some());
 385        })
 386        .unwrap();
 387}
 388
 389#[gpui::test]
 390async fn test_handle_start_debugging_request(
 391    executor: BackgroundExecutor,
 392    cx: &mut TestAppContext,
 393) {
 394    init_test(cx);
 395
 396    let fs = FakeFs::new(executor.clone());
 397
 398    fs.insert_tree(
 399        path!("/project"),
 400        json!({
 401            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 402        }),
 403    )
 404    .await;
 405
 406    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 407    let workspace = init_test_workspace(&project, cx).await;
 408    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 409
 410    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 411    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 412
 413    let fake_config = json!({"one": "two"});
 414    let launched_with = Arc::new(parking_lot::Mutex::new(None));
 415
 416    let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
 417        let launched_with = launched_with.clone();
 418        move |client| {
 419            let launched_with = launched_with.clone();
 420            client.on_request::<dap::requests::Launch, _>(move |_, args| {
 421                launched_with.lock().replace(args.raw);
 422                Ok(())
 423            });
 424            client.on_request::<dap::requests::Attach, _>(move |_, _| {
 425                assert!(false, "should not get attach request");
 426                Ok(())
 427            });
 428        }
 429    });
 430
 431    let sessions = workspace
 432        .update(cx, |workspace, _window, cx| {
 433            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 434            debug_panel.read(cx).sessions().collect::<Vec<_>>()
 435        })
 436        .unwrap();
 437    assert_eq!(sessions.len(), 1);
 438    client
 439        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 440            request: StartDebuggingRequestArgumentsRequest::Launch,
 441            configuration: fake_config.clone(),
 442        })
 443        .await;
 444
 445    cx.run_until_parked();
 446
 447    workspace
 448        .update(cx, |workspace, _window, cx| {
 449            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 450
 451            // Active session changes on spawn, as the parent has never stopped.
 452            let active_session = debug_panel
 453                .read(cx)
 454                .active_session()
 455                .unwrap()
 456                .read(cx)
 457                .session(cx);
 458            let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
 459            assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
 460            assert_eq!(
 461                active_session.read(cx).parent_session(),
 462                Some(&current_sessions[0].read(cx).session(cx))
 463            );
 464
 465            assert_eq!(current_sessions.len(), 2);
 466            assert_eq!(current_sessions[0], sessions[0]);
 467
 468            let parent_session = current_sessions[1]
 469                .read(cx)
 470                .session(cx)
 471                .read(cx)
 472                .parent_session()
 473                .unwrap();
 474            assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
 475
 476            // We should preserve the original binary (params to spawn process etc.) except for launch params
 477            // (as they come from reverse spawn request).
 478            let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
 479            original_binary.request_args = StartDebuggingRequestArguments {
 480                request: StartDebuggingRequestArgumentsRequest::Launch,
 481                configuration: fake_config.clone(),
 482            };
 483
 484            assert_eq!(
 485                current_sessions[1]
 486                    .read(cx)
 487                    .session(cx)
 488                    .read(cx)
 489                    .binary()
 490                    .unwrap(),
 491                &original_binary
 492            );
 493        })
 494        .unwrap();
 495
 496    assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
 497}
 498
 499// // covers that we always send a response back, if something when wrong,
 500// // while spawning the terminal
 501#[gpui::test]
 502async fn test_handle_error_run_in_terminal_reverse_request(
 503    executor: BackgroundExecutor,
 504    cx: &mut TestAppContext,
 505) {
 506    init_test(cx);
 507
 508    let send_response = Arc::new(AtomicBool::new(false));
 509
 510    let fs = FakeFs::new(executor.clone());
 511
 512    fs.insert_tree(
 513        path!("/project"),
 514        json!({
 515            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 516        }),
 517    )
 518    .await;
 519
 520    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 521    let workspace = init_test_workspace(&project, cx).await;
 522    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 523
 524    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 525    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 526
 527    client
 528        .on_response::<RunInTerminal, _>({
 529            let send_response = send_response.clone();
 530            move |response| {
 531                send_response.store(true, Ordering::SeqCst);
 532
 533                assert!(!response.success);
 534                assert!(response.body.is_some());
 535            }
 536        })
 537        .await;
 538
 539    client
 540        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 541            kind: None,
 542            title: None,
 543            cwd: "".into(),
 544            args: vec!["oops".into(), "oops".into()],
 545            env: None,
 546            args_can_be_interpreted_by_shell: None,
 547        })
 548        .await;
 549
 550    cx.run_until_parked();
 551
 552    assert!(
 553        send_response.load(std::sync::atomic::Ordering::SeqCst),
 554        "Expected to receive response from reverse request"
 555    );
 556
 557    workspace
 558        .update(cx, |workspace, _window, cx| {
 559            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
 560
 561            assert_eq!(
 562                0,
 563                terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
 564            );
 565        })
 566        .unwrap();
 567}
 568
 569#[gpui::test]
 570async fn test_handle_start_debugging_reverse_request(
 571    executor: BackgroundExecutor,
 572    cx: &mut TestAppContext,
 573) {
 574    cx.executor().allow_parking();
 575    init_test(cx);
 576
 577    let send_response = Arc::new(AtomicBool::new(false));
 578
 579    let fs = FakeFs::new(executor.clone());
 580
 581    fs.insert_tree(
 582        path!("/project"),
 583        json!({
 584            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 585        }),
 586    )
 587    .await;
 588
 589    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 590    let workspace = init_test_workspace(&project, cx).await;
 591    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 592
 593    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 594    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 595
 596    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 597        Ok(dap::ThreadsResponse {
 598            threads: vec![dap::Thread {
 599                id: 1,
 600                name: "Thread 1".into(),
 601            }],
 602        })
 603    });
 604
 605    client
 606        .on_response::<StartDebugging, _>({
 607            let send_response = send_response.clone();
 608            move |response| {
 609                send_response.store(true, Ordering::SeqCst);
 610
 611                assert!(response.success);
 612                assert!(response.body.is_some());
 613            }
 614        })
 615        .await;
 616    // Set up handlers for sessions spawned with reverse request too.
 617    let _reverse_request_subscription =
 618        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 619    client
 620        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 621            configuration: json!({}),
 622            request: StartDebuggingRequestArgumentsRequest::Launch,
 623        })
 624        .await;
 625
 626    cx.run_until_parked();
 627
 628    let child_session = project.update(cx, |project, cx| {
 629        project
 630            .dap_store()
 631            .read(cx)
 632            .session_by_id(SessionId(1))
 633            .unwrap()
 634    });
 635    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
 636
 637    child_client.on_request::<dap::requests::Threads, _>(move |_, _| {
 638        Ok(dap::ThreadsResponse {
 639            threads: vec![dap::Thread {
 640                id: 1,
 641                name: "Thread 1".into(),
 642            }],
 643        })
 644    });
 645
 646    child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 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
 668#[gpui::test]
 669async fn test_shutdown_children_when_parent_session_shutdown(
 670    executor: BackgroundExecutor,
 671    cx: &mut TestAppContext,
 672) {
 673    init_test(cx);
 674
 675    let fs = FakeFs::new(executor.clone());
 676
 677    fs.insert_tree(
 678        path!("/project"),
 679        json!({
 680            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 681        }),
 682    )
 683    .await;
 684
 685    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 686    let dap_store = project.update(cx, |project, _| project.dap_store());
 687    let workspace = init_test_workspace(&project, cx).await;
 688    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 689
 690    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 691    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 692
 693    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 694        Ok(dap::ThreadsResponse {
 695            threads: vec![dap::Thread {
 696                id: 1,
 697                name: "Thread 1".into(),
 698            }],
 699        })
 700    });
 701
 702    client.on_response::<StartDebugging, _>(move |_| {}).await;
 703    // Set up handlers for sessions spawned with reverse request too.
 704    let _reverse_request_subscription =
 705        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 706    // start first child session
 707    client
 708        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 709            configuration: json!({}),
 710            request: StartDebuggingRequestArgumentsRequest::Launch,
 711        })
 712        .await;
 713
 714    cx.run_until_parked();
 715
 716    // start second child session
 717    client
 718        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 719            configuration: json!({}),
 720            request: StartDebuggingRequestArgumentsRequest::Launch,
 721        })
 722        .await;
 723
 724    cx.run_until_parked();
 725
 726    // configure first child session
 727    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 728        dap_store.session_by_id(SessionId(1)).unwrap()
 729    });
 730    let first_child_client =
 731        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 732
 733    first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 734
 735    // configure second child session
 736    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 737        dap_store.session_by_id(SessionId(2)).unwrap()
 738    });
 739    let second_child_client =
 740        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 741
 742    second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 743
 744    cx.run_until_parked();
 745
 746    // shutdown parent session
 747    dap_store
 748        .update(cx, |dap_store, cx| {
 749            dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
 750        })
 751        .await
 752        .unwrap();
 753
 754    // assert parent session and all children sessions are shutdown
 755    dap_store.update(cx, |dap_store, cx| {
 756        assert!(
 757            dap_store
 758                .session_by_id(parent_session.read(cx).session_id())
 759                .is_none()
 760        );
 761        assert!(
 762            dap_store
 763                .session_by_id(first_child_session.read(cx).session_id())
 764                .is_none()
 765        );
 766        assert!(
 767            dap_store
 768                .session_by_id(second_child_session.read(cx).session_id())
 769                .is_none()
 770        );
 771    });
 772}
 773
 774#[gpui::test]
 775async fn test_shutdown_parent_session_if_all_children_are_shutdown(
 776    executor: BackgroundExecutor,
 777    cx: &mut TestAppContext,
 778) {
 779    init_test(cx);
 780
 781    let fs = FakeFs::new(executor.clone());
 782
 783    fs.insert_tree(
 784        path!("/project"),
 785        json!({
 786            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 787        }),
 788    )
 789    .await;
 790
 791    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 792    let dap_store = project.update(cx, |project, _| project.dap_store());
 793    let workspace = init_test_workspace(&project, cx).await;
 794    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 795
 796    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 797    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 798
 799    client.on_response::<StartDebugging, _>(move |_| {}).await;
 800    // Set up handlers for sessions spawned with reverse request too.
 801    let _reverse_request_subscription =
 802        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 803    // start first child session
 804    client
 805        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 806            configuration: json!({}),
 807            request: StartDebuggingRequestArgumentsRequest::Launch,
 808        })
 809        .await;
 810
 811    cx.run_until_parked();
 812
 813    // start second child session
 814    client
 815        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 816            configuration: json!({}),
 817            request: StartDebuggingRequestArgumentsRequest::Launch,
 818        })
 819        .await;
 820
 821    cx.run_until_parked();
 822
 823    // configure first child session
 824    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 825        dap_store.session_by_id(SessionId(1)).unwrap()
 826    });
 827    let first_child_client =
 828        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 829
 830    first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 831
 832    // configure second child session
 833    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 834        dap_store.session_by_id(SessionId(2)).unwrap()
 835    });
 836    let second_child_client =
 837        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 838
 839    second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 840
 841    cx.run_until_parked();
 842
 843    // shutdown first child session
 844    dap_store
 845        .update(cx, |dap_store, cx| {
 846            dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
 847        })
 848        .await
 849        .unwrap();
 850
 851    // assert parent session and second child session still exist
 852    dap_store.update(cx, |dap_store, cx| {
 853        assert!(
 854            dap_store
 855                .session_by_id(parent_session.read(cx).session_id())
 856                .is_some()
 857        );
 858        assert!(
 859            dap_store
 860                .session_by_id(first_child_session.read(cx).session_id())
 861                .is_none()
 862        );
 863        assert!(
 864            dap_store
 865                .session_by_id(second_child_session.read(cx).session_id())
 866                .is_some()
 867        );
 868    });
 869
 870    // shutdown first child session
 871    dap_store
 872        .update(cx, |dap_store, cx| {
 873            dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
 874        })
 875        .await
 876        .unwrap();
 877
 878    // assert parent session got shutdown by second child session
 879    // because it was the last child
 880    dap_store.update(cx, |dap_store, cx| {
 881        assert!(
 882            dap_store
 883                .session_by_id(parent_session.read(cx).session_id())
 884                .is_none()
 885        );
 886        assert!(
 887            dap_store
 888                .session_by_id(second_child_session.read(cx).session_id())
 889                .is_none()
 890        );
 891    });
 892}
 893
 894#[gpui::test]
 895async fn test_debug_panel_item_thread_status_reset_on_failure(
 896    executor: BackgroundExecutor,
 897    cx: &mut TestAppContext,
 898) {
 899    init_test(cx);
 900
 901    let fs = FakeFs::new(executor.clone());
 902
 903    fs.insert_tree(
 904        path!("/project"),
 905        json!({
 906            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 907        }),
 908    )
 909    .await;
 910
 911    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 912    let workspace = init_test_workspace(&project, cx).await;
 913    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 914
 915    let session = start_debug_session(&workspace, cx, |client| {
 916        client.on_request::<dap::requests::Initialize, _>(move |_, _| {
 917            Ok(dap::Capabilities {
 918                supports_step_back: Some(true),
 919                ..Default::default()
 920            })
 921        });
 922    })
 923    .unwrap();
 924
 925    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 926    const THREAD_ID_NUM: i64 = 1;
 927
 928    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 929        Ok(dap::ThreadsResponse {
 930            threads: vec![dap::Thread {
 931                id: THREAD_ID_NUM,
 932                name: "Thread 1".into(),
 933            }],
 934        })
 935    });
 936
 937    client.on_request::<Launch, _>(move |_, _| Ok(()));
 938
 939    client.on_request::<StackTrace, _>(move |_, _| {
 940        Ok(dap::StackTraceResponse {
 941            stack_frames: Vec::default(),
 942            total_frames: None,
 943        })
 944    });
 945
 946    client.on_request::<Next, _>(move |_, _| {
 947        Err(ErrorResponse {
 948            error: Some(dap::Message {
 949                id: 1,
 950                format: "error".into(),
 951                variables: None,
 952                send_telemetry: None,
 953                show_user: None,
 954                url: None,
 955                url_label: None,
 956            }),
 957        })
 958    });
 959
 960    client.on_request::<StepOut, _>(move |_, _| {
 961        Err(ErrorResponse {
 962            error: Some(dap::Message {
 963                id: 1,
 964                format: "error".into(),
 965                variables: None,
 966                send_telemetry: None,
 967                show_user: None,
 968                url: None,
 969                url_label: None,
 970            }),
 971        })
 972    });
 973
 974    client.on_request::<StepIn, _>(move |_, _| {
 975        Err(ErrorResponse {
 976            error: Some(dap::Message {
 977                id: 1,
 978                format: "error".into(),
 979                variables: None,
 980                send_telemetry: None,
 981                show_user: None,
 982                url: None,
 983                url_label: None,
 984            }),
 985        })
 986    });
 987
 988    client.on_request::<StepBack, _>(move |_, _| {
 989        Err(ErrorResponse {
 990            error: Some(dap::Message {
 991                id: 1,
 992                format: "error".into(),
 993                variables: None,
 994                send_telemetry: None,
 995                show_user: None,
 996                url: None,
 997                url_label: None,
 998            }),
 999        })
1000    });
1001
1002    client.on_request::<Continue, _>(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
1016    client
1017        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1018            reason: dap::StoppedEventReason::Pause,
1019            description: None,
1020            thread_id: Some(1),
1021            preserve_focus_hint: None,
1022            text: None,
1023            all_threads_stopped: None,
1024            hit_breakpoint_ids: None,
1025        }))
1026        .await;
1027
1028    cx.run_until_parked();
1029
1030    let running_state = active_debug_session_panel(workspace, cx)
1031        .read_with(cx, |item, _| item.running_state().clone());
1032
1033    cx.run_until_parked();
1034    let thread_id = ThreadId(1);
1035
1036    for operation in &[
1037        "step_over",
1038        "continue_thread",
1039        "step_back",
1040        "step_in",
1041        "step_out",
1042    ] {
1043        running_state.update(cx, |running_state, cx| match *operation {
1044            "step_over" => running_state.step_over(cx),
1045            "continue_thread" => running_state.continue_thread(cx),
1046            "step_back" => running_state.step_back(cx),
1047            "step_in" => running_state.step_in(cx),
1048            "step_out" => running_state.step_out(cx),
1049            _ => unreachable!(),
1050        });
1051
1052        // Check that we step the thread status to the correct intermediate state
1053        running_state.update(cx, |running_state, cx| {
1054            assert_eq!(
1055                running_state
1056                    .thread_status(cx)
1057                    .expect("There should be an active thread selected"),
1058                match *operation {
1059                    "continue_thread" => ThreadStatus::Running,
1060                    _ => ThreadStatus::Stepping,
1061                },
1062                "Thread status was not set to correct intermediate state after {} request",
1063                operation
1064            );
1065        });
1066
1067        cx.run_until_parked();
1068
1069        running_state.update(cx, |running_state, cx| {
1070            assert_eq!(
1071                running_state
1072                    .thread_status(cx)
1073                    .expect("There should be an active thread selected"),
1074                ThreadStatus::Stopped,
1075                "Thread status not reset to Stopped after failed {}",
1076                operation
1077            );
1078
1079            // update state to running, so we can test it actually changes the status back to stopped
1080            running_state
1081                .session()
1082                .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1083        });
1084    }
1085}
1086
1087#[gpui::test]
1088async fn test_send_breakpoints_when_editor_has_been_saved(
1089    executor: BackgroundExecutor,
1090    cx: &mut TestAppContext,
1091) {
1092    init_test(cx);
1093
1094    let fs = FakeFs::new(executor.clone());
1095
1096    fs.insert_tree(
1097        path!("/project"),
1098        json!({
1099            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1100        }),
1101    )
1102    .await;
1103
1104    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1105    let workspace = init_test_workspace(&project, cx).await;
1106    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1107    let project_path = Path::new(path!("/project"));
1108    let worktree = project
1109        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1110        .expect("This worktree should exist in project")
1111        .0;
1112
1113    let worktree_id = workspace
1114        .update(cx, |_, _, cx| worktree.read(cx).id())
1115        .unwrap();
1116
1117    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1118    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1119
1120    let buffer = project
1121        .update(cx, |project, cx| {
1122            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1123        })
1124        .await
1125        .unwrap();
1126
1127    let (editor, cx) = cx.add_window_view(|window, cx| {
1128        Editor::new(
1129            EditorMode::full(),
1130            MultiBuffer::build_from_buffer(buffer, cx),
1131            Some(project.clone()),
1132            window,
1133            cx,
1134        )
1135    });
1136
1137    client.on_request::<Launch, _>(move |_, _| Ok(()));
1138
1139    client.on_request::<StackTrace, _>(move |_, _| {
1140        Ok(dap::StackTraceResponse {
1141            stack_frames: Vec::default(),
1142            total_frames: None,
1143        })
1144    });
1145
1146    client
1147        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1148            reason: dap::StoppedEventReason::Pause,
1149            description: None,
1150            thread_id: Some(1),
1151            preserve_focus_hint: None,
1152            text: None,
1153            all_threads_stopped: None,
1154            hit_breakpoint_ids: None,
1155        }))
1156        .await;
1157
1158    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1159    client.on_request::<SetBreakpoints, _>({
1160        let called_set_breakpoints = called_set_breakpoints.clone();
1161        move |_, args| {
1162            assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1163            assert_eq!(
1164                vec![SourceBreakpoint {
1165                    line: 2,
1166                    column: None,
1167                    condition: None,
1168                    hit_condition: None,
1169                    log_message: None,
1170                    mode: None
1171                }],
1172                args.breakpoints.unwrap()
1173            );
1174            assert!(!args.source_modified.unwrap());
1175
1176            called_set_breakpoints.store(true, Ordering::SeqCst);
1177
1178            Ok(dap::SetBreakpointsResponse {
1179                breakpoints: Vec::default(),
1180            })
1181        }
1182    });
1183
1184    editor.update_in(cx, |editor, window, cx| {
1185        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1186        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1187    });
1188
1189    cx.run_until_parked();
1190
1191    assert!(
1192        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1193        "SetBreakpoint request must be called"
1194    );
1195
1196    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1197    client.on_request::<SetBreakpoints, _>({
1198        let called_set_breakpoints = called_set_breakpoints.clone();
1199        move |_, args| {
1200            assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1201            assert_eq!(
1202                vec![SourceBreakpoint {
1203                    line: 3,
1204                    column: None,
1205                    condition: None,
1206                    hit_condition: None,
1207                    log_message: None,
1208                    mode: None
1209                }],
1210                args.breakpoints.unwrap()
1211            );
1212            assert!(args.source_modified.unwrap());
1213
1214            called_set_breakpoints.store(true, Ordering::SeqCst);
1215
1216            Ok(dap::SetBreakpointsResponse {
1217                breakpoints: Vec::default(),
1218            })
1219        }
1220    });
1221
1222    editor.update_in(cx, |editor, window, cx| {
1223        editor.move_up(&zed_actions::editor::MoveUp, window, cx);
1224        editor.insert("new text\n", window, cx);
1225    });
1226
1227    editor
1228        .update_in(cx, |editor, window, cx| {
1229            editor.save(
1230                SaveOptions {
1231                    format: true,
1232                    force_format: false,
1233                    autosave: false,
1234                },
1235                project.clone(),
1236                window,
1237                cx,
1238            )
1239        })
1240        .await
1241        .unwrap();
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 after editor is saved"
1248    );
1249}
1250
1251#[gpui::test]
1252async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1253    executor: BackgroundExecutor,
1254    cx: &mut TestAppContext,
1255) {
1256    init_test(cx);
1257
1258    let fs = FakeFs::new(executor.clone());
1259
1260    fs.insert_tree(
1261        path!("/project"),
1262        json!({
1263            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1264            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1265            "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1266        }),
1267    )
1268    .await;
1269
1270    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1271    let workspace = init_test_workspace(&project, cx).await;
1272    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1273    let project_path = Path::new(path!("/project"));
1274    let worktree = project
1275        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1276        .expect("This worktree should exist in project")
1277        .0;
1278
1279    let worktree_id = workspace
1280        .update(cx, |_, _, cx| worktree.read(cx).id())
1281        .unwrap();
1282
1283    let first = project
1284        .update(cx, |project, cx| {
1285            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1286        })
1287        .await
1288        .unwrap();
1289
1290    let second = project
1291        .update(cx, |project, cx| {
1292            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1293        })
1294        .await
1295        .unwrap();
1296
1297    let (first_editor, cx) = cx.add_window_view(|window, cx| {
1298        Editor::new(
1299            EditorMode::full(),
1300            MultiBuffer::build_from_buffer(first, cx),
1301            Some(project.clone()),
1302            window,
1303            cx,
1304        )
1305    });
1306
1307    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1308        Editor::new(
1309            EditorMode::full(),
1310            MultiBuffer::build_from_buffer(second, cx),
1311            Some(project.clone()),
1312            window,
1313            cx,
1314        )
1315    });
1316
1317    first_editor.update_in(cx, |editor, window, cx| {
1318        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1319        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1320        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1321        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1322        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1323    });
1324
1325    second_editor.update_in(cx, |editor, window, cx| {
1326        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1327        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1328        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1329        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1330        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1331    });
1332
1333    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1334    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1335
1336    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1337
1338    client.on_request::<SetBreakpoints, _>({
1339        move |_, args| {
1340            assert!(
1341                args.breakpoints.is_none_or(|bps| bps.is_empty()),
1342                "Send empty breakpoint sets to clear them from DAP servers"
1343            );
1344
1345            match args
1346                .source
1347                .path
1348                .expect("We should always send a breakpoint's path")
1349                .as_str()
1350            {
1351                path!("/project/main.rs") | path!("/project/second.rs") => {}
1352                _ => {
1353                    panic!("Unset breakpoints for path that doesn't have any")
1354                }
1355            }
1356
1357            called_set_breakpoints.store(true, Ordering::SeqCst);
1358
1359            Ok(dap::SetBreakpointsResponse {
1360                breakpoints: Vec::default(),
1361            })
1362        }
1363    });
1364
1365    cx.dispatch_action(crate::ClearAllBreakpoints);
1366    cx.run_until_parked();
1367}
1368
1369#[gpui::test]
1370async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1371    executor: BackgroundExecutor,
1372    cx: &mut TestAppContext,
1373) {
1374    init_test(cx);
1375
1376    let fs = FakeFs::new(executor.clone());
1377
1378    fs.insert_tree(
1379        path!("/project"),
1380        json!({
1381            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1382        }),
1383    )
1384    .await;
1385
1386    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1387    let workspace = init_test_workspace(&project, cx).await;
1388    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1389
1390    start_debug_session(&workspace, cx, |client| {
1391        client.on_request::<dap::requests::Initialize, _>(|_, _| {
1392            Err(ErrorResponse {
1393                error: Some(Message {
1394                    format: "failed to launch".to_string(),
1395                    id: 1,
1396                    variables: None,
1397                    send_telemetry: None,
1398                    show_user: None,
1399                    url: None,
1400                    url_label: None,
1401                }),
1402            })
1403        });
1404    })
1405    .ok();
1406
1407    cx.run_until_parked();
1408
1409    project.update(cx, |project, cx| {
1410        assert!(
1411            project.dap_store().read(cx).sessions().count() == 0,
1412            "Session wouldn't exist if it was shutdown"
1413        );
1414    });
1415}
1416
1417#[gpui::test]
1418async fn test_we_send_arguments_from_user_config(
1419    executor: BackgroundExecutor,
1420    cx: &mut TestAppContext,
1421) {
1422    init_test(cx);
1423
1424    let fs = FakeFs::new(executor.clone());
1425
1426    fs.insert_tree(
1427        path!("/project"),
1428        json!({
1429            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1430        }),
1431    )
1432    .await;
1433
1434    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1435    let workspace = init_test_workspace(&project, cx).await;
1436    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1437    let debug_definition = DebugTaskDefinition {
1438        adapter: "fake-adapter".into(),
1439        config: json!({
1440            "request": "launch",
1441            "program": "main.rs".to_owned(),
1442            "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1443            "cwd": path!("/Random_path"),
1444            "env": json!({ "KEY": "VALUE" }),
1445        }),
1446        label: "test".into(),
1447        tcp_connection: None,
1448    };
1449
1450    let launch_handler_called = Arc::new(AtomicBool::new(false));
1451
1452    start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1453        let launch_handler_called = launch_handler_called.clone();
1454
1455        move |client| {
1456            let debug_definition = debug_definition.clone();
1457            let launch_handler_called = launch_handler_called.clone();
1458
1459            client.on_request::<dap::requests::Launch, _>(move |_, args| {
1460                launch_handler_called.store(true, Ordering::SeqCst);
1461
1462                assert_eq!(args.raw, debug_definition.config);
1463
1464                Ok(())
1465            });
1466        }
1467    })
1468    .ok();
1469
1470    cx.run_until_parked();
1471
1472    assert!(
1473        launch_handler_called.load(Ordering::SeqCst),
1474        "Launch request handler was not called"
1475    );
1476}
1477
1478#[gpui::test]
1479async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1480    init_test(cx);
1481
1482    let fs = FakeFs::new(executor.clone());
1483
1484    fs.insert_tree(
1485        path!("/project"),
1486        json!({
1487            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1488            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1489        }),
1490    )
1491    .await;
1492
1493    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1494    let workspace = init_test_workspace(&project, cx).await;
1495    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1496    let project_path = Path::new(path!("/project"));
1497    let worktree = project
1498        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1499        .expect("This worktree should exist in project")
1500        .0;
1501
1502    let worktree_id = workspace
1503        .update(cx, |_, _, cx| worktree.read(cx).id())
1504        .unwrap();
1505
1506    let main_buffer = project
1507        .update(cx, |project, cx| {
1508            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1509        })
1510        .await
1511        .unwrap();
1512
1513    let second_buffer = project
1514        .update(cx, |project, cx| {
1515            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1516        })
1517        .await
1518        .unwrap();
1519
1520    let (main_editor, cx) = cx.add_window_view(|window, cx| {
1521        Editor::new(
1522            EditorMode::full(),
1523            MultiBuffer::build_from_buffer(main_buffer, cx),
1524            Some(project.clone()),
1525            window,
1526            cx,
1527        )
1528    });
1529
1530    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1531        Editor::new(
1532            EditorMode::full(),
1533            MultiBuffer::build_from_buffer(second_buffer, cx),
1534            Some(project.clone()),
1535            window,
1536            cx,
1537        )
1538    });
1539
1540    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1541    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1542
1543    client.on_request::<dap::requests::Threads, _>(move |_, _| {
1544        Ok(dap::ThreadsResponse {
1545            threads: vec![dap::Thread {
1546                id: 1,
1547                name: "Thread 1".into(),
1548            }],
1549        })
1550    });
1551
1552    client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1553        Ok(dap::ScopesResponse {
1554            scopes: Vec::default(),
1555        })
1556    });
1557
1558    client.on_request::<StackTrace, _>(move |_, args| {
1559        assert_eq!(args.thread_id, 1);
1560
1561        Ok(dap::StackTraceResponse {
1562            stack_frames: vec![dap::StackFrame {
1563                id: 1,
1564                name: "frame 1".into(),
1565                source: Some(dap::Source {
1566                    name: Some("main.rs".into()),
1567                    path: Some(path!("/project/main.rs").into()),
1568                    source_reference: None,
1569                    presentation_hint: None,
1570                    origin: None,
1571                    sources: None,
1572                    adapter_data: None,
1573                    checksums: None,
1574                }),
1575                line: 2,
1576                column: 0,
1577                end_line: None,
1578                end_column: None,
1579                can_restart: None,
1580                instruction_pointer_reference: None,
1581                module_id: None,
1582                presentation_hint: None,
1583            }],
1584            total_frames: None,
1585        })
1586    });
1587
1588    client
1589        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1590            reason: dap::StoppedEventReason::Breakpoint,
1591            description: None,
1592            thread_id: Some(1),
1593            preserve_focus_hint: None,
1594            text: None,
1595            all_threads_stopped: None,
1596            hit_breakpoint_ids: None,
1597        }))
1598        .await;
1599
1600    cx.run_until_parked();
1601
1602    main_editor.update_in(cx, |editor, window, cx| {
1603        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1604
1605        assert_eq!(
1606            active_debug_lines.len(),
1607            1,
1608            "There should be only one active debug line"
1609        );
1610
1611        let point = editor
1612            .snapshot(window, cx)
1613            .buffer_snapshot()
1614            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1615
1616        assert_eq!(point.row, 1);
1617    });
1618
1619    second_editor.update(cx, |editor, _| {
1620        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1621
1622        assert!(
1623            active_debug_lines.is_empty(),
1624            "There shouldn't be any active debug lines"
1625        );
1626    });
1627
1628    let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1629    client.on_request::<StackTrace, _>({
1630        let handled_second_stacktrace = handled_second_stacktrace.clone();
1631        move |_, args| {
1632            handled_second_stacktrace.store(true, Ordering::SeqCst);
1633            assert_eq!(args.thread_id, 1);
1634
1635            Ok(dap::StackTraceResponse {
1636                stack_frames: vec![dap::StackFrame {
1637                    id: 2,
1638                    name: "frame 2".into(),
1639                    source: Some(dap::Source {
1640                        name: Some("second.rs".into()),
1641                        path: Some(path!("/project/second.rs").into()),
1642                        source_reference: None,
1643                        presentation_hint: None,
1644                        origin: None,
1645                        sources: None,
1646                        adapter_data: None,
1647                        checksums: None,
1648                    }),
1649                    line: 3,
1650                    column: 0,
1651                    end_line: None,
1652                    end_column: None,
1653                    can_restart: None,
1654                    instruction_pointer_reference: None,
1655                    module_id: None,
1656                    presentation_hint: None,
1657                }],
1658                total_frames: None,
1659            })
1660        }
1661    });
1662
1663    client
1664        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1665            reason: dap::StoppedEventReason::Breakpoint,
1666            description: None,
1667            thread_id: Some(1),
1668            preserve_focus_hint: None,
1669            text: None,
1670            all_threads_stopped: None,
1671            hit_breakpoint_ids: None,
1672        }))
1673        .await;
1674
1675    cx.run_until_parked();
1676
1677    second_editor.update_in(cx, |editor, window, cx| {
1678        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1679
1680        assert_eq!(
1681            active_debug_lines.len(),
1682            1,
1683            "There should be only one active debug line"
1684        );
1685
1686        let point = editor
1687            .snapshot(window, cx)
1688            .buffer_snapshot()
1689            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1690
1691        assert_eq!(point.row, 2);
1692    });
1693
1694    main_editor.update(cx, |editor, _| {
1695        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1696
1697        assert!(
1698            active_debug_lines.is_empty(),
1699            "There shouldn't be any active debug lines"
1700        );
1701    });
1702
1703    assert!(
1704        handled_second_stacktrace.load(Ordering::SeqCst),
1705        "Second stacktrace request handler was not called"
1706    );
1707
1708    client
1709        .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1710            thread_id: 0,
1711            all_threads_continued: Some(true),
1712        }))
1713        .await;
1714
1715    cx.run_until_parked();
1716
1717    second_editor.update(cx, |editor, _| {
1718        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1719
1720        assert!(
1721            active_debug_lines.is_empty(),
1722            "There shouldn't be any active debug lines"
1723        );
1724    });
1725
1726    main_editor.update(cx, |editor, _| {
1727        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1728
1729        assert!(
1730            active_debug_lines.is_empty(),
1731            "There shouldn't be any active debug lines"
1732        );
1733    });
1734
1735    // Clean up
1736    let shutdown_session = project.update(cx, |project, cx| {
1737        project.dap_store().update(cx, |dap_store, cx| {
1738            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1739        })
1740    });
1741
1742    shutdown_session.await.unwrap();
1743
1744    main_editor.update(cx, |editor, _| {
1745        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1746
1747        assert!(
1748            active_debug_lines.is_empty(),
1749            "There shouldn't be any active debug lines after session shutdown"
1750        );
1751    });
1752
1753    second_editor.update(cx, |editor, _| {
1754        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1755
1756        assert!(
1757            active_debug_lines.is_empty(),
1758            "There shouldn't be any active debug lines after session shutdown"
1759        );
1760    });
1761}
1762
1763#[gpui::test]
1764async fn test_debug_adapters_shutdown_on_app_quit(
1765    executor: BackgroundExecutor,
1766    cx: &mut TestAppContext,
1767) {
1768    init_test(cx);
1769
1770    let fs = FakeFs::new(executor.clone());
1771
1772    fs.insert_tree(
1773        path!("/project"),
1774        json!({
1775            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1776        }),
1777    )
1778    .await;
1779
1780    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1781    let workspace = init_test_workspace(&project, cx).await;
1782    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1783
1784    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1785    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1786
1787    let disconnect_request_received = Arc::new(AtomicBool::new(false));
1788    let disconnect_clone = disconnect_request_received.clone();
1789
1790    client.on_request::<Disconnect, _>(move |_, _| {
1791        disconnect_clone.store(true, Ordering::SeqCst);
1792        Ok(())
1793    });
1794
1795    executor.run_until_parked();
1796
1797    workspace
1798        .update(cx, |workspace, _, cx| {
1799            let panel = workspace.panel::<DebugPanel>(cx).unwrap();
1800            panel.read_with(cx, |panel, _| {
1801                assert!(
1802                    panel.sessions().next().is_some(),
1803                    "Debug session should be active"
1804                );
1805            });
1806        })
1807        .unwrap();
1808
1809    cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1810
1811    executor.run_until_parked();
1812
1813    assert!(
1814        disconnect_request_received.load(Ordering::SeqCst),
1815        "Disconnect request should have been sent to the adapter on app shutdown"
1816    );
1817}
1818
1819#[gpui::test]
1820async fn test_breakpoint_jumps_only_in_proper_split_view(
1821    executor: BackgroundExecutor,
1822    cx: &mut TestAppContext,
1823) {
1824    init_test(cx);
1825
1826    let fs = FakeFs::new(executor.clone());
1827
1828    fs.insert_tree(
1829        path!("/project"),
1830        json!({
1831            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1832            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1833        }),
1834    )
1835    .await;
1836
1837    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1838    let workspace = init_test_workspace(&project, cx).await;
1839    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1840
1841    let project_path = Path::new(path!("/project"));
1842    let worktree = project
1843        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1844        .expect("This worktree should exist in project")
1845        .0;
1846
1847    let worktree_id = workspace
1848        .update(cx, |_, _, cx| worktree.read(cx).id())
1849        .unwrap();
1850
1851    // Open main.rs in pane A (the initial pane)
1852    let pane_a = workspace
1853        .update(cx, |multi, _window, cx| {
1854            multi.workspace().read(cx).active_pane().clone()
1855        })
1856        .unwrap();
1857
1858    let open_main = workspace
1859        .update(cx, |multi, window, cx| {
1860            multi.workspace().update(cx, |workspace, cx| {
1861                workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
1862            })
1863        })
1864        .unwrap();
1865    open_main.await.unwrap();
1866
1867    cx.run_until_parked();
1868
1869    // Split pane A to the right, creating pane B
1870    let pane_b = workspace
1871        .update(cx, |multi, window, cx| {
1872            multi.workspace().update(cx, |workspace, cx| {
1873                workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
1874            })
1875        })
1876        .unwrap();
1877
1878    cx.run_until_parked();
1879
1880    // Open main.rs in pane B
1881    let weak_pane_b = pane_b.downgrade();
1882    let open_main_in_b = workspace
1883        .update(cx, |multi, window, cx| {
1884            multi.workspace().update(cx, |workspace, cx| {
1885                workspace.open_path(
1886                    (worktree_id, rel_path("main.rs")),
1887                    Some(weak_pane_b),
1888                    true,
1889                    window,
1890                    cx,
1891                )
1892            })
1893        })
1894        .unwrap();
1895    open_main_in_b.await.unwrap();
1896
1897    cx.run_until_parked();
1898
1899    // Also open second.rs in pane B as an inactive tab
1900    let weak_pane_b = pane_b.downgrade();
1901    let open_second_in_b = workspace
1902        .update(cx, |multi, window, cx| {
1903            multi.workspace().update(cx, |workspace, cx| {
1904                workspace.open_path(
1905                    (worktree_id, rel_path("second.rs")),
1906                    Some(weak_pane_b),
1907                    true,
1908                    window,
1909                    cx,
1910                )
1911            })
1912        })
1913        .unwrap();
1914    open_second_in_b.await.unwrap();
1915
1916    cx.run_until_parked();
1917
1918    // Switch pane B back to main.rs so second.rs is inactive there
1919    let weak_pane_b = pane_b.downgrade();
1920    let reactivate_main_in_b = workspace
1921        .update(cx, |multi, window, cx| {
1922            multi.workspace().update(cx, |workspace, cx| {
1923                workspace.open_path(
1924                    (worktree_id, rel_path("main.rs")),
1925                    Some(weak_pane_b),
1926                    true,
1927                    window,
1928                    cx,
1929                )
1930            })
1931        })
1932        .unwrap();
1933    reactivate_main_in_b.await.unwrap();
1934
1935    cx.run_until_parked();
1936
1937    // Now open second.rs in pane A, making main.rs an inactive tab there
1938    let weak_pane_a = pane_a.downgrade();
1939    let open_second = workspace
1940        .update(cx, |multi, window, cx| {
1941            multi.workspace().update(cx, |workspace, cx| {
1942                workspace.open_path(
1943                    (worktree_id, rel_path("second.rs")),
1944                    Some(weak_pane_a),
1945                    true,
1946                    window,
1947                    cx,
1948                )
1949            })
1950        })
1951        .unwrap();
1952    open_second.await.unwrap();
1953
1954    cx.run_until_parked();
1955
1956    // Layout:
1957    //   Pane A: second.rs (active), main.rs (inactive tab)
1958    //   Pane B: main.rs (active), second.rs (inactive tab)
1959
1960    // Verify pane A's active item is second.rs (main.rs is an inactive tab)
1961    workspace
1962        .read_with(cx, |_multi, cx| {
1963            let active = pane_a.read(cx).active_item().unwrap();
1964            let editor = active.to_any_view().downcast::<Editor>().unwrap();
1965            let path = editor.read(cx).project_path(cx).unwrap();
1966            assert_eq!(
1967                path.path.file_name().unwrap(),
1968                "second.rs",
1969                "Pane A should have second.rs active",
1970            );
1971        })
1972        .unwrap();
1973
1974    // Verify pane B's active item is main.rs
1975    workspace
1976        .read_with(cx, |_multi, cx| {
1977            let active = pane_b.read(cx).active_item().unwrap();
1978            let editor = active.to_any_view().downcast::<Editor>().unwrap();
1979            let path = editor.read(cx).project_path(cx).unwrap();
1980            assert_eq!(
1981                path.path.file_name().unwrap(),
1982                "main.rs",
1983                "Pane B should have main.rs active",
1984            );
1985        })
1986        .unwrap();
1987
1988    // Start a debug session and trigger a breakpoint stop on main.rs line 2
1989    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1990    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1991
1992    client.on_request::<Threads, _>(move |_, _| {
1993        Ok(dap::ThreadsResponse {
1994            threads: vec![dap::Thread {
1995                id: 1,
1996                name: "Thread 1".into(),
1997            }],
1998        })
1999    });
2000
2001    client.on_request::<dap::requests::Scopes, _>(move |_, _| {
2002        Ok(dap::ScopesResponse {
2003            scopes: Vec::default(),
2004        })
2005    });
2006
2007    client.on_request::<StackTrace, _>(move |_, args| {
2008        assert_eq!(args.thread_id, 1);
2009
2010        Ok(dap::StackTraceResponse {
2011            stack_frames: vec![dap::StackFrame {
2012                id: 1,
2013                name: "frame 1".into(),
2014                source: Some(dap::Source {
2015                    name: Some("main.rs".into()),
2016                    path: Some(path!("/project/main.rs").into()),
2017                    source_reference: None,
2018                    presentation_hint: None,
2019                    origin: None,
2020                    sources: None,
2021                    adapter_data: None,
2022                    checksums: None,
2023                }),
2024                line: 2,
2025                column: 0,
2026                end_line: None,
2027                end_column: None,
2028                can_restart: None,
2029                instruction_pointer_reference: None,
2030                module_id: None,
2031                presentation_hint: None,
2032            }],
2033            total_frames: None,
2034        })
2035    });
2036
2037    client
2038        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2039            reason: dap::StoppedEventReason::Breakpoint,
2040            description: None,
2041            thread_id: Some(1),
2042            preserve_focus_hint: None,
2043            text: None,
2044            all_threads_stopped: None,
2045            hit_breakpoint_ids: None,
2046        }))
2047        .await;
2048
2049    cx.run_until_parked();
2050
2051    // After first breakpoint stop on main.rs:
2052    // Pane A should still have second.rs as its active item because
2053    // main.rs was only an inactive tab there. The debugger should have jumped
2054    // to main.rs only in pane B where it was already the active tab.
2055    workspace
2056        .read_with(cx, |_multi, cx| {
2057            let pane_a_active = pane_a.read(cx).active_item().unwrap();
2058            let pane_a_editor = pane_a_active.to_any_view().downcast::<Editor>().unwrap();
2059            let pane_a_path = pane_a_editor.read(cx).project_path(cx).unwrap();
2060            assert_eq!(
2061                pane_a_path.path.file_name().unwrap(),
2062                "second.rs",
2063                "Pane A should still have second.rs as active item. \
2064                 The debugger should not switch active tabs in panes where the \
2065                 breakpoint file is not the active tab (issue #40602)",
2066            );
2067        })
2068        .unwrap();
2069
2070    // There should be exactly one active debug line across all editors in all panes
2071    workspace
2072        .read_with(cx, |_multi, cx| {
2073            let mut total_active_debug_lines = 0;
2074            for pane in [&pane_a, &pane_b] {
2075                for item in pane.read(cx).items() {
2076                    if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2077                        total_active_debug_lines += editor
2078                            .read(cx)
2079                            .highlighted_rows::<ActiveDebugLine>()
2080                            .count();
2081                    }
2082                }
2083            }
2084            assert_eq!(
2085                total_active_debug_lines, 1,
2086                "There should be exactly one active debug line across all editors in all panes"
2087            );
2088        })
2089        .unwrap();
2090
2091    // Pane B should show the debug highlight on main.rs
2092    workspace
2093        .read_with(cx, |_multi, cx| {
2094            let pane_b_active = pane_b.read(cx).active_item().unwrap();
2095            let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2096
2097            let active_debug_lines: Vec<_> = pane_b_editor
2098                .read(cx)
2099                .highlighted_rows::<ActiveDebugLine>()
2100                .collect();
2101
2102            assert_eq!(
2103                active_debug_lines.len(),
2104                1,
2105                "Pane B's main.rs editor should have the active debug line"
2106            );
2107        })
2108        .unwrap();
2109
2110    // Second breakpoint stop: now on second.rs line 3.
2111    // Even though pane A has second.rs as its active tab, the debug line
2112    // should open in pane B (the persistent debug pane) because pane B
2113    // had the last active debug line.
2114    client.on_request::<StackTrace, _>(move |_, args| {
2115        assert_eq!(args.thread_id, 1);
2116
2117        Ok(dap::StackTraceResponse {
2118            stack_frames: vec![dap::StackFrame {
2119                id: 2,
2120                name: "frame 2".into(),
2121                source: Some(dap::Source {
2122                    name: Some("second.rs".into()),
2123                    path: Some(path!("/project/second.rs").into()),
2124                    source_reference: None,
2125                    presentation_hint: None,
2126                    origin: None,
2127                    sources: None,
2128                    adapter_data: None,
2129                    checksums: None,
2130                }),
2131                line: 3,
2132                column: 0,
2133                end_line: None,
2134                end_column: None,
2135                can_restart: None,
2136                instruction_pointer_reference: None,
2137                module_id: None,
2138                presentation_hint: None,
2139            }],
2140            total_frames: None,
2141        })
2142    });
2143
2144    client
2145        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2146            reason: dap::StoppedEventReason::Breakpoint,
2147            description: None,
2148            thread_id: Some(1),
2149            preserve_focus_hint: None,
2150            text: None,
2151            all_threads_stopped: None,
2152            hit_breakpoint_ids: None,
2153        }))
2154        .await;
2155
2156    cx.run_until_parked();
2157
2158    // Pane B should now have second.rs as the active tab with the debug line,
2159    // because pane B was the last pane that had the debug line (persistent debug pane).
2160    workspace
2161        .read_with(cx, |_multi, cx| {
2162            let pane_b_active = pane_b.read(cx).active_item().unwrap();
2163            let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2164            let pane_b_path = pane_b_editor.read(cx).project_path(cx).unwrap();
2165            assert_eq!(
2166                pane_b_path.path.file_name().unwrap(),
2167                "second.rs",
2168                "Pane B should have switched to second.rs because it is the persistent debug pane",
2169            );
2170
2171            let active_debug_lines: Vec<_> = pane_b_editor
2172                .read(cx)
2173                .highlighted_rows::<ActiveDebugLine>()
2174                .collect();
2175
2176            assert_eq!(
2177                active_debug_lines.len(),
2178                1,
2179                "Pane B's second.rs editor should have the active debug line"
2180            );
2181        })
2182        .unwrap();
2183
2184    // There should still be exactly one active debug line across all editors
2185    workspace
2186        .read_with(cx, |_multi, cx| {
2187            let mut total_active_debug_lines = 0;
2188            for pane in [&pane_a, &pane_b] {
2189                for item in pane.read(cx).items() {
2190                    if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2191                        total_active_debug_lines += editor
2192                            .read(cx)
2193                            .highlighted_rows::<ActiveDebugLine>()
2194                            .count();
2195                    }
2196                }
2197            }
2198            assert_eq!(
2199                total_active_debug_lines, 1,
2200                "There should be exactly one active debug line across all editors after second stop"
2201            );
2202        })
2203        .unwrap();
2204
2205    // === New case: Move the debug pane (pane B) active item to a new pane C ===
2206    // This simulates a user dragging the tab with the active debug line to a new split.
2207    // The debugger should track that the debug line moved to pane C and use pane C
2208    // for subsequent debug stops.
2209
2210    // Split pane B to create pane C
2211    let pane_c = workspace
2212        .update(cx, |multi, window, cx| {
2213            multi.workspace().update(cx, |workspace, cx| {
2214                workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
2215            })
2216        })
2217        .unwrap();
2218
2219    cx.run_until_parked();
2220
2221    // Move the active item (second.rs with debug line) from pane B to pane C
2222    workspace
2223        .update(cx, |_multi, window, cx| {
2224            move_active_item(&pane_b, &pane_c, true, false, window, cx);
2225        })
2226        .unwrap();
2227
2228    cx.run_until_parked();
2229
2230    // Verify pane C now has second.rs as active item
2231    workspace
2232        .read_with(cx, |_multi, cx| {
2233            let pane_c_active = pane_c.read(cx).active_item().unwrap();
2234            let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2235            let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2236            assert_eq!(
2237                pane_c_path.path.file_name().unwrap(),
2238                "second.rs",
2239                "Pane C should have second.rs after moving it from pane B",
2240            );
2241        })
2242        .unwrap();
2243
2244    // Third breakpoint stop: back on main.rs line 2.
2245    // The debug line should appear in pane C because that's where the debug line
2246    // was moved to. The debugger should track pane moves.
2247    client.on_request::<StackTrace, _>(move |_, args| {
2248        assert_eq!(args.thread_id, 1);
2249
2250        Ok(dap::StackTraceResponse {
2251            stack_frames: vec![dap::StackFrame {
2252                id: 3,
2253                name: "frame 3".into(),
2254                source: Some(dap::Source {
2255                    name: Some("main.rs".into()),
2256                    path: Some(path!("/project/main.rs").into()),
2257                    source_reference: None,
2258                    presentation_hint: None,
2259                    origin: None,
2260                    sources: None,
2261                    adapter_data: None,
2262                    checksums: None,
2263                }),
2264                line: 2,
2265                column: 0,
2266                end_line: None,
2267                end_column: None,
2268                can_restart: None,
2269                instruction_pointer_reference: None,
2270                module_id: None,
2271                presentation_hint: None,
2272            }],
2273            total_frames: None,
2274        })
2275    });
2276
2277    client
2278        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2279            reason: dap::StoppedEventReason::Breakpoint,
2280            description: None,
2281            thread_id: Some(1),
2282            preserve_focus_hint: None,
2283            text: None,
2284            all_threads_stopped: None,
2285            hit_breakpoint_ids: None,
2286        }))
2287        .await;
2288
2289    cx.run_until_parked();
2290
2291    // Pane C should now have main.rs as the active tab with the debug line,
2292    // because pane C is where the debug line was moved to from pane B.
2293    workspace
2294        .read_with(cx, |_multi, cx| {
2295            let pane_c_active = pane_c.read(cx).active_item().unwrap();
2296            let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2297            let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2298            assert_eq!(
2299                pane_c_path.path.file_name().unwrap(),
2300                "main.rs",
2301                "Pane C should have switched to main.rs because it is now the persistent debug pane \
2302                 (the debug line was moved here from pane B)",
2303            );
2304
2305            let active_debug_lines: Vec<_> = pane_c_editor
2306                .read(cx)
2307                .highlighted_rows::<ActiveDebugLine>()
2308                .collect();
2309
2310            assert_eq!(
2311                active_debug_lines.len(),
2312                1,
2313                "Pane C's main.rs editor should have the active debug line"
2314            );
2315        })
2316        .unwrap();
2317
2318    // There should still be exactly one active debug line across all editors
2319    workspace
2320        .read_with(cx, |_multi, cx| {
2321            let mut total_active_debug_lines = 0;
2322            for pane in [&pane_a, &pane_b, &pane_c] {
2323                for item in pane.read(cx).items() {
2324                    if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2325                        total_active_debug_lines += editor
2326                            .read(cx)
2327                            .highlighted_rows::<ActiveDebugLine>()
2328                            .count();
2329                    }
2330                }
2331            }
2332            assert_eq!(
2333                total_active_debug_lines, 1,
2334                "There should be exactly one active debug line across all editors after third stop"
2335            );
2336        })
2337        .unwrap();
2338
2339    // Clean up
2340    let shutdown_session = project.update(cx, |project, cx| {
2341        project.dap_store().update(cx, |dap_store, cx| {
2342            dap_store.shutdown_session(session.read(cx).session_id(), cx)
2343        })
2344    });
2345
2346    shutdown_session.await.unwrap();
2347}
2348
2349#[gpui::test]
2350async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
2351    executor: BackgroundExecutor,
2352    cx: &mut TestAppContext,
2353) {
2354    init_test(cx);
2355
2356    let fs = FakeFs::new(executor.clone());
2357
2358    fs.insert_tree(
2359        path!("/project"),
2360        json!({
2361            "main.rs": "First line\nSecond line\nThird line\nFourth line",
2362        }),
2363    )
2364    .await;
2365
2366    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2367    let workspace = init_test_workspace(&project, cx).await;
2368    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2369
2370    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
2371    let parent_session_id = cx.read(|cx| parent_session.read(cx).session_id());
2372    let parent_client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
2373
2374    let disconnect_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2375    let parent_disconnect_called = Arc::new(AtomicBool::new(false));
2376    let parent_disconnect_clone = parent_disconnect_called.clone();
2377    let disconnect_count_clone = disconnect_count.clone();
2378
2379    parent_client.on_request::<Disconnect, _>(move |_, _| {
2380        parent_disconnect_clone.store(true, Ordering::SeqCst);
2381        disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2382
2383        for _ in 0..50 {
2384            if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2385                break;
2386            }
2387            std::thread::sleep(std::time::Duration::from_millis(1));
2388        }
2389
2390        Ok(())
2391    });
2392
2393    parent_client
2394        .on_response::<StartDebugging, _>(move |_| {})
2395        .await;
2396    let _subscription = project::debugger::test::intercept_debug_sessions(cx, |_| {});
2397
2398    parent_client
2399        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
2400            configuration: json!({}),
2401            request: StartDebuggingRequestArgumentsRequest::Launch,
2402        })
2403        .await;
2404
2405    cx.run_until_parked();
2406
2407    let child_session = project.update(cx, |project, cx| {
2408        project
2409            .dap_store()
2410            .read(cx)
2411            .session_by_id(SessionId(1))
2412            .unwrap()
2413    });
2414    let child_session_id = cx.read(|cx| child_session.read(cx).session_id());
2415    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
2416
2417    let child_disconnect_called = Arc::new(AtomicBool::new(false));
2418    let child_disconnect_clone = child_disconnect_called.clone();
2419    let disconnect_count_clone = disconnect_count.clone();
2420
2421    child_client.on_request::<Disconnect, _>(move |_, _| {
2422        child_disconnect_clone.store(true, Ordering::SeqCst);
2423        disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2424
2425        for _ in 0..50 {
2426            if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2427                break;
2428            }
2429            std::thread::sleep(std::time::Duration::from_millis(1));
2430        }
2431
2432        Ok(())
2433    });
2434
2435    executor.run_until_parked();
2436
2437    project.update(cx, |project, cx| {
2438        let store = project.dap_store().read(cx);
2439        assert!(store.session_by_id(parent_session_id).is_some());
2440        assert!(store.session_by_id(child_session_id).is_some());
2441    });
2442
2443    cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
2444
2445    executor.run_until_parked();
2446
2447    let parent_disconnect_check = parent_disconnect_called.clone();
2448    let child_disconnect_check = child_disconnect_called.clone();
2449    let executor_clone = executor.clone();
2450    let both_disconnected = executor
2451        .spawn(async move {
2452            let parent_disconnect = parent_disconnect_check;
2453            let child_disconnect = child_disconnect_check;
2454
2455            // We only have 100ms to shutdown the app
2456            for _ in 0..100 {
2457                if parent_disconnect.load(Ordering::SeqCst)
2458                    && child_disconnect.load(Ordering::SeqCst)
2459                {
2460                    return true;
2461                }
2462
2463                executor_clone
2464                    .timer(std::time::Duration::from_millis(1))
2465                    .await;
2466            }
2467
2468            false
2469        })
2470        .await;
2471
2472    assert!(
2473        both_disconnected,
2474        "Both parent and child sessions should receive disconnect requests"
2475    );
2476
2477    assert!(
2478        parent_disconnect_called.load(Ordering::SeqCst),
2479        "Parent session should have received disconnect request"
2480    );
2481    assert!(
2482        child_disconnect_called.load(Ordering::SeqCst),
2483        "Child session should have received disconnect request"
2484    );
2485}
2486
2487#[gpui::test]
2488async fn test_restart_request_is_not_sent_more_than_once_until_response(
2489    executor: BackgroundExecutor,
2490    cx: &mut TestAppContext,
2491) {
2492    init_test(cx);
2493
2494    let fs = FakeFs::new(executor.clone());
2495
2496    fs.insert_tree(
2497        path!("/project"),
2498        json!({
2499            "main.rs": "First line\nSecond line\nThird line\nFourth line",
2500        }),
2501    )
2502    .await;
2503
2504    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2505    let workspace = init_test_workspace(&project, cx).await;
2506    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2507
2508    let session = start_debug_session(&workspace, cx, move |client| {
2509        client.on_request::<dap::requests::Initialize, _>(move |_, _| {
2510            Ok(dap::Capabilities {
2511                supports_restart_request: Some(true),
2512                ..Default::default()
2513            })
2514        });
2515    })
2516    .unwrap();
2517
2518    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
2519
2520    let restart_count = Arc::new(AtomicUsize::new(0));
2521
2522    client.on_request::<dap::requests::Restart, _>({
2523        let restart_count = restart_count.clone();
2524        move |_, _| {
2525            restart_count.fetch_add(1, Ordering::SeqCst);
2526            Ok(())
2527        }
2528    });
2529
2530    // This works because the restart request sender is on the foreground thread
2531    // so it will start running after the gpui update stack is cleared
2532    session.update(cx, |session, cx| {
2533        session.restart(None, cx);
2534        session.restart(None, cx);
2535        session.restart(None, cx);
2536    });
2537
2538    cx.run_until_parked();
2539
2540    assert_eq!(
2541        restart_count.load(Ordering::SeqCst),
2542        1,
2543        "Only one restart request should be sent while a restart is in-flight"
2544    );
2545
2546    session.update(cx, |session, cx| {
2547        session.restart(None, cx);
2548    });
2549
2550    cx.run_until_parked();
2551
2552    assert_eq!(
2553        restart_count.load(Ordering::SeqCst),
2554        2,
2555        "A second restart should be allowed after the first one completes"
2556    );
2557}