debugger_panel.rs

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