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