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
 626        Some(
 627            div.border_b_1()
 628                .border_color(cx.theme().colors().border)
 629                .p_1()
 630                .justify_between()
 631                .w_full()
 632                .when(is_side, |this| this.gap_1())
 633                .child(
 634                    h_flex()
 635                        .child(
 636                            h_flex().gap_2().w_full().when_some(
 637                                active_session
 638                                    .as_ref()
 639                                    .map(|session| session.read(cx).running_state()),
 640                                |this, running_state| {
 641                                    let thread_status =
 642                                        running_state.read(cx).thread_status(cx).unwrap_or(
 643                                            project::debugger::session::ThreadStatus::Exited,
 644                                        );
 645                                    let capabilities = running_state.read(cx).capabilities(cx);
 646                                    let supports_detach =
 647                                        running_state.read(cx).session().read(cx).is_attached();
 648                                    this.map(|this| {
 649                                        if thread_status == ThreadStatus::Running {
 650                                            this.child(
 651                                                IconButton::new(
 652                                                    "debug-pause",
 653                                                    IconName::DebugPause,
 654                                                )
 655                                                .icon_size(IconSize::XSmall)
 656                                                .shape(ui::IconButtonShape::Square)
 657                                                .on_click(window.listener_for(
 658                                                    &running_state,
 659                                                    |this, _, _window, cx| {
 660                                                        this.pause_thread(cx);
 661                                                    },
 662                                                ))
 663                                                .tooltip({
 664                                                    let focus_handle = focus_handle.clone();
 665                                                    move |window, cx| {
 666                                                        Tooltip::for_action_in(
 667                                                            "Pause program",
 668                                                            &Pause,
 669                                                            &focus_handle,
 670                                                            window,
 671                                                            cx,
 672                                                        )
 673                                                    }
 674                                                }),
 675                                            )
 676                                        } else {
 677                                            this.child(
 678                                                IconButton::new(
 679                                                    "debug-continue",
 680                                                    IconName::DebugContinue,
 681                                                )
 682                                                .icon_size(IconSize::XSmall)
 683                                                .shape(ui::IconButtonShape::Square)
 684                                                .on_click(window.listener_for(
 685                                                    &running_state,
 686                                                    |this, _, _window, cx| this.continue_thread(cx),
 687                                                ))
 688                                                .disabled(thread_status != ThreadStatus::Stopped)
 689                                                .tooltip({
 690                                                    let focus_handle = focus_handle.clone();
 691                                                    move |window, cx| {
 692                                                        Tooltip::for_action_in(
 693                                                            "Continue program",
 694                                                            &Continue,
 695                                                            &focus_handle,
 696                                                            window,
 697                                                            cx,
 698                                                        )
 699                                                    }
 700                                                }),
 701                                            )
 702                                        }
 703                                    })
 704                                    .child(
 705                                        IconButton::new("debug-step-over", IconName::ArrowRight)
 706                                            .icon_size(IconSize::XSmall)
 707                                            .shape(ui::IconButtonShape::Square)
 708                                            .on_click(window.listener_for(
 709                                                &running_state,
 710                                                |this, _, _window, cx| {
 711                                                    this.step_over(cx);
 712                                                },
 713                                            ))
 714                                            .disabled(thread_status != ThreadStatus::Stopped)
 715                                            .tooltip({
 716                                                let focus_handle = focus_handle.clone();
 717                                                move |window, cx| {
 718                                                    Tooltip::for_action_in(
 719                                                        "Step over",
 720                                                        &StepOver,
 721                                                        &focus_handle,
 722                                                        window,
 723                                                        cx,
 724                                                    )
 725                                                }
 726                                            }),
 727                                    )
 728                                    .child(
 729                                        IconButton::new(
 730                                            "debug-step-into",
 731                                            IconName::ArrowDownRight,
 732                                        )
 733                                        .icon_size(IconSize::XSmall)
 734                                        .shape(ui::IconButtonShape::Square)
 735                                        .on_click(window.listener_for(
 736                                            &running_state,
 737                                            |this, _, _window, cx| {
 738                                                this.step_in(cx);
 739                                            },
 740                                        ))
 741                                        .disabled(thread_status != ThreadStatus::Stopped)
 742                                        .tooltip({
 743                                            let focus_handle = focus_handle.clone();
 744                                            move |window, cx| {
 745                                                Tooltip::for_action_in(
 746                                                    "Step in",
 747                                                    &StepInto,
 748                                                    &focus_handle,
 749                                                    window,
 750                                                    cx,
 751                                                )
 752                                            }
 753                                        }),
 754                                    )
 755                                    .child(
 756                                        IconButton::new("debug-step-out", IconName::ArrowUpRight)
 757                                            .icon_size(IconSize::XSmall)
 758                                            .shape(ui::IconButtonShape::Square)
 759                                            .on_click(window.listener_for(
 760                                                &running_state,
 761                                                |this, _, _window, cx| {
 762                                                    this.step_out(cx);
 763                                                },
 764                                            ))
 765                                            .disabled(thread_status != ThreadStatus::Stopped)
 766                                            .tooltip({
 767                                                let focus_handle = focus_handle.clone();
 768                                                move |window, cx| {
 769                                                    Tooltip::for_action_in(
 770                                                        "Step out",
 771                                                        &StepOut,
 772                                                        &focus_handle,
 773                                                        window,
 774                                                        cx,
 775                                                    )
 776                                                }
 777                                            }),
 778                                    )
 779                                    .child(Divider::vertical())
 780                                    .child(
 781                                        IconButton::new("debug-restart", IconName::DebugRestart)
 782                                            .icon_size(IconSize::XSmall)
 783                                            .on_click(window.listener_for(
 784                                                &running_state,
 785                                                |this, _, window, cx| {
 786                                                    this.rerun_session(window, cx);
 787                                                },
 788                                            ))
 789                                            .tooltip({
 790                                                let focus_handle = focus_handle.clone();
 791                                                move |window, cx| {
 792                                                    Tooltip::for_action_in(
 793                                                        "Rerun Session",
 794                                                        &RerunSession,
 795                                                        &focus_handle,
 796                                                        window,
 797                                                        cx,
 798                                                    )
 799                                                }
 800                                            }),
 801                                    )
 802                                    .child(
 803                                        IconButton::new("debug-stop", IconName::Power)
 804                                            .icon_size(IconSize::XSmall)
 805                                            .on_click(window.listener_for(
 806                                                &running_state,
 807                                                |this, _, _window, cx| {
 808                                                    this.stop_thread(cx);
 809                                                },
 810                                            ))
 811                                            .disabled(
 812                                                thread_status != ThreadStatus::Stopped
 813                                                    && thread_status != ThreadStatus::Running,
 814                                            )
 815                                            .tooltip({
 816                                                let focus_handle = focus_handle.clone();
 817                                                let label = if capabilities
 818                                                    .supports_terminate_threads_request
 819                                                    .unwrap_or_default()
 820                                                {
 821                                                    "Terminate Thread"
 822                                                } else {
 823                                                    "Terminate All Threads"
 824                                                };
 825                                                move |window, cx| {
 826                                                    Tooltip::for_action_in(
 827                                                        label,
 828                                                        &Stop,
 829                                                        &focus_handle,
 830                                                        window,
 831                                                        cx,
 832                                                    )
 833                                                }
 834                                            }),
 835                                    )
 836                                    .when(
 837                                        supports_detach,
 838                                        |div| {
 839                                            div.child(
 840                                                IconButton::new(
 841                                                    "debug-disconnect",
 842                                                    IconName::DebugDetach,
 843                                                )
 844                                                .disabled(
 845                                                    thread_status != ThreadStatus::Stopped
 846                                                        && thread_status != ThreadStatus::Running,
 847                                                )
 848                                                .icon_size(IconSize::XSmall)
 849                                                .on_click(window.listener_for(
 850                                                    &running_state,
 851                                                    |this, _, _, cx| {
 852                                                        this.detach_client(cx);
 853                                                    },
 854                                                ))
 855                                                .tooltip({
 856                                                    let focus_handle = focus_handle.clone();
 857                                                    move |window, cx| {
 858                                                        Tooltip::for_action_in(
 859                                                            "Detach",
 860                                                            &Detach,
 861                                                            &focus_handle,
 862                                                            window,
 863                                                            cx,
 864                                                        )
 865                                                    }
 866                                                }),
 867                                            )
 868                                        },
 869                                    )
 870                                },
 871                            ),
 872                        )
 873                        .justify_around()
 874                        .when(is_side, |this| {
 875                            this.child(new_session_button())
 876                                .child(documentation_button())
 877                        }),
 878                )
 879                .child(
 880                    h_flex()
 881                        .gap_2()
 882                        .when(is_side, |this| this.justify_between())
 883                        .child(
 884                            h_flex().when_some(
 885                                active_session
 886                                    .as_ref()
 887                                    .map(|session| session.read(cx).running_state())
 888                                    .cloned(),
 889                                |this, running_state| {
 890                                    this.children({
 891                                        let running_state = running_state.clone();
 892                                        let threads =
 893                                            running_state.update(cx, |running_state, cx| {
 894                                                let session = running_state.session();
 895                                                session.read(cx).is_started().then(|| {
 896                                                    session.update(cx, |session, cx| {
 897                                                        session.threads(cx)
 898                                                    })
 899                                                })
 900                                            });
 901
 902                                        threads.and_then(|threads| {
 903                                            self.render_thread_dropdown(
 904                                                &running_state,
 905                                                threads,
 906                                                window,
 907                                                cx,
 908                                            )
 909                                        })
 910                                    })
 911                                    .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
 912                                },
 913                            ),
 914                        )
 915                        .child(
 916                            h_flex()
 917                                .children(self.render_session_menu(
 918                                    self.active_session(),
 919                                    self.running_state(cx),
 920                                    window,
 921                                    cx,
 922                                ))
 923                                .when(!is_side, |this| {
 924                                    this.child(new_session_button())
 925                                        .child(documentation_button())
 926                                }),
 927                        ),
 928                ),
 929        )
 930    }
 931
 932    pub(crate) fn activate_pane_in_direction(
 933        &mut self,
 934        direction: SplitDirection,
 935        window: &mut Window,
 936        cx: &mut Context<Self>,
 937    ) {
 938        if let Some(session) = self.active_session() {
 939            session.update(cx, |session, cx| {
 940                session.running_state().update(cx, |running, cx| {
 941                    running.activate_pane_in_direction(direction, window, cx);
 942                })
 943            });
 944        }
 945    }
 946
 947    pub(crate) fn activate_item(
 948        &mut self,
 949        item: DebuggerPaneItem,
 950        window: &mut Window,
 951        cx: &mut Context<Self>,
 952    ) {
 953        if let Some(session) = self.active_session() {
 954            session.update(cx, |session, cx| {
 955                session.running_state().update(cx, |running, cx| {
 956                    running.activate_item(item, window, cx);
 957                });
 958            });
 959        }
 960    }
 961
 962    pub(crate) fn activate_session_by_id(
 963        &mut self,
 964        session_id: SessionId,
 965        window: &mut Window,
 966        cx: &mut Context<Self>,
 967    ) {
 968        if let Some(session) = self
 969            .sessions
 970            .iter()
 971            .find(|session| session.read(cx).session_id(cx) == session_id)
 972        {
 973            self.activate_session(session.clone(), window, cx);
 974        }
 975    }
 976
 977    pub(crate) fn activate_session(
 978        &mut self,
 979        session_item: Entity<DebugSession>,
 980        window: &mut Window,
 981        cx: &mut Context<Self>,
 982    ) {
 983        debug_assert!(self.sessions.contains(&session_item));
 984        session_item.focus_handle(cx).focus(window);
 985        session_item.update(cx, |this, cx| {
 986            this.running_state().update(cx, |this, cx| {
 987                this.go_to_selected_stack_frame(window, cx);
 988            });
 989        });
 990        self.active_session = Some(session_item);
 991        cx.notify();
 992    }
 993
 994    pub(crate) fn go_to_scenario_definition(
 995        &self,
 996        kind: TaskSourceKind,
 997        scenario: DebugScenario,
 998        worktree_id: WorktreeId,
 999        window: &mut Window,
1000        cx: &mut Context<Self>,
1001    ) -> Task<Result<()>> {
1002        let Some(workspace) = self.workspace.upgrade() else {
1003            return Task::ready(Ok(()));
1004        };
1005        let project_path = match kind {
1006            TaskSourceKind::AbsPath { abs_path, .. } => {
1007                let Some(project_path) = workspace
1008                    .read(cx)
1009                    .project()
1010                    .read(cx)
1011                    .project_path_for_absolute_path(&abs_path, cx)
1012                else {
1013                    return Task::ready(Err(anyhow!("no abs path")));
1014                };
1015
1016                project_path
1017            }
1018            TaskSourceKind::Worktree {
1019                id,
1020                directory_in_worktree: dir,
1021                ..
1022            } => {
1023                let relative_path = if dir.ends_with(".vscode") {
1024                    dir.join("launch.json")
1025                } else {
1026                    dir.join("debug.json")
1027                };
1028                ProjectPath {
1029                    worktree_id: id,
1030                    path: Arc::from(relative_path),
1031                }
1032            }
1033            _ => return self.save_scenario(scenario, worktree_id, window, cx),
1034        };
1035
1036        let editor = workspace.update(cx, |workspace, cx| {
1037            workspace.open_path(project_path, None, true, window, cx)
1038        });
1039        cx.spawn_in(window, async move |_, cx| {
1040            let editor = editor.await?;
1041            let editor = cx
1042                .update(|_, cx| editor.act_as::<Editor>(cx))?
1043                .context("expected editor")?;
1044
1045            // unfortunately debug tasks don't have an easy way to globally
1046            // identify them. to jump to the one that you just created or an
1047            // 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
1048            editor.update_in(cx, |editor, window, cx| {
1049                let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| {
1050                    if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") {
1051                        Some(row)
1052                    } else {
1053                        None
1054                    }
1055                });
1056                if let Some(row) = row {
1057                    editor.go_to_singleton_buffer_point(
1058                        text::Point::new(row as u32, 4),
1059                        window,
1060                        cx,
1061                    );
1062                }
1063            })?;
1064
1065            Ok(())
1066        })
1067    }
1068
1069    pub(crate) fn save_scenario(
1070        &self,
1071        scenario: DebugScenario,
1072        worktree_id: WorktreeId,
1073        window: &mut Window,
1074        cx: &mut Context<Self>,
1075    ) -> Task<Result<()>> {
1076        let this = cx.weak_entity();
1077        let project = self.project.clone();
1078        self.workspace
1079            .update(cx, |workspace, cx| {
1080                let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
1081                    return Task::ready(Err(anyhow!("Couldn't get worktree path")));
1082                };
1083
1084                let serialized_scenario = serde_json::to_value(scenario);
1085
1086                cx.spawn_in(window, async move |workspace, cx| {
1087                    let serialized_scenario = serialized_scenario?;
1088                    let fs =
1089                        workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
1090
1091                    path.push(paths::local_settings_folder_relative_path());
1092                    if !fs.is_dir(path.as_path()).await {
1093                        fs.create_dir(path.as_path()).await?;
1094                    }
1095                    path.pop();
1096
1097                    path.push(paths::local_debug_file_relative_path());
1098                    let path = path.as_path();
1099
1100                    if !fs.is_file(path).await {
1101                        fs.create_file(path, Default::default()).await?;
1102                        fs.write(
1103                            path,
1104                            settings::initial_local_debug_tasks_content()
1105                                .to_string()
1106                                .as_bytes(),
1107                        )
1108                        .await?;
1109                    }
1110                    let project_path = workspace.update(cx, |workspace, cx| {
1111                        workspace
1112                            .project()
1113                            .read(cx)
1114                            .project_path_for_absolute_path(&path, cx)
1115                            .context(
1116                                "Couldn't get project path for .zed/debug.json in active worktree",
1117                            )
1118                    })??;
1119
1120                    let editor = this
1121                        .update_in(cx, |this, window, cx| {
1122                            this.workspace.update(cx, |workspace, cx| {
1123                                workspace.open_path(project_path, None, true, window, cx)
1124                            })
1125                        })??
1126                        .await?;
1127                    let editor = cx
1128                        .update(|_, cx| editor.act_as::<Editor>(cx))?
1129                        .context("expected editor")?;
1130
1131                    let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
1132                        .lines()
1133                        .map(|l| format!("  {l}"))
1134                        .join("\n");
1135
1136                    editor
1137                        .update_in(cx, |editor, window, cx| {
1138                            Self::insert_task_into_editor(editor, new_scenario, project, window, cx)
1139                        })??
1140                        .await
1141                })
1142            })
1143            .unwrap_or_else(|err| Task::ready(Err(err)))
1144    }
1145
1146    pub fn insert_task_into_editor(
1147        editor: &mut Editor,
1148        new_scenario: String,
1149        project: Entity<Project>,
1150        window: &mut Window,
1151        cx: &mut Context<Editor>,
1152    ) -> Result<Task<Result<()>>> {
1153        static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
1154            Query::new(
1155                &tree_sitter_json::LANGUAGE.into(),
1156                "(document (array (object) @object))", // TODO: use "." anchor to only match last object
1157            )
1158            .expect("Failed to create LAST_ITEM_QUERY")
1159        });
1160        static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
1161            Query::new(
1162                &tree_sitter_json::LANGUAGE.into(),
1163                "(document (array) @array)",
1164            )
1165            .expect("Failed to create EMPTY_ARRAY_QUERY")
1166        });
1167
1168        let content = editor.text(cx);
1169        let mut parser = tree_sitter::Parser::new();
1170        parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
1171        let mut cursor = tree_sitter::QueryCursor::new();
1172        let syntax_tree = parser
1173            .parse(&content, None)
1174            .context("could not parse debug.json")?;
1175        let mut matches = cursor.matches(
1176            &LAST_ITEM_QUERY,
1177            syntax_tree.root_node(),
1178            content.as_bytes(),
1179        );
1180
1181        let mut last_offset = None;
1182        while let Some(mat) = matches.next() {
1183            if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
1184                last_offset = Some(pos)
1185            }
1186        }
1187        let mut edits = Vec::new();
1188        let mut cursor_position = 0;
1189
1190        if let Some(pos) = last_offset {
1191            edits.push((pos..pos, format!(",\n{new_scenario}")));
1192            cursor_position = pos + ",\n  ".len();
1193        } else {
1194            let mut matches = cursor.matches(
1195                &EMPTY_ARRAY_QUERY,
1196                syntax_tree.root_node(),
1197                content.as_bytes(),
1198            );
1199
1200            if let Some(mat) = matches.next() {
1201                if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
1202                    edits.push((pos..pos, format!("\n{new_scenario}\n")));
1203                    cursor_position = pos + "\n  ".len();
1204                }
1205            } else {
1206                edits.push((0..0, format!("[\n{}\n]", new_scenario)));
1207                cursor_position = "[\n  ".len();
1208            }
1209        }
1210        editor.transact(window, cx, |editor, window, cx| {
1211            editor.edit(edits, cx);
1212            let snapshot = editor
1213                .buffer()
1214                .read(cx)
1215                .as_singleton()
1216                .unwrap()
1217                .read(cx)
1218                .snapshot();
1219            let point = cursor_position.to_point(&snapshot);
1220            editor.go_to_singleton_buffer_point(point, window, cx);
1221        });
1222        Ok(editor.save(SaveOptions::default(), project, window, cx))
1223    }
1224
1225    pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1226        self.thread_picker_menu_handle.toggle(window, cx);
1227    }
1228
1229    pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1230        self.session_picker_menu_handle.toggle(window, cx);
1231    }
1232
1233    fn toggle_zoom(
1234        &mut self,
1235        _: &workspace::ToggleZoom,
1236        window: &mut Window,
1237        cx: &mut Context<Self>,
1238    ) {
1239        if self.is_zoomed {
1240            cx.emit(PanelEvent::ZoomOut);
1241        } else {
1242            if !self.focus_handle(cx).contains_focused(window, cx) {
1243                cx.focus_self(window);
1244            }
1245            cx.emit(PanelEvent::ZoomIn);
1246        }
1247    }
1248
1249    fn label_for_child_session(
1250        &self,
1251        parent_session: &Entity<Session>,
1252        request: &StartDebuggingRequestArguments,
1253        cx: &mut Context<'_, Self>,
1254    ) -> SharedString {
1255        let adapter = parent_session.read(cx).adapter();
1256        if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
1257            if let Some(label) = adapter.label_for_child_session(request) {
1258                return label.into();
1259            }
1260        }
1261        let mut label = parent_session.read(cx).label().clone();
1262        if !label.ends_with("(child)") {
1263            label = format!("{label} (child)").into();
1264        }
1265        label
1266    }
1267}
1268
1269async fn register_session_inner(
1270    this: &WeakEntity<DebugPanel>,
1271    session: Entity<Session>,
1272    cx: &mut AsyncWindowContext,
1273) -> Result<Entity<DebugSession>> {
1274    let adapter_name = session.read_with(cx, |session, _| session.adapter())?;
1275    this.update_in(cx, |_, window, cx| {
1276        cx.subscribe_in(
1277            &session,
1278            window,
1279            move |this, session, event: &SessionStateEvent, window, cx| match event {
1280                SessionStateEvent::Restart => {
1281                    this.handle_restart_request(session.clone(), window, cx);
1282                }
1283                SessionStateEvent::SpawnChildSession { request } => {
1284                    this.handle_start_debugging_request(request, session.clone(), window, cx);
1285                }
1286                _ => {}
1287            },
1288        )
1289        .detach();
1290    })
1291    .ok();
1292    let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
1293    let debug_session = this.update_in(cx, |this, window, cx| {
1294        let parent_session = this
1295            .sessions
1296            .iter()
1297            .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
1298            .cloned();
1299        this.sessions.retain(|session| {
1300            !session
1301                .read(cx)
1302                .running_state()
1303                .read(cx)
1304                .session()
1305                .read(cx)
1306                .is_terminated()
1307        });
1308
1309        let debug_session = DebugSession::running(
1310            this.project.clone(),
1311            this.workspace.clone(),
1312            parent_session
1313                .as_ref()
1314                .map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
1315            session,
1316            serialized_layout,
1317            this.position(window, cx).axis(),
1318            window,
1319            cx,
1320        );
1321
1322        // We might want to make this an event subscription and only notify when a new thread is selected
1323        // This is used to filter the command menu correctly
1324        cx.observe(
1325            &debug_session.read(cx).running_state().clone(),
1326            |_, _, cx| cx.notify(),
1327        )
1328        .detach();
1329        let insert_position = this
1330            .sessions
1331            .iter()
1332            .position(|session| Some(session) == parent_session.as_ref())
1333            .map(|position| position + 1)
1334            .unwrap_or(this.sessions.len());
1335        // Maintain topological sort order of sessions
1336        this.sessions.insert(insert_position, debug_session.clone());
1337
1338        debug_session
1339    })?;
1340    Ok(debug_session)
1341}
1342
1343impl EventEmitter<PanelEvent> for DebugPanel {}
1344impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1345
1346impl Focusable for DebugPanel {
1347    fn focus_handle(&self, _: &App) -> FocusHandle {
1348        self.focus_handle.clone()
1349    }
1350}
1351
1352impl Panel for DebugPanel {
1353    fn persistent_name() -> &'static str {
1354        "DebugPanel"
1355    }
1356
1357    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1358        match DebuggerSettings::get_global(cx).dock {
1359            DebugPanelDockPosition::Left => DockPosition::Left,
1360            DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1361            DebugPanelDockPosition::Right => DockPosition::Right,
1362        }
1363    }
1364
1365    fn position_is_valid(&self, _: DockPosition) -> bool {
1366        true
1367    }
1368
1369    fn set_position(
1370        &mut self,
1371        position: DockPosition,
1372        window: &mut Window,
1373        cx: &mut Context<Self>,
1374    ) {
1375        if position.axis() != self.position(window, cx).axis() {
1376            self.sessions.iter().for_each(|session_item| {
1377                session_item.update(cx, |item, cx| {
1378                    item.running_state()
1379                        .update(cx, |state, _| state.invert_axies())
1380                })
1381            });
1382        }
1383
1384        settings::update_settings_file::<DebuggerSettings>(
1385            self.fs.clone(),
1386            cx,
1387            move |settings, _| {
1388                let dock = match position {
1389                    DockPosition::Left => DebugPanelDockPosition::Left,
1390                    DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1391                    DockPosition::Right => DebugPanelDockPosition::Right,
1392                };
1393                settings.dock = dock;
1394            },
1395        );
1396    }
1397
1398    fn size(&self, _window: &Window, _: &App) -> Pixels {
1399        self.size
1400    }
1401
1402    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1403        self.size = size.unwrap_or(px(300.));
1404    }
1405
1406    fn remote_id() -> Option<proto::PanelId> {
1407        Some(proto::PanelId::DebugPanel)
1408    }
1409
1410    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1411        Some(IconName::Debug)
1412    }
1413
1414    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1415        if DebuggerSettings::get_global(cx).button {
1416            Some("Debug Panel")
1417        } else {
1418            None
1419        }
1420    }
1421
1422    fn toggle_action(&self) -> Box<dyn Action> {
1423        Box::new(ToggleFocus)
1424    }
1425
1426    fn pane(&self) -> Option<Entity<Pane>> {
1427        None
1428    }
1429
1430    fn activation_priority(&self) -> u32 {
1431        9
1432    }
1433
1434    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1435
1436    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1437        self.is_zoomed
1438    }
1439
1440    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1441        self.is_zoomed = zoomed;
1442        cx.notify();
1443    }
1444}
1445
1446impl Render for DebugPanel {
1447    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1448        let this = cx.weak_entity();
1449
1450        if self
1451            .active_session
1452            .as_ref()
1453            .map(|session| session.read(cx).running_state())
1454            .map(|state| state.read(cx).has_open_context_menu(cx))
1455            .unwrap_or(false)
1456        {
1457            self.context_menu.take();
1458        }
1459
1460        v_flex()
1461            .when(!self.is_zoomed, |this| {
1462                this.when_else(
1463                    self.position(window, cx) == DockPosition::Bottom,
1464                    |this| this.max_h(self.size),
1465                    |this| this.max_w(self.size),
1466                )
1467            })
1468            .size_full()
1469            .key_context("DebugPanel")
1470            .child(h_flex().children(self.top_controls_strip(window, cx)))
1471            .track_focus(&self.focus_handle(cx))
1472            .on_action({
1473                let this = this.clone();
1474                move |_: &workspace::ActivatePaneLeft, window, cx| {
1475                    this.update(cx, |this, cx| {
1476                        this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1477                    })
1478                    .ok();
1479                }
1480            })
1481            .on_action({
1482                let this = this.clone();
1483                move |_: &workspace::ActivatePaneRight, window, cx| {
1484                    this.update(cx, |this, cx| {
1485                        this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1486                    })
1487                    .ok();
1488                }
1489            })
1490            .on_action({
1491                let this = this.clone();
1492                move |_: &workspace::ActivatePaneUp, window, cx| {
1493                    this.update(cx, |this, cx| {
1494                        this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1495                    })
1496                    .ok();
1497                }
1498            })
1499            .on_action({
1500                let this = this.clone();
1501                move |_: &workspace::ActivatePaneDown, window, cx| {
1502                    this.update(cx, |this, cx| {
1503                        this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1504                    })
1505                    .ok();
1506                }
1507            })
1508            .on_action({
1509                let this = this.clone();
1510                move |_: &FocusConsole, window, cx| {
1511                    this.update(cx, |this, cx| {
1512                        this.activate_item(DebuggerPaneItem::Console, window, cx);
1513                    })
1514                    .ok();
1515                }
1516            })
1517            .on_action({
1518                let this = this.clone();
1519                move |_: &FocusVariables, window, cx| {
1520                    this.update(cx, |this, cx| {
1521                        this.activate_item(DebuggerPaneItem::Variables, window, cx);
1522                    })
1523                    .ok();
1524                }
1525            })
1526            .on_action({
1527                let this = this.clone();
1528                move |_: &FocusBreakpointList, window, cx| {
1529                    this.update(cx, |this, cx| {
1530                        this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1531                    })
1532                    .ok();
1533                }
1534            })
1535            .on_action({
1536                let this = this.clone();
1537                move |_: &FocusFrames, window, cx| {
1538                    this.update(cx, |this, cx| {
1539                        this.activate_item(DebuggerPaneItem::Frames, window, cx);
1540                    })
1541                    .ok();
1542                }
1543            })
1544            .on_action({
1545                let this = this.clone();
1546                move |_: &FocusModules, window, cx| {
1547                    this.update(cx, |this, cx| {
1548                        this.activate_item(DebuggerPaneItem::Modules, window, cx);
1549                    })
1550                    .ok();
1551                }
1552            })
1553            .on_action({
1554                let this = this.clone();
1555                move |_: &FocusLoadedSources, window, cx| {
1556                    this.update(cx, |this, cx| {
1557                        this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1558                    })
1559                    .ok();
1560                }
1561            })
1562            .on_action({
1563                let this = this.clone();
1564                move |_: &FocusTerminal, window, cx| {
1565                    this.update(cx, |this, cx| {
1566                        this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1567                    })
1568                    .ok();
1569                }
1570            })
1571            .on_action({
1572                let this = this.clone();
1573                move |_: &ToggleThreadPicker, window, cx| {
1574                    this.update(cx, |this, cx| {
1575                        this.toggle_thread_picker(window, cx);
1576                    })
1577                    .ok();
1578                }
1579            })
1580            .on_action({
1581                let this = this.clone();
1582                move |_: &ToggleSessionPicker, window, cx| {
1583                    this.update(cx, |this, cx| {
1584                        this.toggle_session_picker(window, cx);
1585                    })
1586                    .ok();
1587                }
1588            })
1589            .on_action(cx.listener(Self::toggle_zoom))
1590            .on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
1591                let Some(session) = panel.active_session() else {
1592                    return;
1593                };
1594                let active_pane = session
1595                    .read(cx)
1596                    .running_state()
1597                    .read(cx)
1598                    .active_pane()
1599                    .clone();
1600                active_pane.update(cx, |pane, cx| {
1601                    let is_zoomed = pane.is_zoomed();
1602                    pane.set_zoomed(!is_zoomed, cx);
1603                });
1604                cx.notify();
1605            }))
1606            .on_action(cx.listener(Self::copy_debug_adapter_arguments))
1607            .when(self.active_session.is_some(), |this| {
1608                this.on_mouse_down(
1609                    MouseButton::Right,
1610                    cx.listener(|this, event: &MouseDownEvent, window, cx| {
1611                        if this
1612                            .active_session
1613                            .as_ref()
1614                            .map(|session| {
1615                                let state = session.read(cx).running_state();
1616                                state.read(cx).has_pane_at_position(event.position)
1617                            })
1618                            .unwrap_or(false)
1619                        {
1620                            this.deploy_context_menu(event.position, window, cx);
1621                        }
1622                    }),
1623                )
1624                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1625                    deferred(
1626                        anchored()
1627                            .position(*position)
1628                            .anchor(gpui::Corner::TopLeft)
1629                            .child(menu.clone()),
1630                    )
1631                    .with_priority(1)
1632                }))
1633            })
1634            .map(|this| {
1635                if let Some(active_session) = self.active_session.clone() {
1636                    this.child(active_session)
1637                } else {
1638                    let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
1639                    let welcome_experience = v_flex()
1640                        .when_else(
1641                            docked_to_bottom,
1642                            |this| this.w_2_3().h_full().pr_8(),
1643                            |this| this.w_full().h_1_3(),
1644                        )
1645                        .items_center()
1646                        .justify_center()
1647                        .gap_2()
1648                        .child(
1649                            Button::new("spawn-new-session-empty-state", "New Session")
1650                                .icon(IconName::Plus)
1651                                .icon_size(IconSize::XSmall)
1652                                .icon_color(Color::Muted)
1653                                .icon_position(IconPosition::Start)
1654                                .on_click(|_, window, cx| {
1655                                    window.dispatch_action(crate::Start.boxed_clone(), cx);
1656                                }),
1657                        )
1658                        .child(
1659                            Button::new("edit-debug-settings", "Edit debug.json")
1660                                .icon(IconName::Code)
1661                                .icon_size(IconSize::XSmall)
1662                                .color(Color::Muted)
1663                                .icon_color(Color::Muted)
1664                                .icon_position(IconPosition::Start)
1665                                .on_click(|_, window, cx| {
1666                                    window.dispatch_action(
1667                                        zed_actions::OpenProjectDebugTasks.boxed_clone(),
1668                                        cx,
1669                                    );
1670                                }),
1671                        )
1672                        .child(
1673                            Button::new("open-debugger-docs", "Debugger Docs")
1674                                .icon(IconName::Book)
1675                                .color(Color::Muted)
1676                                .icon_size(IconSize::XSmall)
1677                                .icon_color(Color::Muted)
1678                                .icon_position(IconPosition::Start)
1679                                .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
1680                        )
1681                        .child(
1682                            Button::new(
1683                                "spawn-new-session-install-extensions",
1684                                "Debugger Extensions",
1685                            )
1686                            .icon(IconName::Blocks)
1687                            .color(Color::Muted)
1688                            .icon_size(IconSize::XSmall)
1689                            .icon_color(Color::Muted)
1690                            .icon_position(IconPosition::Start)
1691                            .on_click(|_, window, cx| {
1692                                window.dispatch_action(
1693                                    zed_actions::Extensions {
1694                                        category_filter: Some(
1695                                            zed_actions::ExtensionCategoryFilter::DebugAdapters,
1696                                        ),
1697                                    }
1698                                    .boxed_clone(),
1699                                    cx,
1700                                );
1701                            }),
1702                        );
1703                    let breakpoint_list =
1704                        v_flex()
1705                            .group("base-breakpoint-list")
1706                            .items_start()
1707                            .when_else(
1708                                docked_to_bottom,
1709                                |this| this.min_w_1_3().h_full(),
1710                                |this| this.w_full().h_2_3(),
1711                            )
1712                            .p_1()
1713                            .child(
1714                                h_flex()
1715                                    .pl_1()
1716                                    .w_full()
1717                                    .justify_between()
1718                                    .child(Label::new("Breakpoints").size(LabelSize::Small))
1719                                    .child(h_flex().visible_on_hover("base-breakpoint-list").child(
1720                                        self.breakpoint_list.read(cx).render_control_strip(),
1721                                    ))
1722                                    .track_focus(&self.breakpoint_list.focus_handle(cx)),
1723                            )
1724                            .child(Divider::horizontal())
1725                            .child(self.breakpoint_list.clone());
1726                    this.child(
1727                        v_flex()
1728                            .h_full()
1729                            .gap_1()
1730                            .items_center()
1731                            .justify_center()
1732                            .child(
1733                                div()
1734                                    .when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
1735                                    .size_full()
1736                                    .map(|this| {
1737                                        if docked_to_bottom {
1738                                            this.items_start()
1739                                                .child(breakpoint_list)
1740                                                .child(Divider::vertical())
1741                                                .child(welcome_experience)
1742                                        } else {
1743                                            this.items_end()
1744                                                .child(welcome_experience)
1745                                                .child(Divider::horizontal())
1746                                                .child(breakpoint_list)
1747                                        }
1748                                    }),
1749                            ),
1750                    )
1751                }
1752            })
1753            .into_any()
1754    }
1755}
1756
1757struct DebuggerProvider(Entity<DebugPanel>);
1758
1759impl workspace::DebuggerProvider for DebuggerProvider {
1760    fn start_session(
1761        &self,
1762        definition: DebugScenario,
1763        context: TaskContext,
1764        buffer: Option<Entity<Buffer>>,
1765        worktree_id: Option<WorktreeId>,
1766        window: &mut Window,
1767        cx: &mut App,
1768    ) {
1769        self.0.update(cx, |_, cx| {
1770            cx.defer_in(window, move |this, window, cx| {
1771                this.start_session(definition, context, buffer, worktree_id, window, cx);
1772            })
1773        })
1774    }
1775
1776    fn spawn_task_or_modal(
1777        &self,
1778        workspace: &mut Workspace,
1779        action: &tasks_ui::Spawn,
1780        window: &mut Window,
1781        cx: &mut Context<Workspace>,
1782    ) {
1783        spawn_task_or_modal(workspace, action, window, cx);
1784    }
1785
1786    fn debug_scenario_scheduled(&self, cx: &mut App) {
1787        self.0.update(cx, |this, _| {
1788            this.debug_scenario_scheduled_last = true;
1789        });
1790    }
1791
1792    fn task_scheduled(&self, cx: &mut App) {
1793        self.0.update(cx, |this, _| {
1794            this.debug_scenario_scheduled_last = false;
1795        })
1796    }
1797
1798    fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
1799        self.0.read(cx).debug_scenario_scheduled_last
1800    }
1801
1802    fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
1803        let session = self.0.read(cx).active_session()?;
1804        let thread = session.read(cx).running_state().read(cx).thread_id()?;
1805        session.read(cx).session(cx).read(cx).thread_state(thread)
1806    }
1807}