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                                    let supports_detach =
 546                                        running_state.read(cx).session().read(cx).is_attached();
 547                                    this.map(|this| {
 548                                        if thread_status == ThreadStatus::Running {
 549                                            this.child(
 550                                                IconButton::new(
 551                                                    "debug-pause",
 552                                                    IconName::DebugPause,
 553                                                )
 554                                                .icon_size(IconSize::XSmall)
 555                                                .shape(ui::IconButtonShape::Square)
 556                                                .on_click(window.listener_for(
 557                                                    &running_state,
 558                                                    |this, _, _window, cx| {
 559                                                        this.pause_thread(cx);
 560                                                    },
 561                                                ))
 562                                                .tooltip({
 563                                                    let focus_handle = focus_handle.clone();
 564                                                    move |window, cx| {
 565                                                        Tooltip::for_action_in(
 566                                                            "Pause program",
 567                                                            &Pause,
 568                                                            &focus_handle,
 569                                                            window,
 570                                                            cx,
 571                                                        )
 572                                                    }
 573                                                }),
 574                                            )
 575                                        } else {
 576                                            this.child(
 577                                                IconButton::new(
 578                                                    "debug-continue",
 579                                                    IconName::DebugContinue,
 580                                                )
 581                                                .icon_size(IconSize::XSmall)
 582                                                .shape(ui::IconButtonShape::Square)
 583                                                .on_click(window.listener_for(
 584                                                    &running_state,
 585                                                    |this, _, _window, cx| this.continue_thread(cx),
 586                                                ))
 587                                                .disabled(thread_status != ThreadStatus::Stopped)
 588                                                .tooltip({
 589                                                    let focus_handle = focus_handle.clone();
 590                                                    move |window, cx| {
 591                                                        Tooltip::for_action_in(
 592                                                            "Continue program",
 593                                                            &Continue,
 594                                                            &focus_handle,
 595                                                            window,
 596                                                            cx,
 597                                                        )
 598                                                    }
 599                                                }),
 600                                            )
 601                                        }
 602                                    })
 603                                    .child(
 604                                        IconButton::new("debug-step-over", IconName::ArrowRight)
 605                                            .icon_size(IconSize::XSmall)
 606                                            .shape(ui::IconButtonShape::Square)
 607                                            .on_click(window.listener_for(
 608                                                &running_state,
 609                                                |this, _, _window, cx| {
 610                                                    this.step_over(cx);
 611                                                },
 612                                            ))
 613                                            .disabled(thread_status != ThreadStatus::Stopped)
 614                                            .tooltip({
 615                                                let focus_handle = focus_handle.clone();
 616                                                move |window, cx| {
 617                                                    Tooltip::for_action_in(
 618                                                        "Step over",
 619                                                        &StepOver,
 620                                                        &focus_handle,
 621                                                        window,
 622                                                        cx,
 623                                                    )
 624                                                }
 625                                            }),
 626                                    )
 627                                    .child(
 628                                        IconButton::new("debug-step-out", IconName::ArrowUpRight)
 629                                            .icon_size(IconSize::XSmall)
 630                                            .shape(ui::IconButtonShape::Square)
 631                                            .on_click(window.listener_for(
 632                                                &running_state,
 633                                                |this, _, _window, cx| {
 634                                                    this.step_out(cx);
 635                                                },
 636                                            ))
 637                                            .disabled(thread_status != ThreadStatus::Stopped)
 638                                            .tooltip({
 639                                                let focus_handle = focus_handle.clone();
 640                                                move |window, cx| {
 641                                                    Tooltip::for_action_in(
 642                                                        "Step out",
 643                                                        &StepOut,
 644                                                        &focus_handle,
 645                                                        window,
 646                                                        cx,
 647                                                    )
 648                                                }
 649                                            }),
 650                                    )
 651                                    .child(
 652                                        IconButton::new(
 653                                            "debug-step-into",
 654                                            IconName::ArrowDownRight,
 655                                        )
 656                                        .icon_size(IconSize::XSmall)
 657                                        .shape(ui::IconButtonShape::Square)
 658                                        .on_click(window.listener_for(
 659                                            &running_state,
 660                                            |this, _, _window, cx| {
 661                                                this.step_in(cx);
 662                                            },
 663                                        ))
 664                                        .disabled(thread_status != ThreadStatus::Stopped)
 665                                        .tooltip({
 666                                            let focus_handle = focus_handle.clone();
 667                                            move |window, cx| {
 668                                                Tooltip::for_action_in(
 669                                                    "Step in",
 670                                                    &StepInto,
 671                                                    &focus_handle,
 672                                                    window,
 673                                                    cx,
 674                                                )
 675                                            }
 676                                        }),
 677                                    )
 678                                    .child(Divider::vertical())
 679                                    .child(
 680                                        IconButton::new("debug-restart", IconName::DebugRestart)
 681                                            .icon_size(IconSize::XSmall)
 682                                            .on_click(window.listener_for(
 683                                                &running_state,
 684                                                |this, _, _window, cx| {
 685                                                    this.restart_session(cx);
 686                                                },
 687                                            ))
 688                                            .tooltip({
 689                                                let focus_handle = focus_handle.clone();
 690                                                move |window, cx| {
 691                                                    Tooltip::for_action_in(
 692                                                        "Restart",
 693                                                        &Restart,
 694                                                        &focus_handle,
 695                                                        window,
 696                                                        cx,
 697                                                    )
 698                                                }
 699                                            }),
 700                                    )
 701                                    .child(
 702                                        IconButton::new("debug-stop", IconName::Power)
 703                                            .icon_size(IconSize::XSmall)
 704                                            .on_click(window.listener_for(
 705                                                &running_state,
 706                                                |this, _, _window, cx| {
 707                                                    this.stop_thread(cx);
 708                                                },
 709                                            ))
 710                                            .disabled(
 711                                                thread_status != ThreadStatus::Stopped
 712                                                    && thread_status != ThreadStatus::Running,
 713                                            )
 714                                            .tooltip({
 715                                                let focus_handle = focus_handle.clone();
 716                                                let label = if capabilities
 717                                                    .supports_terminate_threads_request
 718                                                    .unwrap_or_default()
 719                                                {
 720                                                    "Terminate Thread"
 721                                                } else {
 722                                                    "Terminate All Threads"
 723                                                };
 724                                                move |window, cx| {
 725                                                    Tooltip::for_action_in(
 726                                                        label,
 727                                                        &Stop,
 728                                                        &focus_handle,
 729                                                        window,
 730                                                        cx,
 731                                                    )
 732                                                }
 733                                            }),
 734                                    )
 735                                    .when(
 736                                        supports_detach,
 737                                        |div| {
 738                                            div.child(
 739                                                IconButton::new(
 740                                                    "debug-disconnect",
 741                                                    IconName::DebugDetach,
 742                                                )
 743                                                .disabled(
 744                                                    thread_status != ThreadStatus::Stopped
 745                                                        && thread_status != ThreadStatus::Running,
 746                                                )
 747                                                .icon_size(IconSize::XSmall)
 748                                                .on_click(window.listener_for(
 749                                                    &running_state,
 750                                                    |this, _, _, cx| {
 751                                                        this.detach_client(cx);
 752                                                    },
 753                                                ))
 754                                                .tooltip({
 755                                                    let focus_handle = focus_handle.clone();
 756                                                    move |window, cx| {
 757                                                        Tooltip::for_action_in(
 758                                                            "Detach",
 759                                                            &Detach,
 760                                                            &focus_handle,
 761                                                            window,
 762                                                            cx,
 763                                                        )
 764                                                    }
 765                                                }),
 766                                            )
 767                                        },
 768                                    )
 769                                },
 770                            ),
 771                        )
 772                        .justify_around()
 773                        .when(is_side, |this| {
 774                            this.child(new_session_button())
 775                                .child(documentation_button())
 776                        }),
 777                )
 778                .child(
 779                    h_flex()
 780                        .gap_2()
 781                        .when(is_side, |this| this.justify_between())
 782                        .child(
 783                            h_flex().when_some(
 784                                active_session
 785                                    .as_ref()
 786                                    .map(|session| session.read(cx).running_state())
 787                                    .cloned(),
 788                                |this, running_state| {
 789                                    this.children({
 790                                        let running_state = running_state.clone();
 791                                        let threads =
 792                                            running_state.update(cx, |running_state, cx| {
 793                                                let session = running_state.session();
 794                                                session
 795                                                    .update(cx, |session, cx| session.threads(cx))
 796                                            });
 797
 798                                        self.render_thread_dropdown(
 799                                            &running_state,
 800                                            threads,
 801                                            window,
 802                                            cx,
 803                                        )
 804                                    })
 805                                    .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
 806                                },
 807                            ),
 808                        )
 809                        .child(
 810                            h_flex()
 811                                .children(self.render_session_menu(
 812                                    self.active_session(),
 813                                    self.running_state(cx),
 814                                    window,
 815                                    cx,
 816                                ))
 817                                .when(!is_side, |this| {
 818                                    this.child(new_session_button())
 819                                        .child(documentation_button())
 820                                }),
 821                        ),
 822                ),
 823        )
 824    }
 825
 826    pub(crate) fn activate_pane_in_direction(
 827        &mut self,
 828        direction: SplitDirection,
 829        window: &mut Window,
 830        cx: &mut Context<Self>,
 831    ) {
 832        if let Some(session) = self.active_session() {
 833            session.update(cx, |session, cx| {
 834                session.running_state().update(cx, |running, cx| {
 835                    running.activate_pane_in_direction(direction, window, cx);
 836                })
 837            });
 838        }
 839    }
 840
 841    pub(crate) fn activate_item(
 842        &mut self,
 843        item: DebuggerPaneItem,
 844        window: &mut Window,
 845        cx: &mut Context<Self>,
 846    ) {
 847        if let Some(session) = self.active_session() {
 848            session.update(cx, |session, cx| {
 849                session.running_state().update(cx, |running, cx| {
 850                    running.activate_item(item, window, cx);
 851                });
 852            });
 853        }
 854    }
 855
 856    pub(crate) fn activate_session_by_id(
 857        &mut self,
 858        session_id: SessionId,
 859        window: &mut Window,
 860        cx: &mut Context<Self>,
 861    ) {
 862        if let Some(session) = self
 863            .sessions
 864            .iter()
 865            .find(|session| session.read(cx).session_id(cx) == session_id)
 866        {
 867            self.activate_session(session.clone(), window, cx);
 868        }
 869    }
 870
 871    pub(crate) fn activate_session(
 872        &mut self,
 873        session_item: Entity<DebugSession>,
 874        window: &mut Window,
 875        cx: &mut Context<Self>,
 876    ) {
 877        debug_assert!(self.sessions.contains(&session_item));
 878        session_item.focus_handle(cx).focus(window);
 879        session_item.update(cx, |this, cx| {
 880            this.running_state().update(cx, |this, cx| {
 881                this.go_to_selected_stack_frame(window, cx);
 882            });
 883        });
 884        self.active_session = Some(session_item);
 885        cx.notify();
 886    }
 887
 888    // TODO: restore once we have proper comment preserving file edits
 889    // pub(crate) fn save_scenario(
 890    //     &self,
 891    //     scenario: &DebugScenario,
 892    //     worktree_id: WorktreeId,
 893    //     window: &mut Window,
 894    //     cx: &mut App,
 895    // ) -> Task<Result<ProjectPath>> {
 896    //     self.workspace
 897    //         .update(cx, |workspace, cx| {
 898    //             let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
 899    //                 return Task::ready(Err(anyhow!("Couldn't get worktree path")));
 900    //             };
 901
 902    //             let serialized_scenario = serde_json::to_value(scenario);
 903
 904    //             cx.spawn_in(window, async move |workspace, cx| {
 905    //                 let serialized_scenario = serialized_scenario?;
 906    //                 let fs =
 907    //                     workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
 908
 909    //                 path.push(paths::local_settings_folder_relative_path());
 910    //                 if !fs.is_dir(path.as_path()).await {
 911    //                     fs.create_dir(path.as_path()).await?;
 912    //                 }
 913    //                 path.pop();
 914
 915    //                 path.push(paths::local_debug_file_relative_path());
 916    //                 let path = path.as_path();
 917
 918    //                 if !fs.is_file(path).await {
 919    //                     fs.create_file(path, Default::default()).await?;
 920    //                     fs.write(
 921    //                         path,
 922    //                         initial_local_debug_tasks_content().to_string().as_bytes(),
 923    //                     )
 924    //                     .await?;
 925    //                 }
 926
 927    //                 let content = fs.load(path).await?;
 928    //                 let mut values =
 929    //                     serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
 930    //                 values.push(serialized_scenario);
 931    //                 fs.save(
 932    //                     path,
 933    //                     &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
 934    //                     Default::default(),
 935    //                 )
 936    //                 .await?;
 937
 938    //                 workspace.update(cx, |workspace, cx| {
 939    //                     workspace
 940    //                         .project()
 941    //                         .read(cx)
 942    //                         .project_path_for_absolute_path(&path, cx)
 943    //                         .context(
 944    //                             "Couldn't get project path for .zed/debug.json in active worktree",
 945    //                         )
 946    //                 })?
 947    //             })
 948    //         })
 949    //         .unwrap_or_else(|err| Task::ready(Err(err)))
 950    // }
 951
 952    pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 953        self.thread_picker_menu_handle.toggle(window, cx);
 954    }
 955
 956    pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 957        self.session_picker_menu_handle.toggle(window, cx);
 958    }
 959
 960    fn toggle_zoom(
 961        &mut self,
 962        _: &workspace::ToggleZoom,
 963        window: &mut Window,
 964        cx: &mut Context<Self>,
 965    ) {
 966        if self.is_zoomed {
 967            cx.emit(PanelEvent::ZoomOut);
 968        } else {
 969            if !self.focus_handle(cx).contains_focused(window, cx) {
 970                cx.focus_self(window);
 971            }
 972            cx.emit(PanelEvent::ZoomIn);
 973        }
 974    }
 975
 976    fn label_for_child_session(
 977        &self,
 978        parent_session: &Entity<Session>,
 979        request: &StartDebuggingRequestArguments,
 980        cx: &mut Context<'_, Self>,
 981    ) -> SharedString {
 982        let adapter = parent_session.read(cx).adapter();
 983        if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
 984            if let Some(label) = adapter.label_for_child_session(request) {
 985                return label.into();
 986            }
 987        }
 988        let mut label = parent_session.read(cx).label().clone();
 989        if !label.ends_with("(child)") {
 990            label = format!("{label} (child)").into();
 991        }
 992        label
 993    }
 994}
 995
 996async fn register_session_inner(
 997    this: &WeakEntity<DebugPanel>,
 998    session: Entity<Session>,
 999    cx: &mut AsyncWindowContext,
1000) -> Result<Entity<DebugSession>> {
1001    let adapter_name = session.read_with(cx, |session, _| session.adapter())?;
1002    this.update_in(cx, |_, window, cx| {
1003        cx.subscribe_in(
1004            &session,
1005            window,
1006            move |this, session, event: &SessionStateEvent, window, cx| match event {
1007                SessionStateEvent::Restart => {
1008                    this.handle_restart_request(session.clone(), window, cx);
1009                }
1010                SessionStateEvent::SpawnChildSession { request } => {
1011                    this.handle_start_debugging_request(request, session.clone(), window, cx);
1012                }
1013                _ => {}
1014            },
1015        )
1016        .detach();
1017    })
1018    .ok();
1019    let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
1020    let debug_session = this.update_in(cx, |this, window, cx| {
1021        let parent_session = this
1022            .sessions
1023            .iter()
1024            .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
1025            .cloned();
1026        this.sessions.retain(|session| {
1027            !session
1028                .read(cx)
1029                .running_state()
1030                .read(cx)
1031                .session()
1032                .read(cx)
1033                .is_terminated()
1034        });
1035
1036        let debug_session = DebugSession::running(
1037            this.project.clone(),
1038            this.workspace.clone(),
1039            parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
1040            session,
1041            serialized_layout,
1042            this.position(window, cx).axis(),
1043            window,
1044            cx,
1045        );
1046
1047        // We might want to make this an event subscription and only notify when a new thread is selected
1048        // This is used to filter the command menu correctly
1049        cx.observe(
1050            &debug_session.read(cx).running_state().clone(),
1051            |_, _, cx| cx.notify(),
1052        )
1053        .detach();
1054
1055        this.sessions.push(debug_session.clone());
1056
1057        debug_session
1058    })?;
1059    Ok(debug_session)
1060}
1061
1062impl EventEmitter<PanelEvent> for DebugPanel {}
1063impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1064
1065impl Focusable for DebugPanel {
1066    fn focus_handle(&self, _: &App) -> FocusHandle {
1067        self.focus_handle.clone()
1068    }
1069}
1070
1071impl Panel for DebugPanel {
1072    fn persistent_name() -> &'static str {
1073        "DebugPanel"
1074    }
1075
1076    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1077        match DebuggerSettings::get_global(cx).dock {
1078            DebugPanelDockPosition::Left => DockPosition::Left,
1079            DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1080            DebugPanelDockPosition::Right => DockPosition::Right,
1081        }
1082    }
1083
1084    fn position_is_valid(&self, _: DockPosition) -> bool {
1085        true
1086    }
1087
1088    fn set_position(
1089        &mut self,
1090        position: DockPosition,
1091        window: &mut Window,
1092        cx: &mut Context<Self>,
1093    ) {
1094        if position.axis() != self.position(window, cx).axis() {
1095            self.sessions.iter().for_each(|session_item| {
1096                session_item.update(cx, |item, cx| {
1097                    item.running_state()
1098                        .update(cx, |state, _| state.invert_axies())
1099                })
1100            });
1101        }
1102
1103        settings::update_settings_file::<DebuggerSettings>(
1104            self.fs.clone(),
1105            cx,
1106            move |settings, _| {
1107                let dock = match position {
1108                    DockPosition::Left => DebugPanelDockPosition::Left,
1109                    DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1110                    DockPosition::Right => DebugPanelDockPosition::Right,
1111                };
1112                settings.dock = dock;
1113            },
1114        );
1115    }
1116
1117    fn size(&self, _window: &Window, _: &App) -> Pixels {
1118        self.size
1119    }
1120
1121    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1122        self.size = size.unwrap_or(px(300.));
1123    }
1124
1125    fn remote_id() -> Option<proto::PanelId> {
1126        Some(proto::PanelId::DebugPanel)
1127    }
1128
1129    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1130        Some(IconName::Debug)
1131    }
1132
1133    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1134        if DebuggerSettings::get_global(cx).button {
1135            Some("Debug Panel")
1136        } else {
1137            None
1138        }
1139    }
1140
1141    fn toggle_action(&self) -> Box<dyn Action> {
1142        Box::new(ToggleFocus)
1143    }
1144
1145    fn pane(&self) -> Option<Entity<Pane>> {
1146        None
1147    }
1148
1149    fn activation_priority(&self) -> u32 {
1150        9
1151    }
1152
1153    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1154
1155    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1156        self.is_zoomed
1157    }
1158
1159    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1160        self.is_zoomed = zoomed;
1161        cx.notify();
1162    }
1163}
1164
1165impl Render for DebugPanel {
1166    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1167        let has_sessions = self.sessions.len() > 0;
1168        let this = cx.weak_entity();
1169        debug_assert_eq!(has_sessions, self.active_session.is_some());
1170
1171        if self
1172            .active_session
1173            .as_ref()
1174            .map(|session| session.read(cx).running_state())
1175            .map(|state| state.read(cx).has_open_context_menu(cx))
1176            .unwrap_or(false)
1177        {
1178            self.context_menu.take();
1179        }
1180
1181        v_flex()
1182            .size_full()
1183            .key_context("DebugPanel")
1184            .child(h_flex().children(self.top_controls_strip(window, cx)))
1185            .track_focus(&self.focus_handle(cx))
1186            .on_action({
1187                let this = this.clone();
1188                move |_: &workspace::ActivatePaneLeft, window, cx| {
1189                    this.update(cx, |this, cx| {
1190                        this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1191                    })
1192                    .ok();
1193                }
1194            })
1195            .on_action({
1196                let this = this.clone();
1197                move |_: &workspace::ActivatePaneRight, window, cx| {
1198                    this.update(cx, |this, cx| {
1199                        this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1200                    })
1201                    .ok();
1202                }
1203            })
1204            .on_action({
1205                let this = this.clone();
1206                move |_: &workspace::ActivatePaneUp, window, cx| {
1207                    this.update(cx, |this, cx| {
1208                        this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1209                    })
1210                    .ok();
1211                }
1212            })
1213            .on_action({
1214                let this = this.clone();
1215                move |_: &workspace::ActivatePaneDown, window, cx| {
1216                    this.update(cx, |this, cx| {
1217                        this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1218                    })
1219                    .ok();
1220                }
1221            })
1222            .on_action({
1223                let this = this.clone();
1224                move |_: &FocusConsole, window, cx| {
1225                    this.update(cx, |this, cx| {
1226                        this.activate_item(DebuggerPaneItem::Console, window, cx);
1227                    })
1228                    .ok();
1229                }
1230            })
1231            .on_action({
1232                let this = this.clone();
1233                move |_: &FocusVariables, window, cx| {
1234                    this.update(cx, |this, cx| {
1235                        this.activate_item(DebuggerPaneItem::Variables, window, cx);
1236                    })
1237                    .ok();
1238                }
1239            })
1240            .on_action({
1241                let this = this.clone();
1242                move |_: &FocusBreakpointList, window, cx| {
1243                    this.update(cx, |this, cx| {
1244                        this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1245                    })
1246                    .ok();
1247                }
1248            })
1249            .on_action({
1250                let this = this.clone();
1251                move |_: &FocusFrames, window, cx| {
1252                    this.update(cx, |this, cx| {
1253                        this.activate_item(DebuggerPaneItem::Frames, window, cx);
1254                    })
1255                    .ok();
1256                }
1257            })
1258            .on_action({
1259                let this = this.clone();
1260                move |_: &FocusModules, window, cx| {
1261                    this.update(cx, |this, cx| {
1262                        this.activate_item(DebuggerPaneItem::Modules, window, cx);
1263                    })
1264                    .ok();
1265                }
1266            })
1267            .on_action({
1268                let this = this.clone();
1269                move |_: &FocusLoadedSources, window, cx| {
1270                    this.update(cx, |this, cx| {
1271                        this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1272                    })
1273                    .ok();
1274                }
1275            })
1276            .on_action({
1277                let this = this.clone();
1278                move |_: &FocusTerminal, window, cx| {
1279                    this.update(cx, |this, cx| {
1280                        this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1281                    })
1282                    .ok();
1283                }
1284            })
1285            .on_action({
1286                let this = this.clone();
1287                move |_: &ToggleThreadPicker, window, cx| {
1288                    this.update(cx, |this, cx| {
1289                        this.toggle_thread_picker(window, cx);
1290                    })
1291                    .ok();
1292                }
1293            })
1294            .on_action({
1295                let this = this.clone();
1296                move |_: &ToggleSessionPicker, window, cx| {
1297                    this.update(cx, |this, cx| {
1298                        this.toggle_session_picker(window, cx);
1299                    })
1300                    .ok();
1301                }
1302            })
1303            .on_action(cx.listener(Self::toggle_zoom))
1304            .on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
1305                let Some(session) = panel.active_session() else {
1306                    return;
1307                };
1308                let active_pane = session
1309                    .read(cx)
1310                    .running_state()
1311                    .read(cx)
1312                    .active_pane()
1313                    .clone();
1314                active_pane.update(cx, |pane, cx| {
1315                    let is_zoomed = pane.is_zoomed();
1316                    pane.set_zoomed(!is_zoomed, cx);
1317                });
1318                cx.notify();
1319            }))
1320            .when(self.active_session.is_some(), |this| {
1321                this.on_mouse_down(
1322                    MouseButton::Right,
1323                    cx.listener(|this, event: &MouseDownEvent, window, cx| {
1324                        if this
1325                            .active_session
1326                            .as_ref()
1327                            .map(|session| {
1328                                let state = session.read(cx).running_state();
1329                                state.read(cx).has_pane_at_position(event.position)
1330                            })
1331                            .unwrap_or(false)
1332                        {
1333                            this.deploy_context_menu(event.position, window, cx);
1334                        }
1335                    }),
1336                )
1337                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1338                    deferred(
1339                        anchored()
1340                            .position(*position)
1341                            .anchor(gpui::Corner::TopLeft)
1342                            .child(menu.clone()),
1343                    )
1344                    .with_priority(1)
1345                }))
1346            })
1347            .map(|this| {
1348                if has_sessions {
1349                    this.children(self.active_session.clone())
1350                } else {
1351                    this.child(
1352                        v_flex()
1353                            .h_full()
1354                            .gap_1()
1355                            .items_center()
1356                            .justify_center()
1357                            .child(
1358                                h_flex().child(
1359                                    Label::new("No Debugging Sessions")
1360                                        .size(LabelSize::Small)
1361                                        .color(Color::Muted),
1362                                ),
1363                            )
1364                            .child(
1365                                h_flex().flex_shrink().child(
1366                                    Button::new("spawn-new-session-empty-state", "New Session")
1367                                        .size(ButtonSize::Large)
1368                                        .on_click(|_, window, cx| {
1369                                            window.dispatch_action(crate::Start.boxed_clone(), cx);
1370                                        }),
1371                                ),
1372                            ),
1373                    )
1374                }
1375            })
1376            .into_any()
1377    }
1378}
1379
1380struct DebuggerProvider(Entity<DebugPanel>);
1381
1382impl workspace::DebuggerProvider for DebuggerProvider {
1383    fn start_session(
1384        &self,
1385        definition: DebugScenario,
1386        context: TaskContext,
1387        buffer: Option<Entity<Buffer>>,
1388        window: &mut Window,
1389        cx: &mut App,
1390    ) {
1391        self.0.update(cx, |_, cx| {
1392            cx.defer_in(window, |this, window, cx| {
1393                this.start_session(definition, context, buffer, None, window, cx);
1394            })
1395        })
1396    }
1397
1398    fn spawn_task_or_modal(
1399        &self,
1400        workspace: &mut Workspace,
1401        action: &tasks_ui::Spawn,
1402        window: &mut Window,
1403        cx: &mut Context<Workspace>,
1404    ) {
1405        spawn_task_or_modal(workspace, action, window, cx);
1406    }
1407
1408    fn debug_scenario_scheduled(&self, cx: &mut App) {
1409        self.0.update(cx, |this, _| {
1410            this.debug_scenario_scheduled_last = true;
1411        });
1412    }
1413
1414    fn task_scheduled(&self, cx: &mut App) {
1415        self.0.update(cx, |this, _| {
1416            this.debug_scenario_scheduled_last = false;
1417        })
1418    }
1419
1420    fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
1421        self.0.read(cx).debug_scenario_scheduled_last
1422    }
1423
1424    fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
1425        let session = self.0.read(cx).active_session()?;
1426        let thread = session.read(cx).running_state().read(cx).thread_id()?;
1427        session.read(cx).session(cx).read(cx).thread_state(thread)
1428    }
1429}