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