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