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::{Item, dock::Panel};
  38
  39#[gpui::test]
  40async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
  41    init_test(cx);
  42
  43    let fs = FakeFs::new(executor.clone());
  44
  45    fs.insert_tree(
  46        path!("/project"),
  47        json!({
  48            "main.rs": "First line\nSecond line\nThird line\nFourth line",
  49        }),
  50    )
  51    .await;
  52
  53    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
  54    let workspace = init_test_workspace(&project, cx).await;
  55    let cx = &mut VisualTestContext::from_window(*workspace, cx);
  56
  57    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
  58    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
  59
  60    client.on_request::<Threads, _>(move |_, _| {
  61        Ok(dap::ThreadsResponse {
  62            threads: vec![dap::Thread {
  63                id: 1,
  64                name: "Thread 1".into(),
  65            }],
  66        })
  67    });
  68
  69    client.on_request::<StackTrace, _>(move |_, _| {
  70        Ok(dap::StackTraceResponse {
  71            stack_frames: Vec::default(),
  72            total_frames: None,
  73        })
  74    });
  75
  76    cx.run_until_parked();
  77
  78    // assert we have a debug panel item before the session has stopped
  79    workspace
  80        .update(cx, |workspace, _window, cx| {
  81            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
  82            let active_session =
  83                debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
  84
  85            let running_state = active_session.update(cx, |active_session, _| {
  86                active_session.running_state().clone()
  87            });
  88
  89            debug_panel.update(cx, |this, cx| {
  90                assert!(this.active_session().is_some());
  91                assert!(running_state.read(cx).selected_thread_id().is_none());
  92            });
  93        })
  94        .unwrap();
  95
  96    client
  97        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
  98            reason: dap::StoppedEventReason::Pause,
  99            description: None,
 100            thread_id: Some(1),
 101            preserve_focus_hint: None,
 102            text: None,
 103            all_threads_stopped: None,
 104            hit_breakpoint_ids: None,
 105        }))
 106        .await;
 107
 108    cx.run_until_parked();
 109
 110    workspace
 111        .update(cx, |workspace, _window, cx| {
 112            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 113            let active_session = debug_panel
 114                .update(cx, |this, _| this.active_session())
 115                .unwrap();
 116
 117            let running_state = active_session.update(cx, |active_session, _| {
 118                active_session.running_state().clone()
 119            });
 120
 121            assert_eq!(client.id(), running_state.read(cx).session_id());
 122            assert_eq!(
 123                ThreadId(1),
 124                running_state.read(cx).selected_thread_id().unwrap()
 125            );
 126        })
 127        .unwrap();
 128
 129    let shutdown_session = project.update(cx, |project, cx| {
 130        project.dap_store().update(cx, |dap_store, cx| {
 131            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 132        })
 133    });
 134
 135    shutdown_session.await.unwrap();
 136
 137    // assert we still have a debug panel item after the client shutdown
 138    workspace
 139        .update(cx, |workspace, _window, cx| {
 140            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 141
 142            let active_session = debug_panel
 143                .update(cx, |this, _| this.active_session())
 144                .unwrap();
 145
 146            let running_state = active_session.update(cx, |active_session, _| {
 147                active_session.running_state().clone()
 148            });
 149
 150            debug_panel.update(cx, |this, cx| {
 151                assert!(this.active_session().is_some());
 152                assert_eq!(
 153                    ThreadId(1),
 154                    running_state.read(cx).selected_thread_id().unwrap()
 155                );
 156            });
 157        })
 158        .unwrap();
 159}
 160
 161#[gpui::test]
 162async fn test_we_can_only_have_one_panel_per_debug_session(
 163    executor: BackgroundExecutor,
 164    cx: &mut TestAppContext,
 165) {
 166    init_test(cx);
 167
 168    let fs = FakeFs::new(executor.clone());
 169
 170    fs.insert_tree(
 171        path!("/project"),
 172        json!({
 173            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 174        }),
 175    )
 176    .await;
 177
 178    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 179    let workspace = init_test_workspace(&project, cx).await;
 180    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 181
 182    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 183    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 184
 185    client.on_request::<Threads, _>(move |_, _| {
 186        Ok(dap::ThreadsResponse {
 187            threads: vec![dap::Thread {
 188                id: 1,
 189                name: "Thread 1".into(),
 190            }],
 191        })
 192    });
 193
 194    client.on_request::<StackTrace, _>(move |_, _| {
 195        Ok(dap::StackTraceResponse {
 196            stack_frames: Vec::default(),
 197            total_frames: None,
 198        })
 199    });
 200
 201    cx.run_until_parked();
 202
 203    // assert we have a debug panel item before the session has stopped
 204    workspace
 205        .update(cx, |workspace, _window, cx| {
 206            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 207
 208            debug_panel.update(cx, |this, _| {
 209                assert!(this.active_session().is_some());
 210            });
 211        })
 212        .unwrap();
 213
 214    client
 215        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 216            reason: dap::StoppedEventReason::Pause,
 217            description: None,
 218            thread_id: Some(1),
 219            preserve_focus_hint: None,
 220            text: None,
 221            all_threads_stopped: None,
 222            hit_breakpoint_ids: None,
 223        }))
 224        .await;
 225
 226    cx.run_until_parked();
 227
 228    // assert we added a debug panel item
 229    workspace
 230        .update(cx, |workspace, _window, cx| {
 231            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 232            let active_session = debug_panel
 233                .update(cx, |this, _| this.active_session())
 234                .unwrap();
 235
 236            let running_state = active_session.update(cx, |active_session, _| {
 237                active_session.running_state().clone()
 238            });
 239
 240            assert_eq!(client.id(), active_session.read(cx).session_id(cx));
 241            assert_eq!(
 242                ThreadId(1),
 243                running_state.read(cx).selected_thread_id().unwrap()
 244            );
 245        })
 246        .unwrap();
 247
 248    client
 249        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 250            reason: dap::StoppedEventReason::Pause,
 251            description: None,
 252            thread_id: Some(2),
 253            preserve_focus_hint: None,
 254            text: None,
 255            all_threads_stopped: None,
 256            hit_breakpoint_ids: None,
 257        }))
 258        .await;
 259
 260    cx.run_until_parked();
 261
 262    workspace
 263        .update(cx, |workspace, _window, cx| {
 264            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 265            let active_session = debug_panel
 266                .update(cx, |this, _| this.active_session())
 267                .unwrap();
 268
 269            let running_state = active_session.update(cx, |active_session, _| {
 270                active_session.running_state().clone()
 271            });
 272
 273            assert_eq!(client.id(), active_session.read(cx).session_id(cx));
 274            assert_eq!(
 275                ThreadId(1),
 276                running_state.read(cx).selected_thread_id().unwrap()
 277            );
 278        })
 279        .unwrap();
 280
 281    let shutdown_session = project.update(cx, |project, cx| {
 282        project.dap_store().update(cx, |dap_store, cx| {
 283            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 284        })
 285    });
 286
 287    shutdown_session.await.unwrap();
 288
 289    // assert we still have a debug panel item after the client shutdown
 290    workspace
 291        .update(cx, |workspace, _window, cx| {
 292            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 293            let active_session = debug_panel
 294                .update(cx, |this, _| this.active_session())
 295                .unwrap();
 296
 297            let running_state = active_session.update(cx, |active_session, _| {
 298                active_session.running_state().clone()
 299            });
 300
 301            debug_panel.update(cx, |this, cx| {
 302                assert!(this.active_session().is_some());
 303                assert_eq!(
 304                    ThreadId(1),
 305                    running_state.read(cx).selected_thread_id().unwrap()
 306                );
 307            });
 308        })
 309        .unwrap();
 310}
 311
 312#[gpui::test]
 313async fn test_handle_successful_run_in_terminal_reverse_request(
 314    executor: BackgroundExecutor,
 315    cx: &mut TestAppContext,
 316) {
 317    // needed because the debugger launches a terminal which starts a background PTY
 318    cx.executor().allow_parking();
 319    init_test(cx);
 320
 321    let send_response = Arc::new(AtomicBool::new(false));
 322
 323    let fs = FakeFs::new(executor.clone());
 324
 325    fs.insert_tree(
 326        path!("/project"),
 327        json!({
 328            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 329        }),
 330    )
 331    .await;
 332
 333    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 334    let workspace = init_test_workspace(&project, cx).await;
 335    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 336
 337    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 338    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 339
 340    client
 341        .on_response::<RunInTerminal, _>({
 342            let send_response = send_response.clone();
 343            move |response| {
 344                send_response.store(true, Ordering::SeqCst);
 345
 346                assert!(response.success);
 347                assert!(response.body.is_some());
 348            }
 349        })
 350        .await;
 351
 352    client
 353        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 354            kind: None,
 355            title: None,
 356            cwd: std::env::temp_dir().to_string_lossy().into_owned(),
 357            args: vec![],
 358            env: None,
 359            args_can_be_interpreted_by_shell: None,
 360        })
 361        .await;
 362
 363    cx.run_until_parked();
 364
 365    assert!(
 366        send_response.load(std::sync::atomic::Ordering::SeqCst),
 367        "Expected to receive response from reverse request"
 368    );
 369
 370    workspace
 371        .update(cx, |workspace, _window, cx| {
 372            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 373            let session = debug_panel.read(cx).active_session().unwrap();
 374            let running = session.read(cx).running_state();
 375            assert_eq!(
 376                running
 377                    .read(cx)
 378                    .pane_items_status(cx)
 379                    .get(&DebuggerPaneItem::Terminal),
 380                Some(&true)
 381            );
 382            assert!(running.read(cx).debug_terminal.read(cx).terminal.is_some());
 383        })
 384        .unwrap();
 385}
 386
 387#[gpui::test]
 388async fn test_handle_start_debugging_request(
 389    executor: BackgroundExecutor,
 390    cx: &mut TestAppContext,
 391) {
 392    init_test(cx);
 393
 394    let fs = FakeFs::new(executor.clone());
 395
 396    fs.insert_tree(
 397        path!("/project"),
 398        json!({
 399            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 400        }),
 401    )
 402    .await;
 403
 404    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 405    let workspace = init_test_workspace(&project, cx).await;
 406    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 407
 408    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 409    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 410
 411    let fake_config = json!({"one": "two"});
 412    let launched_with = Arc::new(parking_lot::Mutex::new(None));
 413
 414    let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
 415        let launched_with = launched_with.clone();
 416        move |client| {
 417            let launched_with = launched_with.clone();
 418            client.on_request::<dap::requests::Launch, _>(move |_, args| {
 419                launched_with.lock().replace(args.raw);
 420                Ok(())
 421            });
 422            client.on_request::<dap::requests::Attach, _>(move |_, _| {
 423                assert!(false, "should not get attach request");
 424                Ok(())
 425            });
 426        }
 427    });
 428
 429    let sessions = workspace
 430        .update(cx, |workspace, _window, cx| {
 431            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 432            debug_panel.read(cx).sessions().collect::<Vec<_>>()
 433        })
 434        .unwrap();
 435    assert_eq!(sessions.len(), 1);
 436    client
 437        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 438            request: StartDebuggingRequestArgumentsRequest::Launch,
 439            configuration: fake_config.clone(),
 440        })
 441        .await;
 442
 443    cx.run_until_parked();
 444
 445    workspace
 446        .update(cx, |workspace, _window, cx| {
 447            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 448
 449            // Active session changes on spawn, as the parent has never stopped.
 450            let active_session = debug_panel
 451                .read(cx)
 452                .active_session()
 453                .unwrap()
 454                .read(cx)
 455                .session(cx);
 456            let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
 457            assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
 458            assert_eq!(
 459                active_session.read(cx).parent_session(),
 460                Some(&current_sessions[0].read(cx).session(cx))
 461            );
 462
 463            assert_eq!(current_sessions.len(), 2);
 464            assert_eq!(current_sessions[0], sessions[0]);
 465
 466            let parent_session = current_sessions[1]
 467                .read(cx)
 468                .session(cx)
 469                .read(cx)
 470                .parent_session()
 471                .unwrap();
 472            assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
 473
 474            // We should preserve the original binary (params to spawn process etc.) except for launch params
 475            // (as they come from reverse spawn request).
 476            let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
 477            original_binary.request_args = StartDebuggingRequestArguments {
 478                request: StartDebuggingRequestArgumentsRequest::Launch,
 479                configuration: fake_config.clone(),
 480            };
 481
 482            assert_eq!(
 483                current_sessions[1]
 484                    .read(cx)
 485                    .session(cx)
 486                    .read(cx)
 487                    .binary()
 488                    .unwrap(),
 489                &original_binary
 490            );
 491        })
 492        .unwrap();
 493
 494    assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
 495}
 496
 497// // covers that we always send a response back, if something when wrong,
 498// // while spawning the terminal
 499#[gpui::test]
 500async fn test_handle_error_run_in_terminal_reverse_request(
 501    executor: BackgroundExecutor,
 502    cx: &mut TestAppContext,
 503) {
 504    init_test(cx);
 505
 506    let send_response = Arc::new(AtomicBool::new(false));
 507
 508    let fs = FakeFs::new(executor.clone());
 509
 510    fs.insert_tree(
 511        path!("/project"),
 512        json!({
 513            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 514        }),
 515    )
 516    .await;
 517
 518    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 519    let workspace = init_test_workspace(&project, cx).await;
 520    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 521
 522    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 523    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 524
 525    client
 526        .on_response::<RunInTerminal, _>({
 527            let send_response = send_response.clone();
 528            move |response| {
 529                send_response.store(true, Ordering::SeqCst);
 530
 531                assert!(!response.success);
 532                assert!(response.body.is_some());
 533            }
 534        })
 535        .await;
 536
 537    client
 538        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 539            kind: None,
 540            title: None,
 541            cwd: "".into(),
 542            args: vec!["oops".into(), "oops".into()],
 543            env: None,
 544            args_can_be_interpreted_by_shell: None,
 545        })
 546        .await;
 547
 548    cx.run_until_parked();
 549
 550    assert!(
 551        send_response.load(std::sync::atomic::Ordering::SeqCst),
 552        "Expected to receive response from reverse request"
 553    );
 554
 555    workspace
 556        .update(cx, |workspace, _window, cx| {
 557            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
 558
 559            assert_eq!(
 560                0,
 561                terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
 562            );
 563        })
 564        .unwrap();
 565}
 566
 567#[gpui::test]
 568async fn test_handle_start_debugging_reverse_request(
 569    executor: BackgroundExecutor,
 570    cx: &mut TestAppContext,
 571) {
 572    cx.executor().allow_parking();
 573    init_test(cx);
 574
 575    let send_response = Arc::new(AtomicBool::new(false));
 576
 577    let fs = FakeFs::new(executor.clone());
 578
 579    fs.insert_tree(
 580        path!("/project"),
 581        json!({
 582            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 583        }),
 584    )
 585    .await;
 586
 587    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 588    let workspace = init_test_workspace(&project, cx).await;
 589    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 590
 591    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 592    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 593
 594    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 595        Ok(dap::ThreadsResponse {
 596            threads: vec![dap::Thread {
 597                id: 1,
 598                name: "Thread 1".into(),
 599            }],
 600        })
 601    });
 602
 603    client
 604        .on_response::<StartDebugging, _>({
 605            let send_response = send_response.clone();
 606            move |response| {
 607                send_response.store(true, Ordering::SeqCst);
 608
 609                assert!(response.success);
 610                assert!(response.body.is_some());
 611            }
 612        })
 613        .await;
 614    // Set up handlers for sessions spawned with reverse request too.
 615    let _reverse_request_subscription =
 616        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 617    client
 618        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 619            configuration: json!({}),
 620            request: StartDebuggingRequestArgumentsRequest::Launch,
 621        })
 622        .await;
 623
 624    cx.run_until_parked();
 625
 626    let child_session = project.update(cx, |project, cx| {
 627        project
 628            .dap_store()
 629            .read(cx)
 630            .session_by_id(SessionId(1))
 631            .unwrap()
 632    });
 633    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
 634
 635    child_client.on_request::<dap::requests::Threads, _>(move |_, _| {
 636        Ok(dap::ThreadsResponse {
 637            threads: vec![dap::Thread {
 638                id: 1,
 639                name: "Thread 1".into(),
 640            }],
 641        })
 642    });
 643
 644    child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 645
 646    child_client
 647        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 648            reason: dap::StoppedEventReason::Pause,
 649            description: None,
 650            thread_id: Some(2),
 651            preserve_focus_hint: None,
 652            text: None,
 653            all_threads_stopped: None,
 654            hit_breakpoint_ids: None,
 655        }))
 656        .await;
 657
 658    cx.run_until_parked();
 659
 660    assert!(
 661        send_response.load(std::sync::atomic::Ordering::SeqCst),
 662        "Expected to receive response from reverse request"
 663    );
 664}
 665
 666#[gpui::test]
 667async fn test_shutdown_children_when_parent_session_shutdown(
 668    executor: BackgroundExecutor,
 669    cx: &mut TestAppContext,
 670) {
 671    init_test(cx);
 672
 673    let fs = FakeFs::new(executor.clone());
 674
 675    fs.insert_tree(
 676        path!("/project"),
 677        json!({
 678            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 679        }),
 680    )
 681    .await;
 682
 683    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 684    let dap_store = project.update(cx, |project, _| project.dap_store());
 685    let workspace = init_test_workspace(&project, cx).await;
 686    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 687
 688    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 689    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 690
 691    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 692        Ok(dap::ThreadsResponse {
 693            threads: vec![dap::Thread {
 694                id: 1,
 695                name: "Thread 1".into(),
 696            }],
 697        })
 698    });
 699
 700    client.on_response::<StartDebugging, _>(move |_| {}).await;
 701    // Set up handlers for sessions spawned with reverse request too.
 702    let _reverse_request_subscription =
 703        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 704    // start first child session
 705    client
 706        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 707            configuration: json!({}),
 708            request: StartDebuggingRequestArgumentsRequest::Launch,
 709        })
 710        .await;
 711
 712    cx.run_until_parked();
 713
 714    // start second child session
 715    client
 716        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 717            configuration: json!({}),
 718            request: StartDebuggingRequestArgumentsRequest::Launch,
 719        })
 720        .await;
 721
 722    cx.run_until_parked();
 723
 724    // configure first child session
 725    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 726        dap_store.session_by_id(SessionId(1)).unwrap()
 727    });
 728    let first_child_client =
 729        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 730
 731    first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 732
 733    // configure second child session
 734    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 735        dap_store.session_by_id(SessionId(2)).unwrap()
 736    });
 737    let second_child_client =
 738        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 739
 740    second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 741
 742    cx.run_until_parked();
 743
 744    // shutdown parent session
 745    dap_store
 746        .update(cx, |dap_store, cx| {
 747            dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
 748        })
 749        .await
 750        .unwrap();
 751
 752    // assert parent session and all children sessions are shutdown
 753    dap_store.update(cx, |dap_store, cx| {
 754        assert!(
 755            dap_store
 756                .session_by_id(parent_session.read(cx).session_id())
 757                .is_none()
 758        );
 759        assert!(
 760            dap_store
 761                .session_by_id(first_child_session.read(cx).session_id())
 762                .is_none()
 763        );
 764        assert!(
 765            dap_store
 766                .session_by_id(second_child_session.read(cx).session_id())
 767                .is_none()
 768        );
 769    });
 770}
 771
 772#[gpui::test]
 773async fn test_shutdown_parent_session_if_all_children_are_shutdown(
 774    executor: BackgroundExecutor,
 775    cx: &mut TestAppContext,
 776) {
 777    init_test(cx);
 778
 779    let fs = FakeFs::new(executor.clone());
 780
 781    fs.insert_tree(
 782        path!("/project"),
 783        json!({
 784            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 785        }),
 786    )
 787    .await;
 788
 789    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 790    let dap_store = project.update(cx, |project, _| project.dap_store());
 791    let workspace = init_test_workspace(&project, cx).await;
 792    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 793
 794    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
 795    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 796
 797    client.on_response::<StartDebugging, _>(move |_| {}).await;
 798    // Set up handlers for sessions spawned with reverse request too.
 799    let _reverse_request_subscription =
 800        project::debugger::test::intercept_debug_sessions(cx, |_| {});
 801    // start first child session
 802    client
 803        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 804            configuration: json!({}),
 805            request: StartDebuggingRequestArgumentsRequest::Launch,
 806        })
 807        .await;
 808
 809    cx.run_until_parked();
 810
 811    // start second child session
 812    client
 813        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 814            configuration: json!({}),
 815            request: StartDebuggingRequestArgumentsRequest::Launch,
 816        })
 817        .await;
 818
 819    cx.run_until_parked();
 820
 821    // configure first child session
 822    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 823        dap_store.session_by_id(SessionId(1)).unwrap()
 824    });
 825    let first_child_client =
 826        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 827
 828    first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 829
 830    // configure second child session
 831    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 832        dap_store.session_by_id(SessionId(2)).unwrap()
 833    });
 834    let second_child_client =
 835        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 836
 837    second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
 838
 839    cx.run_until_parked();
 840
 841    // shutdown first child session
 842    dap_store
 843        .update(cx, |dap_store, cx| {
 844            dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
 845        })
 846        .await
 847        .unwrap();
 848
 849    // assert parent session and second child session still exist
 850    dap_store.update(cx, |dap_store, cx| {
 851        assert!(
 852            dap_store
 853                .session_by_id(parent_session.read(cx).session_id())
 854                .is_some()
 855        );
 856        assert!(
 857            dap_store
 858                .session_by_id(first_child_session.read(cx).session_id())
 859                .is_none()
 860        );
 861        assert!(
 862            dap_store
 863                .session_by_id(second_child_session.read(cx).session_id())
 864                .is_some()
 865        );
 866    });
 867
 868    // shutdown first child session
 869    dap_store
 870        .update(cx, |dap_store, cx| {
 871            dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
 872        })
 873        .await
 874        .unwrap();
 875
 876    // assert parent session got shutdown by second child session
 877    // because it was the last child
 878    dap_store.update(cx, |dap_store, cx| {
 879        assert!(
 880            dap_store
 881                .session_by_id(parent_session.read(cx).session_id())
 882                .is_none()
 883        );
 884        assert!(
 885            dap_store
 886                .session_by_id(second_child_session.read(cx).session_id())
 887                .is_none()
 888        );
 889    });
 890}
 891
 892#[gpui::test]
 893async fn test_debug_panel_item_thread_status_reset_on_failure(
 894    executor: BackgroundExecutor,
 895    cx: &mut TestAppContext,
 896) {
 897    init_test(cx);
 898
 899    let fs = FakeFs::new(executor.clone());
 900
 901    fs.insert_tree(
 902        path!("/project"),
 903        json!({
 904            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 905        }),
 906    )
 907    .await;
 908
 909    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 910    let workspace = init_test_workspace(&project, cx).await;
 911    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 912
 913    let session = start_debug_session(&workspace, cx, |client| {
 914        client.on_request::<dap::requests::Initialize, _>(move |_, _| {
 915            Ok(dap::Capabilities {
 916                supports_step_back: Some(true),
 917                ..Default::default()
 918            })
 919        });
 920    })
 921    .unwrap();
 922
 923    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 924    const THREAD_ID_NUM: i64 = 1;
 925
 926    client.on_request::<dap::requests::Threads, _>(move |_, _| {
 927        Ok(dap::ThreadsResponse {
 928            threads: vec![dap::Thread {
 929                id: THREAD_ID_NUM,
 930                name: "Thread 1".into(),
 931            }],
 932        })
 933    });
 934
 935    client.on_request::<Launch, _>(move |_, _| Ok(()));
 936
 937    client.on_request::<StackTrace, _>(move |_, _| {
 938        Ok(dap::StackTraceResponse {
 939            stack_frames: Vec::default(),
 940            total_frames: None,
 941        })
 942    });
 943
 944    client.on_request::<Next, _>(move |_, _| {
 945        Err(ErrorResponse {
 946            error: Some(dap::Message {
 947                id: 1,
 948                format: "error".into(),
 949                variables: None,
 950                send_telemetry: None,
 951                show_user: None,
 952                url: None,
 953                url_label: None,
 954            }),
 955        })
 956    });
 957
 958    client.on_request::<StepOut, _>(move |_, _| {
 959        Err(ErrorResponse {
 960            error: Some(dap::Message {
 961                id: 1,
 962                format: "error".into(),
 963                variables: None,
 964                send_telemetry: None,
 965                show_user: None,
 966                url: None,
 967                url_label: None,
 968            }),
 969        })
 970    });
 971
 972    client.on_request::<StepIn, _>(move |_, _| {
 973        Err(ErrorResponse {
 974            error: Some(dap::Message {
 975                id: 1,
 976                format: "error".into(),
 977                variables: None,
 978                send_telemetry: None,
 979                show_user: None,
 980                url: None,
 981                url_label: None,
 982            }),
 983        })
 984    });
 985
 986    client.on_request::<StepBack, _>(move |_, _| {
 987        Err(ErrorResponse {
 988            error: Some(dap::Message {
 989                id: 1,
 990                format: "error".into(),
 991                variables: None,
 992                send_telemetry: None,
 993                show_user: None,
 994                url: None,
 995                url_label: None,
 996            }),
 997        })
 998    });
 999
1000    client.on_request::<Continue, _>(move |_, _| {
1001        Err(ErrorResponse {
1002            error: Some(dap::Message {
1003                id: 1,
1004                format: "error".into(),
1005                variables: None,
1006                send_telemetry: None,
1007                show_user: None,
1008                url: None,
1009                url_label: None,
1010            }),
1011        })
1012    });
1013
1014    client
1015        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1016            reason: dap::StoppedEventReason::Pause,
1017            description: None,
1018            thread_id: Some(1),
1019            preserve_focus_hint: None,
1020            text: None,
1021            all_threads_stopped: None,
1022            hit_breakpoint_ids: None,
1023        }))
1024        .await;
1025
1026    cx.run_until_parked();
1027
1028    let running_state = active_debug_session_panel(workspace, cx)
1029        .read_with(cx, |item, _| item.running_state().clone());
1030
1031    cx.run_until_parked();
1032    let thread_id = ThreadId(1);
1033
1034    for operation in &[
1035        "step_over",
1036        "continue_thread",
1037        "step_back",
1038        "step_in",
1039        "step_out",
1040    ] {
1041        running_state.update(cx, |running_state, cx| match *operation {
1042            "step_over" => running_state.step_over(cx),
1043            "continue_thread" => running_state.continue_thread(cx),
1044            "step_back" => running_state.step_back(cx),
1045            "step_in" => running_state.step_in(cx),
1046            "step_out" => running_state.step_out(cx),
1047            _ => unreachable!(),
1048        });
1049
1050        // Check that we step the thread status to the correct intermediate state
1051        running_state.update(cx, |running_state, cx| {
1052            assert_eq!(
1053                running_state
1054                    .thread_status(cx)
1055                    .expect("There should be an active thread selected"),
1056                match *operation {
1057                    "continue_thread" => ThreadStatus::Running,
1058                    _ => ThreadStatus::Stepping,
1059                },
1060                "Thread status was not set to correct intermediate state after {} request",
1061                operation
1062            );
1063        });
1064
1065        cx.run_until_parked();
1066
1067        running_state.update(cx, |running_state, cx| {
1068            assert_eq!(
1069                running_state
1070                    .thread_status(cx)
1071                    .expect("There should be an active thread selected"),
1072                ThreadStatus::Stopped,
1073                "Thread status not reset to Stopped after failed {}",
1074                operation
1075            );
1076
1077            // update state to running, so we can test it actually changes the status back to stopped
1078            running_state
1079                .session()
1080                .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1081        });
1082    }
1083}
1084
1085#[gpui::test]
1086async fn test_send_breakpoints_when_editor_has_been_saved(
1087    executor: BackgroundExecutor,
1088    cx: &mut TestAppContext,
1089) {
1090    init_test(cx);
1091
1092    let fs = FakeFs::new(executor.clone());
1093
1094    fs.insert_tree(
1095        path!("/project"),
1096        json!({
1097            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1098        }),
1099    )
1100    .await;
1101
1102    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1103    let workspace = init_test_workspace(&project, cx).await;
1104    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1105    let project_path = Path::new(path!("/project"));
1106    let worktree = project
1107        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1108        .expect("This worktree should exist in project")
1109        .0;
1110
1111    let worktree_id = workspace
1112        .update(cx, |_, _, cx| worktree.read(cx).id())
1113        .unwrap();
1114
1115    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1116    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1117
1118    let buffer = project
1119        .update(cx, |project, cx| {
1120            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1121        })
1122        .await
1123        .unwrap();
1124
1125    let (editor, cx) = cx.add_window_view(|window, cx| {
1126        Editor::new(
1127            EditorMode::full(),
1128            MultiBuffer::build_from_buffer(buffer, cx),
1129            Some(project.clone()),
1130            window,
1131            cx,
1132        )
1133    });
1134
1135    client.on_request::<Launch, _>(move |_, _| Ok(()));
1136
1137    client.on_request::<StackTrace, _>(move |_, _| {
1138        Ok(dap::StackTraceResponse {
1139            stack_frames: Vec::default(),
1140            total_frames: None,
1141        })
1142    });
1143
1144    client
1145        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1146            reason: dap::StoppedEventReason::Pause,
1147            description: None,
1148            thread_id: Some(1),
1149            preserve_focus_hint: None,
1150            text: None,
1151            all_threads_stopped: None,
1152            hit_breakpoint_ids: None,
1153        }))
1154        .await;
1155
1156    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1157    client.on_request::<SetBreakpoints, _>({
1158        let called_set_breakpoints = called_set_breakpoints.clone();
1159        move |_, args| {
1160            assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1161            assert_eq!(
1162                vec![SourceBreakpoint {
1163                    line: 2,
1164                    column: None,
1165                    condition: None,
1166                    hit_condition: None,
1167                    log_message: None,
1168                    mode: None
1169                }],
1170                args.breakpoints.unwrap()
1171            );
1172            assert!(!args.source_modified.unwrap());
1173
1174            called_set_breakpoints.store(true, Ordering::SeqCst);
1175
1176            Ok(dap::SetBreakpointsResponse {
1177                breakpoints: Vec::default(),
1178            })
1179        }
1180    });
1181
1182    editor.update_in(cx, |editor, window, cx| {
1183        editor.move_down(&actions::MoveDown, window, cx);
1184        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1185    });
1186
1187    cx.run_until_parked();
1188
1189    assert!(
1190        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1191        "SetBreakpoint request must be called"
1192    );
1193
1194    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1195    client.on_request::<SetBreakpoints, _>({
1196        let called_set_breakpoints = called_set_breakpoints.clone();
1197        move |_, args| {
1198            assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1199            assert_eq!(
1200                vec![SourceBreakpoint {
1201                    line: 3,
1202                    column: None,
1203                    condition: None,
1204                    hit_condition: None,
1205                    log_message: None,
1206                    mode: None
1207                }],
1208                args.breakpoints.unwrap()
1209            );
1210            assert!(args.source_modified.unwrap());
1211
1212            called_set_breakpoints.store(true, Ordering::SeqCst);
1213
1214            Ok(dap::SetBreakpointsResponse {
1215                breakpoints: Vec::default(),
1216            })
1217        }
1218    });
1219
1220    editor.update_in(cx, |editor, window, cx| {
1221        editor.move_up(&actions::MoveUp, window, cx);
1222        editor.insert("new text\n", window, cx);
1223    });
1224
1225    editor
1226        .update_in(cx, |editor, window, cx| {
1227            editor.save(
1228                SaveOptions {
1229                    format: true,
1230                    autosave: false,
1231                },
1232                project.clone(),
1233                window,
1234                cx,
1235            )
1236        })
1237        .await
1238        .unwrap();
1239
1240    cx.run_until_parked();
1241
1242    assert!(
1243        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1244        "SetBreakpoint request must be called after editor is saved"
1245    );
1246}
1247
1248#[gpui::test]
1249async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1250    executor: BackgroundExecutor,
1251    cx: &mut TestAppContext,
1252) {
1253    init_test(cx);
1254
1255    let fs = FakeFs::new(executor.clone());
1256
1257    fs.insert_tree(
1258        path!("/project"),
1259        json!({
1260            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1261            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1262            "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1263        }),
1264    )
1265    .await;
1266
1267    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1268    let workspace = init_test_workspace(&project, cx).await;
1269    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1270    let project_path = Path::new(path!("/project"));
1271    let worktree = project
1272        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1273        .expect("This worktree should exist in project")
1274        .0;
1275
1276    let worktree_id = workspace
1277        .update(cx, |_, _, cx| worktree.read(cx).id())
1278        .unwrap();
1279
1280    let first = project
1281        .update(cx, |project, cx| {
1282            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1283        })
1284        .await
1285        .unwrap();
1286
1287    let second = project
1288        .update(cx, |project, cx| {
1289            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1290        })
1291        .await
1292        .unwrap();
1293
1294    let (first_editor, cx) = cx.add_window_view(|window, cx| {
1295        Editor::new(
1296            EditorMode::full(),
1297            MultiBuffer::build_from_buffer(first, cx),
1298            Some(project.clone()),
1299            window,
1300            cx,
1301        )
1302    });
1303
1304    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1305        Editor::new(
1306            EditorMode::full(),
1307            MultiBuffer::build_from_buffer(second, cx),
1308            Some(project.clone()),
1309            window,
1310            cx,
1311        )
1312    });
1313
1314    first_editor.update_in(cx, |editor, window, cx| {
1315        editor.move_down(&actions::MoveDown, window, cx);
1316        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1317        editor.move_down(&actions::MoveDown, window, cx);
1318        editor.move_down(&actions::MoveDown, window, cx);
1319        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1320    });
1321
1322    second_editor.update_in(cx, |editor, window, cx| {
1323        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1324        editor.move_down(&actions::MoveDown, window, cx);
1325        editor.move_down(&actions::MoveDown, window, cx);
1326        editor.move_down(&actions::MoveDown, window, cx);
1327        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1328    });
1329
1330    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1331    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1332
1333    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1334
1335    client.on_request::<SetBreakpoints, _>({
1336        move |_, args| {
1337            assert!(
1338                args.breakpoints.is_none_or(|bps| bps.is_empty()),
1339                "Send empty breakpoint sets to clear them from DAP servers"
1340            );
1341
1342            match args
1343                .source
1344                .path
1345                .expect("We should always send a breakpoint's path")
1346                .as_str()
1347            {
1348                path!("/project/main.rs") | path!("/project/second.rs") => {}
1349                _ => {
1350                    panic!("Unset breakpoints for path that doesn't have any")
1351                }
1352            }
1353
1354            called_set_breakpoints.store(true, Ordering::SeqCst);
1355
1356            Ok(dap::SetBreakpointsResponse {
1357                breakpoints: Vec::default(),
1358            })
1359        }
1360    });
1361
1362    cx.dispatch_action(crate::ClearAllBreakpoints);
1363    cx.run_until_parked();
1364}
1365
1366#[gpui::test]
1367async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1368    executor: BackgroundExecutor,
1369    cx: &mut TestAppContext,
1370) {
1371    init_test(cx);
1372
1373    let fs = FakeFs::new(executor.clone());
1374
1375    fs.insert_tree(
1376        path!("/project"),
1377        json!({
1378            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1379        }),
1380    )
1381    .await;
1382
1383    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1384    let workspace = init_test_workspace(&project, cx).await;
1385    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1386
1387    start_debug_session(&workspace, cx, |client| {
1388        client.on_request::<dap::requests::Initialize, _>(|_, _| {
1389            Err(ErrorResponse {
1390                error: Some(Message {
1391                    format: "failed to launch".to_string(),
1392                    id: 1,
1393                    variables: None,
1394                    send_telemetry: None,
1395                    show_user: None,
1396                    url: None,
1397                    url_label: None,
1398                }),
1399            })
1400        });
1401    })
1402    .ok();
1403
1404    cx.run_until_parked();
1405
1406    project.update(cx, |project, cx| {
1407        assert!(
1408            project.dap_store().read(cx).sessions().count() == 0,
1409            "Session wouldn't exist if it was shutdown"
1410        );
1411    });
1412}
1413
1414#[gpui::test]
1415async fn test_we_send_arguments_from_user_config(
1416    executor: BackgroundExecutor,
1417    cx: &mut TestAppContext,
1418) {
1419    init_test(cx);
1420
1421    let fs = FakeFs::new(executor.clone());
1422
1423    fs.insert_tree(
1424        path!("/project"),
1425        json!({
1426            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1427        }),
1428    )
1429    .await;
1430
1431    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1432    let workspace = init_test_workspace(&project, cx).await;
1433    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1434    let debug_definition = DebugTaskDefinition {
1435        adapter: "fake-adapter".into(),
1436        config: json!({
1437            "request": "launch",
1438            "program": "main.rs".to_owned(),
1439            "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1440            "cwd": path!("/Random_path"),
1441            "env": json!({ "KEY": "VALUE" }),
1442        }),
1443        label: "test".into(),
1444        tcp_connection: None,
1445    };
1446
1447    let launch_handler_called = Arc::new(AtomicBool::new(false));
1448
1449    start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1450        let launch_handler_called = launch_handler_called.clone();
1451
1452        move |client| {
1453            let debug_definition = debug_definition.clone();
1454            let launch_handler_called = launch_handler_called.clone();
1455
1456            client.on_request::<dap::requests::Launch, _>(move |_, args| {
1457                launch_handler_called.store(true, Ordering::SeqCst);
1458
1459                assert_eq!(args.raw, debug_definition.config);
1460
1461                Ok(())
1462            });
1463        }
1464    })
1465    .ok();
1466
1467    cx.run_until_parked();
1468
1469    assert!(
1470        launch_handler_called.load(Ordering::SeqCst),
1471        "Launch request handler was not called"
1472    );
1473}
1474
1475#[gpui::test]
1476async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1477    init_test(cx);
1478
1479    let fs = FakeFs::new(executor.clone());
1480
1481    fs.insert_tree(
1482        path!("/project"),
1483        json!({
1484            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1485            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1486        }),
1487    )
1488    .await;
1489
1490    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1491    let workspace = init_test_workspace(&project, cx).await;
1492    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1493    let project_path = Path::new(path!("/project"));
1494    let worktree = project
1495        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1496        .expect("This worktree should exist in project")
1497        .0;
1498
1499    let worktree_id = workspace
1500        .update(cx, |_, _, cx| worktree.read(cx).id())
1501        .unwrap();
1502
1503    let main_buffer = project
1504        .update(cx, |project, cx| {
1505            project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1506        })
1507        .await
1508        .unwrap();
1509
1510    let second_buffer = project
1511        .update(cx, |project, cx| {
1512            project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1513        })
1514        .await
1515        .unwrap();
1516
1517    let (main_editor, cx) = cx.add_window_view(|window, cx| {
1518        Editor::new(
1519            EditorMode::full(),
1520            MultiBuffer::build_from_buffer(main_buffer, cx),
1521            Some(project.clone()),
1522            window,
1523            cx,
1524        )
1525    });
1526
1527    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1528        Editor::new(
1529            EditorMode::full(),
1530            MultiBuffer::build_from_buffer(second_buffer, cx),
1531            Some(project.clone()),
1532            window,
1533            cx,
1534        )
1535    });
1536
1537    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1538    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1539
1540    client.on_request::<dap::requests::Threads, _>(move |_, _| {
1541        Ok(dap::ThreadsResponse {
1542            threads: vec![dap::Thread {
1543                id: 1,
1544                name: "Thread 1".into(),
1545            }],
1546        })
1547    });
1548
1549    client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1550        Ok(dap::ScopesResponse {
1551            scopes: Vec::default(),
1552        })
1553    });
1554
1555    client.on_request::<StackTrace, _>(move |_, args| {
1556        assert_eq!(args.thread_id, 1);
1557
1558        Ok(dap::StackTraceResponse {
1559            stack_frames: vec![dap::StackFrame {
1560                id: 1,
1561                name: "frame 1".into(),
1562                source: Some(dap::Source {
1563                    name: Some("main.rs".into()),
1564                    path: Some(path!("/project/main.rs").into()),
1565                    source_reference: None,
1566                    presentation_hint: None,
1567                    origin: None,
1568                    sources: None,
1569                    adapter_data: None,
1570                    checksums: None,
1571                }),
1572                line: 2,
1573                column: 0,
1574                end_line: None,
1575                end_column: None,
1576                can_restart: None,
1577                instruction_pointer_reference: None,
1578                module_id: None,
1579                presentation_hint: None,
1580            }],
1581            total_frames: None,
1582        })
1583    });
1584
1585    client
1586        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1587            reason: dap::StoppedEventReason::Breakpoint,
1588            description: None,
1589            thread_id: Some(1),
1590            preserve_focus_hint: None,
1591            text: None,
1592            all_threads_stopped: None,
1593            hit_breakpoint_ids: None,
1594        }))
1595        .await;
1596
1597    cx.run_until_parked();
1598
1599    main_editor.update_in(cx, |editor, window, cx| {
1600        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1601
1602        assert_eq!(
1603            active_debug_lines.len(),
1604            1,
1605            "There should be only one active debug line"
1606        );
1607
1608        let point = editor
1609            .snapshot(window, cx)
1610            .buffer_snapshot()
1611            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1612
1613        assert_eq!(point.row, 1);
1614    });
1615
1616    second_editor.update(cx, |editor, _| {
1617        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1618
1619        assert!(
1620            active_debug_lines.is_empty(),
1621            "There shouldn't be any active debug lines"
1622        );
1623    });
1624
1625    let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1626    client.on_request::<StackTrace, _>({
1627        let handled_second_stacktrace = handled_second_stacktrace.clone();
1628        move |_, args| {
1629            handled_second_stacktrace.store(true, Ordering::SeqCst);
1630            assert_eq!(args.thread_id, 1);
1631
1632            Ok(dap::StackTraceResponse {
1633                stack_frames: vec![dap::StackFrame {
1634                    id: 2,
1635                    name: "frame 2".into(),
1636                    source: Some(dap::Source {
1637                        name: Some("second.rs".into()),
1638                        path: Some(path!("/project/second.rs").into()),
1639                        source_reference: None,
1640                        presentation_hint: None,
1641                        origin: None,
1642                        sources: None,
1643                        adapter_data: None,
1644                        checksums: None,
1645                    }),
1646                    line: 3,
1647                    column: 0,
1648                    end_line: None,
1649                    end_column: None,
1650                    can_restart: None,
1651                    instruction_pointer_reference: None,
1652                    module_id: None,
1653                    presentation_hint: None,
1654                }],
1655                total_frames: None,
1656            })
1657        }
1658    });
1659
1660    client
1661        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1662            reason: dap::StoppedEventReason::Breakpoint,
1663            description: None,
1664            thread_id: Some(1),
1665            preserve_focus_hint: None,
1666            text: None,
1667            all_threads_stopped: None,
1668            hit_breakpoint_ids: None,
1669        }))
1670        .await;
1671
1672    cx.run_until_parked();
1673
1674    second_editor.update_in(cx, |editor, window, cx| {
1675        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1676
1677        assert_eq!(
1678            active_debug_lines.len(),
1679            1,
1680            "There should be only one active debug line"
1681        );
1682
1683        let point = editor
1684            .snapshot(window, cx)
1685            .buffer_snapshot()
1686            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1687
1688        assert_eq!(point.row, 2);
1689    });
1690
1691    main_editor.update(cx, |editor, _| {
1692        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1693
1694        assert!(
1695            active_debug_lines.is_empty(),
1696            "There shouldn't be any active debug lines"
1697        );
1698    });
1699
1700    assert!(
1701        handled_second_stacktrace.load(Ordering::SeqCst),
1702        "Second stacktrace request handler was not called"
1703    );
1704
1705    client
1706        .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1707            thread_id: 0,
1708            all_threads_continued: Some(true),
1709        }))
1710        .await;
1711
1712    cx.run_until_parked();
1713
1714    second_editor.update(cx, |editor, _| {
1715        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1716
1717        assert!(
1718            active_debug_lines.is_empty(),
1719            "There shouldn't be any active debug lines"
1720        );
1721    });
1722
1723    main_editor.update(cx, |editor, _| {
1724        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1725
1726        assert!(
1727            active_debug_lines.is_empty(),
1728            "There shouldn't be any active debug lines"
1729        );
1730    });
1731
1732    // Clean up
1733    let shutdown_session = project.update(cx, |project, cx| {
1734        project.dap_store().update(cx, |dap_store, cx| {
1735            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1736        })
1737    });
1738
1739    shutdown_session.await.unwrap();
1740
1741    main_editor.update(cx, |editor, _| {
1742        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1743
1744        assert!(
1745            active_debug_lines.is_empty(),
1746            "There shouldn't be any active debug lines after session shutdown"
1747        );
1748    });
1749
1750    second_editor.update(cx, |editor, _| {
1751        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1752
1753        assert!(
1754            active_debug_lines.is_empty(),
1755            "There shouldn't be any active debug lines after session shutdown"
1756        );
1757    });
1758}
1759
1760#[gpui::test]
1761async fn test_debug_adapters_shutdown_on_app_quit(
1762    executor: BackgroundExecutor,
1763    cx: &mut TestAppContext,
1764) {
1765    init_test(cx);
1766
1767    let fs = FakeFs::new(executor.clone());
1768
1769    fs.insert_tree(
1770        path!("/project"),
1771        json!({
1772            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1773        }),
1774    )
1775    .await;
1776
1777    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1778    let workspace = init_test_workspace(&project, cx).await;
1779    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1780
1781    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1782    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1783
1784    let disconnect_request_received = Arc::new(AtomicBool::new(false));
1785    let disconnect_clone = disconnect_request_received.clone();
1786
1787    client.on_request::<Disconnect, _>(move |_, _| {
1788        disconnect_clone.store(true, Ordering::SeqCst);
1789        Ok(())
1790    });
1791
1792    executor.run_until_parked();
1793
1794    workspace
1795        .update(cx, |workspace, _, cx| {
1796            let panel = workspace.panel::<DebugPanel>(cx).unwrap();
1797            panel.read_with(cx, |panel, _| {
1798                assert!(
1799                    panel.sessions().next().is_some(),
1800                    "Debug session should be active"
1801                );
1802            });
1803        })
1804        .unwrap();
1805
1806    cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1807
1808    executor.run_until_parked();
1809
1810    assert!(
1811        disconnect_request_received.load(Ordering::SeqCst),
1812        "Disconnect request should have been sent to the adapter on app shutdown"
1813    );
1814}
1815
1816#[gpui::test]
1817async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
1818    executor: BackgroundExecutor,
1819    cx: &mut TestAppContext,
1820) {
1821    init_test(cx);
1822
1823    let fs = FakeFs::new(executor.clone());
1824
1825    fs.insert_tree(
1826        path!("/project"),
1827        json!({
1828            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1829        }),
1830    )
1831    .await;
1832
1833    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1834    let workspace = init_test_workspace(&project, cx).await;
1835    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1836
1837    let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1838    let parent_session_id = cx.read(|cx| parent_session.read(cx).session_id());
1839    let parent_client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
1840
1841    let disconnect_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1842    let parent_disconnect_called = Arc::new(AtomicBool::new(false));
1843    let parent_disconnect_clone = parent_disconnect_called.clone();
1844    let disconnect_count_clone = disconnect_count.clone();
1845
1846    parent_client.on_request::<Disconnect, _>(move |_, _| {
1847        parent_disconnect_clone.store(true, Ordering::SeqCst);
1848        disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
1849
1850        for _ in 0..50 {
1851            if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
1852                break;
1853            }
1854            std::thread::sleep(std::time::Duration::from_millis(1));
1855        }
1856
1857        Ok(())
1858    });
1859
1860    parent_client
1861        .on_response::<StartDebugging, _>(move |_| {})
1862        .await;
1863    let _subscription = project::debugger::test::intercept_debug_sessions(cx, |_| {});
1864
1865    parent_client
1866        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
1867            configuration: json!({}),
1868            request: StartDebuggingRequestArgumentsRequest::Launch,
1869        })
1870        .await;
1871
1872    cx.run_until_parked();
1873
1874    let child_session = project.update(cx, |project, cx| {
1875        project
1876            .dap_store()
1877            .read(cx)
1878            .session_by_id(SessionId(1))
1879            .unwrap()
1880    });
1881    let child_session_id = cx.read(|cx| child_session.read(cx).session_id());
1882    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
1883
1884    let child_disconnect_called = Arc::new(AtomicBool::new(false));
1885    let child_disconnect_clone = child_disconnect_called.clone();
1886    let disconnect_count_clone = disconnect_count.clone();
1887
1888    child_client.on_request::<Disconnect, _>(move |_, _| {
1889        child_disconnect_clone.store(true, Ordering::SeqCst);
1890        disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
1891
1892        for _ in 0..50 {
1893            if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
1894                break;
1895            }
1896            std::thread::sleep(std::time::Duration::from_millis(1));
1897        }
1898
1899        Ok(())
1900    });
1901
1902    executor.run_until_parked();
1903
1904    project.update(cx, |project, cx| {
1905        let store = project.dap_store().read(cx);
1906        assert!(store.session_by_id(parent_session_id).is_some());
1907        assert!(store.session_by_id(child_session_id).is_some());
1908    });
1909
1910    cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1911
1912    executor.run_until_parked();
1913
1914    let parent_disconnect_check = parent_disconnect_called.clone();
1915    let child_disconnect_check = child_disconnect_called.clone();
1916    let executor_clone = executor.clone();
1917    let both_disconnected = executor
1918        .spawn(async move {
1919            let parent_disconnect = parent_disconnect_check;
1920            let child_disconnect = child_disconnect_check;
1921
1922            // We only have 100ms to shutdown the app
1923            for _ in 0..100 {
1924                if parent_disconnect.load(Ordering::SeqCst)
1925                    && child_disconnect.load(Ordering::SeqCst)
1926                {
1927                    return true;
1928                }
1929
1930                executor_clone
1931                    .timer(std::time::Duration::from_millis(1))
1932                    .await;
1933            }
1934
1935            false
1936        })
1937        .await;
1938
1939    assert!(
1940        both_disconnected,
1941        "Both parent and child sessions should receive disconnect requests"
1942    );
1943
1944    assert!(
1945        parent_disconnect_called.load(Ordering::SeqCst),
1946        "Parent session should have received disconnect request"
1947    );
1948    assert!(
1949        child_disconnect_called.load(Ordering::SeqCst),
1950        "Child session should have received disconnect request"
1951    );
1952}