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