debugger_panel.rs

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