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