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;
  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().to_string(),
 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, "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, "main.rs"), cx)
1280        })
1281        .await
1282        .unwrap();
1283
1284    let second = project
1285        .update(cx, |project, cx| {
1286            project.open_buffer((worktree_id, "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        let called_set_breakpoints = called_set_breakpoints.clone();
1334        move |_, args| {
1335            assert!(
1336                args.breakpoints.is_none_or(|bps| bps.is_empty()),
1337                "Send empty breakpoint sets to clear them from DAP servers"
1338            );
1339
1340            match args
1341                .source
1342                .path
1343                .expect("We should always send a breakpoint's path")
1344                .as_str()
1345            {
1346                path!("/project/main.rs") | path!("/project/second.rs") => {}
1347                _ => {
1348                    panic!("Unset breakpoints for path that doesn't have any")
1349                }
1350            }
1351
1352            called_set_breakpoints.store(true, Ordering::SeqCst);
1353
1354            Ok(dap::SetBreakpointsResponse {
1355                breakpoints: Vec::default(),
1356            })
1357        }
1358    });
1359
1360    cx.dispatch_action(crate::ClearAllBreakpoints);
1361    cx.run_until_parked();
1362}
1363
1364#[gpui::test]
1365async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1366    executor: BackgroundExecutor,
1367    cx: &mut TestAppContext,
1368) {
1369    init_test(cx);
1370
1371    let fs = FakeFs::new(executor.clone());
1372
1373    fs.insert_tree(
1374        path!("/project"),
1375        json!({
1376            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1377        }),
1378    )
1379    .await;
1380
1381    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1382    let workspace = init_test_workspace(&project, cx).await;
1383    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1384
1385    start_debug_session(&workspace, cx, |client| {
1386        client.on_request::<dap::requests::Initialize, _>(|_, _| {
1387            Err(ErrorResponse {
1388                error: Some(Message {
1389                    format: "failed to launch".to_string(),
1390                    id: 1,
1391                    variables: None,
1392                    send_telemetry: None,
1393                    show_user: None,
1394                    url: None,
1395                    url_label: None,
1396                }),
1397            })
1398        });
1399    })
1400    .ok();
1401
1402    cx.run_until_parked();
1403
1404    project.update(cx, |project, cx| {
1405        assert!(
1406            project.dap_store().read(cx).sessions().count() == 0,
1407            "Session wouldn't exist if it was shutdown"
1408        );
1409    });
1410}
1411
1412#[gpui::test]
1413async fn test_we_send_arguments_from_user_config(
1414    executor: BackgroundExecutor,
1415    cx: &mut TestAppContext,
1416) {
1417    init_test(cx);
1418
1419    let fs = FakeFs::new(executor.clone());
1420
1421    fs.insert_tree(
1422        path!("/project"),
1423        json!({
1424            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1425        }),
1426    )
1427    .await;
1428
1429    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1430    let workspace = init_test_workspace(&project, cx).await;
1431    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1432    let debug_definition = DebugTaskDefinition {
1433        adapter: "fake-adapter".into(),
1434        config: json!({
1435            "request": "launch",
1436            "program": "main.rs".to_owned(),
1437            "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1438            "cwd": path!("/Random_path"),
1439            "env": json!({ "KEY": "VALUE" }),
1440        }),
1441        label: "test".into(),
1442        tcp_connection: None,
1443    };
1444
1445    let launch_handler_called = Arc::new(AtomicBool::new(false));
1446
1447    start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1448        let debug_definition = debug_definition.clone();
1449        let launch_handler_called = launch_handler_called.clone();
1450
1451        move |client| {
1452            let debug_definition = debug_definition.clone();
1453            let launch_handler_called = launch_handler_called.clone();
1454
1455            client.on_request::<dap::requests::Launch, _>(move |_, args| {
1456                launch_handler_called.store(true, Ordering::SeqCst);
1457
1458                assert_eq!(args.raw, debug_definition.config);
1459
1460                Ok(())
1461            });
1462        }
1463    })
1464    .ok();
1465
1466    cx.run_until_parked();
1467
1468    assert!(
1469        launch_handler_called.load(Ordering::SeqCst),
1470        "Launch request handler was not called"
1471    );
1472}
1473
1474#[gpui::test]
1475async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1476    init_test(cx);
1477
1478    let fs = FakeFs::new(executor.clone());
1479
1480    fs.insert_tree(
1481        path!("/project"),
1482        json!({
1483            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1484            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1485        }),
1486    )
1487    .await;
1488
1489    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1490    let workspace = init_test_workspace(&project, cx).await;
1491    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1492    let project_path = Path::new(path!("/project"));
1493    let worktree = project
1494        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1495        .expect("This worktree should exist in project")
1496        .0;
1497
1498    let worktree_id = workspace
1499        .update(cx, |_, _, cx| worktree.read(cx).id())
1500        .unwrap();
1501
1502    let main_buffer = project
1503        .update(cx, |project, cx| {
1504            project.open_buffer((worktree_id, "main.rs"), cx)
1505        })
1506        .await
1507        .unwrap();
1508
1509    let second_buffer = project
1510        .update(cx, |project, cx| {
1511            project.open_buffer((worktree_id, "second.rs"), cx)
1512        })
1513        .await
1514        .unwrap();
1515
1516    let (main_editor, cx) = cx.add_window_view(|window, cx| {
1517        Editor::new(
1518            EditorMode::full(),
1519            MultiBuffer::build_from_buffer(main_buffer, cx),
1520            Some(project.clone()),
1521            window,
1522            cx,
1523        )
1524    });
1525
1526    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1527        Editor::new(
1528            EditorMode::full(),
1529            MultiBuffer::build_from_buffer(second_buffer, cx),
1530            Some(project.clone()),
1531            window,
1532            cx,
1533        )
1534    });
1535
1536    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1537    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1538
1539    client.on_request::<dap::requests::Threads, _>(move |_, _| {
1540        Ok(dap::ThreadsResponse {
1541            threads: vec![dap::Thread {
1542                id: 1,
1543                name: "Thread 1".into(),
1544            }],
1545        })
1546    });
1547
1548    client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1549        Ok(dap::ScopesResponse {
1550            scopes: Vec::default(),
1551        })
1552    });
1553
1554    client.on_request::<StackTrace, _>(move |_, args| {
1555        assert_eq!(args.thread_id, 1);
1556
1557        Ok(dap::StackTraceResponse {
1558            stack_frames: vec![dap::StackFrame {
1559                id: 1,
1560                name: "frame 1".into(),
1561                source: Some(dap::Source {
1562                    name: Some("main.rs".into()),
1563                    path: Some(path!("/project/main.rs").into()),
1564                    source_reference: None,
1565                    presentation_hint: None,
1566                    origin: None,
1567                    sources: None,
1568                    adapter_data: None,
1569                    checksums: None,
1570                }),
1571                line: 2,
1572                column: 0,
1573                end_line: None,
1574                end_column: None,
1575                can_restart: None,
1576                instruction_pointer_reference: None,
1577                module_id: None,
1578                presentation_hint: None,
1579            }],
1580            total_frames: None,
1581        })
1582    });
1583
1584    client
1585        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1586            reason: dap::StoppedEventReason::Breakpoint,
1587            description: None,
1588            thread_id: Some(1),
1589            preserve_focus_hint: None,
1590            text: None,
1591            all_threads_stopped: None,
1592            hit_breakpoint_ids: None,
1593        }))
1594        .await;
1595
1596    cx.run_until_parked();
1597
1598    main_editor.update_in(cx, |editor, window, cx| {
1599        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1600
1601        assert_eq!(
1602            active_debug_lines.len(),
1603            1,
1604            "There should be only one active debug line"
1605        );
1606
1607        let point = editor
1608            .snapshot(window, cx)
1609            .buffer_snapshot
1610            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1611
1612        assert_eq!(point.row, 1);
1613    });
1614
1615    second_editor.update(cx, |editor, _| {
1616        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1617
1618        assert!(
1619            active_debug_lines.is_empty(),
1620            "There shouldn't be any active debug lines"
1621        );
1622    });
1623
1624    let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1625    client.on_request::<StackTrace, _>({
1626        let handled_second_stacktrace = handled_second_stacktrace.clone();
1627        move |_, args| {
1628            handled_second_stacktrace.store(true, Ordering::SeqCst);
1629            assert_eq!(args.thread_id, 1);
1630
1631            Ok(dap::StackTraceResponse {
1632                stack_frames: vec![dap::StackFrame {
1633                    id: 2,
1634                    name: "frame 2".into(),
1635                    source: Some(dap::Source {
1636                        name: Some("second.rs".into()),
1637                        path: Some(path!("/project/second.rs").into()),
1638                        source_reference: None,
1639                        presentation_hint: None,
1640                        origin: None,
1641                        sources: None,
1642                        adapter_data: None,
1643                        checksums: None,
1644                    }),
1645                    line: 3,
1646                    column: 0,
1647                    end_line: None,
1648                    end_column: None,
1649                    can_restart: None,
1650                    instruction_pointer_reference: None,
1651                    module_id: None,
1652                    presentation_hint: None,
1653                }],
1654                total_frames: None,
1655            })
1656        }
1657    });
1658
1659    client
1660        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1661            reason: dap::StoppedEventReason::Breakpoint,
1662            description: None,
1663            thread_id: Some(1),
1664            preserve_focus_hint: None,
1665            text: None,
1666            all_threads_stopped: None,
1667            hit_breakpoint_ids: None,
1668        }))
1669        .await;
1670
1671    cx.run_until_parked();
1672
1673    second_editor.update_in(cx, |editor, window, cx| {
1674        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1675
1676        assert_eq!(
1677            active_debug_lines.len(),
1678            1,
1679            "There should be only one active debug line"
1680        );
1681
1682        let point = editor
1683            .snapshot(window, cx)
1684            .buffer_snapshot
1685            .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1686
1687        assert_eq!(point.row, 2);
1688    });
1689
1690    main_editor.update(cx, |editor, _| {
1691        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1692
1693        assert!(
1694            active_debug_lines.is_empty(),
1695            "There shouldn't be any active debug lines"
1696        );
1697    });
1698
1699    assert!(
1700        handled_second_stacktrace.load(Ordering::SeqCst),
1701        "Second stacktrace request handler was not called"
1702    );
1703
1704    client
1705        .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1706            thread_id: 0,
1707            all_threads_continued: Some(true),
1708        }))
1709        .await;
1710
1711    cx.run_until_parked();
1712
1713    second_editor.update(cx, |editor, _| {
1714        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1715
1716        assert!(
1717            active_debug_lines.is_empty(),
1718            "There shouldn't be any active debug lines"
1719        );
1720    });
1721
1722    main_editor.update(cx, |editor, _| {
1723        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1724
1725        assert!(
1726            active_debug_lines.is_empty(),
1727            "There shouldn't be any active debug lines"
1728        );
1729    });
1730
1731    // Clean up
1732    let shutdown_session = project.update(cx, |project, cx| {
1733        project.dap_store().update(cx, |dap_store, cx| {
1734            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1735        })
1736    });
1737
1738    shutdown_session.await.unwrap();
1739
1740    main_editor.update(cx, |editor, _| {
1741        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1742
1743        assert!(
1744            active_debug_lines.is_empty(),
1745            "There shouldn't be any active debug lines after session shutdown"
1746        );
1747    });
1748
1749    second_editor.update(cx, |editor, _| {
1750        let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1751
1752        assert!(
1753            active_debug_lines.is_empty(),
1754            "There shouldn't be any active debug lines after session shutdown"
1755        );
1756    });
1757}
1758
1759#[gpui::test]
1760async fn test_debug_adapters_shutdown_on_app_quit(
1761    executor: BackgroundExecutor,
1762    cx: &mut TestAppContext,
1763) {
1764    init_test(cx);
1765
1766    let fs = FakeFs::new(executor.clone());
1767
1768    fs.insert_tree(
1769        path!("/project"),
1770        json!({
1771            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1772        }),
1773    )
1774    .await;
1775
1776    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1777    let workspace = init_test_workspace(&project, cx).await;
1778    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1779
1780    let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1781    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1782
1783    let disconnect_request_received = Arc::new(AtomicBool::new(false));
1784    let disconnect_clone = disconnect_request_received.clone();
1785
1786    let disconnect_clone_for_handler = disconnect_clone.clone();
1787    client.on_request::<Disconnect, _>(move |_, _| {
1788        disconnect_clone_for_handler.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 both_disconnected = executor
1917        .spawn(async move {
1918            let parent_disconnect = parent_disconnect_check;
1919            let child_disconnect = child_disconnect_check;
1920
1921            // We only have 100ms to shutdown the app
1922            for _ in 0..100 {
1923                if parent_disconnect.load(Ordering::SeqCst)
1924                    && child_disconnect.load(Ordering::SeqCst)
1925                {
1926                    return true;
1927                }
1928
1929                gpui::Timer::after(std::time::Duration::from_millis(1)).await;
1930            }
1931
1932            false
1933        })
1934        .await;
1935
1936    assert!(
1937        both_disconnected,
1938        "Both parent and child sessions should receive disconnect requests"
1939    );
1940
1941    assert!(
1942        parent_disconnect_called.load(Ordering::SeqCst),
1943        "Parent session should have received disconnect request"
1944    );
1945    assert!(
1946        child_disconnect_called.load(Ordering::SeqCst),
1947        "Child session should have received disconnect request"
1948    );
1949}