debugger_panel.rs

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