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                    autosave: false,
1233                },
1234                project.clone(),
1235                window,
1236                cx,
1237            )
1238        })
1239        .await
1240        .unwrap();
1241
1242    cx.run_until_parked();
1243
1244    assert!(
1245        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1246        "SetBreakpoint request must be called after editor is saved"
1247    );
1248}
1249
1250#[gpui::test]
1251async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1252    executor: BackgroundExecutor,
1253    cx: &mut TestAppContext,
1254) {
1255    init_test(cx);
1256
1257    let fs = FakeFs::new(executor.clone());
1258
1259    fs.insert_tree(
1260        path!("/project"),
1261        json!({
1262            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1263            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1264            "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1265        }),
1266    )
1267    .await;
1268
1269    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1270    let workspace = init_test_workspace(&project, cx).await;
1271    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1272    let project_path = Path::new(path!("/project"));
1273    let worktree = project
1274        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1275        .expect("This worktree should exist in project")
1276        .0;
1277
1278    let worktree_id = workspace
1279        .update(cx, |_, _, cx| worktree.read(cx).id())
1280        .unwrap();
1281
1282    let first = project
1283        .update(cx, |project, cx| {
1284            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1285        })
1286        .await
1287        .unwrap();
1288
1289    let second = project
1290        .update(cx, |project, cx| {
1291            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1292        })
1293        .await
1294        .unwrap();
1295
1296    let (first_editor, cx) = cx.add_window_view(|window, cx| {
1297        Editor::new(
1298            EditorMode::full(),
1299            MultiBuffer::build_from_buffer(first, cx),
1300            Some(project.clone()),
1301            window,
1302            cx,
1303        )
1304    });
1305
1306    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1307        Editor::new(
1308            EditorMode::full(),
1309            MultiBuffer::build_from_buffer(second, cx),
1310            Some(project.clone()),
1311            window,
1312            cx,
1313        )
1314    });
1315
1316    first_editor.update_in(cx, |editor, window, cx| {
1317        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1318        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1319        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1320        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1321        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1322    });
1323
1324    second_editor.update_in(cx, |editor, window, cx| {
1325        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1326        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1327        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1328        editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1329        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1330    });
1331
1332    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1333    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1334
1335    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1336
1337    client.on_request::<SetBreakpoints, _>({
1338        move |_, args| {
1339            assert!(
1340                args.breakpoints.is_none_or(|bps| bps.is_empty()),
1341                "Send empty breakpoint sets to clear them from DAP servers"
1342            );
1343
1344            match args
1345                .source
1346                .path
1347                .expect("We should always send a breakpoint's path")
1348                .as_str()
1349            {
1350                path!("/project/main.rs") | path!("/project/second.rs") => {}
1351                _ => {
1352                    panic!("Unset breakpoints for path that doesn't have any")
1353                }
1354            }
1355
1356            called_set_breakpoints.store(true, Ordering::SeqCst);
1357
1358            Ok(dap::SetBreakpointsResponse {
1359                breakpoints: Vec::default(),
1360            })
1361        }
1362    });
1363
1364    cx.dispatch_action(crate::ClearAllBreakpoints);
1365    cx.run_until_parked();
1366}
1367
1368#[gpui::test]
1369async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1370    executor: BackgroundExecutor,
1371    cx: &mut TestAppContext,
1372) {
1373    init_test(cx);
1374
1375    let fs = FakeFs::new(executor.clone());
1376
1377    fs.insert_tree(
1378        path!("/project"),
1379        json!({
1380            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1381        }),
1382    )
1383    .await;
1384
1385    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1386    let workspace = init_test_workspace(&project, cx).await;
1387    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1388
1389    start_debug_session(&workspace, cx, |client| {
1390        client.on_request::<dap::requests::Initialize, _>(|_, _| {
1391            Err(ErrorResponse {
1392                error: Some(Message {
1393                    format: "failed to launch".to_string(),
1394                    id: 1,
1395                    variables: None,
1396                    send_telemetry: None,
1397                    show_user: None,
1398                    url: None,
1399                    url_label: None,
1400                }),
1401            })
1402        });
1403    })
1404    .ok();
1405
1406    cx.run_until_parked();
1407
1408    project.update(cx, |project, cx| {
1409        assert!(
1410            project.dap_store().read(cx).sessions().count() == 0,
1411            "Session wouldn't exist if it was shutdown"
1412        );
1413    });
1414}
1415
1416#[gpui::test]
1417async fn test_we_send_arguments_from_user_config(
1418    executor: BackgroundExecutor,
1419    cx: &mut TestAppContext,
1420) {
1421    init_test(cx);
1422
1423    let fs = FakeFs::new(executor.clone());
1424
1425    fs.insert_tree(
1426        path!("/project"),
1427        json!({
1428            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1429        }),
1430    )
1431    .await;
1432
1433    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1434    let workspace = init_test_workspace(&project, cx).await;
1435    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1436    let debug_definition = DebugTaskDefinition {
1437        adapter: "fake-adapter".into(),
1438        config: json!({
1439            "request": "launch",
1440            "program": "main.rs".to_owned(),
1441            "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1442            "cwd": path!("/Random_path"),
1443            "env": json!({ "KEY": "VALUE" }),
1444        }),
1445        label: "test".into(),
1446        tcp_connection: None,
1447    };
1448
1449    let launch_handler_called = Arc::new(AtomicBool::new(false));
1450
1451    start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1452        let launch_handler_called = launch_handler_called.clone();
1453
1454        move |client| {
1455            let debug_definition = debug_definition.clone();
1456            let launch_handler_called = launch_handler_called.clone();
1457
1458            client.on_request::<dap::requests::Launch, _>(move |_, args| {
1459                launch_handler_called.store(true, Ordering::SeqCst);
1460
1461                assert_eq!(args.raw, debug_definition.config);
1462
1463                Ok(())
1464            });
1465        }
1466    })
1467    .ok();
1468
1469    cx.run_until_parked();
1470
1471    assert!(
1472        launch_handler_called.load(Ordering::SeqCst),
1473        "Launch request handler was not called"
1474    );
1475}
1476
1477#[gpui::test]
1478async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1479    init_test(cx);
1480
1481    let fs = FakeFs::new(executor.clone());
1482
1483    fs.insert_tree(
1484        path!("/project"),
1485        json!({
1486            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1487            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1488        }),
1489    )
1490    .await;
1491
1492    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1493    let workspace = init_test_workspace(&project, cx).await;
1494    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1495    let project_path = Path::new(path!("/project"));
1496    let worktree = project
1497        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1498        .expect("This worktree should exist in project")
1499        .0;
1500
1501    let worktree_id = workspace
1502        .update(cx, |_, _, cx| worktree.read(cx).id())
1503        .unwrap();
1504
1505    let main_buffer = project
1506        .update(cx, |project, cx| {
1507            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1508        })
1509        .await
1510        .unwrap();
1511
1512    let second_buffer = project
1513        .update(cx, |project, cx| {
1514            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1515        })
1516        .await
1517        .unwrap();
1518
1519    let (main_editor, cx) = cx.add_window_view(|window, cx| {
1520        Editor::new(
1521            EditorMode::full(),
1522            MultiBuffer::build_from_buffer(main_buffer, cx),
1523            Some(project.clone()),
1524            window,
1525            cx,
1526        )
1527    });
1528
1529    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1530        Editor::new(
1531            EditorMode::full(),
1532            MultiBuffer::build_from_buffer(second_buffer, cx),
1533            Some(project.clone()),
1534            window,
1535            cx,
1536        )
1537    });
1538
1539    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1540    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1541
1542    client.on_request::<dap::requests::Threads, _>(move |_, _| {
1543        Ok(dap::ThreadsResponse {
1544            threads: vec![dap::Thread {
1545                id: 1,
1546                name: "Thread 1".into(),
1547            }],
1548        })
1549    });
1550
1551    client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1552        Ok(dap::ScopesResponse {
1553            scopes: Vec::default(),
1554        })
1555    });
1556
1557    client.on_request::<StackTrace, _>(move |_, args| {
1558        assert_eq!(args.thread_id, 1);
1559
1560        Ok(dap::StackTraceResponse {
1561            stack_frames: vec![dap::StackFrame {
1562                id: 1,
1563                name: "frame 1".into(),
1564                source: Some(dap::Source {
1565                    name: Some("main.rs".into()),
1566                    path: Some(path!("/project/main.rs").into()),
1567                    source_reference: None,
1568                    presentation_hint: None,
1569                    origin: None,
1570                    sources: None,
1571                    adapter_data: None,
1572                    checksums: None,
1573                }),
1574                line: 2,
1575                column: 0,
1576                end_line: None,
1577                end_column: None,
1578                can_restart: None,
1579                instruction_pointer_reference: None,
1580                module_id: None,
1581                presentation_hint: None,
1582            }],
1583            total_frames: None,
1584        })
1585    });
1586
1587    client
1588        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1589            reason: dap::StoppedEventReason::Breakpoint,
1590            description: None,
1591            thread_id: Some(1),
1592            preserve_focus_hint: None,
1593            text: None,
1594            all_threads_stopped: None,
1595            hit_breakpoint_ids: None,
1596        }))
1597        .await;
1598
1599    cx.run_until_parked();
1600
1601    main_editor.update_in(cx, |editor, window, cx| {
1602        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1603
1604        assert_eq!(
1605            active_debug_lines.len(),
1606            1,
1607            "There should be only one active debug line"
1608        );
1609
1610        let point = editor
1611            .snapshot(window, cx)
1612            .buffer_snapshot()
1613            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1614
1615        assert_eq!(point.row, 1);
1616    });
1617
1618    second_editor.update(cx, |editor, _| {
1619        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1620
1621        assert!(
1622            active_debug_lines.is_empty(),
1623            "There shouldn't be any active debug lines"
1624        );
1625    });
1626
1627    let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1628    client.on_request::<StackTrace, _>({
1629        let handled_second_stacktrace = handled_second_stacktrace.clone();
1630        move |_, args| {
1631            handled_second_stacktrace.store(true, Ordering::SeqCst);
1632            assert_eq!(args.thread_id, 1);
1633
1634            Ok(dap::StackTraceResponse {
1635                stack_frames: vec![dap::StackFrame {
1636                    id: 2,
1637                    name: "frame 2".into(),
1638                    source: Some(dap::Source {
1639                        name: Some("second.rs".into()),
1640                        path: Some(path!("/project/second.rs").into()),
1641                        source_reference: None,
1642                        presentation_hint: None,
1643                        origin: None,
1644                        sources: None,
1645                        adapter_data: None,
1646                        checksums: None,
1647                    }),
1648                    line: 3,
1649                    column: 0,
1650                    end_line: None,
1651                    end_column: None,
1652                    can_restart: None,
1653                    instruction_pointer_reference: None,
1654                    module_id: None,
1655                    presentation_hint: None,
1656                }],
1657                total_frames: None,
1658            })
1659        }
1660    });
1661
1662    client
1663        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1664            reason: dap::StoppedEventReason::Breakpoint,
1665            description: None,
1666            thread_id: Some(1),
1667            preserve_focus_hint: None,
1668            text: None,
1669            all_threads_stopped: None,
1670            hit_breakpoint_ids: None,
1671        }))
1672        .await;
1673
1674    cx.run_until_parked();
1675
1676    second_editor.update_in(cx, |editor, window, cx| {
1677        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1678
1679        assert_eq!(
1680            active_debug_lines.len(),
1681            1,
1682            "There should be only one active debug line"
1683        );
1684
1685        let point = editor
1686            .snapshot(window, cx)
1687            .buffer_snapshot()
1688            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1689
1690        assert_eq!(point.row, 2);
1691    });
1692
1693    main_editor.update(cx, |editor, _| {
1694        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1695
1696        assert!(
1697            active_debug_lines.is_empty(),
1698            "There shouldn't be any active debug lines"
1699        );
1700    });
1701
1702    assert!(
1703        handled_second_stacktrace.load(Ordering::SeqCst),
1704        "Second stacktrace request handler was not called"
1705    );
1706
1707    client
1708        .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1709            thread_id: 0,
1710            all_threads_continued: Some(true),
1711        }))
1712        .await;
1713
1714    cx.run_until_parked();
1715
1716    second_editor.update(cx, |editor, _| {
1717        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1718
1719        assert!(
1720            active_debug_lines.is_empty(),
1721            "There shouldn't be any active debug lines"
1722        );
1723    });
1724
1725    main_editor.update(cx, |editor, _| {
1726        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1727
1728        assert!(
1729            active_debug_lines.is_empty(),
1730            "There shouldn't be any active debug lines"
1731        );
1732    });
1733
1734    // Clean up
1735    let shutdown_session = project.update(cx, |project, cx| {
1736        project.dap_store().update(cx, |dap_store, cx| {
1737            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1738        })
1739    });
1740
1741    shutdown_session.await.unwrap();
1742
1743    main_editor.update(cx, |editor, _| {
1744        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1745
1746        assert!(
1747            active_debug_lines.is_empty(),
1748            "There shouldn't be any active debug lines after session shutdown"
1749        );
1750    });
1751
1752    second_editor.update(cx, |editor, _| {
1753        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1754
1755        assert!(
1756            active_debug_lines.is_empty(),
1757            "There shouldn't be any active debug lines after session shutdown"
1758        );
1759    });
1760}
1761
1762#[gpui::test]
1763async fn test_debug_adapters_shutdown_on_app_quit(
1764    executor: BackgroundExecutor,
1765    cx: &mut TestAppContext,
1766) {
1767    init_test(cx);
1768
1769    let fs = FakeFs::new(executor.clone());
1770
1771    fs.insert_tree(
1772        path!("/project"),
1773        json!({
1774            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1775        }),
1776    )
1777    .await;
1778
1779    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1780    let workspace = init_test_workspace(&project, cx).await;
1781    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1782
1783    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1784    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1785
1786    let disconnect_request_received = Arc::new(AtomicBool::new(false));
1787    let disconnect_clone = disconnect_request_received.clone();
1788
1789    client.on_request::<Disconnect, _>(move |_, _| {
1790        disconnect_clone.store(true, Ordering::SeqCst);
1791        Ok(())
1792    });
1793
1794    executor.run_until_parked();
1795
1796    workspace
1797        .update(cx, |workspace, _, cx| {
1798            let panel = workspace.panel::<DebugPanel>(cx).unwrap();
1799            panel.read_with(cx, |panel, _| {
1800                assert!(
1801                    panel.sessions().next().is_some(),
1802                    "Debug session should be active"
1803                );
1804            });
1805        })
1806        .unwrap();
1807
1808    cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1809
1810    executor.run_until_parked();
1811
1812    assert!(
1813        disconnect_request_received.load(Ordering::SeqCst),
1814        "Disconnect request should have been sent to the adapter on app shutdown"
1815    );
1816}
1817
1818#[gpui::test]
1819async fn test_breakpoint_jumps_only_in_proper_split_view(
1820    executor: BackgroundExecutor,
1821    cx: &mut TestAppContext,
1822) {
1823    init_test(cx);
1824
1825    let fs = FakeFs::new(executor.clone());
1826
1827    fs.insert_tree(
1828        path!("/project"),
1829        json!({
1830            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1831            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1832        }),
1833    )
1834    .await;
1835
1836    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1837    let workspace = init_test_workspace(&project, cx).await;
1838    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1839
1840    let project_path = Path::new(path!("/project"));
1841    let worktree = project
1842        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1843        .expect("This worktree should exist in project")
1844        .0;
1845
1846    let worktree_id = workspace
1847        .update(cx, |_, _, cx| worktree.read(cx).id())
1848        .unwrap();
1849
1850    // Open main.rs in pane A (the initial pane)
1851    let pane_a = workspace
1852        .update(cx, |multi, _window, cx| {
1853            multi.workspace().read(cx).active_pane().clone()
1854        })
1855        .unwrap();
1856
1857    let open_main = workspace
1858        .update(cx, |multi, window, cx| {
1859            multi.workspace().update(cx, |workspace, cx| {
1860                workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
1861            })
1862        })
1863        .unwrap();
1864    open_main.await.unwrap();
1865
1866    cx.run_until_parked();
1867
1868    // Split pane A to the right, creating pane B
1869    let pane_b = workspace
1870        .update(cx, |multi, window, cx| {
1871            multi.workspace().update(cx, |workspace, cx| {
1872                workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
1873            })
1874        })
1875        .unwrap();
1876
1877    cx.run_until_parked();
1878
1879    // Open main.rs in pane B
1880    let weak_pane_b = pane_b.downgrade();
1881    let open_main_in_b = workspace
1882        .update(cx, |multi, window, cx| {
1883            multi.workspace().update(cx, |workspace, cx| {
1884                workspace.open_path(
1885                    (worktree_id, rel_path("main.rs")),
1886                    Some(weak_pane_b),
1887                    true,
1888                    window,
1889                    cx,
1890                )
1891            })
1892        })
1893        .unwrap();
1894    open_main_in_b.await.unwrap();
1895
1896    cx.run_until_parked();
1897
1898    // Also open second.rs in pane B as an inactive tab
1899    let weak_pane_b = pane_b.downgrade();
1900    let open_second_in_b = workspace
1901        .update(cx, |multi, window, cx| {
1902            multi.workspace().update(cx, |workspace, cx| {
1903                workspace.open_path(
1904                    (worktree_id, rel_path("second.rs")),
1905                    Some(weak_pane_b),
1906                    true,
1907                    window,
1908                    cx,
1909                )
1910            })
1911        })
1912        .unwrap();
1913    open_second_in_b.await.unwrap();
1914
1915    cx.run_until_parked();
1916
1917    // Switch pane B back to main.rs so second.rs is inactive there
1918    let weak_pane_b = pane_b.downgrade();
1919    let reactivate_main_in_b = workspace
1920        .update(cx, |multi, window, cx| {
1921            multi.workspace().update(cx, |workspace, cx| {
1922                workspace.open_path(
1923                    (worktree_id, rel_path("main.rs")),
1924                    Some(weak_pane_b),
1925                    true,
1926                    window,
1927                    cx,
1928                )
1929            })
1930        })
1931        .unwrap();
1932    reactivate_main_in_b.await.unwrap();
1933
1934    cx.run_until_parked();
1935
1936    // Now open second.rs in pane A, making main.rs an inactive tab there
1937    let weak_pane_a = pane_a.downgrade();
1938    let open_second = workspace
1939        .update(cx, |multi, window, cx| {
1940            multi.workspace().update(cx, |workspace, cx| {
1941                workspace.open_path(
1942                    (worktree_id, rel_path("second.rs")),
1943                    Some(weak_pane_a),
1944                    true,
1945                    window,
1946                    cx,
1947                )
1948            })
1949        })
1950        .unwrap();
1951    open_second.await.unwrap();
1952
1953    cx.run_until_parked();
1954
1955    // Layout:
1956    //   Pane A: second.rs (active), main.rs (inactive tab)
1957    //   Pane B: main.rs (active), second.rs (inactive tab)
1958
1959    // Verify pane A's active item is second.rs (main.rs is an inactive tab)
1960    workspace
1961        .read_with(cx, |_multi, cx| {
1962            let active = pane_a.read(cx).active_item().unwrap();
1963            let editor = active.to_any_view().downcast::<Editor>().unwrap();
1964            let path = editor.read(cx).project_path(cx).unwrap();
1965            assert_eq!(
1966                path.path.file_name().unwrap(),
1967                "second.rs",
1968                "Pane A should have second.rs active",
1969            );
1970        })
1971        .unwrap();
1972
1973    // Verify pane B's active item is main.rs
1974    workspace
1975        .read_with(cx, |_multi, cx| {
1976            let active = pane_b.read(cx).active_item().unwrap();
1977            let editor = active.to_any_view().downcast::<Editor>().unwrap();
1978            let path = editor.read(cx).project_path(cx).unwrap();
1979            assert_eq!(
1980                path.path.file_name().unwrap(),
1981                "main.rs",
1982                "Pane B should have main.rs active",
1983            );
1984        })
1985        .unwrap();
1986
1987    // Start a debug session and trigger a breakpoint stop on main.rs line 2
1988    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1989    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1990
1991    client.on_request::<Threads, _>(move |_, _| {
1992        Ok(dap::ThreadsResponse {
1993            threads: vec![dap::Thread {
1994                id: 1,
1995                name: "Thread 1".into(),
1996            }],
1997        })
1998    });
1999
2000    client.on_request::<dap::requests::Scopes, _>(move |_, _| {
2001        Ok(dap::ScopesResponse {
2002            scopes: Vec::default(),
2003        })
2004    });
2005
2006    client.on_request::<StackTrace, _>(move |_, args| {
2007        assert_eq!(args.thread_id, 1);
2008
2009        Ok(dap::StackTraceResponse {
2010            stack_frames: vec![dap::StackFrame {
2011                id: 1,
2012                name: "frame 1".into(),
2013                source: Some(dap::Source {
2014                    name: Some("main.rs".into()),
2015                    path: Some(path!("/project/main.rs").into()),
2016                    source_reference: None,
2017                    presentation_hint: None,
2018                    origin: None,
2019                    sources: None,
2020                    adapter_data: None,
2021                    checksums: None,
2022                }),
2023                line: 2,
2024                column: 0,
2025                end_line: None,
2026                end_column: None,
2027                can_restart: None,
2028                instruction_pointer_reference: None,
2029                module_id: None,
2030                presentation_hint: None,
2031            }],
2032            total_frames: None,
2033        })
2034    });
2035
2036    client
2037        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2038            reason: dap::StoppedEventReason::Breakpoint,
2039            description: None,
2040            thread_id: Some(1),
2041            preserve_focus_hint: None,
2042            text: None,
2043            all_threads_stopped: None,
2044            hit_breakpoint_ids: None,
2045        }))
2046        .await;
2047
2048    cx.run_until_parked();
2049
2050    // After first breakpoint stop on main.rs:
2051    // Pane A should still have second.rs as its active item because
2052    // main.rs was only an inactive tab there. The debugger should have jumped
2053    // to main.rs only in pane B where it was already the active tab.
2054    workspace
2055        .read_with(cx, |_multi, cx| {
2056            let pane_a_active = pane_a.read(cx).active_item().unwrap();
2057            let pane_a_editor = pane_a_active.to_any_view().downcast::<Editor>().unwrap();
2058            let pane_a_path = pane_a_editor.read(cx).project_path(cx).unwrap();
2059            assert_eq!(
2060                pane_a_path.path.file_name().unwrap(),
2061                "second.rs",
2062                "Pane A should still have second.rs as active item. \
2063                 The debugger should not switch active tabs in panes where the \
2064                 breakpoint file is not the active tab (issue #40602)",
2065            );
2066        })
2067        .unwrap();
2068
2069    // There should be exactly one active debug line across all editors in all panes
2070    workspace
2071        .read_with(cx, |_multi, cx| {
2072            let mut total_active_debug_lines = 0;
2073            for pane in [&pane_a, &pane_b] {
2074                for item in pane.read(cx).items() {
2075                    if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2076                        total_active_debug_lines += editor
2077                            .read(cx)
2078                            .highlighted_rows::<ActiveDebugLine>()
2079                            .count();
2080                    }
2081                }
2082            }
2083            assert_eq!(
2084                total_active_debug_lines, 1,
2085                "There should be exactly one active debug line across all editors in all panes"
2086            );
2087        })
2088        .unwrap();
2089
2090    // Pane B should show the debug highlight on main.rs
2091    workspace
2092        .read_with(cx, |_multi, cx| {
2093            let pane_b_active = pane_b.read(cx).active_item().unwrap();
2094            let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2095
2096            let active_debug_lines: Vec<_> = pane_b_editor
2097                .read(cx)
2098                .highlighted_rows::<ActiveDebugLine>()
2099                .collect();
2100
2101            assert_eq!(
2102                active_debug_lines.len(),
2103                1,
2104                "Pane B's main.rs editor should have the active debug line"
2105            );
2106        })
2107        .unwrap();
2108
2109    // Second breakpoint stop: now on second.rs line 3.
2110    // Even though pane A has second.rs as its active tab, the debug line
2111    // should open in pane B (the persistent debug pane) because pane B
2112    // had the last active debug line.
2113    client.on_request::<StackTrace, _>(move |_, args| {
2114        assert_eq!(args.thread_id, 1);
2115
2116        Ok(dap::StackTraceResponse {
2117            stack_frames: vec![dap::StackFrame {
2118                id: 2,
2119                name: "frame 2".into(),
2120                source: Some(dap::Source {
2121                    name: Some("second.rs".into()),
2122                    path: Some(path!("/project/second.rs").into()),
2123                    source_reference: None,
2124                    presentation_hint: None,
2125                    origin: None,
2126                    sources: None,
2127                    adapter_data: None,
2128                    checksums: None,
2129                }),
2130                line: 3,
2131                column: 0,
2132                end_line: None,
2133                end_column: None,
2134                can_restart: None,
2135                instruction_pointer_reference: None,
2136                module_id: None,
2137                presentation_hint: None,
2138            }],
2139            total_frames: None,
2140        })
2141    });
2142
2143    client
2144        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2145            reason: dap::StoppedEventReason::Breakpoint,
2146            description: None,
2147            thread_id: Some(1),
2148            preserve_focus_hint: None,
2149            text: None,
2150            all_threads_stopped: None,
2151            hit_breakpoint_ids: None,
2152        }))
2153        .await;
2154
2155    cx.run_until_parked();
2156
2157    // Pane B should now have second.rs as the active tab with the debug line,
2158    // because pane B was the last pane that had the debug line (persistent debug pane).
2159    workspace
2160        .read_with(cx, |_multi, cx| {
2161            let pane_b_active = pane_b.read(cx).active_item().unwrap();
2162            let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2163            let pane_b_path = pane_b_editor.read(cx).project_path(cx).unwrap();
2164            assert_eq!(
2165                pane_b_path.path.file_name().unwrap(),
2166                "second.rs",
2167                "Pane B should have switched to second.rs because it is the persistent debug pane",
2168            );
2169
2170            let active_debug_lines: Vec<_> = pane_b_editor
2171                .read(cx)
2172                .highlighted_rows::<ActiveDebugLine>()
2173                .collect();
2174
2175            assert_eq!(
2176                active_debug_lines.len(),
2177                1,
2178                "Pane B's second.rs editor should have the active debug line"
2179            );
2180        })
2181        .unwrap();
2182
2183    // There should still be exactly one active debug line across all editors
2184    workspace
2185        .read_with(cx, |_multi, cx| {
2186            let mut total_active_debug_lines = 0;
2187            for pane in [&pane_a, &pane_b] {
2188                for item in pane.read(cx).items() {
2189                    if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2190                        total_active_debug_lines += editor
2191                            .read(cx)
2192                            .highlighted_rows::<ActiveDebugLine>()
2193                            .count();
2194                    }
2195                }
2196            }
2197            assert_eq!(
2198                total_active_debug_lines, 1,
2199                "There should be exactly one active debug line across all editors after second stop"
2200            );
2201        })
2202        .unwrap();
2203
2204    // === New case: Move the debug pane (pane B) active item to a new pane C ===
2205    // This simulates a user dragging the tab with the active debug line to a new split.
2206    // The debugger should track that the debug line moved to pane C and use pane C
2207    // for subsequent debug stops.
2208
2209    // Split pane B to create pane C
2210    let pane_c = workspace
2211        .update(cx, |multi, window, cx| {
2212            multi.workspace().update(cx, |workspace, cx| {
2213                workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
2214            })
2215        })
2216        .unwrap();
2217
2218    cx.run_until_parked();
2219
2220    // Move the active item (second.rs with debug line) from pane B to pane C
2221    workspace
2222        .update(cx, |_multi, window, cx| {
2223            move_active_item(&pane_b, &pane_c, true, false, window, cx);
2224        })
2225        .unwrap();
2226
2227    cx.run_until_parked();
2228
2229    // Verify pane C now has second.rs as active item
2230    workspace
2231        .read_with(cx, |_multi, cx| {
2232            let pane_c_active = pane_c.read(cx).active_item().unwrap();
2233            let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2234            let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2235            assert_eq!(
2236                pane_c_path.path.file_name().unwrap(),
2237                "second.rs",
2238                "Pane C should have second.rs after moving it from pane B",
2239            );
2240        })
2241        .unwrap();
2242
2243    // Third breakpoint stop: back on main.rs line 2.
2244    // The debug line should appear in pane C because that's where the debug line
2245    // was moved to. The debugger should track pane moves.
2246    client.on_request::<StackTrace, _>(move |_, args| {
2247        assert_eq!(args.thread_id, 1);
2248
2249        Ok(dap::StackTraceResponse {
2250            stack_frames: vec![dap::StackFrame {
2251                id: 3,
2252                name: "frame 3".into(),
2253                source: Some(dap::Source {
2254                    name: Some("main.rs".into()),
2255                    path: Some(path!("/project/main.rs").into()),
2256                    source_reference: None,
2257                    presentation_hint: None,
2258                    origin: None,
2259                    sources: None,
2260                    adapter_data: None,
2261                    checksums: None,
2262                }),
2263                line: 2,
2264                column: 0,
2265                end_line: None,
2266                end_column: None,
2267                can_restart: None,
2268                instruction_pointer_reference: None,
2269                module_id: None,
2270                presentation_hint: None,
2271            }],
2272            total_frames: None,
2273        })
2274    });
2275
2276    client
2277        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2278            reason: dap::StoppedEventReason::Breakpoint,
2279            description: None,
2280            thread_id: Some(1),
2281            preserve_focus_hint: None,
2282            text: None,
2283            all_threads_stopped: None,
2284            hit_breakpoint_ids: None,
2285        }))
2286        .await;
2287
2288    cx.run_until_parked();
2289
2290    // Pane C should now have main.rs as the active tab with the debug line,
2291    // because pane C is where the debug line was moved to from pane B.
2292    workspace
2293        .read_with(cx, |_multi, cx| {
2294            let pane_c_active = pane_c.read(cx).active_item().unwrap();
2295            let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2296            let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2297            assert_eq!(
2298                pane_c_path.path.file_name().unwrap(),
2299                "main.rs",
2300                "Pane C should have switched to main.rs because it is now the persistent debug pane \
2301                 (the debug line was moved here from pane B)",
2302            );
2303
2304            let active_debug_lines: Vec<_> = pane_c_editor
2305                .read(cx)
2306                .highlighted_rows::<ActiveDebugLine>()
2307                .collect();
2308
2309            assert_eq!(
2310                active_debug_lines.len(),
2311                1,
2312                "Pane C's main.rs editor should have the active debug line"
2313            );
2314        })
2315        .unwrap();
2316
2317    // There should still be exactly one active debug line across all editors
2318    workspace
2319        .read_with(cx, |_multi, cx| {
2320            let mut total_active_debug_lines = 0;
2321            for pane in [&pane_a, &pane_b, &pane_c] {
2322                for item in pane.read(cx).items() {
2323                    if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2324                        total_active_debug_lines += editor
2325                            .read(cx)
2326                            .highlighted_rows::<ActiveDebugLine>()
2327                            .count();
2328                    }
2329                }
2330            }
2331            assert_eq!(
2332                total_active_debug_lines, 1,
2333                "There should be exactly one active debug line across all editors after third stop"
2334            );
2335        })
2336        .unwrap();
2337
2338    // Clean up
2339    let shutdown_session = project.update(cx, |project, cx| {
2340        project.dap_store().update(cx, |dap_store, cx| {
2341            dap_store.shutdown_session(session.read(cx).session_id(), cx)
2342        })
2343    });
2344
2345    shutdown_session.await.unwrap();
2346}
2347
2348#[gpui::test]
2349async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
2350    executor: BackgroundExecutor,
2351    cx: &mut TestAppContext,
2352) {
2353    init_test(cx);
2354
2355    let fs = FakeFs::new(executor.clone());
2356
2357    fs.insert_tree(
2358        path!("/project"),
2359        json!({
2360            "main.rs": "First line\nSecond line\nThird line\nFourth line",
2361        }),
2362    )
2363    .await;
2364
2365    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2366    let workspace = init_test_workspace(&project, cx).await;
2367    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2368
2369    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
2370    let parent_session_id = cx.read(|cx| parent_session.read(cx).session_id());
2371    let parent_client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
2372
2373    let disconnect_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2374    let parent_disconnect_called = Arc::new(AtomicBool::new(false));
2375    let parent_disconnect_clone = parent_disconnect_called.clone();
2376    let disconnect_count_clone = disconnect_count.clone();
2377
2378    parent_client.on_request::<Disconnect, _>(move |_, _| {
2379        parent_disconnect_clone.store(true, Ordering::SeqCst);
2380        disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2381
2382        for _ in 0..50 {
2383            if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2384                break;
2385            }
2386            std::thread::sleep(std::time::Duration::from_millis(1));
2387        }
2388
2389        Ok(())
2390    });
2391
2392    parent_client
2393        .on_response::<StartDebugging, _>(move |_| {})
2394        .await;
2395    let _subscription = project::debugger::test::intercept_debug_sessions(cx, |_| {});
2396
2397    parent_client
2398        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
2399            configuration: json!({}),
2400            request: StartDebuggingRequestArgumentsRequest::Launch,
2401        })
2402        .await;
2403
2404    cx.run_until_parked();
2405
2406    let child_session = project.update(cx, |project, cx| {
2407        project
2408            .dap_store()
2409            .read(cx)
2410            .session_by_id(SessionId(1))
2411            .unwrap()
2412    });
2413    let child_session_id = cx.read(|cx| child_session.read(cx).session_id());
2414    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
2415
2416    let child_disconnect_called = Arc::new(AtomicBool::new(false));
2417    let child_disconnect_clone = child_disconnect_called.clone();
2418    let disconnect_count_clone = disconnect_count.clone();
2419
2420    child_client.on_request::<Disconnect, _>(move |_, _| {
2421        child_disconnect_clone.store(true, Ordering::SeqCst);
2422        disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2423
2424        for _ in 0..50 {
2425            if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2426                break;
2427            }
2428            std::thread::sleep(std::time::Duration::from_millis(1));
2429        }
2430
2431        Ok(())
2432    });
2433
2434    executor.run_until_parked();
2435
2436    project.update(cx, |project, cx| {
2437        let store = project.dap_store().read(cx);
2438        assert!(store.session_by_id(parent_session_id).is_some());
2439        assert!(store.session_by_id(child_session_id).is_some());
2440    });
2441
2442    cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
2443
2444    executor.run_until_parked();
2445
2446    let parent_disconnect_check = parent_disconnect_called.clone();
2447    let child_disconnect_check = child_disconnect_called.clone();
2448    let executor_clone = executor.clone();
2449    let both_disconnected = executor
2450        .spawn(async move {
2451            let parent_disconnect = parent_disconnect_check;
2452            let child_disconnect = child_disconnect_check;
2453
2454            // We only have 100ms to shutdown the app
2455            for _ in 0..100 {
2456                if parent_disconnect.load(Ordering::SeqCst)
2457                    && child_disconnect.load(Ordering::SeqCst)
2458                {
2459                    return true;
2460                }
2461
2462                executor_clone
2463                    .timer(std::time::Duration::from_millis(1))
2464                    .await;
2465            }
2466
2467            false
2468        })
2469        .await;
2470
2471    assert!(
2472        both_disconnected,
2473        "Both parent and child sessions should receive disconnect requests"
2474    );
2475
2476    assert!(
2477        parent_disconnect_called.load(Ordering::SeqCst),
2478        "Parent session should have received disconnect request"
2479    );
2480    assert!(
2481        child_disconnect_called.load(Ordering::SeqCst),
2482        "Child session should have received disconnect request"
2483    );
2484}
2485
2486#[gpui::test]
2487async fn test_restart_request_is_not_sent_more_than_once_until_response(
2488    executor: BackgroundExecutor,
2489    cx: &mut TestAppContext,
2490) {
2491    init_test(cx);
2492
2493    let fs = FakeFs::new(executor.clone());
2494
2495    fs.insert_tree(
2496        path!("/project"),
2497        json!({
2498            "main.rs": "First line\nSecond line\nThird line\nFourth line",
2499        }),
2500    )
2501    .await;
2502
2503    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2504    let workspace = init_test_workspace(&project, cx).await;
2505    let cx = &mut VisualTestContext::from_window(*workspace, cx);
2506
2507    let session = start_debug_session(&workspace, cx, move |client| {
2508        client.on_request::<dap::requests::Initialize, _>(move |_, _| {
2509            Ok(dap::Capabilities {
2510                supports_restart_request: Some(true),
2511                ..Default::default()
2512            })
2513        });
2514    })
2515    .unwrap();
2516
2517    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
2518
2519    let restart_count = Arc::new(AtomicUsize::new(0));
2520
2521    client.on_request::<dap::requests::Restart, _>({
2522        let restart_count = restart_count.clone();
2523        move |_, _| {
2524            restart_count.fetch_add(1, Ordering::SeqCst);
2525            Ok(())
2526        }
2527    });
2528
2529    // This works because the restart request sender is on the foreground thread
2530    // so it will start running after the gpui update stack is cleared
2531    session.update(cx, |session, cx| {
2532        session.restart(None, cx);
2533        session.restart(None, cx);
2534        session.restart(None, cx);
2535    });
2536
2537    cx.run_until_parked();
2538
2539    assert_eq!(
2540        restart_count.load(Ordering::SeqCst),
2541        1,
2542        "Only one restart request should be sent while a restart is in-flight"
2543    );
2544
2545    session.update(cx, |session, cx| {
2546        session.restart(None, cx);
2547    });
2548
2549    cx.run_until_parked();
2550
2551    assert_eq!(
2552        restart_count.load(Ordering::SeqCst),
2553        2,
2554        "A second restart should be allowed after the first one completes"
2555    );
2556}