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