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(
 757                                            "debug-enable-breakpoint",
 758                                            IconName::DebugDisabledBreakpoint,
 759                                        )
 760                                        .icon_size(IconSize::XSmall)
 761                                        .shape(ui::IconButtonShape::Square)
 762                                        .disabled(thread_status != ThreadStatus::Stopped),
 763                                    )
 764                                    .child(
 765                                        IconButton::new(
 766                                            "debug-disable-breakpoint",
 767                                            IconName::CircleOff,
 768                                        )
 769                                        .icon_size(IconSize::XSmall)
 770                                        .shape(ui::IconButtonShape::Square)
 771                                        .disabled(thread_status != ThreadStatus::Stopped),
 772                                    )
 773                                    .child(
 774                                        IconButton::new(
 775                                            "debug-disable-all-breakpoints",
 776                                            IconName::BugOff,
 777                                        )
 778                                        .icon_size(IconSize::XSmall)
 779                                        .shape(ui::IconButtonShape::Square)
 780                                        .disabled(
 781                                            thread_status == ThreadStatus::Exited
 782                                                || thread_status == ThreadStatus::Ended,
 783                                        )
 784                                        .on_click(window.listener_for(
 785                                            &running_state,
 786                                            |this, _, _window, cx| {
 787                                                this.toggle_ignore_breakpoints(cx);
 788                                            },
 789                                        ))
 790                                        .tooltip({
 791                                            let focus_handle = focus_handle.clone();
 792                                            move |window, cx| {
 793                                                Tooltip::for_action_in(
 794                                                    "Disable all breakpoints",
 795                                                    &ToggleIgnoreBreakpoints,
 796                                                    &focus_handle,
 797                                                    window,
 798                                                    cx,
 799                                                )
 800                                            }
 801                                        }),
 802                                    )
 803                                    .child(Divider::vertical())
 804                                    .child(
 805                                        IconButton::new("debug-restart", IconName::DebugRestart)
 806                                            .icon_size(IconSize::XSmall)
 807                                            .on_click(window.listener_for(
 808                                                &running_state,
 809                                                |this, _, _window, cx| {
 810                                                    this.restart_session(cx);
 811                                                },
 812                                            ))
 813                                            .tooltip({
 814                                                let focus_handle = focus_handle.clone();
 815                                                move |window, cx| {
 816                                                    Tooltip::for_action_in(
 817                                                        "Restart",
 818                                                        &Restart,
 819                                                        &focus_handle,
 820                                                        window,
 821                                                        cx,
 822                                                    )
 823                                                }
 824                                            }),
 825                                    )
 826                                    .child(
 827                                        IconButton::new("debug-stop", IconName::Power)
 828                                            .icon_size(IconSize::XSmall)
 829                                            .on_click(window.listener_for(
 830                                                &running_state,
 831                                                |this, _, _window, cx| {
 832                                                    this.stop_thread(cx);
 833                                                },
 834                                            ))
 835                                            .disabled(
 836                                                thread_status != ThreadStatus::Stopped
 837                                                    && thread_status != ThreadStatus::Running,
 838                                            )
 839                                            .tooltip({
 840                                                let focus_handle = focus_handle.clone();
 841                                                let label = if capabilities
 842                                                    .supports_terminate_threads_request
 843                                                    .unwrap_or_default()
 844                                                {
 845                                                    "Terminate Thread"
 846                                                } else {
 847                                                    "Terminate All Threads"
 848                                                };
 849                                                move |window, cx| {
 850                                                    Tooltip::for_action_in(
 851                                                        label,
 852                                                        &Stop,
 853                                                        &focus_handle,
 854                                                        window,
 855                                                        cx,
 856                                                    )
 857                                                }
 858                                            }),
 859                                    )
 860                                    .child(
 861                                        IconButton::new("debug-disconnect", IconName::DebugDetach)
 862                                            .icon_size(IconSize::XSmall)
 863                                            .on_click(window.listener_for(
 864                                                &running_state,
 865                                                |this, _, _, cx| {
 866                                                    this.detach_client(cx);
 867                                                },
 868                                            ))
 869                                            .tooltip({
 870                                                let focus_handle = focus_handle.clone();
 871                                                move |window, cx| {
 872                                                    Tooltip::for_action_in(
 873                                                        "Detach",
 874                                                        &Detach,
 875                                                        &focus_handle,
 876                                                        window,
 877                                                        cx,
 878                                                    )
 879                                                }
 880                                            }),
 881                                    )
 882                                },
 883                            ),
 884                        )
 885                        .justify_around()
 886                        .when(is_side, |this| this.child(new_session_button())),
 887                )
 888                .child(
 889                    h_flex()
 890                        .gap_2()
 891                        .when(is_side, |this| this.justify_between())
 892                        .child(
 893                            h_flex().when_some(
 894                                active_session
 895                                    .as_ref()
 896                                    .map(|session| session.read(cx).running_state())
 897                                    .cloned(),
 898                                |this, running_state| {
 899                                    this.children({
 900                                        let running_state = running_state.clone();
 901                                        let threads =
 902                                            running_state.update(cx, |running_state, cx| {
 903                                                let session = running_state.session();
 904                                                session
 905                                                    .update(cx, |session, cx| session.threads(cx))
 906                                            });
 907
 908                                        self.render_thread_dropdown(
 909                                            &running_state,
 910                                            threads,
 911                                            window,
 912                                            cx,
 913                                        )
 914                                    })
 915                                    .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
 916                                },
 917                            ),
 918                        )
 919                        .child(
 920                            h_flex()
 921                                .children(self.render_session_menu(
 922                                    self.active_session(),
 923                                    self.running_state(cx),
 924                                    window,
 925                                    cx,
 926                                ))
 927                                .when(!is_side, |this| this.child(new_session_button())),
 928                        ),
 929                ),
 930        )
 931    }
 932
 933    pub(crate) fn activate_pane_in_direction(
 934        &mut self,
 935        direction: SplitDirection,
 936        window: &mut Window,
 937        cx: &mut Context<Self>,
 938    ) {
 939        if let Some(session) = self.active_session() {
 940            session.update(cx, |session, cx| {
 941                session.running_state().update(cx, |running, cx| {
 942                    running.activate_pane_in_direction(direction, window, cx);
 943                })
 944            });
 945        }
 946    }
 947
 948    pub(crate) fn activate_item(
 949        &mut self,
 950        item: DebuggerPaneItem,
 951        window: &mut Window,
 952        cx: &mut Context<Self>,
 953    ) {
 954        if let Some(session) = self.active_session() {
 955            session.update(cx, |session, cx| {
 956                session.running_state().update(cx, |running, cx| {
 957                    running.activate_item(item, window, cx);
 958                });
 959            });
 960        }
 961    }
 962
 963    pub(crate) fn activate_session(
 964        &mut self,
 965        session_item: Entity<DebugSession>,
 966        window: &mut Window,
 967        cx: &mut Context<Self>,
 968    ) {
 969        debug_assert!(self.sessions.contains(&session_item));
 970        session_item.focus_handle(cx).focus(window);
 971        session_item.update(cx, |this, cx| {
 972            this.running_state().update(cx, |this, cx| {
 973                this.go_to_selected_stack_frame(window, cx);
 974            });
 975        });
 976        self.active_session = Some(session_item.clone());
 977        cx.notify();
 978    }
 979
 980    pub(crate) fn save_scenario(
 981        &self,
 982        scenario: &DebugScenario,
 983        worktree_id: WorktreeId,
 984        window: &mut Window,
 985        cx: &mut App,
 986    ) -> Task<Result<ProjectPath>> {
 987        self.workspace
 988            .update(cx, |workspace, cx| {
 989                let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
 990                    return Task::ready(Err(anyhow!("Couldn't get worktree path")));
 991                };
 992
 993                let serialized_scenario = serde_json::to_value(scenario);
 994
 995                cx.spawn_in(window, async move |workspace, cx| {
 996                    let serialized_scenario = serialized_scenario?;
 997                    let fs =
 998                        workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
 999
1000                    path.push(paths::local_settings_folder_relative_path());
1001                    if !fs.is_dir(path.as_path()).await {
1002                        fs.create_dir(path.as_path()).await?;
1003                    }
1004                    path.pop();
1005
1006                    path.push(paths::local_debug_file_relative_path());
1007                    let path = path.as_path();
1008
1009                    if !fs.is_file(path).await {
1010                        let content =
1011                            serde_json::to_string_pretty(&serde_json::Value::Array(vec![
1012                                serialized_scenario,
1013                            ]))?;
1014
1015                        fs.create_file(path, Default::default()).await?;
1016                        fs.save(path, &content.into(), Default::default()).await?;
1017                    } else {
1018                        let content = fs.load(path).await?;
1019                        let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
1020                        values.push(serialized_scenario);
1021                        fs.save(
1022                            path,
1023                            &serde_json::to_string_pretty(&values).map(Into::into)?,
1024                            Default::default(),
1025                        )
1026                        .await?;
1027                    }
1028
1029                    workspace.update(cx, |workspace, cx| {
1030                        workspace
1031                            .project()
1032                            .read(cx)
1033                            .project_path_for_absolute_path(&path, cx)
1034                            .context(
1035                                "Couldn't get project path for .zed/debug.json in active worktree",
1036                            )
1037                    })?
1038                })
1039            })
1040            .unwrap_or_else(|err| Task::ready(Err(err)))
1041    }
1042
1043    pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1044        self.thread_picker_menu_handle.toggle(window, cx);
1045    }
1046
1047    pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1048        self.session_picker_menu_handle.toggle(window, cx);
1049    }
1050}
1051
1052impl EventEmitter<PanelEvent> for DebugPanel {}
1053impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1054
1055impl Focusable for DebugPanel {
1056    fn focus_handle(&self, _: &App) -> FocusHandle {
1057        self.focus_handle.clone()
1058    }
1059}
1060
1061impl Panel for DebugPanel {
1062    fn persistent_name() -> &'static str {
1063        "DebugPanel"
1064    }
1065
1066    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1067        match DebuggerSettings::get_global(cx).dock {
1068            DebugPanelDockPosition::Left => DockPosition::Left,
1069            DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1070            DebugPanelDockPosition::Right => DockPosition::Right,
1071        }
1072    }
1073
1074    fn position_is_valid(&self, _: DockPosition) -> bool {
1075        true
1076    }
1077
1078    fn set_position(
1079        &mut self,
1080        position: DockPosition,
1081        window: &mut Window,
1082        cx: &mut Context<Self>,
1083    ) {
1084        if position.axis() != self.position(window, cx).axis() {
1085            self.sessions.iter().for_each(|session_item| {
1086                session_item.update(cx, |item, cx| {
1087                    item.running_state()
1088                        .update(cx, |state, _| state.invert_axies())
1089                })
1090            });
1091        }
1092
1093        settings::update_settings_file::<DebuggerSettings>(
1094            self.fs.clone(),
1095            cx,
1096            move |settings, _| {
1097                let dock = match position {
1098                    DockPosition::Left => DebugPanelDockPosition::Left,
1099                    DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1100                    DockPosition::Right => DebugPanelDockPosition::Right,
1101                };
1102                settings.dock = dock;
1103            },
1104        );
1105    }
1106
1107    fn size(&self, _window: &Window, _: &App) -> Pixels {
1108        self.size
1109    }
1110
1111    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1112        self.size = size.unwrap_or(px(300.));
1113    }
1114
1115    fn remote_id() -> Option<proto::PanelId> {
1116        Some(proto::PanelId::DebugPanel)
1117    }
1118
1119    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1120        Some(IconName::Debug)
1121    }
1122
1123    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1124        if DebuggerSettings::get_global(cx).button {
1125            Some("Debug Panel")
1126        } else {
1127            None
1128        }
1129    }
1130
1131    fn toggle_action(&self) -> Box<dyn Action> {
1132        Box::new(ToggleFocus)
1133    }
1134
1135    fn pane(&self) -> Option<Entity<Pane>> {
1136        None
1137    }
1138
1139    fn activation_priority(&self) -> u32 {
1140        9
1141    }
1142
1143    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1144}
1145
1146impl Render for DebugPanel {
1147    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1148        let has_sessions = self.sessions.len() > 0;
1149        let this = cx.weak_entity();
1150        debug_assert_eq!(has_sessions, self.active_session.is_some());
1151
1152        if self
1153            .active_session
1154            .as_ref()
1155            .map(|session| session.read(cx).running_state())
1156            .map(|state| state.read(cx).has_open_context_menu(cx))
1157            .unwrap_or(false)
1158        {
1159            self.context_menu.take();
1160        }
1161
1162        v_flex()
1163            .size_full()
1164            .key_context("DebugPanel")
1165            .child(h_flex().children(self.top_controls_strip(window, cx)))
1166            .track_focus(&self.focus_handle(cx))
1167            .on_action({
1168                let this = this.clone();
1169                move |_: &workspace::ActivatePaneLeft, window, cx| {
1170                    this.update(cx, |this, cx| {
1171                        this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1172                    })
1173                    .ok();
1174                }
1175            })
1176            .on_action({
1177                let this = this.clone();
1178                move |_: &workspace::ActivatePaneRight, window, cx| {
1179                    this.update(cx, |this, cx| {
1180                        this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1181                    })
1182                    .ok();
1183                }
1184            })
1185            .on_action({
1186                let this = this.clone();
1187                move |_: &workspace::ActivatePaneUp, window, cx| {
1188                    this.update(cx, |this, cx| {
1189                        this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1190                    })
1191                    .ok();
1192                }
1193            })
1194            .on_action({
1195                let this = this.clone();
1196                move |_: &workspace::ActivatePaneDown, window, cx| {
1197                    this.update(cx, |this, cx| {
1198                        this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1199                    })
1200                    .ok();
1201                }
1202            })
1203            .on_action({
1204                let this = this.clone();
1205                move |_: &FocusConsole, window, cx| {
1206                    this.update(cx, |this, cx| {
1207                        this.activate_item(DebuggerPaneItem::Console, window, cx);
1208                    })
1209                    .ok();
1210                }
1211            })
1212            .on_action({
1213                let this = this.clone();
1214                move |_: &FocusVariables, window, cx| {
1215                    this.update(cx, |this, cx| {
1216                        this.activate_item(DebuggerPaneItem::Variables, window, cx);
1217                    })
1218                    .ok();
1219                }
1220            })
1221            .on_action({
1222                let this = this.clone();
1223                move |_: &FocusBreakpointList, window, cx| {
1224                    this.update(cx, |this, cx| {
1225                        this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1226                    })
1227                    .ok();
1228                }
1229            })
1230            .on_action({
1231                let this = this.clone();
1232                move |_: &FocusFrames, window, cx| {
1233                    this.update(cx, |this, cx| {
1234                        this.activate_item(DebuggerPaneItem::Frames, window, cx);
1235                    })
1236                    .ok();
1237                }
1238            })
1239            .on_action({
1240                let this = this.clone();
1241                move |_: &FocusModules, window, cx| {
1242                    this.update(cx, |this, cx| {
1243                        this.activate_item(DebuggerPaneItem::Modules, window, cx);
1244                    })
1245                    .ok();
1246                }
1247            })
1248            .on_action({
1249                let this = this.clone();
1250                move |_: &FocusLoadedSources, window, cx| {
1251                    this.update(cx, |this, cx| {
1252                        this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1253                    })
1254                    .ok();
1255                }
1256            })
1257            .on_action({
1258                let this = this.clone();
1259                move |_: &FocusTerminal, window, cx| {
1260                    this.update(cx, |this, cx| {
1261                        this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1262                    })
1263                    .ok();
1264                }
1265            })
1266            .on_action({
1267                let this = this.clone();
1268                move |_: &ToggleThreadPicker, window, cx| {
1269                    this.update(cx, |this, cx| {
1270                        this.toggle_thread_picker(window, cx);
1271                    })
1272                    .ok();
1273                }
1274            })
1275            .on_action({
1276                let this = this.clone();
1277                move |_: &ToggleSessionPicker, window, cx| {
1278                    this.update(cx, |this, cx| {
1279                        this.toggle_session_picker(window, cx);
1280                    })
1281                    .ok();
1282                }
1283            })
1284            .when(self.active_session.is_some(), |this| {
1285                this.on_mouse_down(
1286                    MouseButton::Right,
1287                    cx.listener(|this, event: &MouseDownEvent, window, cx| {
1288                        if this
1289                            .active_session
1290                            .as_ref()
1291                            .map(|session| {
1292                                let state = session.read(cx).running_state();
1293                                state.read(cx).has_pane_at_position(event.position)
1294                            })
1295                            .unwrap_or(false)
1296                        {
1297                            this.deploy_context_menu(event.position, window, cx);
1298                        }
1299                    }),
1300                )
1301                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1302                    deferred(
1303                        anchored()
1304                            .position(*position)
1305                            .anchor(gpui::Corner::TopLeft)
1306                            .child(menu.clone()),
1307                    )
1308                    .with_priority(1)
1309                }))
1310            })
1311            .map(|this| {
1312                if has_sessions {
1313                    this.children(self.active_session.clone())
1314                } else {
1315                    this.child(
1316                        v_flex()
1317                            .h_full()
1318                            .gap_1()
1319                            .items_center()
1320                            .justify_center()
1321                            .child(
1322                                h_flex().child(
1323                                    Label::new("No Debugging Sessions")
1324                                        .size(LabelSize::Small)
1325                                        .color(Color::Muted),
1326                                ),
1327                            )
1328                            .child(
1329                                h_flex().flex_shrink().child(
1330                                    Button::new("spawn-new-session-empty-state", "New Session")
1331                                        .size(ButtonSize::Large)
1332                                        .on_click(|_, window, cx| {
1333                                            window.dispatch_action(crate::Start.boxed_clone(), cx);
1334                                        }),
1335                                ),
1336                            ),
1337                    )
1338                }
1339            })
1340            .into_any()
1341    }
1342}
1343
1344struct DebuggerProvider(Entity<DebugPanel>);
1345
1346impl workspace::DebuggerProvider for DebuggerProvider {
1347    fn start_session(
1348        &self,
1349        definition: DebugScenario,
1350        context: TaskContext,
1351        buffer: Option<Entity<Buffer>>,
1352        window: &mut Window,
1353        cx: &mut App,
1354    ) {
1355        self.0.update(cx, |_, cx| {
1356            cx.defer_in(window, |this, window, cx| {
1357                this.start_session(definition, context, buffer, None, window, cx);
1358            })
1359        })
1360    }
1361}