debugger_panel.rs

   1use crate::persistence::DebuggerPaneItem;
   2use crate::session::DebugSession;
   3use crate::session::running::RunningState;
   4use crate::{
   5    ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
   6    FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
   7    NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
   8    ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
   9};
  10use anyhow::Result;
  11use dap::adapters::DebugAdapterName;
  12use dap::debugger_settings::DebugPanelDockPosition;
  13use dap::{
  14    ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
  15    client::SessionId, debugger_settings::DebuggerSettings,
  16};
  17use dap::{DapRegistry, StartDebuggingRequestArguments};
  18use gpui::{
  19    Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
  20    EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
  21    WeakEntity, actions, anchored, deferred,
  22};
  23
  24use language::Buffer;
  25use project::debugger::session::{Session, SessionStateEvent};
  26use project::{Fs, WorktreeId};
  27use project::{Project, debugger::session::ThreadStatus};
  28use rpc::proto::{self};
  29use settings::Settings;
  30use std::sync::Arc;
  31use task::{DebugScenario, TaskContext};
  32use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
  33use util::maybe;
  34use workspace::SplitDirection;
  35use workspace::{
  36    Pane, Workspace,
  37    dock::{DockPosition, Panel, PanelEvent},
  38};
  39
  40pub enum DebugPanelEvent {
  41    Exited(SessionId),
  42    Terminated(SessionId),
  43    Stopped {
  44        client_id: SessionId,
  45        event: StoppedEvent,
  46        go_to_stack_frame: bool,
  47    },
  48    Thread((SessionId, ThreadEvent)),
  49    Continued((SessionId, ContinuedEvent)),
  50    Output((SessionId, OutputEvent)),
  51    Module((SessionId, ModuleEvent)),
  52    LoadedSource((SessionId, LoadedSourceEvent)),
  53    ClientShutdown(SessionId),
  54    CapabilitiesChanged(SessionId),
  55}
  56
  57actions!(debug_panel, [ToggleFocus]);
  58
  59pub struct DebugPanel {
  60    size: Pixels,
  61    sessions: Vec<Entity<DebugSession>>,
  62    active_session: Option<Entity<DebugSession>>,
  63    project: Entity<Project>,
  64    workspace: WeakEntity<Workspace>,
  65    focus_handle: FocusHandle,
  66    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
  67    debug_scenario_scheduled_last: bool,
  68    pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
  69    pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
  70    fs: Arc<dyn Fs>,
  71    is_zoomed: bool,
  72    _subscriptions: [Subscription; 1],
  73}
  74
  75impl DebugPanel {
  76    pub fn new(
  77        workspace: &Workspace,
  78        window: &mut Window,
  79        cx: &mut Context<Workspace>,
  80    ) -> Entity<Self> {
  81        cx.new(|cx| {
  82            let project = workspace.project().clone();
  83            let focus_handle = cx.focus_handle();
  84            let thread_picker_menu_handle = PopoverMenuHandle::default();
  85            let session_picker_menu_handle = PopoverMenuHandle::default();
  86
  87            let focus_subscription = cx.on_focus(
  88                &focus_handle,
  89                window,
  90                |this: &mut DebugPanel, window, cx| {
  91                    this.focus_active_item(window, cx);
  92                },
  93            );
  94
  95            Self {
  96                size: px(300.),
  97                sessions: vec![],
  98                active_session: None,
  99                focus_handle,
 100                project,
 101                workspace: workspace.weak_handle(),
 102                context_menu: None,
 103                fs: workspace.app_state().fs.clone(),
 104                thread_picker_menu_handle,
 105                session_picker_menu_handle,
 106                is_zoomed: false,
 107                _subscriptions: [focus_subscription],
 108                debug_scenario_scheduled_last: true,
 109            }
 110        })
 111    }
 112
 113    pub(crate) fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 114        let Some(session) = self.active_session.clone() else {
 115            return;
 116        };
 117        let active_pane = session
 118            .read(cx)
 119            .running_state()
 120            .read(cx)
 121            .active_pane()
 122            .clone();
 123        active_pane.update(cx, |pane, cx| {
 124            pane.focus_active_item(window, cx);
 125        });
 126    }
 127
 128    pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
 129        self.sessions.clone()
 130    }
 131
 132    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
 133        self.active_session.clone()
 134    }
 135
 136    pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
 137        self.active_session()
 138            .map(|session| session.read(cx).running_state().clone())
 139    }
 140
 141    pub fn load(
 142        workspace: WeakEntity<Workspace>,
 143        cx: &mut AsyncWindowContext,
 144    ) -> Task<Result<Entity<Self>>> {
 145        cx.spawn(async move |cx| {
 146            workspace.update_in(cx, |workspace, window, cx| {
 147                let debug_panel = DebugPanel::new(workspace, window, cx);
 148
 149                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
 150                    workspace.project().read(cx).breakpoint_store().update(
 151                        cx,
 152                        |breakpoint_store, cx| {
 153                            breakpoint_store.clear_breakpoints(cx);
 154                        },
 155                    )
 156                });
 157
 158                workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
 159
 160                debug_panel
 161            })
 162        })
 163    }
 164
 165    pub fn start_session(
 166        &mut self,
 167        scenario: DebugScenario,
 168        task_context: TaskContext,
 169        active_buffer: Option<Entity<Buffer>>,
 170        worktree_id: Option<WorktreeId>,
 171        window: &mut Window,
 172        cx: &mut Context<Self>,
 173    ) {
 174        let dap_store = self.project.read(cx).dap_store();
 175        let session = dap_store.update(cx, |dap_store, cx| {
 176            dap_store.new_session(
 177                scenario.label.clone(),
 178                DebugAdapterName(scenario.adapter.clone()),
 179                None,
 180                cx,
 181            )
 182        });
 183        let worktree = worktree_id.or_else(|| {
 184            active_buffer
 185                .as_ref()
 186                .and_then(|buffer| buffer.read(cx).file())
 187                .map(|f| f.worktree_id(cx))
 188        });
 189        let Some(worktree) = worktree
 190            .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
 191            .or_else(|| self.project.read(cx).visible_worktrees(cx).next())
 192        else {
 193            log::debug!("Could not find a worktree to spawn the debug session in");
 194            return;
 195        };
 196        self.debug_scenario_scheduled_last = true;
 197        if let Some(inventory) = self
 198            .project
 199            .read(cx)
 200            .task_store()
 201            .read(cx)
 202            .task_inventory()
 203            .cloned()
 204        {
 205            inventory.update(cx, |inventory, _| {
 206                inventory.scenario_scheduled(scenario.clone());
 207            })
 208        }
 209        let task = cx.spawn_in(window, {
 210            let session = session.clone();
 211            async move |this, cx| {
 212                let debug_session =
 213                    Self::register_session(this.clone(), session.clone(), true, cx).await?;
 214                let definition = debug_session
 215                    .update_in(cx, |debug_session, window, cx| {
 216                        debug_session.running_state().update(cx, |running, cx| {
 217                            running.resolve_scenario(
 218                                scenario,
 219                                task_context,
 220                                active_buffer,
 221                                worktree_id,
 222                                window,
 223                                cx,
 224                            )
 225                        })
 226                    })?
 227                    .await?;
 228                dap_store
 229                    .update(cx, |dap_store, cx| {
 230                        dap_store.boot_session(session.clone(), definition, worktree, cx)
 231                    })?
 232                    .await
 233            }
 234        });
 235
 236        cx.spawn(async move |_, cx| {
 237            if let Err(error) = task.await {
 238                log::error!("{error}");
 239                session
 240                    .update(cx, |session, cx| {
 241                        session
 242                            .console_output(cx)
 243                            .unbounded_send(format!("error: {}", error))
 244                            .ok();
 245                        session.shutdown(cx)
 246                    })?
 247                    .await;
 248            }
 249            anyhow::Ok(())
 250        })
 251        .detach_and_log_err(cx);
 252    }
 253
 254    pub(crate) fn rerun_last_session(
 255        &mut self,
 256        workspace: &mut Workspace,
 257        window: &mut Window,
 258        cx: &mut Context<Self>,
 259    ) {
 260        let task_store = workspace.project().read(cx).task_store().clone();
 261        let Some(task_inventory) = task_store.read(cx).task_inventory() else {
 262            return;
 263        };
 264        let workspace = self.workspace.clone();
 265        let Some(scenario) = task_inventory.read(cx).last_scheduled_scenario().cloned() else {
 266            window.defer(cx, move |window, cx| {
 267                workspace
 268                    .update(cx, |workspace, cx| {
 269                        NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
 270                    })
 271                    .ok();
 272            });
 273            return;
 274        };
 275
 276        cx.spawn_in(window, async move |this, cx| {
 277            let task_contexts = workspace
 278                .update_in(cx, |workspace, window, cx| {
 279                    tasks_ui::task_contexts(workspace, window, cx)
 280                })?
 281                .await;
 282
 283            let task_context = task_contexts.active_context().cloned().unwrap_or_default();
 284            let worktree_id = task_contexts.worktree();
 285
 286            this.update_in(cx, |this, window, cx| {
 287                this.start_session(
 288                    scenario.clone(),
 289                    task_context,
 290                    None,
 291                    worktree_id,
 292                    window,
 293                    cx,
 294                );
 295            })
 296        })
 297        .detach();
 298    }
 299
 300    pub(crate) async fn register_session(
 301        this: WeakEntity<Self>,
 302        session: Entity<Session>,
 303        focus: bool,
 304        cx: &mut AsyncWindowContext,
 305    ) -> Result<Entity<DebugSession>> {
 306        let debug_session = register_session_inner(&this, session, cx).await?;
 307
 308        let workspace = this.update_in(cx, |this, window, cx| {
 309            if focus {
 310                this.activate_session(debug_session.clone(), window, cx);
 311            }
 312
 313            this.workspace.clone()
 314        })?;
 315        workspace.update_in(cx, |workspace, window, cx| {
 316            workspace.focus_panel::<Self>(window, cx);
 317        })?;
 318        Ok(debug_session)
 319    }
 320
 321    pub(crate) fn handle_restart_request(
 322        &mut self,
 323        mut curr_session: Entity<Session>,
 324        window: &mut Window,
 325        cx: &mut Context<Self>,
 326    ) {
 327        while let Some(parent_session) = curr_session.read(cx).parent_session().cloned() {
 328            curr_session = parent_session;
 329        }
 330
 331        let Some(worktree) = curr_session.read(cx).worktree() else {
 332            log::error!("Attempted to restart a non-running session");
 333            return;
 334        };
 335
 336        let dap_store_handle = self.project.read(cx).dap_store().clone();
 337        let label = curr_session.read(cx).label().clone();
 338        let adapter = curr_session.read(cx).adapter().clone();
 339        let binary = curr_session.read(cx).binary().cloned().unwrap();
 340        let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
 341
 342        cx.spawn_in(window, async move |this, cx| {
 343            task.await;
 344
 345            let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
 346                let session = dap_store.new_session(label, adapter, None, cx);
 347
 348                let task = session.update(cx, |session, cx| {
 349                    session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
 350                });
 351                (session, task)
 352            })?;
 353            Self::register_session(this.clone(), session.clone(), true, cx).await?;
 354
 355            if let Err(error) = task.await {
 356                session
 357                    .update(cx, |session, cx| {
 358                        session
 359                            .console_output(cx)
 360                            .unbounded_send(format!(
 361                                "Session failed to restart with error: {}",
 362                                error
 363                            ))
 364                            .ok();
 365                        session.shutdown(cx)
 366                    })?
 367                    .await;
 368
 369                return Err(error);
 370            };
 371
 372            Ok(())
 373        })
 374        .detach_and_log_err(cx);
 375    }
 376
 377    pub fn handle_start_debugging_request(
 378        &mut self,
 379        request: &StartDebuggingRequestArguments,
 380        parent_session: Entity<Session>,
 381        window: &mut Window,
 382        cx: &mut Context<Self>,
 383    ) {
 384        let Some(worktree) = parent_session.read(cx).worktree() else {
 385            log::error!("Attempted to start a child-session from a non-running session");
 386            return;
 387        };
 388
 389        let dap_store_handle = self.project.read(cx).dap_store().clone();
 390        let label = self.label_for_child_session(&parent_session, request, cx);
 391        let adapter = parent_session.read(cx).adapter().clone();
 392        let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
 393            log::error!("Attempted to start a child-session without a binary");
 394            return;
 395        };
 396        binary.request_args = request.clone();
 397        cx.spawn_in(window, async move |this, cx| {
 398            let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
 399                let session =
 400                    dap_store.new_session(label, adapter, Some(parent_session.clone()), cx);
 401
 402                let task = session.update(cx, |session, cx| {
 403                    session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
 404                });
 405                (session, task)
 406            })?;
 407            Self::register_session(this, session, false, cx).await?;
 408            task.await
 409        })
 410        .detach_and_log_err(cx);
 411    }
 412
 413    pub(crate) fn close_session(
 414        &mut self,
 415        entity_id: EntityId,
 416        window: &mut Window,
 417        cx: &mut Context<Self>,
 418    ) {
 419        let Some(session) = self
 420            .sessions
 421            .iter()
 422            .find(|other| entity_id == other.entity_id())
 423            .cloned()
 424        else {
 425            return;
 426        };
 427        session.update(cx, |this, cx| {
 428            this.running_state().update(cx, |this, cx| {
 429                this.serialize_layout(window, cx);
 430            });
 431        });
 432        let session_id = session.update(cx, |this, cx| this.session_id(cx));
 433        let should_prompt = self
 434            .project
 435            .update(cx, |this, cx| {
 436                let session = this.dap_store().read(cx).session_by_id(session_id);
 437                session.map(|session| !session.read(cx).is_terminated())
 438            })
 439            .unwrap_or_default();
 440
 441        cx.spawn_in(window, async move |this, cx| {
 442            if should_prompt {
 443                let response = cx.prompt(
 444                    gpui::PromptLevel::Warning,
 445                    "This Debug Session is still running. Are you sure you want to terminate it?",
 446                    None,
 447                    &["Yes", "No"],
 448                );
 449                if response.await == Ok(1) {
 450                    return;
 451                }
 452            }
 453            session.update(cx, |session, cx| session.shutdown(cx)).ok();
 454            this.update(cx, |this, cx| {
 455                this.sessions.retain(|other| entity_id != other.entity_id());
 456
 457                if let Some(active_session_id) = this
 458                    .active_session
 459                    .as_ref()
 460                    .map(|session| session.entity_id())
 461                {
 462                    if active_session_id == entity_id {
 463                        this.active_session = this.sessions.first().cloned();
 464                    }
 465                }
 466                cx.notify()
 467            })
 468            .ok();
 469        })
 470        .detach();
 471    }
 472
 473    pub(crate) fn deploy_context_menu(
 474        &mut self,
 475        position: Point<Pixels>,
 476        window: &mut Window,
 477        cx: &mut Context<Self>,
 478    ) {
 479        if let Some(running_state) = self
 480            .active_session
 481            .as_ref()
 482            .map(|session| session.read(cx).running_state().clone())
 483        {
 484            let pane_items_status = running_state.read(cx).pane_items_status(cx);
 485            let this = cx.weak_entity();
 486
 487            let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
 488                for (item_kind, is_visible) in pane_items_status.into_iter() {
 489                    menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
 490                        let this = this.clone();
 491                        move |window, cx| {
 492                            this.update(cx, |this, cx| {
 493                                if let Some(running_state) = this
 494                                    .active_session
 495                                    .as_ref()
 496                                    .map(|session| session.read(cx).running_state().clone())
 497                                {
 498                                    running_state.update(cx, |state, cx| {
 499                                        if is_visible {
 500                                            state.remove_pane_item(item_kind, window, cx);
 501                                        } else {
 502                                            state.add_pane_item(item_kind, position, window, cx);
 503                                        }
 504                                    })
 505                                }
 506                            })
 507                            .ok();
 508                        }
 509                    });
 510                }
 511
 512                menu
 513            });
 514
 515            window.focus(&context_menu.focus_handle(cx));
 516            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
 517                this.context_menu.take();
 518                cx.notify();
 519            });
 520            self.context_menu = Some((context_menu, position, subscription));
 521        }
 522    }
 523
 524    fn copy_debug_adapter_arguments(
 525        &mut self,
 526        _: &CopyDebugAdapterArguments,
 527        _window: &mut Window,
 528        cx: &mut Context<Self>,
 529    ) {
 530        let content = maybe!({
 531            let mut session = self.active_session()?.read(cx).session(cx);
 532            while let Some(parent) = session.read(cx).parent_session().cloned() {
 533                session = parent;
 534            }
 535            let binary = session.read(cx).binary()?;
 536            let content = serde_json::to_string_pretty(&binary).ok()?;
 537            Some(content)
 538        });
 539        if let Some(content) = content {
 540            cx.write_to_clipboard(ClipboardItem::new_string(content));
 541        }
 542    }
 543
 544    pub(crate) fn top_controls_strip(
 545        &mut self,
 546        window: &mut Window,
 547        cx: &mut Context<Self>,
 548    ) -> Option<Div> {
 549        let active_session = self.active_session.clone();
 550        let focus_handle = self.focus_handle.clone();
 551        let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
 552        let div = if is_side { v_flex() } else { h_flex() };
 553
 554        let new_session_button = || {
 555            IconButton::new("debug-new-session", IconName::Plus)
 556                .icon_size(IconSize::Small)
 557                .on_click({
 558                    move |_, window, cx| window.dispatch_action(crate::Start.boxed_clone(), cx)
 559                })
 560                .tooltip({
 561                    let focus_handle = focus_handle.clone();
 562                    move |window, cx| {
 563                        Tooltip::for_action_in(
 564                            "Start Debug Session",
 565                            &crate::Start,
 566                            &focus_handle,
 567                            window,
 568                            cx,
 569                        )
 570                    }
 571                })
 572        };
 573        let documentation_button = || {
 574            IconButton::new("debug-open-documentation", IconName::CircleHelp)
 575                .icon_size(IconSize::Small)
 576                .on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
 577                .tooltip(Tooltip::text("Open Documentation"))
 578        };
 579
 580        Some(
 581            div.border_b_1()
 582                .border_color(cx.theme().colors().border)
 583                .p_1()
 584                .justify_between()
 585                .w_full()
 586                .when(is_side, |this| this.gap_1())
 587                .child(
 588                    h_flex()
 589                        .child(
 590                            h_flex().gap_2().w_full().when_some(
 591                                active_session
 592                                    .as_ref()
 593                                    .map(|session| session.read(cx).running_state()),
 594                                |this, running_state| {
 595                                    let thread_status =
 596                                        running_state.read(cx).thread_status(cx).unwrap_or(
 597                                            project::debugger::session::ThreadStatus::Exited,
 598                                        );
 599                                    let capabilities = running_state.read(cx).capabilities(cx);
 600                                    let supports_detach =
 601                                        running_state.read(cx).session().read(cx).is_attached();
 602                                    this.map(|this| {
 603                                        if thread_status == ThreadStatus::Running {
 604                                            this.child(
 605                                                IconButton::new(
 606                                                    "debug-pause",
 607                                                    IconName::DebugPause,
 608                                                )
 609                                                .icon_size(IconSize::XSmall)
 610                                                .shape(ui::IconButtonShape::Square)
 611                                                .on_click(window.listener_for(
 612                                                    &running_state,
 613                                                    |this, _, _window, cx| {
 614                                                        this.pause_thread(cx);
 615                                                    },
 616                                                ))
 617                                                .tooltip({
 618                                                    let focus_handle = focus_handle.clone();
 619                                                    move |window, cx| {
 620                                                        Tooltip::for_action_in(
 621                                                            "Pause program",
 622                                                            &Pause,
 623                                                            &focus_handle,
 624                                                            window,
 625                                                            cx,
 626                                                        )
 627                                                    }
 628                                                }),
 629                                            )
 630                                        } else {
 631                                            this.child(
 632                                                IconButton::new(
 633                                                    "debug-continue",
 634                                                    IconName::DebugContinue,
 635                                                )
 636                                                .icon_size(IconSize::XSmall)
 637                                                .shape(ui::IconButtonShape::Square)
 638                                                .on_click(window.listener_for(
 639                                                    &running_state,
 640                                                    |this, _, _window, cx| this.continue_thread(cx),
 641                                                ))
 642                                                .disabled(thread_status != ThreadStatus::Stopped)
 643                                                .tooltip({
 644                                                    let focus_handle = focus_handle.clone();
 645                                                    move |window, cx| {
 646                                                        Tooltip::for_action_in(
 647                                                            "Continue program",
 648                                                            &Continue,
 649                                                            &focus_handle,
 650                                                            window,
 651                                                            cx,
 652                                                        )
 653                                                    }
 654                                                }),
 655                                            )
 656                                        }
 657                                    })
 658                                    .child(
 659                                        IconButton::new("debug-step-over", IconName::ArrowRight)
 660                                            .icon_size(IconSize::XSmall)
 661                                            .shape(ui::IconButtonShape::Square)
 662                                            .on_click(window.listener_for(
 663                                                &running_state,
 664                                                |this, _, _window, cx| {
 665                                                    this.step_over(cx);
 666                                                },
 667                                            ))
 668                                            .disabled(thread_status != ThreadStatus::Stopped)
 669                                            .tooltip({
 670                                                let focus_handle = focus_handle.clone();
 671                                                move |window, cx| {
 672                                                    Tooltip::for_action_in(
 673                                                        "Step over",
 674                                                        &StepOver,
 675                                                        &focus_handle,
 676                                                        window,
 677                                                        cx,
 678                                                    )
 679                                                }
 680                                            }),
 681                                    )
 682                                    .child(
 683                                        IconButton::new("debug-step-out", IconName::ArrowUpRight)
 684                                            .icon_size(IconSize::XSmall)
 685                                            .shape(ui::IconButtonShape::Square)
 686                                            .on_click(window.listener_for(
 687                                                &running_state,
 688                                                |this, _, _window, cx| {
 689                                                    this.step_out(cx);
 690                                                },
 691                                            ))
 692                                            .disabled(thread_status != ThreadStatus::Stopped)
 693                                            .tooltip({
 694                                                let focus_handle = focus_handle.clone();
 695                                                move |window, cx| {
 696                                                    Tooltip::for_action_in(
 697                                                        "Step out",
 698                                                        &StepOut,
 699                                                        &focus_handle,
 700                                                        window,
 701                                                        cx,
 702                                                    )
 703                                                }
 704                                            }),
 705                                    )
 706                                    .child(
 707                                        IconButton::new(
 708                                            "debug-step-into",
 709                                            IconName::ArrowDownRight,
 710                                        )
 711                                        .icon_size(IconSize::XSmall)
 712                                        .shape(ui::IconButtonShape::Square)
 713                                        .on_click(window.listener_for(
 714                                            &running_state,
 715                                            |this, _, _window, cx| {
 716                                                this.step_in(cx);
 717                                            },
 718                                        ))
 719                                        .disabled(thread_status != ThreadStatus::Stopped)
 720                                        .tooltip({
 721                                            let focus_handle = focus_handle.clone();
 722                                            move |window, cx| {
 723                                                Tooltip::for_action_in(
 724                                                    "Step in",
 725                                                    &StepInto,
 726                                                    &focus_handle,
 727                                                    window,
 728                                                    cx,
 729                                                )
 730                                            }
 731                                        }),
 732                                    )
 733                                    .child(Divider::vertical())
 734                                    .child(
 735                                        IconButton::new("debug-restart", IconName::DebugRestart)
 736                                            .icon_size(IconSize::XSmall)
 737                                            .on_click(window.listener_for(
 738                                                &running_state,
 739                                                |this, _, _window, cx| {
 740                                                    this.restart_session(cx);
 741                                                },
 742                                            ))
 743                                            .tooltip({
 744                                                let focus_handle = focus_handle.clone();
 745                                                move |window, cx| {
 746                                                    Tooltip::for_action_in(
 747                                                        "Restart",
 748                                                        &Restart,
 749                                                        &focus_handle,
 750                                                        window,
 751                                                        cx,
 752                                                    )
 753                                                }
 754                                            }),
 755                                    )
 756                                    .child(
 757                                        IconButton::new("debug-stop", IconName::Power)
 758                                            .icon_size(IconSize::XSmall)
 759                                            .on_click(window.listener_for(
 760                                                &running_state,
 761                                                |this, _, _window, cx| {
 762                                                    this.stop_thread(cx);
 763                                                },
 764                                            ))
 765                                            .disabled(
 766                                                thread_status != ThreadStatus::Stopped
 767                                                    && thread_status != ThreadStatus::Running,
 768                                            )
 769                                            .tooltip({
 770                                                let focus_handle = focus_handle.clone();
 771                                                let label = if capabilities
 772                                                    .supports_terminate_threads_request
 773                                                    .unwrap_or_default()
 774                                                {
 775                                                    "Terminate Thread"
 776                                                } else {
 777                                                    "Terminate All Threads"
 778                                                };
 779                                                move |window, cx| {
 780                                                    Tooltip::for_action_in(
 781                                                        label,
 782                                                        &Stop,
 783                                                        &focus_handle,
 784                                                        window,
 785                                                        cx,
 786                                                    )
 787                                                }
 788                                            }),
 789                                    )
 790                                    .when(
 791                                        supports_detach,
 792                                        |div| {
 793                                            div.child(
 794                                                IconButton::new(
 795                                                    "debug-disconnect",
 796                                                    IconName::DebugDetach,
 797                                                )
 798                                                .disabled(
 799                                                    thread_status != ThreadStatus::Stopped
 800                                                        && thread_status != ThreadStatus::Running,
 801                                                )
 802                                                .icon_size(IconSize::XSmall)
 803                                                .on_click(window.listener_for(
 804                                                    &running_state,
 805                                                    |this, _, _, cx| {
 806                                                        this.detach_client(cx);
 807                                                    },
 808                                                ))
 809                                                .tooltip({
 810                                                    let focus_handle = focus_handle.clone();
 811                                                    move |window, cx| {
 812                                                        Tooltip::for_action_in(
 813                                                            "Detach",
 814                                                            &Detach,
 815                                                            &focus_handle,
 816                                                            window,
 817                                                            cx,
 818                                                        )
 819                                                    }
 820                                                }),
 821                                            )
 822                                        },
 823                                    )
 824                                },
 825                            ),
 826                        )
 827                        .justify_around()
 828                        .when(is_side, |this| {
 829                            this.child(new_session_button())
 830                                .child(documentation_button())
 831                        }),
 832                )
 833                .child(
 834                    h_flex()
 835                        .gap_2()
 836                        .when(is_side, |this| this.justify_between())
 837                        .child(
 838                            h_flex().when_some(
 839                                active_session
 840                                    .as_ref()
 841                                    .map(|session| session.read(cx).running_state())
 842                                    .cloned(),
 843                                |this, running_state| {
 844                                    this.children({
 845                                        let running_state = running_state.clone();
 846                                        let threads =
 847                                            running_state.update(cx, |running_state, cx| {
 848                                                let session = running_state.session();
 849                                                session
 850                                                    .update(cx, |session, cx| session.threads(cx))
 851                                            });
 852
 853                                        self.render_thread_dropdown(
 854                                            &running_state,
 855                                            threads,
 856                                            window,
 857                                            cx,
 858                                        )
 859                                    })
 860                                    .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
 861                                },
 862                            ),
 863                        )
 864                        .child(
 865                            h_flex()
 866                                .children(self.render_session_menu(
 867                                    self.active_session(),
 868                                    self.running_state(cx),
 869                                    window,
 870                                    cx,
 871                                ))
 872                                .when(!is_side, |this| {
 873                                    this.child(new_session_button())
 874                                        .child(documentation_button())
 875                                }),
 876                        ),
 877                ),
 878        )
 879    }
 880
 881    pub(crate) fn activate_pane_in_direction(
 882        &mut self,
 883        direction: SplitDirection,
 884        window: &mut Window,
 885        cx: &mut Context<Self>,
 886    ) {
 887        if let Some(session) = self.active_session() {
 888            session.update(cx, |session, cx| {
 889                session.running_state().update(cx, |running, cx| {
 890                    running.activate_pane_in_direction(direction, window, cx);
 891                })
 892            });
 893        }
 894    }
 895
 896    pub(crate) fn activate_item(
 897        &mut self,
 898        item: DebuggerPaneItem,
 899        window: &mut Window,
 900        cx: &mut Context<Self>,
 901    ) {
 902        if let Some(session) = self.active_session() {
 903            session.update(cx, |session, cx| {
 904                session.running_state().update(cx, |running, cx| {
 905                    running.activate_item(item, window, cx);
 906                });
 907            });
 908        }
 909    }
 910
 911    pub(crate) fn activate_session_by_id(
 912        &mut self,
 913        session_id: SessionId,
 914        window: &mut Window,
 915        cx: &mut Context<Self>,
 916    ) {
 917        if let Some(session) = self
 918            .sessions
 919            .iter()
 920            .find(|session| session.read(cx).session_id(cx) == session_id)
 921        {
 922            self.activate_session(session.clone(), window, cx);
 923        }
 924    }
 925
 926    pub(crate) fn activate_session(
 927        &mut self,
 928        session_item: Entity<DebugSession>,
 929        window: &mut Window,
 930        cx: &mut Context<Self>,
 931    ) {
 932        debug_assert!(self.sessions.contains(&session_item));
 933        session_item.focus_handle(cx).focus(window);
 934        session_item.update(cx, |this, cx| {
 935            this.running_state().update(cx, |this, cx| {
 936                this.go_to_selected_stack_frame(window, cx);
 937            });
 938        });
 939        self.active_session = Some(session_item);
 940        cx.notify();
 941    }
 942
 943    // TODO: restore once we have proper comment preserving file edits
 944    // pub(crate) fn save_scenario(
 945    //     &self,
 946    //     scenario: &DebugScenario,
 947    //     worktree_id: WorktreeId,
 948    //     window: &mut Window,
 949    //     cx: &mut App,
 950    // ) -> Task<Result<ProjectPath>> {
 951    //     self.workspace
 952    //         .update(cx, |workspace, cx| {
 953    //             let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
 954    //                 return Task::ready(Err(anyhow!("Couldn't get worktree path")));
 955    //             };
 956
 957    //             let serialized_scenario = serde_json::to_value(scenario);
 958
 959    //             cx.spawn_in(window, async move |workspace, cx| {
 960    //                 let serialized_scenario = serialized_scenario?;
 961    //                 let fs =
 962    //                     workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
 963
 964    //                 path.push(paths::local_settings_folder_relative_path());
 965    //                 if !fs.is_dir(path.as_path()).await {
 966    //                     fs.create_dir(path.as_path()).await?;
 967    //                 }
 968    //                 path.pop();
 969
 970    //                 path.push(paths::local_debug_file_relative_path());
 971    //                 let path = path.as_path();
 972
 973    //                 if !fs.is_file(path).await {
 974    //                     fs.create_file(path, Default::default()).await?;
 975    //                     fs.write(
 976    //                         path,
 977    //                         initial_local_debug_tasks_content().to_string().as_bytes(),
 978    //                     )
 979    //                     .await?;
 980    //                 }
 981
 982    //                 let content = fs.load(path).await?;
 983    //                 let mut values =
 984    //                     serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
 985    //                 values.push(serialized_scenario);
 986    //                 fs.save(
 987    //                     path,
 988    //                     &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
 989    //                     Default::default(),
 990    //                 )
 991    //                 .await?;
 992
 993    //                 workspace.update(cx, |workspace, cx| {
 994    //                     workspace
 995    //                         .project()
 996    //                         .read(cx)
 997    //                         .project_path_for_absolute_path(&path, cx)
 998    //                         .context(
 999    //                             "Couldn't get project path for .zed/debug.json in active worktree",
