debugger_panel.rs

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