debugger_panel.rs

   1use crate::*;
   2use dap::{
   3    client::SessionId,
   4    requests::{
   5        Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
   6        StartDebugging, StepBack, StepIn, StepOut, Threads,
   7    },
   8    DebugRequestType, ErrorResponse, RunInTerminalRequestArguments, SourceBreakpoint,
   9    StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
  10};
  11use editor::{
  12    actions::{self},
  13    Editor, EditorMode, MultiBuffer,
  14};
  15use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
  16use project::{
  17    debugger::session::{ThreadId, ThreadStatus},
  18    FakeFs, Project,
  19};
  20use serde_json::json;
  21use std::{
  22    path::Path,
  23    sync::{
  24        atomic::{AtomicBool, Ordering},
  25        Arc,
  26    },
  27};
  28use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
  29use tests::{active_debug_session_panel, init_test, init_test_workspace};
  30use util::path;
  31use workspace::{dock::Panel, Item};
  32
  33#[gpui::test]
  34async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
  35    init_test(cx);
  36
  37    let fs = FakeFs::new(executor.clone());
  38
  39    fs.insert_tree(
  40        "/project",
  41        json!({
  42            "main.rs": "First line\nSecond line\nThird line\nFourth line",
  43        }),
  44    )
  45    .await;
  46
  47    let project = Project::test(fs, ["/project".as_ref()], cx).await;
  48    let workspace = init_test_workspace(&project, cx).await;
  49    let cx = &mut VisualTestContext::from_window(*workspace, cx);
  50
  51    let task = project.update(cx, |project, cx| {
  52        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
  53    });
  54
  55    let session = task.await.unwrap();
  56    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
  57
  58    client
  59        .on_request::<Threads, _>(move |_, _| {
  60            Ok(dap::ThreadsResponse {
  61                threads: vec![dap::Thread {
  62                    id: 1,
  63                    name: "Thread 1".into(),
  64                }],
  65            })
  66        })
  67        .await;
  68
  69    client
  70        .on_request::<StackTrace, _>(move |_, _| {
  71            Ok(dap::StackTraceResponse {
  72                stack_frames: Vec::default(),
  73                total_frames: None,
  74            })
  75        })
  76        .await;
  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 = debug_panel.update(cx, |debug_panel, cx| {
  83                debug_panel.active_session(cx).unwrap()
  84            });
  85
  86            let running_state = active_session.update(cx, |active_session, _| {
  87                active_session
  88                    .mode()
  89                    .as_running()
  90                    .expect("Session should be running by this point")
  91                    .clone()
  92            });
  93
  94            debug_panel.update(cx, |this, cx| {
  95                assert!(this.active_session(cx).is_some());
  96                // we have one active session and one inert item
  97                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
  98                assert!(running_state.read(cx).selected_thread_id().is_none());
  99            });
 100        })
 101        .unwrap();
 102
 103    client
 104        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 105            reason: dap::StoppedEventReason::Pause,
 106            description: None,
 107            thread_id: Some(1),
 108            preserve_focus_hint: None,
 109            text: None,
 110            all_threads_stopped: None,
 111            hit_breakpoint_ids: None,
 112        }))
 113        .await;
 114
 115    cx.run_until_parked();
 116
 117    workspace
 118        .update(cx, |workspace, _window, cx| {
 119            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 120            let active_session = debug_panel
 121                .update(cx, |this, cx| this.active_session(cx))
 122                .unwrap();
 123
 124            let running_state = active_session.update(cx, |active_session, _| {
 125                active_session
 126                    .mode()
 127                    .as_running()
 128                    .expect("Session should be running by this point")
 129                    .clone()
 130            });
 131
 132            // we have one active session and one inert item
 133            assert_eq!(
 134                2,
 135                debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
 136            );
 137            assert_eq!(client.id(), running_state.read(cx).session_id());
 138            assert_eq!(
 139                ThreadId(1),
 140                running_state.read(cx).selected_thread_id().unwrap()
 141            );
 142        })
 143        .unwrap();
 144
 145    let shutdown_session = project.update(cx, |project, cx| {
 146        project.dap_store().update(cx, |dap_store, cx| {
 147            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 148        })
 149    });
 150
 151    shutdown_session.await.unwrap();
 152
 153    // assert we still have a debug panel item after the client shutdown
 154    workspace
 155        .update(cx, |workspace, _window, cx| {
 156            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 157
 158            let active_session = debug_panel
 159                .update(cx, |this, cx| this.active_session(cx))
 160                .unwrap();
 161
 162            let running_state = active_session.update(cx, |active_session, _| {
 163                active_session
 164                    .mode()
 165                    .as_running()
 166                    .expect("Session should be running by this point")
 167                    .clone()
 168            });
 169
 170            debug_panel.update(cx, |this, cx| {
 171                assert!(this.active_session(cx).is_some());
 172                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
 173                assert_eq!(
 174                    ThreadId(1),
 175                    running_state.read(cx).selected_thread_id().unwrap()
 176                );
 177            });
 178        })
 179        .unwrap();
 180}
 181
 182#[gpui::test]
 183async fn test_we_can_only_have_one_panel_per_debug_session(
 184    executor: BackgroundExecutor,
 185    cx: &mut TestAppContext,
 186) {
 187    init_test(cx);
 188
 189    let fs = FakeFs::new(executor.clone());
 190
 191    fs.insert_tree(
 192        "/project",
 193        json!({
 194            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 195        }),
 196    )
 197    .await;
 198
 199    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 200    let workspace = init_test_workspace(&project, cx).await;
 201    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 202
 203    let task = project.update(cx, |project, cx| {
 204        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
 205    });
 206
 207    let session = task.await.unwrap();
 208    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 209
 210    client
 211        .on_request::<Threads, _>(move |_, _| {
 212            Ok(dap::ThreadsResponse {
 213                threads: vec![dap::Thread {
 214                    id: 1,
 215                    name: "Thread 1".into(),
 216                }],
 217            })
 218        })
 219        .await;
 220
 221    client
 222        .on_request::<StackTrace, _>(move |_, _| {
 223            Ok(dap::StackTraceResponse {
 224                stack_frames: Vec::default(),
 225                total_frames: None,
 226            })
 227        })
 228        .await;
 229
 230    // assert we have a debug panel item before the session has stopped
 231    workspace
 232        .update(cx, |workspace, _window, cx| {
 233            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 234
 235            debug_panel.update(cx, |this, cx| {
 236                assert!(this.active_session(cx).is_some());
 237                // we have one active session and one inert item
 238                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
 239            });
 240        })
 241        .unwrap();
 242
 243    client
 244        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 245            reason: dap::StoppedEventReason::Pause,
 246            description: None,
 247            thread_id: Some(1),
 248            preserve_focus_hint: None,
 249            text: None,
 250            all_threads_stopped: None,
 251            hit_breakpoint_ids: None,
 252        }))
 253        .await;
 254
 255    cx.run_until_parked();
 256
 257    // assert we added a debug panel item
 258    workspace
 259        .update(cx, |workspace, _window, cx| {
 260            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 261            let active_session = debug_panel
 262                .update(cx, |this, cx| this.active_session(cx))
 263                .unwrap();
 264
 265            let running_state = active_session.update(cx, |active_session, _| {
 266                active_session
 267                    .mode()
 268                    .as_running()
 269                    .expect("Session should be running by this point")
 270                    .clone()
 271            });
 272
 273            // we have one active session and one inert item
 274            assert_eq!(
 275                2,
 276                debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
 277            );
 278            assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
 279            assert_eq!(
 280                ThreadId(1),
 281                running_state.read(cx).selected_thread_id().unwrap()
 282            );
 283        })
 284        .unwrap();
 285
 286    client
 287        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 288            reason: dap::StoppedEventReason::Pause,
 289            description: None,
 290            thread_id: Some(2),
 291            preserve_focus_hint: None,
 292            text: None,
 293            all_threads_stopped: None,
 294            hit_breakpoint_ids: None,
 295        }))
 296        .await;
 297
 298    cx.run_until_parked();
 299
 300    workspace
 301        .update(cx, |workspace, _window, cx| {
 302            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 303            let active_session = debug_panel
 304                .update(cx, |this, cx| this.active_session(cx))
 305                .unwrap();
 306
 307            let running_state = active_session.update(cx, |active_session, _| {
 308                active_session
 309                    .mode()
 310                    .as_running()
 311                    .expect("Session should be running by this point")
 312                    .clone()
 313            });
 314
 315            // we have one active session and one inert item
 316            assert_eq!(
 317                2,
 318                debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
 319            );
 320            assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
 321            assert_eq!(
 322                ThreadId(1),
 323                running_state.read(cx).selected_thread_id().unwrap()
 324            );
 325        })
 326        .unwrap();
 327
 328    let shutdown_session = project.update(cx, |project, cx| {
 329        project.dap_store().update(cx, |dap_store, cx| {
 330            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 331        })
 332    });
 333
 334    shutdown_session.await.unwrap();
 335
 336    // assert we still have a debug panel item after the client shutdown
 337    workspace
 338        .update(cx, |workspace, _window, cx| {
 339            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
 340            let active_session = debug_panel
 341                .update(cx, |this, cx| this.active_session(cx))
 342                .unwrap();
 343
 344            let running_state = active_session.update(cx, |active_session, _| {
 345                active_session
 346                    .mode()
 347                    .as_running()
 348                    .expect("Session should be running by this point")
 349                    .clone()
 350            });
 351
 352            debug_panel.update(cx, |this, cx| {
 353                assert!(this.active_session(cx).is_some());
 354                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
 355                assert_eq!(
 356                    ThreadId(1),
 357                    running_state.read(cx).selected_thread_id().unwrap()
 358                );
 359            });
 360        })
 361        .unwrap();
 362}
 363
 364#[gpui::test]
 365async fn test_handle_successful_run_in_terminal_reverse_request(
 366    executor: BackgroundExecutor,
 367    cx: &mut TestAppContext,
 368) {
 369    init_test(cx);
 370
 371    let send_response = Arc::new(AtomicBool::new(false));
 372
 373    let fs = FakeFs::new(executor.clone());
 374
 375    fs.insert_tree(
 376        "/project",
 377        json!({
 378            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 379        }),
 380    )
 381    .await;
 382
 383    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 384    let workspace = init_test_workspace(&project, cx).await;
 385    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 386
 387    let task = project.update(cx, |project, cx| {
 388        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
 389    });
 390
 391    let session = task.await.unwrap();
 392    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 393
 394    client
 395        .on_response::<RunInTerminal, _>({
 396            let send_response = send_response.clone();
 397            move |response| {
 398                send_response.store(true, Ordering::SeqCst);
 399
 400                assert!(response.success);
 401                assert!(response.body.is_some());
 402            }
 403        })
 404        .await;
 405
 406    client
 407        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 408            kind: None,
 409            title: None,
 410            cwd: std::env::temp_dir().to_string_lossy().to_string(),
 411            args: vec![],
 412            env: None,
 413            args_can_be_interpreted_by_shell: None,
 414        })
 415        .await;
 416
 417    cx.run_until_parked();
 418
 419    assert!(
 420        send_response.load(std::sync::atomic::Ordering::SeqCst),
 421        "Expected to receive response from reverse request"
 422    );
 423
 424    workspace
 425        .update(cx, |workspace, _window, cx| {
 426            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
 427
 428            let panel = terminal_panel.read(cx).pane().unwrap().read(cx);
 429
 430            assert_eq!(1, panel.items_len());
 431            assert!(panel
 432                .active_item()
 433                .unwrap()
 434                .downcast::<TerminalView>()
 435                .unwrap()
 436                .read(cx)
 437                .terminal()
 438                .read(cx)
 439                .debug_terminal());
 440        })
 441        .unwrap();
 442
 443    let shutdown_session = project.update(cx, |project, cx| {
 444        project.dap_store().update(cx, |dap_store, cx| {
 445            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 446        })
 447    });
 448
 449    shutdown_session.await.unwrap();
 450}
 451
 452// // covers that we always send a response back, if something when wrong,
 453// // while spawning the terminal
 454#[gpui::test]
 455async fn test_handle_error_run_in_terminal_reverse_request(
 456    executor: BackgroundExecutor,
 457    cx: &mut TestAppContext,
 458) {
 459    init_test(cx);
 460
 461    let send_response = Arc::new(AtomicBool::new(false));
 462
 463    let fs = FakeFs::new(executor.clone());
 464
 465    fs.insert_tree(
 466        "/project",
 467        json!({
 468            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 469        }),
 470    )
 471    .await;
 472
 473    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 474    let workspace = init_test_workspace(&project, cx).await;
 475    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 476
 477    let task = project.update(cx, |project, cx| {
 478        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
 479    });
 480
 481    let session = task.await.unwrap();
 482    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 483
 484    client
 485        .on_response::<RunInTerminal, _>({
 486            let send_response = send_response.clone();
 487            move |response| {
 488                send_response.store(true, Ordering::SeqCst);
 489
 490                assert!(!response.success);
 491                assert!(response.body.is_some());
 492            }
 493        })
 494        .await;
 495
 496    client
 497        .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
 498            kind: None,
 499            title: None,
 500            cwd: "/non-existing/path".into(), // invalid/non-existing path will cause the terminal spawn to fail
 501            args: vec![],
 502            env: None,
 503            args_can_be_interpreted_by_shell: None,
 504        })
 505        .await;
 506
 507    cx.run_until_parked();
 508
 509    assert!(
 510        send_response.load(std::sync::atomic::Ordering::SeqCst),
 511        "Expected to receive response from reverse request"
 512    );
 513
 514    workspace
 515        .update(cx, |workspace, _window, cx| {
 516            let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
 517
 518            assert_eq!(
 519                0,
 520                terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
 521            );
 522        })
 523        .unwrap();
 524
 525    let shutdown_session = project.update(cx, |project, cx| {
 526        project.dap_store().update(cx, |dap_store, cx| {
 527            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 528        })
 529    });
 530
 531    shutdown_session.await.unwrap();
 532}
 533
 534#[gpui::test]
 535async fn test_handle_start_debugging_reverse_request(
 536    executor: BackgroundExecutor,
 537    cx: &mut TestAppContext,
 538) {
 539    init_test(cx);
 540
 541    let send_response = Arc::new(AtomicBool::new(false));
 542
 543    let fs = FakeFs::new(executor.clone());
 544
 545    fs.insert_tree(
 546        "/project",
 547        json!({
 548            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 549        }),
 550    )
 551    .await;
 552
 553    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 554    let workspace = init_test_workspace(&project, cx).await;
 555    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 556
 557    let task = project.update(cx, |project, cx| {
 558        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
 559    });
 560
 561    let session = task.await.unwrap();
 562    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 563
 564    client
 565        .on_request::<dap::requests::Threads, _>(move |_, _| {
 566            Ok(dap::ThreadsResponse {
 567                threads: vec![dap::Thread {
 568                    id: 1,
 569                    name: "Thread 1".into(),
 570                }],
 571            })
 572        })
 573        .await;
 574
 575    client
 576        .on_response::<StartDebugging, _>({
 577            let send_response = send_response.clone();
 578            move |response| {
 579                send_response.store(true, Ordering::SeqCst);
 580
 581                assert!(response.success);
 582                assert!(response.body.is_some());
 583            }
 584        })
 585        .await;
 586
 587    client
 588        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 589            configuration: json!({}),
 590            request: StartDebuggingRequestArgumentsRequest::Launch,
 591        })
 592        .await;
 593
 594    cx.run_until_parked();
 595
 596    let child_session = project.update(cx, |project, cx| {
 597        project
 598            .dap_store()
 599            .read(cx)
 600            .session_by_id(SessionId(1))
 601            .unwrap()
 602    });
 603    let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
 604
 605    child_client
 606        .on_request::<dap::requests::Threads, _>(move |_, _| {
 607            Ok(dap::ThreadsResponse {
 608                threads: vec![dap::Thread {
 609                    id: 1,
 610                    name: "Thread 1".into(),
 611                }],
 612            })
 613        })
 614        .await;
 615
 616    child_client
 617        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 618        .await;
 619
 620    child_client
 621        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 622            reason: dap::StoppedEventReason::Pause,
 623            description: None,
 624            thread_id: Some(2),
 625            preserve_focus_hint: None,
 626            text: None,
 627            all_threads_stopped: None,
 628            hit_breakpoint_ids: None,
 629        }))
 630        .await;
 631
 632    cx.run_until_parked();
 633
 634    assert!(
 635        send_response.load(std::sync::atomic::Ordering::SeqCst),
 636        "Expected to receive response from reverse request"
 637    );
 638
 639    let shutdown_session = project.update(cx, |project, cx| {
 640        project.dap_store().update(cx, |dap_store, cx| {
 641            dap_store.shutdown_session(child_session.read(cx).session_id(), cx)
 642        })
 643    });
 644
 645    shutdown_session.await.unwrap();
 646}
 647
 648#[gpui::test]
 649async fn test_shutdown_children_when_parent_session_shutdown(
 650    executor: BackgroundExecutor,
 651    cx: &mut TestAppContext,
 652) {
 653    init_test(cx);
 654
 655    let fs = FakeFs::new(executor.clone());
 656
 657    fs.insert_tree(
 658        "/project",
 659        json!({
 660            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 661        }),
 662    )
 663    .await;
 664
 665    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 666    let dap_store = project.update(cx, |project, _| project.dap_store());
 667    let workspace = init_test_workspace(&project, cx).await;
 668    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 669
 670    let task = project.update(cx, |project, cx| {
 671        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
 672    });
 673
 674    let parent_session = task.await.unwrap();
 675    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 676
 677    client
 678        .on_request::<dap::requests::Threads, _>(move |_, _| {
 679            Ok(dap::ThreadsResponse {
 680                threads: vec![dap::Thread {
 681                    id: 1,
 682                    name: "Thread 1".into(),
 683                }],
 684            })
 685        })
 686        .await;
 687
 688    client.on_response::<StartDebugging, _>(move |_| {}).await;
 689
 690    // start first child session
 691    client
 692        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 693            configuration: json!({}),
 694            request: StartDebuggingRequestArgumentsRequest::Launch,
 695        })
 696        .await;
 697
 698    cx.run_until_parked();
 699
 700    // start second child session
 701    client
 702        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 703            configuration: json!({}),
 704            request: StartDebuggingRequestArgumentsRequest::Launch,
 705        })
 706        .await;
 707
 708    cx.run_until_parked();
 709
 710    // configure first child session
 711    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 712        dap_store.session_by_id(SessionId(1)).unwrap()
 713    });
 714    let first_child_client =
 715        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 716
 717    first_child_client
 718        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 719        .await;
 720
 721    // configure second child session
 722    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 723        dap_store.session_by_id(SessionId(2)).unwrap()
 724    });
 725    let second_child_client =
 726        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 727
 728    second_child_client
 729        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 730        .await;
 731
 732    cx.run_until_parked();
 733
 734    // shutdown parent session
 735    dap_store
 736        .update(cx, |dap_store, cx| {
 737            dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
 738        })
 739        .await
 740        .unwrap();
 741
 742    // assert parent session and all children sessions are shutdown
 743    dap_store.update(cx, |dap_store, cx| {
 744        assert!(dap_store
 745            .session_by_id(parent_session.read(cx).session_id())
 746            .is_none());
 747        assert!(dap_store
 748            .session_by_id(first_child_session.read(cx).session_id())
 749            .is_none());
 750        assert!(dap_store
 751            .session_by_id(second_child_session.read(cx).session_id())
 752            .is_none());
 753    });
 754}
 755
 756#[gpui::test]
 757async fn test_shutdown_parent_session_if_all_children_are_shutdown(
 758    executor: BackgroundExecutor,
 759    cx: &mut TestAppContext,
 760) {
 761    init_test(cx);
 762
 763    let fs = FakeFs::new(executor.clone());
 764
 765    fs.insert_tree(
 766        "/project",
 767        json!({
 768            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 769        }),
 770    )
 771    .await;
 772
 773    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 774    let dap_store = project.update(cx, |project, _| project.dap_store());
 775    let workspace = init_test_workspace(&project, cx).await;
 776    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 777
 778    let task = project.update(cx, |project, cx| {
 779        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
 780    });
 781
 782    let parent_session = task.await.unwrap();
 783    let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
 784
 785    client.on_response::<StartDebugging, _>(move |_| {}).await;
 786
 787    // start first child session
 788    client
 789        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 790            configuration: json!({}),
 791            request: StartDebuggingRequestArgumentsRequest::Launch,
 792        })
 793        .await;
 794
 795    cx.run_until_parked();
 796
 797    // start second child session
 798    client
 799        .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
 800            configuration: json!({}),
 801            request: StartDebuggingRequestArgumentsRequest::Launch,
 802        })
 803        .await;
 804
 805    cx.run_until_parked();
 806
 807    // configure first child session
 808    let first_child_session = dap_store.read_with(cx, |dap_store, _| {
 809        dap_store.session_by_id(SessionId(1)).unwrap()
 810    });
 811    let first_child_client =
 812        first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 813
 814    first_child_client
 815        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 816        .await;
 817
 818    // configure second child session
 819    let second_child_session = dap_store.read_with(cx, |dap_store, _| {
 820        dap_store.session_by_id(SessionId(2)).unwrap()
 821    });
 822    let second_child_client =
 823        second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
 824
 825    second_child_client
 826        .on_request::<Disconnect, _>(move |_, _| Ok(()))
 827        .await;
 828
 829    cx.run_until_parked();
 830
 831    // shutdown first child session
 832    dap_store
 833        .update(cx, |dap_store, cx| {
 834            dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
 835        })
 836        .await
 837        .unwrap();
 838
 839    // assert parent session and second child session still exist
 840    dap_store.update(cx, |dap_store, cx| {
 841        assert!(dap_store
 842            .session_by_id(parent_session.read(cx).session_id())
 843            .is_some());
 844        assert!(dap_store
 845            .session_by_id(first_child_session.read(cx).session_id())
 846            .is_none());
 847        assert!(dap_store
 848            .session_by_id(second_child_session.read(cx).session_id())
 849            .is_some());
 850    });
 851
 852    // shutdown first child session
 853    dap_store
 854        .update(cx, |dap_store, cx| {
 855            dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
 856        })
 857        .await
 858        .unwrap();
 859
 860    // assert parent session got shutdown by second child session
 861    // because it was the last child
 862    dap_store.update(cx, |dap_store, cx| {
 863        assert!(dap_store
 864            .session_by_id(parent_session.read(cx).session_id())
 865            .is_none());
 866        assert!(dap_store
 867            .session_by_id(second_child_session.read(cx).session_id())
 868            .is_none());
 869    });
 870}
 871
 872#[gpui::test]
 873async fn test_debug_panel_item_thread_status_reset_on_failure(
 874    executor: BackgroundExecutor,
 875    cx: &mut TestAppContext,
 876) {
 877    init_test(cx);
 878
 879    let fs = FakeFs::new(executor.clone());
 880
 881    fs.insert_tree(
 882        "/project",
 883        json!({
 884            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 885        }),
 886    )
 887    .await;
 888
 889    let project = Project::test(fs, ["/project".as_ref()], cx).await;
 890    let workspace = init_test_workspace(&project, cx).await;
 891    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 892
 893    let task = project.update(cx, |project, cx| {
 894        project.start_debug_session(
 895            dap::test_config(
 896                DebugRequestType::Launch,
 897                None,
 898                Some(dap::Capabilities {
 899                    supports_step_back: Some(true),
 900                    ..Default::default()
 901                }),
 902            ),
 903            cx,
 904        )
 905    });
 906
 907    let session = task.await.unwrap();
 908    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 909    const THREAD_ID_NUM: u64 = 1;
 910
 911    client
 912        .on_request::<dap::requests::Threads, _>(move |_, _| {
 913            Ok(dap::ThreadsResponse {
 914                threads: vec![dap::Thread {
 915                    id: THREAD_ID_NUM,
 916                    name: "Thread 1".into(),
 917                }],
 918            })
 919        })
 920        .await;
 921
 922    client.on_request::<Launch, _>(move |_, _| Ok(())).await;
 923
 924    client
 925        .on_request::<StackTrace, _>(move |_, _| {
 926            Ok(dap::StackTraceResponse {
 927                stack_frames: Vec::default(),
 928                total_frames: None,
 929            })
 930        })
 931        .await;
 932
 933    client
 934        .on_request::<Next, _>(move |_, _| {
 935            Err(ErrorResponse {
 936                error: Some(dap::Message {
 937                    id: 1,
 938                    format: "error".into(),
 939                    variables: None,
 940                    send_telemetry: None,
 941                    show_user: None,
 942                    url: None,
 943                    url_label: None,
 944                }),
 945            })
 946        })
 947        .await;
 948
 949    client
 950        .on_request::<StepOut, _>(move |_, _| {
 951            Err(ErrorResponse {
 952                error: Some(dap::Message {
 953                    id: 1,
 954                    format: "error".into(),
 955                    variables: None,
 956                    send_telemetry: None,
 957                    show_user: None,
 958                    url: None,
 959                    url_label: None,
 960                }),
 961            })
 962        })
 963        .await;
 964
 965    client
 966        .on_request::<StepIn, _>(move |_, _| {
 967            Err(ErrorResponse {
 968                error: Some(dap::Message {
 969                    id: 1,
 970                    format: "error".into(),
 971                    variables: None,
 972                    send_telemetry: None,
 973                    show_user: None,
 974                    url: None,
 975                    url_label: None,
 976                }),
 977            })
 978        })
 979        .await;
 980
 981    client
 982        .on_request::<StepBack, _>(move |_, _| {
 983            Err(ErrorResponse {
 984                error: Some(dap::Message {
 985                    id: 1,
 986                    format: "error".into(),
 987                    variables: None,
 988                    send_telemetry: None,
 989                    show_user: None,
 990                    url: None,
 991                    url_label: None,
 992                }),
 993            })
 994        })
 995        .await;
 996
 997    client
 998        .on_request::<Continue, _>(move |_, _| {
 999            Err(ErrorResponse {
1000                error: Some(dap::Message {
1001                    id: 1,
1002                    format: "error".into(),
1003                    variables: None,
1004                    send_telemetry: None,
1005                    show_user: None,
1006                    url: None,
1007                    url_label: None,
1008                }),
1009            })
1010        })
1011        .await;
1012
1013    client
1014        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1015            reason: dap::StoppedEventReason::Pause,
1016            description: None,
1017            thread_id: Some(1),
1018            preserve_focus_hint: None,
1019            text: None,
1020            all_threads_stopped: None,
1021            hit_breakpoint_ids: None,
1022        }))
1023        .await;
1024
1025    let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
1026        item.mode()
1027            .as_running()
1028            .expect("Session should be running by this point")
1029            .clone()
1030    });
1031
1032    cx.run_until_parked();
1033    let thread_id = ThreadId(1);
1034
1035    for operation in &[
1036        "step_over",
1037        "continue_thread",
1038        "step_back",
1039        "step_in",
1040        "step_out",
1041    ] {
1042        running_state.update(cx, |running_state, cx| match *operation {
1043            "step_over" => running_state.step_over(cx),
1044            "continue_thread" => running_state.continue_thread(cx),
1045            "step_back" => running_state.step_back(cx),
1046            "step_in" => running_state.step_in(cx),
1047            "step_out" => running_state.step_out(cx),
1048            _ => unreachable!(),
1049        });
1050
1051        // Check that we step the thread status to the correct intermediate state
1052        running_state.update(cx, |running_state, cx| {
1053            assert_eq!(
1054                running_state
1055                    .thread_status(cx)
1056                    .expect("There should be an active thread selected"),
1057                match *operation {
1058                    "continue_thread" => ThreadStatus::Running,
1059                    _ => ThreadStatus::Stepping,
1060                },
1061                "Thread status was not set to correct intermediate state after {} request",
1062                operation
1063            );
1064        });
1065
1066        cx.run_until_parked();
1067
1068        running_state.update(cx, |running_state, cx| {
1069            assert_eq!(
1070                running_state
1071                    .thread_status(cx)
1072                    .expect("There should be an active thread selected"),
1073                ThreadStatus::Stopped,
1074                "Thread status not reset to Stopped after failed {}",
1075                operation
1076            );
1077
1078            // update state to running, so we can test it actually changes the status back to stopped
1079            running_state
1080                .session()
1081                .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1082        });
1083    }
1084
1085    let shutdown_session = project.update(cx, |project, cx| {
1086        project.dap_store().update(cx, |dap_store, cx| {
1087            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1088        })
1089    });
1090
1091    shutdown_session.await.unwrap();
1092}
1093
1094#[gpui::test]
1095async fn test_send_breakpoints_when_editor_has_been_saved(
1096    executor: BackgroundExecutor,
1097    cx: &mut TestAppContext,
1098) {
1099    init_test(cx);
1100
1101    let fs = FakeFs::new(executor.clone());
1102
1103    fs.insert_tree(
1104        path!("/project"),
1105        json!({
1106            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1107        }),
1108    )
1109    .await;
1110
1111    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1112    let workspace = init_test_workspace(&project, cx).await;
1113    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1114    let project_path = Path::new(path!("/project"));
1115    let worktree = project
1116        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1117        .expect("This worktree should exist in project")
1118        .0;
1119
1120    let worktree_id = workspace
1121        .update(cx, |_, _, cx| worktree.read(cx).id())
1122        .unwrap();
1123
1124    let task = project.update(cx, |project, cx| {
1125        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
1126    });
1127
1128    let session = task.await.unwrap();
1129    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1130
1131    let buffer = project
1132        .update(cx, |project, cx| {
1133            project.open_buffer((worktree_id, "main.rs"), cx)
1134        })
1135        .await
1136        .unwrap();
1137
1138    let (editor, cx) = cx.add_window_view(|window, cx| {
1139        Editor::new(
1140            EditorMode::Full,
1141            MultiBuffer::build_from_buffer(buffer, cx),
1142            Some(project.clone()),
1143            window,
1144            cx,
1145        )
1146    });
1147
1148    client.on_request::<Launch, _>(move |_, _| Ok(())).await;
1149
1150    client
1151        .on_request::<StackTrace, _>(move |_, _| {
1152            Ok(dap::StackTraceResponse {
1153                stack_frames: Vec::default(),
1154                total_frames: None,
1155            })
1156        })
1157        .await;
1158
1159    client
1160        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1161            reason: dap::StoppedEventReason::Pause,
1162            description: None,
1163            thread_id: Some(1),
1164            preserve_focus_hint: None,
1165            text: None,
1166            all_threads_stopped: None,
1167            hit_breakpoint_ids: None,
1168        }))
1169        .await;
1170
1171    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1172    client
1173        .on_request::<SetBreakpoints, _>({
1174            let called_set_breakpoints = called_set_breakpoints.clone();
1175            move |_, args| {
1176                assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1177                assert_eq!(
1178                    vec![SourceBreakpoint {
1179                        line: 2,
1180                        column: None,
1181                        condition: None,
1182                        hit_condition: None,
1183                        log_message: None,
1184                        mode: None
1185                    }],
1186                    args.breakpoints.unwrap()
1187                );
1188                assert!(!args.source_modified.unwrap());
1189
1190                called_set_breakpoints.store(true, Ordering::SeqCst);
1191
1192                Ok(dap::SetBreakpointsResponse {
1193                    breakpoints: Vec::default(),
1194                })
1195            }
1196        })
1197        .await;
1198
1199    editor.update_in(cx, |editor, window, cx| {
1200        editor.move_down(&actions::MoveDown, window, cx);
1201        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1202    });
1203
1204    cx.run_until_parked();
1205
1206    assert!(
1207        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1208        "SetBreakpoint request must be called"
1209    );
1210
1211    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1212    client
1213        .on_request::<SetBreakpoints, _>({
1214            let called_set_breakpoints = called_set_breakpoints.clone();
1215            move |_, args| {
1216                assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1217                assert_eq!(
1218                    vec![SourceBreakpoint {
1219                        line: 3,
1220                        column: None,
1221                        condition: None,
1222                        hit_condition: None,
1223                        log_message: None,
1224                        mode: None
1225                    }],
1226                    args.breakpoints.unwrap()
1227                );
1228                assert!(args.source_modified.unwrap());
1229
1230                called_set_breakpoints.store(true, Ordering::SeqCst);
1231
1232                Ok(dap::SetBreakpointsResponse {
1233                    breakpoints: Vec::default(),
1234                })
1235            }
1236        })
1237        .await;
1238
1239    editor.update_in(cx, |editor, window, cx| {
1240        editor.move_up(&actions::MoveUp, window, cx);
1241        editor.insert("new text\n", window, cx);
1242    });
1243
1244    editor
1245        .update_in(cx, |editor, window, cx| {
1246            editor.save(true, project.clone(), window, cx)
1247        })
1248        .await
1249        .unwrap();
1250
1251    cx.run_until_parked();
1252
1253    assert!(
1254        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1255        "SetBreakpoint request must be called after editor is saved"
1256    );
1257
1258    let shutdown_session = project.update(cx, |project, cx| {
1259        project.dap_store().update(cx, |dap_store, cx| {
1260            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1261        })
1262    });
1263
1264    shutdown_session.await.unwrap();
1265}
1266
1267#[gpui::test]
1268async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1269    executor: BackgroundExecutor,
1270    cx: &mut TestAppContext,
1271) {
1272    init_test(cx);
1273
1274    let fs = FakeFs::new(executor.clone());
1275
1276    fs.insert_tree(
1277        path!("/project"),
1278        json!({
1279            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1280            "second.rs": "First line\nSecond line\nThird line\nFourth line",
1281            "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1282        }),
1283    )
1284    .await;
1285
1286    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1287    let workspace = init_test_workspace(&project, cx).await;
1288    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1289    let project_path = Path::new(path!("/project"));
1290    let worktree = project
1291        .update(cx, |project, cx| project.find_worktree(project_path, cx))
1292        .expect("This worktree should exist in project")
1293        .0;
1294
1295    let worktree_id = workspace
1296        .update(cx, |_, _, cx| worktree.read(cx).id())
1297        .unwrap();
1298
1299    let first = project
1300        .update(cx, |project, cx| {
1301            project.open_buffer((worktree_id, "main.rs"), cx)
1302        })
1303        .await
1304        .unwrap();
1305
1306    let second = project
1307        .update(cx, |project, cx| {
1308            project.open_buffer((worktree_id, "second.rs"), cx)
1309        })
1310        .await
1311        .unwrap();
1312
1313    let (first_editor, cx) = cx.add_window_view(|window, cx| {
1314        Editor::new(
1315            EditorMode::Full,
1316            MultiBuffer::build_from_buffer(first, cx),
1317            Some(project.clone()),
1318            window,
1319            cx,
1320        )
1321    });
1322
1323    let (second_editor, cx) = cx.add_window_view(|window, cx| {
1324        Editor::new(
1325            EditorMode::Full,
1326            MultiBuffer::build_from_buffer(second, cx),
1327            Some(project.clone()),
1328            window,
1329            cx,
1330        )
1331    });
1332
1333    first_editor.update_in(cx, |editor, window, cx| {
1334        editor.move_down(&actions::MoveDown, window, cx);
1335        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1336        editor.move_down(&actions::MoveDown, window, cx);
1337        editor.move_down(&actions::MoveDown, window, cx);
1338        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1339    });
1340
1341    second_editor.update_in(cx, |editor, window, cx| {
1342        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1343        editor.move_down(&actions::MoveDown, window, cx);
1344        editor.move_down(&actions::MoveDown, window, cx);
1345        editor.move_down(&actions::MoveDown, window, cx);
1346        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1347    });
1348
1349    let task = project.update(cx, |project, cx| {
1350        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
1351    });
1352
1353    let session = task.await.unwrap();
1354    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1355
1356    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1357
1358    client
1359        .on_request::<SetBreakpoints, _>({
1360            let called_set_breakpoints = called_set_breakpoints.clone();
1361            move |_, args| {
1362                assert!(
1363                    args.breakpoints.is_none_or(|bps| bps.is_empty()),
1364                    "Send empty breakpoint sets to clear them from DAP servers"
1365                );
1366
1367                match args
1368                    .source
1369                    .path
1370                    .expect("We should always send a breakpoint's path")
1371                    .as_str()
1372                {
1373                    "/project/main.rs" | "/project/second.rs" => {}
1374                    _ => {
1375                        panic!("Unset breakpoints for path that doesn't have any")
1376                    }
1377                }
1378
1379                called_set_breakpoints.store(true, Ordering::SeqCst);
1380
1381                Ok(dap::SetBreakpointsResponse {
1382                    breakpoints: Vec::default(),
1383                })
1384            }
1385        })
1386        .await;
1387
1388    cx.dispatch_action(workspace::ClearBreakpoints);
1389    cx.run_until_parked();
1390
1391    let shutdown_session = project.update(cx, |project, cx| {
1392        project.dap_store().update(cx, |dap_store, cx| {
1393            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1394        })
1395    });
1396
1397    shutdown_session.await.unwrap();
1398}
1399
1400#[gpui::test]
1401async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1402    executor: BackgroundExecutor,
1403    cx: &mut TestAppContext,
1404) {
1405    init_test(cx);
1406
1407    let fs = FakeFs::new(executor.clone());
1408
1409    fs.insert_tree(
1410        "/project",
1411        json!({
1412            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1413        }),
1414    )
1415    .await;
1416
1417    let project = Project::test(fs, ["/project".as_ref()], cx).await;
1418    let workspace = init_test_workspace(&project, cx).await;
1419    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1420
1421    let task = project.update(cx, |project, cx| {
1422        project.start_debug_session(
1423            dap::test_config(DebugRequestType::Launch, Some(true), None),
1424            cx,
1425        )
1426    });
1427
1428    assert!(
1429        task.await.is_err(),
1430        "Session should failed to start if launch request fails"
1431    );
1432
1433    cx.run_until_parked();
1434
1435    project.update(cx, |project, cx| {
1436        assert!(
1437            project.dap_store().read(cx).sessions().count() == 0,
1438            "Session wouldn't exist if it was shutdown"
1439        );
1440    });
1441}