debugger_panel.rs

   1use crate::persistence::DebuggerPaneItem;
   2use crate::session::DebugSession;
   3use crate::session::running::RunningState;
   4use crate::{
   5    ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
   6    FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
   7    ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
   8    ToggleSessionPicker, ToggleThreadPicker, persistence,
   9};
  10use anyhow::{Context as _, Result, anyhow};
  11use command_palette_hooks::CommandPaletteFilter;
  12use dap::StartDebuggingRequestArguments;
  13use dap::adapters::DebugAdapterName;
  14use dap::debugger_settings::DebugPanelDockPosition;
  15use dap::{
  16    ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
  17    client::SessionId, debugger_settings::DebuggerSettings,
  18};
  19use gpui::{
  20    Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
  21    FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
  22    actions, anchored, deferred,
  23};
  24
  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::any::TypeId;
  32use std::sync::Arc;
  33use task::{DebugScenario, TaskContext};
  34use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
  35use workspace::SplitDirection;
  36use workspace::{
  37    Pane, Workspace,
  38    dock::{DockPosition, Panel, PanelEvent},
  39};
  40
  41pub enum DebugPanelEvent {
  42    Exited(SessionId),
  43    Terminated(SessionId),
  44    Stopped {
  45        client_id: SessionId,
  46        event: StoppedEvent,
  47        go_to_stack_frame: bool,
  48    },
  49    Thread((SessionId, ThreadEvent)),
  50    Continued((SessionId, ContinuedEvent)),
  51    Output((SessionId, OutputEvent)),
  52    Module((SessionId, ModuleEvent)),
  53    LoadedSource((SessionId, LoadedSourceEvent)),
  54    ClientShutdown(SessionId),
  55    CapabilitiesChanged(SessionId),
  56}
  57
  58actions!(debug_panel, [ToggleFocus]);
  59
  60pub struct DebugPanel {
  61    size: Pixels,
  62    sessions: Vec<Entity<DebugSession>>,
  63    active_session: Option<Entity<DebugSession>>,
  64    project: Entity<Project>,
  65    workspace: WeakEntity<Workspace>,
  66    focus_handle: FocusHandle,
  67    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
  68    pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
  69    pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
  70    fs: Arc<dyn Fs>,
  71}
  72
  73impl DebugPanel {
  74    pub fn new(
  75        workspace: &Workspace,
  76        _window: &mut Window,
  77        cx: &mut Context<Workspace>,
  78    ) -> Entity<Self> {
  79        cx.new(|cx| {
  80            let project = workspace.project().clone();
  81            let focus_handle = cx.focus_handle();
  82            let thread_picker_menu_handle = PopoverMenuHandle::default();
  83            let session_picker_menu_handle = PopoverMenuHandle::default();
  84
  85            let debug_panel = Self {
  86                size: px(300.),
  87                sessions: vec![],
  88                active_session: None,
  89                focus_handle,
  90                project,
  91                workspace: workspace.weak_handle(),
  92                context_menu: None,
  93                fs: workspace.app_state().fs.clone(),
  94                thread_picker_menu_handle,
  95                session_picker_menu_handle,
  96            };
  97
  98            debug_panel
  99        })
 100    }
 101
 102    pub(crate) fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 103        let Some(session) = self.active_session.clone() else {
 104            return;
 105        };
 106        let Some(active_pane) = session
 107            .read(cx)
 108            .running_state()
 109            .read(cx)
 110            .active_pane()
 111            .cloned()
 112        else {
 113            return;
 114        };
 115        active_pane.update(cx, |pane, cx| {
 116            pane.focus_active_item(window, cx);
 117        });
 118    }
 119
 120    pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
 121        self.sessions.clone()
 122    }
 123
 124    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
 125        self.active_session.clone()
 126    }
 127
 128    pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
 129        self.active_session()
 130            .map(|session| session.read(cx).running_state().clone())
 131    }
 132
 133    pub(crate) fn filter_action_types(&self, cx: &mut App) {
 134        let (has_active_session, supports_restart, support_step_back, status) = self
 135            .active_session()
 136            .map(|item| {
 137                let running = item.read(cx).running_state().clone();
 138                let caps = running.read(cx).capabilities(cx);
 139                (
 140                    !running.read(cx).session().read(cx).is_terminated(),
 141                    caps.supports_restart_request.unwrap_or_default(),
 142                    caps.supports_step_back.unwrap_or_default(),
 143                    running.read(cx).thread_status(cx),
 144                )
 145            })
 146            .unwrap_or((false, false, false, None));
 147
 148        let filter = CommandPaletteFilter::global_mut(cx);
 149        let debugger_action_types = [
 150            TypeId::of::<Detach>(),
 151            TypeId::of::<Stop>(),
 152            TypeId::of::<ToggleIgnoreBreakpoints>(),
 153        ];
 154
 155        let running_action_types = [TypeId::of::<Pause>()];
 156
 157        let stopped_action_type = [
 158            TypeId::of::<Continue>(),
 159            TypeId::of::<StepOver>(),
 160            TypeId::of::<StepInto>(),
 161            TypeId::of::<StepOut>(),
 162            TypeId::of::<ShowStackTrace>(),
 163            TypeId::of::<editor::actions::DebuggerRunToCursor>(),
 164            TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
 165        ];
 166
 167        let step_back_action_type = [TypeId::of::<StepBack>()];
 168        let restart_action_type = [TypeId::of::<Restart>()];
 169
 170        if has_active_session {
 171            filter.show_action_types(debugger_action_types.iter());
 172
 173            if supports_restart {
 174                filter.show_action_types(restart_action_type.iter());
 175            } else {
 176                filter.hide_action_types(&restart_action_type);
 177            }
 178
 179            if support_step_back {
 180                filter.show_action_types(step_back_action_type.iter());
 181            } else {
 182                filter.hide_action_types(&step_back_action_type);
 183            }
 184
 185            match status {
 186                Some(ThreadStatus::Running) => {
 187                    filter.show_action_types(running_action_types.iter());
 188                    filter.hide_action_types(&stopped_action_type);
 189                }
 190                Some(ThreadStatus::Stopped) => {
 191                    filter.show_action_types(stopped_action_type.iter());
 192                    filter.hide_action_types(&running_action_types);
 193                }
 194                _ => {
 195                    filter.hide_action_types(&running_action_types);
 196                    filter.hide_action_types(&stopped_action_type);
 197                }
 198            }
 199        } else {
 200            // show only the `debug: start`
 201            filter.hide_action_types(&debugger_action_types);
 202            filter.hide_action_types(&step_back_action_type);
 203            filter.hide_action_types(&restart_action_type);
 204            filter.hide_action_types(&running_action_types);
 205            filter.hide_action_types(&stopped_action_type);
 206        }
 207    }
 208
 209    pub fn load(
 210        workspace: WeakEntity<Workspace>,
 211        cx: &mut AsyncWindowContext,
 212    ) -> Task<Result<Entity<Self>>> {
 213        cx.spawn(async move |cx| {
 214            workspace.update_in(cx, |workspace, window, cx| {
 215                let debug_panel = DebugPanel::new(workspace, window, cx);
 216
 217                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
 218                    workspace.project().read(cx).breakpoint_store().update(
 219                        cx,
 220                        |breakpoint_store, cx| {
 221                            breakpoint_store.clear_breakpoints(cx);
 222                        },
 223                    )
 224                });
 225
 226                cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
 227                    Self::filter_action_types(debug_panel, cx);
 228                })
 229                .detach();
 230
 231                cx.observe(&debug_panel, |_, debug_panel, cx| {
 232                    debug_panel.update(cx, |debug_panel, cx| {
 233                        Self::filter_action_types(debug_panel, cx);
 234                    });
 235                })
 236                .detach();
 237                workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
 238
 239                debug_panel
 240            })
 241        })
 242    }
 243
 244    pub fn start_session(
 245        &mut self,
 246        scenario: DebugScenario,
 247        task_context: TaskContext,
 248        active_buffer: Option<Entity<Buffer>>,
 249        worktree_id: Option<WorktreeId>,
 250        window: &mut Window,
 251        cx: &mut Context<Self>,
 252    ) {
 253        let dap_store = self.project.read(cx).dap_store();
 254        let session = dap_store.update(cx, |dap_store, cx| {
 255            dap_store.new_session(
 256                scenario.label.clone(),
 257                DebugAdapterName(scenario.adapter.clone()),
 258                None,
 259                cx,
 260            )
 261        });
 262        if let Some(inventory) = self
 263            .project
 264            .read(cx)
 265            .task_store()
 266            .read(cx)
 267            .task_inventory()
 268            .cloned()
 269        {
 270            inventory.update(cx, |inventory, _| {
 271                inventory.scenario_scheduled(scenario.clone());
 272            })
 273        }
 274        let task = cx.spawn_in(window, {
 275            let session = session.clone();
 276            async move |this, cx| {
 277                let debug_session =
 278                    Self::register_session(this.clone(), session.clone(), cx).await?;
 279                let definition = debug_session
 280                    .update_in(cx, |debug_session, window, cx| {
 281                        debug_session.running_state().update(cx, |running, cx| {
 282                            running.resolve_scenario(
 283                                scenario,
 284                                task_context,
 285                                active_buffer,
 286                                worktree_id,
 287                                window,
 288                                cx,
 289                            )
 290                        })
 291                    })?
 292                    .await?;
 293
 294                dap_store
 295                    .update(cx, |dap_store, cx| {
 296                        dap_store.boot_session(session.clone(), definition, cx)
 297                    })?
 298                    .await
 299            }
 300        });
 301
 302        cx.spawn(async move |_, cx| {
 303            if let Err(error) = task.await {
 304                session
 305                    .update(cx, |session, cx| {
 306                        session
 307                            .console_output(cx)
 308                            .unbounded_send(format!("error: {}", error))
 309                            .ok();
 310                        session.shutdown(cx)
 311                    })?
 312                    .await;
 313            }
 314            anyhow::Ok(())
 315        })
 316        .detach_and_log_err(cx);
 317    }
 318
 319    pub(crate) async fn register_session(
 320        this: WeakEntity<Self>,
 321        session: Entity<Session>,
 322        cx: &mut AsyncWindowContext,
 323    ) -> Result<Entity<DebugSession>> {
 324        let adapter_name = session.update(cx, |session, _| session.adapter())?;
 325        this.update_in(cx, |_, window, cx| {
 326            cx.subscribe_in(
 327                &session,
 328                window,
 329                move |this, session, event: &SessionStateEvent, window, cx| match event {
 330                    SessionStateEvent::Restart => {
 331                        this.handle_restart_request(session.clone(), window, cx);
 332                    }
 333                    SessionStateEvent::SpawnChildSession { request } => {
 334                        this.handle_start_debugging_request(request, session.clone(), window, cx);
 335                    }
 336                    _ => {}
 337                },
 338            )
 339            .detach();
 340        })
 341        .ok();
 342
 343        let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
 344
 345        let (debug_session, workspace) = this.update_in(cx, |this, window, cx| {
 346            this.sessions.retain(|session| {
 347                !session
 348                    .read(cx)
 349                    .running_state()
 350                    .read(cx)
 351                    .session()
 352                    .read(cx)
 353                    .is_terminated()
 354            });
 355
 356            let debug_session = DebugSession::running(
 357                this.project.clone(),
 358                this.workspace.clone(),
 359                session,
 360                cx.weak_entity(),
 361                serialized_layout,
 362                this.position(window, cx).axis(),
 363                window,
 364                cx,
 365            );
 366
 367            // We might want to make this an event subscription and only notify when a new thread is selected
 368            // This is used to filter the command menu correctly
 369            cx.observe(
 370                &debug_session.read(cx).running_state().clone(),
 371                |_, _, cx| cx.notify(),
 372            )
 373            .detach();
 374
 375            this.sessions.push(debug_session.clone());
 376            this.activate_session(debug_session.clone(), window, cx);
 377
 378            (debug_session, this.workspace.clone())
 379        })?;
 380
 381        workspace.update_in(cx, |workspace, window, cx| {
 382            workspace.focus_panel::<Self>(window, cx);
 383        })?;
 384
 385        Ok(debug_session)
 386    }
 387
 388    pub(crate) fn handle_restart_request(
 389        &mut self,
 390        mut curr_session: Entity<Session>,
 391        window: &mut Window,
 392        cx: &mut Context<Self>,
 393    ) {
 394        while let Some(parent_session) =
 395            curr_session.read_with(cx, |session, _| session.parent_session().cloned())
 396        {
 397            curr_session = parent_session;
 398        }
 399
 400        let Some(worktree) = curr_session.read(cx).worktree() else {
 401            log::error!("Attempted to start a child session from non local debug session");
 402            return;
 403        };
 404
 405        let dap_store_handle = self.project.read(cx).dap_store().clone();
 406        let label = curr_session.read(cx).label().clone();
 407        let adapter = curr_session.read(cx).adapter().clone();
 408        let binary = curr_session.read(cx).binary().clone();
 409        let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
 410
 411        cx.spawn_in(window, async move |this, cx| {
 412            task.await;
 413
 414            let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
 415                let session = dap_store.new_session(label, adapter, None, cx);
 416
 417                let task = session.update(cx, |session, cx| {
 418                    session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
 419                });
 420                (session, task)
 421            })?;
 422            Self::register_session(this, session, cx).await?;
 423            task.await
 424        })
 425        .detach_and_log_err(cx);
 426    }
 427
 428    pub fn handle_start_debugging_request(
 429        &mut self,
 430        request: &StartDebuggingRequestArguments,
 431        parent_session: Entity<Session>,
 432        window: &mut Window,
 433        cx: &mut Context<Self>,
 434    ) {
 435        let Some(worktree) = parent_session.read(cx).worktree() else {
 436            log::error!("Attempted to start a child session from non local debug session");
 437            return;
 438        };
 439
 440        let dap_store_handle = self.project.read(cx).dap_store().clone();
 441        let label = parent_session.read(cx).label().clone();
 442        let adapter = parent_session.read(cx).adapter().clone();
 443        let mut binary = parent_session.read(cx).binary().clone();
 444        binary.request_args = request.clone();
 445
 446        cx.spawn_in(window, async move |this, cx| {
 447            let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
 448                let session =
 449                    dap_store.new_session(label, adapter, Some(parent_session.clone()), cx);
 450
 451                let task = session.update(cx, |session, cx| {
 452                    session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
 453                });
 454                (session, task)
 455            })?;
 456            Self::register_session(this, session, cx).await?;
 457            task.await
 458        })
 459        .detach_and_log_err(cx);
 460    }
 461
 462    pub(crate) fn close_session(
 463        &mut self,
 464        entity_id: EntityId,
 465        window: &mut Window,
 466        cx: &mut Context<Self>,
 467    ) {
 468        let Some(session) = self
 469            .sessions
 470            .iter()
 471            .find(|other| entity_id == other.entity_id())
 472            .cloned()
 473        else {
 474            return;
 475        };
 476        session.update(cx, |this, cx| {
 477            this.running_state().update(cx, |this, cx| {
 478                this.serialize_layout(window, cx);
 479            });
 480        });
 481        let session_id = session.update(cx, |this, cx| this.session_id(cx));
 482        let should_prompt = self
 483            .project
 484            .update(cx, |this, cx| {
 485                let session = this.dap_store().read(cx).session_by_id(session_id);
 486                session.map(|session| !session.read(cx).is_terminated())
 487            })
 488            .unwrap_or_default();
 489
 490        cx.spawn_in(window, async move |this, cx| {
 491            if should_prompt {
 492                let response = cx.prompt(
 493                    gpui::PromptLevel::Warning,
 494                    "This Debug Session is still running. Are you sure you want to terminate it?",
 495                    None,
 496                    &["Yes", "No"],
 497                );
 498                if response.await == Ok(1) {
 499                    return;
 500                }
 501            }
 502            session.update(cx, |session, cx| session.shutdown(cx)).ok();
 503            this.update(cx, |this, cx| {
 504                this.sessions.retain(|other| entity_id != other.entity_id());
 505
 506                if let Some(active_session_id) = this
 507                    .active_session
 508                    .as_ref()
 509                    .map(|session| session.entity_id())
 510                {
 511                    if active_session_id == entity_id {
 512                        this.active_session = this.sessions.first().cloned();
 513                    }
 514                }
 515                cx.notify()
 516            })
 517            .ok();
 518        })
 519        .detach();
 520    }
 521
 522    pub(crate) fn deploy_context_menu(
 523        &mut self,
 524        position: Point<Pixels>,
 525        window: &mut Window,
 526        cx: &mut Context<Self>,
 527    ) {
 528        if let Some(running_state) = self
 529            .active_session
 530            .as_ref()
 531            .map(|session| session.read(cx).running_state().clone())
 532        {
 533            let pane_items_status = running_state.read(cx).pane_items_status(cx);
 534            let this = cx.weak_entity();
 535
 536            let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
 537                for (item_kind, is_visible) in pane_items_status.into_iter() {
 538                    menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
 539                        let this = this.clone();
 540                        move |window, cx| {
 541                            this.update(cx, |this, cx| {
 542                                if let Some(running_state) = this
 543                                    .active_session
 544                                    .as_ref()
 545                                    .map(|session| session.read(cx).running_state().clone())
 546                                {
 547                                    running_state.update(cx, |state, cx| {
 548                                        if is_visible {
 549                                            state.remove_pane_item(item_kind, window, cx);
 550                                        } else {
 551                                            state.add_pane_item(item_kind, position, window, cx);
 552                                        }
 553                                    })
 554                                }
 555                            })
 556                            .ok();
 557                        }
 558                    });
 559                }
 560
 561                menu
 562            });
 563
 564            window.focus(&context_menu.focus_handle(cx));
 565            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
 566                this.context_menu.take();
 567                cx.notify();
 568            });
 569            self.context_menu = Some((context_menu, position, subscription));
 570        }
 571    }
 572
 573    pub(crate) fn top_controls_strip(
 574        &mut self,
 575        window: &mut Window,
 576        cx: &mut Context<Self>,
 577    ) -> Option<Div> {
 578        let active_session = self.active_session.clone();
 579        let focus_handle = self.focus_handle.clone();
 580        let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
 581        let div = if is_side { v_flex() } else { h_flex() };
 582
 583        let new_session_button = || {
 584            IconButton::new("debug-new-session", IconName::Plus)
 585                .icon_size(IconSize::Small)
 586                .on_click({
 587                    move |_, window, cx| window.dispatch_action(crate::Start.boxed_clone(), cx)
 588                })
 589                .tooltip({
 590                    let focus_handle = focus_handle.clone();
 591                    move |window, cx| {
 592                        Tooltip::for_action_in(
 593                            "Start Debug Session",
 594                            &crate::Start,
 595                            &focus_handle,
 596                            window,
 597                            cx,
 598                        )
 599                    }
 600                })
 601        };
 602
 603        Some(
 604            div.border_b_1()
 605                .border_color(cx.theme().colors().border)
 606                .p_1()
 607                .justify_between()
 608                .w_full()
 609                .when(is_side, |this| this.gap_1())
 610                .child(
 611                    h_flex()
 612                        .child(
 613                            h_flex().gap_2().w_full().when_some(
 614                                active_session
 615                                    .as_ref()
 616                                    .map(|session| session.read(cx).running_state()),
 617                                |this, running_state| {
 618                                    let thread_status =
 619                                        running_state.read(cx).thread_status(cx).unwrap_or(
 620                                            project::debugger::session::ThreadStatus::Exited,
 621                                        );
 622                                    let capabilities = running_state.read(cx).capabilities(cx);
 623                                    this.map(|this| {
 624                                        if thread_status == ThreadStatus::Running {
 625                                            this.child(
 626                                                IconButton::new(
 627                                                    "debug-pause",
 628                                                    IconName::DebugPause,
 629                                                )
 630                                                .icon_size(IconSize::XSmall)
 631                                                .shape(ui::IconButtonShape::Square)
 632                                                .on_click(window.listener_for(
 633                                                    &running_state,
 634                                                    |this, _, _window, cx| {
 635                                                        this.pause_thread(cx);
 636                                                    },
 637                                                ))
 638                                                .tooltip({
 639                                                    let focus_handle = focus_handle.clone();
 640                                                    move |window, cx| {
 641                                                        Tooltip::for_action_in(
 642                                                            "Pause program",
 643                                                            &Pause,
 644                                                            &focus_handle,
 645                                                            window,
 646                                                            cx,
 647                                                        )
 648                                                    }
 649                                                }),
 650                                            )
 651                                        } else {
 652                                            this.child(
 653                                                IconButton::new(
 654                                                    "debug-continue",
 655                                                    IconName::DebugContinue,
 656                                                )
 657                                                .icon_size(IconSize::XSmall)
 658                                                .shape(ui::IconButtonShape::Square)
 659                                                .on_click(window.listener_for(
 660                                                    &running_state,
 661                                                    |this, _, _window, cx| this.continue_thread(cx),
 662                                                ))
 663                                                .disabled(thread_status != ThreadStatus::Stopped)
 664                                                .tooltip({
 665                                                    let focus_handle = focus_handle.clone();
 666                                                    move |window, cx| {
 667                                                        Tooltip::for_action_in(
 668                                                            "Continue program",
 669                                                            &Continue,
 670                                                            &focus_handle,
 671                                                            window,
 672                                                            cx,
 673                                                        )
 674                                                    }
 675                                                }),
 676                                            )
 677                                        }
 678                                    })
 679                                    .child(
 680                                        IconButton::new("debug-step-over", IconName::ArrowRight)
 681                                            .icon_size(IconSize::XSmall)
 682                                            .shape(ui::IconButtonShape::Square)
 683                                            .on_click(window.listener_for(
 684                                                &running_state,
 685                                                |this, _, _window, cx| {
 686                                                    this.step_over(cx);
 687                                                },
 688                                            ))
 689                                            .disabled(thread_status != ThreadStatus::Stopped)
 690                                            .tooltip({
 691                                                let focus_handle = focus_handle.clone();
 692                                                move |window, cx| {
 693                                                    Tooltip::for_action_in(
 694                                                        "Step over",
 695                                                        &StepOver,
 696                                                        &focus_handle,
 697                                                        window,
 698                                                        cx,
 699                                                    )
 700                                                }
 701                                            }),
 702                                    )
 703                                    .child(
 704                                        IconButton::new("debug-step-out", IconName::ArrowUpRight)
 705                                            .icon_size(IconSize::XSmall)
 706                                            .shape(ui::IconButtonShape::Square)
 707                                            .on_click(window.listener_for(
 708                                                &running_state,
 709                                                |this, _, _window, cx| {
 710                                                    this.step_out(cx);
 711                                                },
 712                                            ))
 713                                            .disabled(thread_status != ThreadStatus::Stopped)
 714                                            .tooltip({
 715                                                let focus_handle = focus_handle.clone();
 716                                                move |window, cx| {
 717                                                    Tooltip::for_action_in(
 718                                                        "Step out",
 719                                                        &StepOut,
 720                                                        &focus_handle,
 721                                                        window,
 722                                                        cx,
 723                                                    )
 724                                                }
 725                                            }),
 726                                    )
 727                                    .child(
 728                                        IconButton::new(
 729                                            "debug-step-into",
 730                                            IconName::ArrowDownRight,
 731                                        )
 732                                        .icon_size(IconSize::XSmall)
 733                                        .shape(ui::IconButtonShape::Square)
 734                                        .on_click(window.listener_for(
 735                                            &running_state,
 736                                            |this, _, _window, cx| {
 737                                                this.step_in(cx);
 738                                            },
 739                                        ))
 740                                        .disabled(thread_status != ThreadStatus::Stopped)
 741                                        .tooltip({
 742                                            let focus_handle = focus_handle.clone();
 743                                            move |window, cx| {
 744                                                Tooltip::for_action_in(
 745                                                    "Step in",
 746                                                    &StepInto,
 747                                                    &focus_handle,
 748                                                    window,
 749                                                    cx,
 750                                                )
 751                                            }
 752                                        }),
 753                                    )
 754                                    .child(Divider::vertical())
 755                                    .child(
 756                                        IconButton::new("debug-restart", IconName::DebugRestart)
 757                                            .icon_size(IconSize::XSmall)
 758                                            .on_click(window.listener_for(
 759                                                &running_state,
 760                                                |this, _, _window, cx| {
 761                                                    this.restart_session(cx);
 762                                                },
 763                                            ))
 764                                            .tooltip({
 765                                                let focus_handle = focus_handle.clone();
 766                                                move |window, cx| {
 767                                                    Tooltip::for_action_in(
 768                                                        "Restart",
 769                                                        &Restart,
 770                                                        &focus_handle,
 771                                                        window,
 772                                                        cx,
 773                                                    )
 774                                                }
 775                                            }),
 776                                    )
 777                                    .child(
 778                                        IconButton::new("debug-stop", IconName::Power)
 779                                            .icon_size(IconSize::XSmall)
 780                                            .on_click(window.listener_for(
 781                                                &running_state,
 782                                                |this, _, _window, cx| {
 783                                                    this.stop_thread(cx);
 784                                                },
 785                                            ))
 786                                            .disabled(
 787                                                thread_status != ThreadStatus::Stopped
 788                                                    && thread_status != ThreadStatus::Running,
 789                                            )
 790                                            .tooltip({
 791                                                let focus_handle = focus_handle.clone();
 792                                                let label = if capabilities
 793                                                    .supports_terminate_threads_request
 794                                                    .unwrap_or_default()
 795                                                {
 796                                                    "Terminate Thread"
 797                                                } else {
 798                                                    "Terminate All Threads"
 799                                                };
 800                                                move |window, cx| {
 801                                                    Tooltip::for_action_in(
 802                                                        label,
 803                                                        &Stop,
 804                                                        &focus_handle,
 805                                                        window,
 806                                                        cx,
 807                                                    )
 808                                                }
 809                                            }),
 810                                    )
 811                                    .child(
 812                                        IconButton::new("debug-disconnect", IconName::DebugDetach)
 813                                            .icon_size(IconSize::XSmall)
 814                                            .on_click(window.listener_for(
 815                                                &running_state,
 816                                                |this, _, _, cx| {
 817                                                    this.detach_client(cx);
 818                                                },
 819                                            ))
 820                                            .tooltip({
 821                                                let focus_handle = focus_handle.clone();
 822                                                move |window, cx| {
 823                                                    Tooltip::for_action_in(
 824                                                        "Detach",
 825                                                        &Detach,
 826                                                        &focus_handle,
 827                                                        window,
 828                                                        cx,
 829                                                    )
 830                                                }
 831                                            }),
 832                                    )
 833                                },
 834                            ),
 835                        )
 836                        .justify_around()
 837                        .when(is_side, |this| this.child(new_session_button())),
 838                )
 839                .child(
 840                    h_flex()
 841                        .gap_2()
 842                        .when(is_side, |this| this.justify_between())
 843                        .child(
 844                            h_flex().when_some(
 845                                active_session
 846                                    .as_ref()
 847                                    .map(|session| session.read(cx).running_state())
 848                                    .cloned(),
 849                                |this, running_state| {
 850                                    this.children({
 851                                        let running_state = running_state.clone();
 852                                        let threads =
 853                                            running_state.update(cx, |running_state, cx| {
 854                                                let session = running_state.session();
 855                                                session
 856                                                    .update(cx, |session, cx| session.threads(cx))
 857                                            });
 858
 859                                        self.render_thread_dropdown(
 860                                            &running_state,
 861                                            threads,
 862                                            window,
 863                                            cx,
 864                                        )
 865                                    })
 866                                    .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
 867                                },
 868                            ),
 869                        )
 870                        .child(
 871                            h_flex()
 872                                .children(self.render_session_menu(
 873                                    self.active_session(),
 874                                    self.running_state(cx),
 875                                    window,
 876                                    cx,
 877                                ))
 878                                .when(!is_side, |this| this.child(new_session_button())),
 879                        ),
 880                ),
 881        )
 882    }
 883
 884    pub(crate) fn activate_pane_in_direction(
 885        &mut self,
 886        direction: SplitDirection,
 887        window: &mut Window,
 888        cx: &mut Context<Self>,
 889    ) {
 890        if let Some(session) = self.active_session() {
 891            session.update(cx, |session, cx| {
 892                session.running_state().update(cx, |running, cx| {
 893                    running.activate_pane_in_direction(direction, window, cx);
 894                })
 895            });
 896        }
 897    }
 898
 899    pub(crate) fn activate_item(
 900        &mut self,
 901        item: DebuggerPaneItem,
 902        window: &mut Window,
 903        cx: &mut Context<Self>,
 904    ) {
 905        if let Some(session) = self.active_session() {
 906            session.update(cx, |session, cx| {
 907                session.running_state().update(cx, |running, cx| {
 908                    running.activate_item(item, window, cx);
 909                });
 910            });
 911        }
 912    }
 913
 914    pub(crate) fn activate_session(
 915        &mut self,
 916        session_item: Entity<DebugSession>,
 917        window: &mut Window,
 918        cx: &mut Context<Self>,
 919    ) {
 920        debug_assert!(self.sessions.contains(&session_item));
 921        session_item.focus_handle(cx).focus(window);
 922        session_item.update(cx, |this, cx| {
 923            this.running_state().update(cx, |this, cx| {
 924                this.go_to_selected_stack_frame(window, cx);
 925            });
 926        });
 927        self.active_session = Some(session_item.clone());
 928        cx.notify();
 929    }
 930
 931    pub(crate) fn save_scenario(
 932        &self,
 933        scenario: &DebugScenario,
 934        worktree_id: WorktreeId,
 935        window: &mut Window,
 936        cx: &mut App,
 937    ) -> Task<Result<ProjectPath>> {
 938        self.workspace
 939            .update(cx, |workspace, cx| {
 940                let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
 941                    return Task::ready(Err(anyhow!("Couldn't get worktree path")));
 942                };
 943
 944                let serialized_scenario = serde_json::to_value(scenario);
 945
 946                cx.spawn_in(window, async move |workspace, cx| {
 947                    let serialized_scenario = serialized_scenario?;
 948                    let fs =
 949                        workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
 950
 951                    path.push(paths::local_settings_folder_relative_path());
 952                    if !fs.is_dir(path.as_path()).await {
 953                        fs.create_dir(path.as_path()).await?;
 954                    }
 955                    path.pop();
 956
 957                    path.push(paths::local_debug_file_relative_path());
 958                    let path = path.as_path();
 959
 960                    if !fs.is_file(path).await {
 961                        let content =
 962                            serde_json::to_string_pretty(&serde_json::Value::Array(vec![
 963                                serialized_scenario,
 964                            ]))?;
 965
 966                        fs.create_file(path, Default::default()).await?;
 967                        fs.save(path, &content.into(), Default::default()).await?;
 968                    } else {
 969                        let content = fs.load(path).await?;
 970                        let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
 971                        values.push(serialized_scenario);
 972                        fs.save(
 973                            path,
 974                            &serde_json::to_string_pretty(&values).map(Into::into)?,
 975                            Default::default(),
 976                        )
 977                        .await?;
 978                    }
 979
 980                    workspace.update(cx, |workspace, cx| {
 981                        workspace
 982                            .project()
 983                            .read(cx)
 984                            .project_path_for_absolute_path(&path, cx)
 985                            .context(
 986                                "Couldn't get project path for .zed/debug.json in active worktree",
 987                            )
 988                    })?
 989                })
 990            })
 991            .unwrap_or_else(|err| Task::ready(Err(err)))
 992    }
 993
 994    pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 995        self.thread_picker_menu_handle.toggle(window, cx);
 996    }
 997
 998    pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 999        self.session_picker_menu_handle.toggle(window, cx);
