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