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