debugger_panel.rs

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