debugger_panel.rs

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