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