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