1000    //                         )
1001    //                 })?
1002    //             })
1003    //         })
1004    //         .unwrap_or_else(|err| Task::ready(Err(err)))
1005    // }
1006
1007    pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1008        self.thread_picker_menu_handle.toggle(window, cx);
1009    }
1010
1011    pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1012        self.session_picker_menu_handle.toggle(window, cx);
1013    }
1014
1015    fn toggle_zoom(
1016        &mut self,
1017        _: &workspace::ToggleZoom,
1018        window: &mut Window,
1019        cx: &mut Context<Self>,
1020    ) {
1021        if self.is_zoomed {
1022            cx.emit(PanelEvent::ZoomOut);
1023        } else {
1024            if !self.focus_handle(cx).contains_focused(window, cx) {
1025                cx.focus_self(window);
1026            }
1027            cx.emit(PanelEvent::ZoomIn);
1028        }
1029    }
1030
1031    fn label_for_child_session(
1032        &self,
1033        parent_session: &Entity<Session>,
1034        request: &StartDebuggingRequestArguments,
1035        cx: &mut Context<'_, Self>,
1036    ) -> SharedString {
1037        let adapter = parent_session.read(cx).adapter();
1038        if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
1039            if let Some(label) = adapter.label_for_child_session(request) {
1040                return label.into();
1041            }
1042        }
1043        let mut label = parent_session.read(cx).label().clone();
1044        if !label.ends_with("(child)") {
1045            label = format!("{label} (child)").into();
1046        }
1047        label
1048    }
1049}
1050
1051async fn register_session_inner(
1052    this: &WeakEntity<DebugPanel>,
1053    session: Entity<Session>,
1054    cx: &mut AsyncWindowContext,
1055) -> Result<Entity<DebugSession>> {
1056    let adapter_name = session.read_with(cx, |session, _| session.adapter())?;
1057    this.update_in(cx, |_, window, cx| {
1058        cx.subscribe_in(
1059            &session,
1060            window,
1061            move |this, session, event: &SessionStateEvent, window, cx| match event {
1062                SessionStateEvent::Restart => {
1063                    this.handle_restart_request(session.clone(), window, cx);
1064                }
1065                SessionStateEvent::SpawnChildSession { request } => {
1066                    this.handle_start_debugging_request(request, session.clone(), window, cx);
1067                }
1068                _ => {}
1069            },
1070        )
1071        .detach();
1072    })
1073    .ok();
1074    let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
1075    let debug_session = this.update_in(cx, |this, window, cx| {
1076        let parent_session = this
1077            .sessions
1078            .iter()
1079            .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
1080            .cloned();
1081        this.sessions.retain(|session| {
1082            !session
1083                .read(cx)
1084                .running_state()
1085                .read(cx)
1086                .session()
1087                .read(cx)
1088                .is_terminated()
1089        });
1090
1091        let debug_session = DebugSession::running(
1092            this.project.clone(),
1093            this.workspace.clone(),
1094            parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
1095            session,
1096            serialized_layout,
1097            this.position(window, cx).axis(),
1098            window,
1099            cx,
1100        );
1101
1102        // We might want to make this an event subscription and only notify when a new thread is selected
1103        // This is used to filter the command menu correctly
1104        cx.observe(
1105            &debug_session.read(cx).running_state().clone(),
1106            |_, _, cx| cx.notify(),
1107        )
1108        .detach();
1109
1110        this.sessions.push(debug_session.clone());
1111
1112        debug_session
1113    })?;
1114    Ok(debug_session)
1115}
1116
1117impl EventEmitter<PanelEvent> for DebugPanel {}
1118impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1119
1120impl Focusable for DebugPanel {
1121    fn focus_handle(&self, _: &App) -> FocusHandle {
1122        self.focus_handle.clone()
1123    }
1124}
1125
1126impl Panel for DebugPanel {
1127    fn persistent_name() -> &'static str {
1128        "DebugPanel"
1129    }
1130
1131    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1132        match DebuggerSettings::get_global(cx).dock {
1133            DebugPanelDockPosition::Left => DockPosition::Left,
1134            DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1135            DebugPanelDockPosition::Right => DockPosition::Right,
1136        }
1137    }
1138
1139    fn position_is_valid(&self, _: DockPosition) -> bool {
1140        true
1141    }
1142
1143    fn set_position(
1144        &mut self,
1145        position: DockPosition,
1146        window: &mut Window,
1147        cx: &mut Context<Self>,
1148    ) {
1149        if position.axis() != self.position(window, cx).axis() {
1150            self.sessions.iter().for_each(|session_item| {
1151                session_item.update(cx, |item, cx| {
1152                    item.running_state()
1153                        .update(cx, |state, _| state.invert_axies())
1154                })
1155            });
1156        }
1157
1158        settings::update_settings_file::<DebuggerSettings>(
1159            self.fs.clone(),
1160            cx,
1161            move |settings, _| {
1162                let dock = match position {
1163                    DockPosition::Left => DebugPanelDockPosition::Left,
1164                    DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1165                    DockPosition::Right => DebugPanelDockPosition::Right,
1166                };
1167                settings.dock = dock;
1168            },
1169        );
1170    }
1171
1172    fn size(&self, _window: &Window, _: &App) -> Pixels {
1173        self.size
1174    }
1175
1176    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1177        self.size = size.unwrap_or(px(300.));
1178    }
1179
1180    fn remote_id() -> Option<proto::PanelId> {
1181        Some(proto::PanelId::DebugPanel)
1182    }
1183
1184    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1185        Some(IconName::Debug)
1186    }
1187
1188    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1189        if DebuggerSettings::get_global(cx).button {
1190            Some("Debug Panel")
1191        } else {
1192            None
1193        }
1194    }
1195
1196    fn toggle_action(&self) -> Box<dyn Action> {
1197        Box::new(ToggleFocus)
1198    }
1199
1200    fn pane(&self) -> Option<Entity<Pane>> {
1201        None
1202    }
1203
1204    fn activation_priority(&self) -> u32 {
1205        9
1206    }
1207
1208    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1209
1210    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1211        self.is_zoomed
1212    }
1213
1214    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1215        self.is_zoomed = zoomed;
1216        cx.notify();
1217    }
1218}
1219
1220impl Render for DebugPanel {
1221    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1222        let has_sessions = self.sessions.len() > 0;
1223        let this = cx.weak_entity();
1224        debug_assert_eq!(has_sessions, self.active_session.is_some());
1225
1226        if self
1227            .active_session
1228            .as_ref()
1229            .map(|session| session.read(cx).running_state())
1230            .map(|state| state.read(cx).has_open_context_menu(cx))
1231            .unwrap_or(false)
1232        {
1233            self.context_menu.take();
1234        }
1235
1236        v_flex()
1237            .size_full()
1238            .key_context("DebugPanel")
1239            .child(h_flex().children(self.top_controls_strip(window, cx)))
1240            .track_focus(&self.focus_handle(cx))
1241            .on_action({
1242                let this = this.clone();
1243                move |_: &workspace::ActivatePaneLeft, window, cx| {
1244                    this.update(cx, |this, cx| {
1245                        this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1246                    })
1247                    .ok();
1248                }
1249            })
1250            .on_action({
1251                let this = this.clone();
1252                move |_: &workspace::ActivatePaneRight, window, cx| {
1253                    this.update(cx, |this, cx| {
1254                        this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1255                    })
1256                    .ok();
1257                }
1258            })
1259            .on_action({
1260                let this = this.clone();
1261                move |_: &workspace::ActivatePaneUp, window, cx| {
1262                    this.update(cx, |this, cx| {
1263                        this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1264                    })
1265                    .ok();
1266                }
1267            })
1268            .on_action({
1269                let this = this.clone();
1270                move |_: &workspace::ActivatePaneDown, window, cx| {
1271                    this.update(cx, |this, cx| {
1272                        this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1273                    })
1274                    .ok();
1275                }
1276            })
1277            .on_action({
1278                let this = this.clone();
1279                move |_: &FocusConsole, window, cx| {
1280                    this.update(cx, |this, cx| {
1281                        this.activate_item(DebuggerPaneItem::Console, window, cx);
1282                    })
1283                    .ok();
1284                }
1285            })
1286            .on_action({
1287                let this = this.clone();
1288                move |_: &FocusVariables, window, cx| {
1289                    this.update(cx, |this, cx| {
1290                        this.activate_item(DebuggerPaneItem::Variables, window, cx);
1291                    })
1292                    .ok();
1293                }
1294            })
1295            .on_action({
1296                let this = this.clone();
1297                move |_: &FocusBreakpointList, window, cx| {
1298                    this.update(cx, |this, cx| {
1299                        this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1300                    })
1301                    .ok();
1302                }
1303            })
1304            .on_action({
1305                let this = this.clone();
1306                move |_: &FocusFrames, window, cx| {
1307                    this.update(cx, |this, cx| {
1308                        this.activate_item(DebuggerPaneItem::Frames, window, cx);
1309                    })
1310                    .ok();
1311                }
1312            })
1313            .on_action({
1314                let this = this.clone();
1315                move |_: &FocusModules, window, cx| {
1316                    this.update(cx, |this, cx| {
1317                        this.activate_item(DebuggerPaneItem::Modules, window, cx);
1318                    })
1319                    .ok();
1320                }
1321            })
1322            .on_action({
1323                let this = this.clone();
1324                move |_: &FocusLoadedSources, window, cx| {
1325                    this.update(cx, |this, cx| {
1326                        this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1327                    })
1328                    .ok();
1329                }
1330            })
1331            .on_action({
1332                let this = this.clone();
1333                move |_: &FocusTerminal, window, cx| {
1334                    this.update(cx, |this, cx| {
1335                        this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1336                    })
1337                    .ok();
1338                }
1339            })
1340            .on_action({
1341                let this = this.clone();
1342                move |_: &ToggleThreadPicker, window, cx| {
1343                    this.update(cx, |this, cx| {
1344                        this.toggle_thread_picker(window, cx);
1345                    })
1346                    .ok();
1347                }
1348            })
1349            .on_action({
1350                let this = this.clone();
1351                move |_: &ToggleSessionPicker, window, cx| {
1352                    this.update(cx, |this, cx| {
1353                        this.toggle_session_picker(window, cx);
1354                    })
1355                    .ok();
1356                }
1357            })
1358            .on_action(cx.listener(Self::toggle_zoom))
1359            .on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
1360                let Some(session) = panel.active_session() else {
1361                    return;
1362                };
1363                let active_pane = session
1364                    .read(cx)
1365                    .running_state()
1366                    .read(cx)
1367                    .active_pane()
1368                    .clone();
1369                active_pane.update(cx, |pane, cx| {
1370                    let is_zoomed = pane.is_zoomed();
1371                    pane.set_zoomed(!is_zoomed, cx);
1372                });
1373                cx.notify();
1374            }))
1375            .on_action(cx.listener(Self::copy_debug_adapter_arguments))
1376            .when(self.active_session.is_some(), |this| {
1377                this.on_mouse_down(
1378                    MouseButton::Right,
1379                    cx.listener(|this, event: &MouseDownEvent, window, cx| {
1380                        if this
1381                            .active_session
1382                            .as_ref()
1383                            .map(|session| {
1384                                let state = session.read(cx).running_state();
1385                                state.read(cx).has_pane_at_position(event.position)
1386                            })
1387                            .unwrap_or(false)
1388                        {
1389                            this.deploy_context_menu(event.position, window, cx);
1390                        }
1391                    }),
1392                )
1393                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1394                    deferred(
1395                        anchored()
1396                            .position(*position)
1397                            .anchor(gpui::Corner::TopLeft)
1398                            .child(menu.clone()),
1399                    )
1400                    .with_priority(1)
1401                }))
1402            })
1403            .map(|this| {
1404                if has_sessions {
1405                    this.children(self.active_session.clone())
1406                } else {
1407                    this.child(
1408                        v_flex()
1409                            .h_full()
1410                            .gap_1()
1411                            .items_center()
1412                            .justify_center()
1413                            .child(
1414                                h_flex().child(
1415                                    Label::new("No Debugging Sessions")
1416                                        .size(LabelSize::Small)
1417                                        .color(Color::Muted),
1418                                ),
1419                            )
1420                            .child(
1421                                h_flex().flex_shrink().child(
1422                                    Button::new("spawn-new-session-empty-state", "New Session")
1423                                        .size(ButtonSize::Large)
1424                                        .on_click(|_, window, cx| {
1425                                            window.dispatch_action(crate::Start.boxed_clone(), cx);
1426                                        }),
1427                                ),
1428                            ),
1429                    )
1430                }
1431            })
1432            .into_any()
1433    }
1434}
1435
1436struct DebuggerProvider(Entity<DebugPanel>);
1437
1438impl workspace::DebuggerProvider for DebuggerProvider {
1439    fn start_session(
1440        &self,
1441        definition: DebugScenario,
1442        context: TaskContext,
1443        buffer: Option<Entity<Buffer>>,
1444        window: &mut Window,
1445        cx: &mut App,
1446    ) {
1447        self.0.update(cx, |_, cx| {
1448            cx.defer_in(window, |this, window, cx| {
1449                this.start_session(definition, context, buffer, None, window, cx);
1450            })
1451        })
1452    }
1453
1454    fn spawn_task_or_modal(
1455        &self,
1456        workspace: &mut Workspace,
1457        action: &tasks_ui::Spawn,
1458        window: &mut Window,
1459        cx: &mut Context<Workspace>,
1460    ) {
1461        spawn_task_or_modal(workspace, action, window, cx);
1462    }
1463
1464    fn debug_scenario_scheduled(&self, cx: &mut App) {
1465        self.0.update(cx, |this, _| {
1466            this.debug_scenario_scheduled_last = true;
1467        });
1468    }
1469
1470    fn task_scheduled(&self, cx: &mut App) {
1471        self.0.update(cx, |this, _| {
1472            this.debug_scenario_scheduled_last = false;
1473        })
1474    }
1475
1476    fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
1477        self.0.read(cx).debug_scenario_scheduled_last
1478    }
1479
1480    fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
1481        let session = self.0.read(cx).active_session()?;
1482        let thread = session.read(cx).running_state().read(cx).thread_id()?;
1483        session.read(cx).session(cx).read(cx).thread_state(thread)
1484    }
1485}