debugger_panel.rs

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