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    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_debug_panel_item_thread_status_reset_on_failure(
 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 workspace = init_test_workspace(&project, cx).await;
 667    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 668
 669    let task = project.update(cx, |project, cx| {
 670        project.start_debug_session(
 671            dap::test_config(
 672                DebugRequestType::Launch,
 673                None,
 674                Some(dap::Capabilities {
 675                    supports_step_back: Some(true),
 676                    ..Default::default()
 677                }),
 678            ),
 679            cx,
 680        )
 681    });
 682
 683    let session = task.await.unwrap();
 684    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 685    const THREAD_ID_NUM: u64 = 1;
 686
 687    client
 688        .on_request::<dap::requests::Threads, _>(move |_, _| {
 689            Ok(dap::ThreadsResponse {
 690                threads: vec![dap::Thread {
 691                    id: THREAD_ID_NUM,
 692                    name: "Thread 1".into(),
 693                }],
 694            })
 695        })
 696        .await;
 697
 698    client.on_request::<Launch, _>(move |_, _| Ok(())).await;
 699
 700    client
 701        .on_request::<StackTrace, _>(move |_, _| {
 702            Ok(dap::StackTraceResponse {
 703                stack_frames: Vec::default(),
 704                total_frames: None,
 705            })
 706        })
 707        .await;
 708
 709    client
 710        .on_request::<Next, _>(move |_, _| {
 711            Err(ErrorResponse {
 712                error: Some(dap::Message {
 713                    id: 1,
 714                    format: "error".into(),
 715                    variables: None,
 716                    send_telemetry: None,
 717                    show_user: None,
 718                    url: None,
 719                    url_label: None,
 720                }),
 721            })
 722        })
 723        .await;
 724
 725    client
 726        .on_request::<StepOut, _>(move |_, _| {
 727            Err(ErrorResponse {
 728                error: Some(dap::Message {
 729                    id: 1,
 730                    format: "error".into(),
 731                    variables: None,
 732                    send_telemetry: None,
 733                    show_user: None,
 734                    url: None,
 735                    url_label: None,
 736                }),
 737            })
 738        })
 739        .await;
 740
 741    client
 742        .on_request::<StepIn, _>(move |_, _| {
 743            Err(ErrorResponse {
 744                error: Some(dap::Message {
 745                    id: 1,
 746                    format: "error".into(),
 747                    variables: None,
 748                    send_telemetry: None,
 749                    show_user: None,
 750                    url: None,
 751                    url_label: None,
 752                }),
 753            })
 754        })
 755        .await;
 756
 757    client
 758        .on_request::<StepBack, _>(move |_, _| {
 759            Err(ErrorResponse {
 760                error: Some(dap::Message {
 761                    id: 1,
 762                    format: "error".into(),
 763                    variables: None,
 764                    send_telemetry: None,
 765                    show_user: None,
 766                    url: None,
 767                    url_label: None,
 768                }),
 769            })
 770        })
 771        .await;
 772
 773    client
 774        .on_request::<Continue, _>(move |_, _| {
 775            Err(ErrorResponse {
 776                error: Some(dap::Message {
 777                    id: 1,
 778                    format: "error".into(),
 779                    variables: None,
 780                    send_telemetry: None,
 781                    show_user: None,
 782                    url: None,
 783                    url_label: None,
 784                }),
 785            })
 786        })
 787        .await;
 788
 789    client
 790        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 791            reason: dap::StoppedEventReason::Pause,
 792            description: None,
 793            thread_id: Some(1),
 794            preserve_focus_hint: None,
 795            text: None,
 796            all_threads_stopped: None,
 797            hit_breakpoint_ids: None,
 798        }))
 799        .await;
 800
 801    let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
 802        item.mode()
 803            .as_running()
 804            .expect("Session should be running by this point")
 805            .clone()
 806    });
 807
 808    cx.run_until_parked();
 809    let thread_id = ThreadId(1);
 810
 811    for operation in &[
 812        "step_over",
 813        "continue_thread",
 814        "step_back",
 815        "step_in",
 816        "step_out",
 817    ] {
 818        running_state.update(cx, |running_state, cx| match *operation {
 819            "step_over" => running_state.step_over(cx),
 820            "continue_thread" => running_state.continue_thread(cx),
 821            "step_back" => running_state.step_back(cx),
 822            "step_in" => running_state.step_in(cx),
 823            "step_out" => running_state.step_out(cx),
 824            _ => unreachable!(),
 825        });
 826
 827        // Check that we step the thread status to the correct intermediate state
 828        running_state.update(cx, |running_state, cx| {
 829            assert_eq!(
 830                running_state
 831                    .thread_status(cx)
 832                    .expect("There should be an active thread selected"),
 833                match *operation {
 834                    "continue_thread" => ThreadStatus::Running,
 835                    _ => ThreadStatus::Stepping,
 836                },
 837                "Thread status was not set to correct intermediate state after {} request",
 838                operation
 839            );
 840        });
 841
 842        cx.run_until_parked();
 843
 844        running_state.update(cx, |running_state, cx| {
 845            assert_eq!(
 846                running_state
 847                    .thread_status(cx)
 848                    .expect("There should be an active thread selected"),
 849                ThreadStatus::Stopped,
 850                "Thread status not reset to Stopped after failed {}",
 851                operation
 852            );
 853
 854            // update state to running, so we can test it actually changes the status back to stopped
 855            running_state
 856                .session()
 857                .update(cx, |session, cx| session.continue_thread(thread_id, cx));
 858        });
 859    }
 860
 861    let shutdown_session = project.update(cx, |project, cx| {
 862        project.dap_store().update(cx, |dap_store, cx| {
 863            dap_store.shutdown_session(session.read(cx).session_id(), cx)
 864        })
 865    });
 866
 867    shutdown_session.await.unwrap();
 868}
 869
 870#[gpui::test]
 871async fn test_send_breakpoints_when_editor_has_been_saved(
 872    executor: BackgroundExecutor,
 873    cx: &mut TestAppContext,
 874) {
 875    init_test(cx);
 876
 877    let fs = FakeFs::new(executor.clone());
 878
 879    fs.insert_tree(
 880        path!("/project"),
 881        json!({
 882            "main.rs": "First line\nSecond line\nThird line\nFourth line",
 883        }),
 884    )
 885    .await;
 886
 887    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 888    let workspace = init_test_workspace(&project, cx).await;
 889    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 890    let project_path = Path::new(path!("/project"));
 891    let worktree = project
 892        .update(cx, |project, cx| project.find_worktree(project_path, cx))
 893        .expect("This worktree should exist in project")
 894        .0;
 895
 896    let worktree_id = workspace
 897        .update(cx, |_, _, cx| worktree.read(cx).id())
 898        .unwrap();
 899
 900    let task = project.update(cx, |project, cx| {
 901        project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx)
 902    });
 903
 904    let session = task.await.unwrap();
 905    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 906
 907    let buffer = project
 908        .update(cx, |project, cx| {
 909            project.open_buffer((worktree_id, "main.rs"), cx)
 910        })
 911        .await
 912        .unwrap();
 913
 914    let (editor, cx) = cx.add_window_view(|window, cx| {
 915        Editor::new(
 916            EditorMode::Full,
 917            MultiBuffer::build_from_buffer(buffer, cx),
 918            Some(project.clone()),
 919            window,
 920            cx,
 921        )
 922    });
 923
 924    client.on_request::<Launch, _>(move |_, _| Ok(())).await;
 925
 926    client
 927        .on_request::<StackTrace, _>(move |_, _| {
 928            Ok(dap::StackTraceResponse {
 929                stack_frames: Vec::default(),
 930                total_frames: None,
 931            })
 932        })
 933        .await;
 934
 935    client
 936        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 937            reason: dap::StoppedEventReason::Pause,
 938            description: None,
 939            thread_id: Some(1),
 940            preserve_focus_hint: None,
 941            text: None,
 942            all_threads_stopped: None,
 943            hit_breakpoint_ids: None,
 944        }))
 945        .await;
 946
 947    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
 948    client
 949        .on_request::<SetBreakpoints, _>({
 950            let called_set_breakpoints = called_set_breakpoints.clone();
 951            move |_, args| {
 952                assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
 953                assert_eq!(
 954                    vec![SourceBreakpoint {
 955                        line: 2,
 956                        column: None,
 957                        condition: None,
 958                        hit_condition: None,
 959                        log_message: None,
 960                        mode: None
 961                    }],
 962                    args.breakpoints.unwrap()
 963                );
 964                assert!(!args.source_modified.unwrap());
 965
 966                called_set_breakpoints.store(true, Ordering::SeqCst);
 967
 968                Ok(dap::SetBreakpointsResponse {
 969                    breakpoints: Vec::default(),
 970                })
 971            }
 972        })
 973        .await;
 974
 975    editor.update_in(cx, |editor, window, cx| {
 976        editor.move_down(&actions::MoveDown, window, cx);
 977        editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
 978    });
 979
 980    cx.run_until_parked();
 981
 982    assert!(
 983        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
 984        "SetBreakpoint request must be called"
 985    );
 986
 987    let called_set_breakpoints = Arc::new(AtomicBool::new(false));
 988    client
 989        .on_request::<SetBreakpoints, _>({
 990            let called_set_breakpoints = called_set_breakpoints.clone();
 991            move |_, args| {
 992                assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
 993                assert_eq!(
 994                    vec![SourceBreakpoint {
 995                        line: 3,
 996                        column: None,
 997                        condition: None,
 998                        hit_condition: None,
 999                        log_message: None,
1000                        mode: None
1001                    }],
1002                    args.breakpoints.unwrap()
1003                );
1004                assert!(args.source_modified.unwrap());
1005
1006                called_set_breakpoints.store(true, Ordering::SeqCst);
1007
1008                Ok(dap::SetBreakpointsResponse {
1009                    breakpoints: Vec::default(),
1010                })
1011            }
1012        })
1013        .await;
1014
1015    editor.update_in(cx, |editor, window, cx| {
1016        editor.move_up(&actions::MoveUp, window, cx);
1017        editor.insert("new text\n", window, cx);
1018    });
1019
1020    editor
1021        .update_in(cx, |editor, window, cx| {
1022            editor.save(true, project.clone(), window, cx)
1023        })
1024        .await
1025        .unwrap();
1026
1027    cx.run_until_parked();
1028
1029    assert!(
1030        called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1031        "SetBreakpoint request must be called after editor is saved"
1032    );
1033
1034    let shutdown_session = project.update(cx, |project, cx| {
1035        project.dap_store().update(cx, |dap_store, cx| {
1036            dap_store.shutdown_session(session.read(cx).session_id(), cx)
1037        })
1038    });
1039
1040    shutdown_session.await.unwrap();
1041}
1042
1043#[gpui::test]
1044async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1045    executor: BackgroundExecutor,
1046    cx: &mut TestAppContext,
1047) {
1048    init_test(cx);
1049
1050    let fs = FakeFs::new(executor.clone());
1051
1052    fs.insert_tree(
1053        "/project",
1054        json!({
1055            "main.rs": "First line\nSecond line\nThird line\nFourth line",
1056        }),
1057    )
1058    .await;
1059
1060    let project = Project::test(fs, ["/project".as_ref()], cx).await;
1061    let workspace = init_test_workspace(&project, cx).await;
1062    let cx = &mut VisualTestContext::from_window(*workspace, cx);
1063
1064    let task = project.update(cx, |project, cx| {
1065        project.start_debug_session(
1066            dap::test_config(DebugRequestType::Launch, Some(true), None),
1067            cx,
1068        )
1069    });
1070
1071    assert!(
1072        task.await.is_err(),
1073        "Session should failed to start if launch request fails"
1074    );
1075
1076    cx.run_until_parked();
1077
1078    project.update(cx, |project, cx| {
1079        assert!(
1080            project.dap_store().read(cx).sessions().count() == 0,
1081            "Session wouldn't exist if it was shutdown"
1082        );
1083    });
1084}