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