debugger_panel.rs

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