1000    }
1001}
1002
1003impl EventEmitter<PanelEvent> for DebugPanel {}
1004impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1005
1006impl Focusable for DebugPanel {
1007    fn focus_handle(&self, _: &App) -> FocusHandle {
1008        self.focus_handle.clone()
1009    }
1010}
1011
1012impl Panel for DebugPanel {
1013    fn persistent_name() -> &'static str {
1014        "DebugPanel"
1015    }
1016
1017    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1018        match DebuggerSettings::get_global(cx).dock {
1019            DebugPanelDockPosition::Left => DockPosition::Left,
1020            DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1021            DebugPanelDockPosition::Right => DockPosition::Right,
1022        }
1023    }
1024
1025    fn position_is_valid(&self, _: DockPosition) -> bool {
1026        true
1027    }
1028
1029    fn set_position(
1030        &mut self,
1031        position: DockPosition,
1032        window: &mut Window,
1033        cx: &mut Context<Self>,
1034    ) {
1035        if position.axis() != self.position(window, cx).axis() {
1036            self.sessions.iter().for_each(|session_item| {
1037                session_item.update(cx, |item, cx| {
1038                    item.running_state()
1039                        .update(cx, |state, _| state.invert_axies())
1040                })
1041            });
1042        }
1043
1044        settings::update_settings_file::<DebuggerSettings>(
1045            self.fs.clone(),
1046            cx,
1047            move |settings, _| {
1048                let dock = match position {
1049                    DockPosition::Left => DebugPanelDockPosition::Left,
1050                    DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1051                    DockPosition::Right => DebugPanelDockPosition::Right,
1052                };
1053                settings.dock = dock;
1054            },
1055        );
1056    }
1057
1058    fn size(&self, _window: &Window, _: &App) -> Pixels {
1059        self.size
1060    }
1061
1062    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1063        self.size = size.unwrap_or(px(300.));
1064    }
1065
1066    fn remote_id() -> Option<proto::PanelId> {
1067        Some(proto::PanelId::DebugPanel)
1068    }
1069
1070    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1071        Some(IconName::Debug)
1072    }
1073
1074    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1075        if DebuggerSettings::get_global(cx).button {
1076            Some("Debug Panel")
1077        } else {
1078            None
1079        }
1080    }
1081
1082    fn toggle_action(&self) -> Box<dyn Action> {
1083        Box::new(ToggleFocus)
1084    }
1085
1086    fn pane(&self) -> Option<Entity<Pane>> {
1087        None
1088    }
1089
1090    fn activation_priority(&self) -> u32 {
1091        9
1092    }
1093
1094    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1095}
1096
1097impl Render for DebugPanel {
1098    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1099        let has_sessions = self.sessions.len() > 0;
1100        let this = cx.weak_entity();
1101        debug_assert_eq!(has_sessions, self.active_session.is_some());
1102
1103        if self
1104            .active_session
1105            .as_ref()
1106            .map(|session| session.read(cx).running_state())
1107            .map(|state| state.read(cx).has_open_context_menu(cx))
1108            .unwrap_or(false)
1109        {
1110            self.context_menu.take();
1111        }
1112
1113        v_flex()
1114            .size_full()
1115            .key_context("DebugPanel")
1116            .child(h_flex().children(self.top_controls_strip(window, cx)))
1117            .track_focus(&self.focus_handle(cx))
1118            .on_action({
1119                let this = this.clone();
1120                move |_: &workspace::ActivatePaneLeft, window, cx| {
1121                    this.update(cx, |this, cx| {
1122                        this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1123                    })
1124                    .ok();
1125                }
1126            })
1127            .on_action({
1128                let this = this.clone();
1129                move |_: &workspace::ActivatePaneRight, window, cx| {
1130                    this.update(cx, |this, cx| {
1131                        this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1132                    })
1133                    .ok();
1134                }
1135            })
1136            .on_action({
1137                let this = this.clone();
1138                move |_: &workspace::ActivatePaneUp, window, cx| {
1139                    this.update(cx, |this, cx| {
1140                        this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1141                    })
1142                    .ok();
1143                }
1144            })
1145            .on_action({
1146                let this = this.clone();
1147                move |_: &workspace::ActivatePaneDown, window, cx| {
1148                    this.update(cx, |this, cx| {
1149                        this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1150                    })
1151                    .ok();
1152                }
1153            })
1154            .on_action({
1155                let this = this.clone();
1156                move |_: &FocusConsole, window, cx| {
1157                    this.update(cx, |this, cx| {
1158                        this.activate_item(DebuggerPaneItem::Console, window, cx);
1159                    })
1160                    .ok();
1161                }
1162            })
1163            .on_action({
1164                let this = this.clone();
1165                move |_: &FocusVariables, window, cx| {
1166                    this.update(cx, |this, cx| {
1167                        this.activate_item(DebuggerPaneItem::Variables, window, cx);
1168                    })
1169                    .ok();
1170                }
1171            })
1172            .on_action({
1173                let this = this.clone();
1174                move |_: &FocusBreakpointList, window, cx| {
1175                    this.update(cx, |this, cx| {
1176                        this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1177                    })
1178                    .ok();
1179                }
1180            })
1181            .on_action({
1182                let this = this.clone();
1183                move |_: &FocusFrames, window, cx| {
1184                    this.update(cx, |this, cx| {
1185                        this.activate_item(DebuggerPaneItem::Frames, window, cx);
1186                    })
1187                    .ok();
1188                }
1189            })
1190            .on_action({
1191                let this = this.clone();
1192                move |_: &FocusModules, window, cx| {
1193                    this.update(cx, |this, cx| {
1194                        this.activate_item(DebuggerPaneItem::Modules, window, cx);
1195                    })
1196                    .ok();
1197                }
1198            })
1199            .on_action({
1200                let this = this.clone();
1201                move |_: &FocusLoadedSources, window, cx| {
1202                    this.update(cx, |this, cx| {
1203                        this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1204                    })
1205                    .ok();
1206                }
1207            })
1208            .on_action({
1209                let this = this.clone();
1210                move |_: &FocusTerminal, window, cx| {
1211                    this.update(cx, |this, cx| {
1212                        this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1213                    })
1214                    .ok();
1215                }
1216            })
1217            .on_action({
1218                let this = this.clone();
1219                move |_: &ToggleThreadPicker, window, cx| {
1220                    this.update(cx, |this, cx| {
1221                        this.toggle_thread_picker(window, cx);
1222                    })
1223                    .ok();
1224                }
1225            })
1226            .on_action({
1227                let this = this.clone();
1228                move |_: &ToggleSessionPicker, window, cx| {
1229                    this.update(cx, |this, cx| {
1230                        this.toggle_session_picker(window, cx);
1231                    })
1232                    .ok();
1233                }
1234            })
1235            .when(self.active_session.is_some(), |this| {
1236                this.on_mouse_down(
1237                    MouseButton::Right,
1238                    cx.listener(|this, event: &MouseDownEvent, window, cx| {
1239                        if this
1240                            .active_session
1241                            .as_ref()
1242                            .map(|session| {
1243                                let state = session.read(cx).running_state();
1244                                state.read(cx).has_pane_at_position(event.position)
1245                            })
1246                            .unwrap_or(false)
1247                        {
1248                            this.deploy_context_menu(event.position, window, cx);
1249                        }
1250                    }),
1251                )
1252                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1253                    deferred(
1254                        anchored()
1255                            .position(*position)
1256                            .anchor(gpui::Corner::TopLeft)
1257                            .child(menu.clone()),
1258                    )
1259                    .with_priority(1)
1260                }))
1261            })
1262            .map(|this| {
1263                if has_sessions {
1264                    this.children(self.active_session.clone())
1265                } else {
1266                    this.child(
1267                        v_flex()
1268                            .h_full()
1269                            .gap_1()
1270                            .items_center()
1271                            .justify_center()
1272                            .child(
1273                                h_flex().child(
1274                                    Label::new("No Debugging Sessions")
1275                                        .size(LabelSize::Small)
1276                                        .color(Color::Muted),
1277                                ),
1278                            )
1279                            .child(
1280                                h_flex().flex_shrink().child(
1281                                    Button::new("spawn-new-session-empty-state", "New Session")
1282                                        .size(ButtonSize::Large)
1283                                        .on_click(|_, window, cx| {
1284                                            window.dispatch_action(crate::Start.boxed_clone(), cx);
1285                                        }),
1286                                ),
1287                            ),
1288                    )
1289                }
1290            })
1291            .into_any()
1292    }
1293}
1294
1295struct DebuggerProvider(Entity<DebugPanel>);
1296
1297impl workspace::DebuggerProvider for DebuggerProvider {
1298    fn start_session(
1299        &self,
1300        definition: DebugScenario,
1301        context: TaskContext,
1302        buffer: Option<Entity<Buffer>>,
1303        window: &mut Window,
1304        cx: &mut App,
1305    ) {
1306        self.0.update(cx, |_, cx| {
1307            cx.defer_in(window, |this, window, cx| {
1308                this.start_session(definition, context, buffer, None, window, cx);
1309            })
1310        })
1311    }
1312}