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