debugger_panel.rs

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