debugger_panel.rs

   1use crate::{
   2    ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
   3    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
   4};
   5use crate::{new_session_modal::NewSessionModal, session::DebugSession};
   6use anyhow::{Result, anyhow};
   7use collections::HashMap;
   8use command_palette_hooks::CommandPaletteFilter;
   9use dap::{
  10    ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
  11    client::SessionId, debugger_settings::DebuggerSettings,
  12};
  13use futures::{SinkExt as _, channel::mpsc};
  14use gpui::{
  15    Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
  16    FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
  17    actions, anchored, deferred,
  18};
  19
  20use project::{
  21    Project,
  22    debugger::{
  23        dap_store::{self, DapStore},
  24        session::ThreadStatus,
  25    },
  26    terminals::TerminalKind,
  27};
  28use rpc::proto::{self};
  29use settings::Settings;
  30use std::any::TypeId;
  31use std::path::Path;
  32use std::sync::Arc;
  33use task::DebugTaskDefinition;
  34use terminal_view::terminal_panel::TerminalPanel;
  35use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
  36use util::debug_panic;
  37use workspace::{
  38    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]);
  60pub struct DebugPanel {
  61    size: Pixels,
  62    sessions: Vec<Entity<DebugSession>>,
  63    active_session: Option<Entity<DebugSession>>,
  64    /// This represents the last debug definition that was created in the new session modal
  65    pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
  66    project: WeakEntity<Project>,
  67    workspace: WeakEntity<Workspace>,
  68    focus_handle: FocusHandle,
  69    context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
  70    _subscriptions: Vec<Subscription>,
  71}
  72
  73impl DebugPanel {
  74    pub fn new(
  75        workspace: &Workspace,
  76        window: &mut Window,
  77        cx: &mut Context<Workspace>,
  78    ) -> Entity<Self> {
  79        cx.new(|cx| {
  80            let project = workspace.project().clone();
  81            let dap_store = project.read(cx).dap_store();
  82
  83            let weak = cx.weak_entity();
  84
  85            let modal_subscription =
  86                cx.observe_new::<tasks_ui::TasksModal>(move |_, window, cx| {
  87                    let modal_entity = cx.entity();
  88
  89                    weak.update(cx, |_: &mut DebugPanel, cx| {
  90                        let Some(window) = window else {
  91                            log::error!("Debug panel couldn't subscribe to tasks modal because there was no window");
  92                            return;
  93                        };
  94
  95                        cx.subscribe_in(
  96                            &modal_entity,
  97                            window,
  98                            |panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
  99                                panel.workspace.update(cx, |workspace, cx| {
 100                                    let project = workspace.project().clone();
 101                                    workspace.toggle_modal(window, cx, |window, cx| {
 102                                        crate::attach_modal::AttachModal::new(
 103                                            project,
 104                                            event.debug_config.clone(),
 105                                            true,
 106                                            window,
 107                                            cx,
 108                                        )
 109                                    });
 110                                }).ok();
 111                            },
 112                        )
 113                        .detach();
 114                    })
 115                    .ok();
 116                });
 117
 118            let _subscriptions = vec![
 119                cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
 120                modal_subscription,
 121            ];
 122
 123            let debug_panel = Self {
 124                size: px(300.),
 125                sessions: vec![],
 126                active_session: None,
 127                _subscriptions,
 128                past_debug_definition: None,
 129                focus_handle: cx.focus_handle(),
 130                project: project.downgrade(),
 131                workspace: workspace.weak_handle(),
 132                context_menu: None,
 133            };
 134
 135            debug_panel
 136        })
 137    }
 138
 139    fn filter_action_types(&self, cx: &mut App) {
 140        let (has_active_session, supports_restart, support_step_back, status) = self
 141            .active_session()
 142            .map(|item| {
 143                let running = item.read(cx).mode().as_running().cloned();
 144
 145                match running {
 146                    Some(running) => {
 147                        let caps = running.read(cx).capabilities(cx);
 148                        (
 149                            !running.read(cx).session().read(cx).is_terminated(),
 150                            caps.supports_restart_request.unwrap_or_default(),
 151                            caps.supports_step_back.unwrap_or_default(),
 152                            running.read(cx).thread_status(cx),
 153                        )
 154                    }
 155                    None => (false, false, false, None),
 156                }
 157            })
 158            .unwrap_or((false, false, false, None));
 159
 160        let filter = CommandPaletteFilter::global_mut(cx);
 161        let debugger_action_types = [
 162            TypeId::of::<Disconnect>(),
 163            TypeId::of::<Stop>(),
 164            TypeId::of::<ToggleIgnoreBreakpoints>(),
 165        ];
 166
 167        let running_action_types = [TypeId::of::<Pause>()];
 168
 169        let stopped_action_type = [
 170            TypeId::of::<Continue>(),
 171            TypeId::of::<StepOver>(),
 172            TypeId::of::<StepInto>(),
 173            TypeId::of::<StepOut>(),
 174            TypeId::of::<editor::actions::DebuggerRunToCursor>(),
 175            TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
 176        ];
 177
 178        let step_back_action_type = [TypeId::of::<StepBack>()];
 179        let restart_action_type = [TypeId::of::<Restart>()];
 180
 181        if has_active_session {
 182            filter.show_action_types(debugger_action_types.iter());
 183
 184            if supports_restart {
 185                filter.show_action_types(restart_action_type.iter());
 186            } else {
 187                filter.hide_action_types(&restart_action_type);
 188            }
 189
 190            if support_step_back {
 191                filter.show_action_types(step_back_action_type.iter());
 192            } else {
 193                filter.hide_action_types(&step_back_action_type);
 194            }
 195
 196            match status {
 197                Some(ThreadStatus::Running) => {
 198                    filter.show_action_types(running_action_types.iter());
 199                    filter.hide_action_types(&stopped_action_type);
 200                }
 201                Some(ThreadStatus::Stopped) => {
 202                    filter.show_action_types(stopped_action_type.iter());
 203                    filter.hide_action_types(&running_action_types);
 204                }
 205                _ => {
 206                    filter.hide_action_types(&running_action_types);
 207                    filter.hide_action_types(&stopped_action_type);
 208                }
 209            }
 210        } else {
 211            // show only the `debug: start`
 212            filter.hide_action_types(&debugger_action_types);
 213            filter.hide_action_types(&step_back_action_type);
 214            filter.hide_action_types(&restart_action_type);
 215            filter.hide_action_types(&running_action_types);
 216            filter.hide_action_types(&stopped_action_type);
 217        }
 218    }
 219
 220    pub fn load(
 221        workspace: WeakEntity<Workspace>,
 222        cx: AsyncWindowContext,
 223    ) -> Task<Result<Entity<Self>>> {
 224        cx.spawn(async move |cx| {
 225            workspace.update_in(cx, |workspace, window, cx| {
 226                let debug_panel = DebugPanel::new(workspace, window, cx);
 227
 228                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
 229                    workspace.project().read(cx).breakpoint_store().update(
 230                        cx,
 231                        |breakpoint_store, cx| {
 232                            breakpoint_store.clear_breakpoints(cx);
 233                        },
 234                    )
 235                });
 236
 237                cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
 238                    Self::filter_action_types(debug_panel, cx);
 239                })
 240                .detach();
 241
 242                cx.observe(&debug_panel, |_, debug_panel, cx| {
 243                    debug_panel.update(cx, |debug_panel, cx| {
 244                        Self::filter_action_types(debug_panel, cx);
 245                    });
 246                })
 247                .detach();
 248
 249                debug_panel
 250            })
 251        })
 252    }
 253
 254    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
 255        self.active_session.clone()
 256    }
 257
 258    pub fn debug_panel_items_by_client(
 259        &self,
 260        client_id: &SessionId,
 261        cx: &Context<Self>,
 262    ) -> Vec<Entity<DebugSession>> {
 263        self.sessions
 264            .iter()
 265            .filter(|item| item.read(cx).session_id(cx) == *client_id)
 266            .map(|item| item.clone())
 267            .collect()
 268    }
 269
 270    pub fn debug_panel_item_by_client(
 271        &self,
 272        client_id: SessionId,
 273        cx: &mut Context<Self>,
 274    ) -> Option<Entity<DebugSession>> {
 275        self.sessions
 276            .iter()
 277            .find(|item| {
 278                let item = item.read(cx);
 279
 280                item.session_id(cx) == client_id
 281            })
 282            .cloned()
 283    }
 284
 285    fn handle_dap_store_event(
 286        &mut self,
 287        dap_store: &Entity<DapStore>,
 288        event: &dap_store::DapStoreEvent,
 289        window: &mut Window,
 290        cx: &mut Context<Self>,
 291    ) {
 292        match event {
 293            dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
 294                let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
 295                    return log::error!(
 296                        "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
 297                    );
 298                };
 299
 300                let adapter_name = session.read(cx).adapter_name();
 301
 302                let session_id = *session_id;
 303                cx.spawn_in(window, async move |this, cx| {
 304                    let serialized_layout =
 305                        persistence::get_serialized_pane_layout(adapter_name).await;
 306
 307                    this.update_in(cx, |this, window, cx| {
 308                        let Some(project) = this.project.upgrade() else {
 309                            return log::error!(
 310                                "Debug Panel out lived it's weak reference to Project"
 311                            );
 312                        };
 313
 314                        if this
 315                            .sessions
 316                            .iter()
 317                            .any(|item| item.read(cx).session_id(cx) == session_id)
 318                        {
 319                            // We already have an item for this session.
 320                            debug_panic!("We should never reuse session ids");
 321                            return;
 322                        }
 323
 324                        this.sessions.retain(|session| {
 325                            session
 326                                .read(cx)
 327                                .mode()
 328                                .as_running()
 329                                .map_or(false, |running_state| {
 330                                    !running_state.read(cx).session().read(cx).is_terminated()
 331                                })
 332                        });
 333
 334                        let session_item = DebugSession::running(
 335                            project,
 336                            this.workspace.clone(),
 337                            session,
 338                            cx.weak_entity(),
 339                            serialized_layout,
 340                            window,
 341                            cx,
 342                        );
 343
 344                        if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
 345                            // We might want to make this an event subscription and only notify when a new thread is selected
 346                            // This is used to filter the command menu correctly
 347                            cx.observe(&running, |_, _, cx| cx.notify()).detach();
 348                        }
 349
 350                        this.sessions.push(session_item.clone());
 351                        this.activate_session(session_item, window, cx);
 352                    })
 353                })
 354                .detach();
 355            }
 356            dap_store::DapStoreEvent::RunInTerminal {
 357                title,
 358                cwd,
 359                command,
 360                args,
 361                envs,
 362                sender,
 363                ..
 364            } => {
 365                self.handle_run_in_terminal_request(
 366                    title.clone(),
 367                    cwd.clone(),
 368                    command.clone(),
 369                    args.clone(),
 370                    envs.clone(),
 371                    sender.clone(),
 372                    window,
 373                    cx,
 374                )
 375                .detach_and_log_err(cx);
 376            }
 377            _ => {}
 378        }
 379    }
 380
 381    fn handle_run_in_terminal_request(
 382        &self,
 383        title: Option<String>,
 384        cwd: Option<Arc<Path>>,
 385        command: Option<String>,
 386        args: Vec<String>,
 387        envs: HashMap<String, String>,
 388        mut sender: mpsc::Sender<Result<u32>>,
 389        window: &mut Window,
 390        cx: &mut App,
 391    ) -> Task<Result<()>> {
 392        let terminal_task = self.workspace.update(cx, |workspace, cx| {
 393            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
 394                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
 395            });
 396
 397            let terminal_panel = match terminal_panel {
 398                Ok(panel) => panel,
 399                Err(err) => return Task::ready(Err(err)),
 400            };
 401
 402            terminal_panel.update(cx, |terminal_panel, cx| {
 403                let terminal_task = terminal_panel.add_terminal(
 404                    TerminalKind::Debug {
 405                        command,
 406                        args,
 407                        envs,
 408                        cwd,
 409                        title,
 410                    },
 411                    task::RevealStrategy::Always,
 412                    window,
 413                    cx,
 414                );
 415
 416                cx.spawn(async move |_, cx| {
 417                    let pid_task = async move {
 418                        let terminal = terminal_task.await?;
 419
 420                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
 421                    };
 422
 423                    pid_task.await
 424                })
 425            })
 426        });
 427
 428        cx.background_spawn(async move {
 429            match terminal_task {
 430                Ok(pid_task) => match pid_task.await {
 431                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
 432                    Ok(None) => {
 433                        sender
 434                            .send(Err(anyhow!(
 435                                "Terminal was spawned but PID was not available"
 436                            )))
 437                            .await?
 438                    }
 439                    Err(error) => sender.send(Err(anyhow!(error))).await?,
 440                },
 441                Err(error) => sender.send(Err(anyhow!(error))).await?,
 442            };
 443
 444            Ok(())
 445        })
 446    }
 447
 448    fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
 449        let Some(session) = self
 450            .sessions
 451            .iter()
 452            .find(|other| entity_id == other.entity_id())
 453            .cloned()
 454        else {
 455            return;
 456        };
 457        session.update(cx, |this, cx| {
 458            if let Some(running) = this.mode().as_running() {
 459                running.update(cx, |this, cx| {
 460                    this.serialize_layout(window, cx);
 461                });
 462            }
 463        });
 464        let session_id = session.update(cx, |this, cx| this.session_id(cx));
 465        let should_prompt = self
 466            .project
 467            .update(cx, |this, cx| {
 468                let session = this.dap_store().read(cx).session_by_id(session_id);
 469                session.map(|session| !session.read(cx).is_terminated())
 470            })
 471            .ok()
 472            .flatten()
 473            .unwrap_or_default();
 474
 475        cx.spawn_in(window, async move |this, cx| {
 476            if should_prompt {
 477                let response = cx.prompt(
 478                    gpui::PromptLevel::Warning,
 479                    "This Debug Session is still running. Are you sure you want to terminate it?",
 480                    None,
 481                    &["Yes", "No"],
 482                );
 483                if response.await == Ok(1) {
 484                    return;
 485                }
 486            }
 487            session.update(cx, |session, cx| session.shutdown(cx)).ok();
 488            this.update(cx, |this, cx| {
 489                this.sessions.retain(|other| entity_id != other.entity_id());
 490
 491                if let Some(active_session_id) = this
 492                    .active_session
 493                    .as_ref()
 494                    .map(|session| session.entity_id())
 495                {
 496                    if active_session_id == entity_id {
 497                        this.active_session = this.sessions.first().cloned();
 498                    }
 499                }
 500                cx.notify()
 501            })
 502            .ok();
 503        })
 504        .detach();
 505    }
 506    fn sessions_drop_down_menu(
 507        &self,
 508        active_session: &Entity<DebugSession>,
 509        window: &mut Window,
 510        cx: &mut Context<Self>,
 511    ) -> DropdownMenu {
 512        let sessions = self.sessions.clone();
 513        let weak = cx.weak_entity();
 514        let label = active_session.read(cx).label_element(cx);
 515
 516        DropdownMenu::new_with_element(
 517            "debugger-session-list",
 518            label,
 519            ContextMenu::build(window, cx, move |mut this, _, cx| {
 520                let context_menu = cx.weak_entity();
 521                for session in sessions.into_iter() {
 522                    let weak_session = session.downgrade();
 523                    let weak_session_id = weak_session.entity_id();
 524
 525                    this = this.custom_entry(
 526                        {
 527                            let weak = weak.clone();
 528                            let context_menu = context_menu.clone();
 529                            move |_, cx| {
 530                                weak_session
 531                                    .read_with(cx, |session, cx| {
 532                                        let context_menu = context_menu.clone();
 533                                        let id: SharedString =
 534                                            format!("debug-session-{}", session.session_id(cx).0)
 535                                                .into();
 536                                        h_flex()
 537                                            .w_full()
 538                                            .group(id.clone())
 539                                            .justify_between()
 540                                            .child(session.label_element(cx))
 541                                            .child(
 542                                                IconButton::new(
 543                                                    "close-debug-session",
 544                                                    IconName::Close,
 545                                                )
 546                                                .visible_on_hover(id.clone())
 547                                                .icon_size(IconSize::Small)
 548                                                .on_click({
 549                                                    let weak = weak.clone();
 550                                                    move |_, window, cx| {
 551                                                        weak.update(cx, |panel, cx| {
 552                                                            panel.close_session(
 553                                                                weak_session_id,
 554                                                                window,
 555                                                                cx,
 556                                                            );
 557                                                        })
 558                                                        .ok();
 559                                                        context_menu
 560                                                            .update(cx, |this, cx| {
 561                                                                this.cancel(
 562                                                                    &Default::default(),
 563                                                                    window,
 564                                                                    cx,
 565                                                                );
 566                                                            })
 567                                                            .ok();
 568                                                    }
 569                                                }),
 570                                            )
 571                                            .into_any_element()
 572                                    })
 573                                    .unwrap_or_else(|_| div().into_any_element())
 574                            }
 575                        },
 576                        {
 577                            let weak = weak.clone();
 578                            move |window, cx| {
 579                                weak.update(cx, |panel, cx| {
 580                                    panel.activate_session(session.clone(), window, cx);
 581                                })
 582                                .ok();
 583                            }
 584                        },
 585                    );
 586                }
 587                this
 588            }),
 589        )
 590    }
 591
 592    fn deploy_context_menu(
 593        &mut self,
 594        position: Point<Pixels>,
 595        window: &mut Window,
 596        cx: &mut Context<Self>,
 597    ) {
 598        if let Some(running_state) = self
 599            .active_session
 600            .as_ref()
 601            .and_then(|session| session.read(cx).mode().as_running().cloned())
 602        {
 603            let pane_items_status = running_state.read(cx).pane_items_status(cx);
 604            let this = cx.weak_entity();
 605
 606            let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
 607                for (item_kind, is_visible) in pane_items_status.into_iter() {
 608                    menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
 609                        let this = this.clone();
 610                        move |window, cx| {
 611                            this.update(cx, |this, cx| {
 612                                if let Some(running_state) =
 613                                    this.active_session.as_ref().and_then(|session| {
 614                                        session.read(cx).mode().as_running().cloned()
 615                                    })
 616                                {
 617                                    running_state.update(cx, |state, cx| {
 618                                        if is_visible {
 619                                            state.remove_pane_item(item_kind, window, cx);
 620                                        } else {
 621                                            state.add_pane_item(item_kind, position, window, cx);
 622                                        }
 623                                    })
 624                                }
 625                            })
 626                            .ok();
 627                        }
 628                    });
 629                }
 630
 631                menu
 632            });
 633
 634            window.focus(&context_menu.focus_handle(cx));
 635            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
 636                this.context_menu.take();
 637                cx.notify();
 638            });
 639            self.context_menu = Some((context_menu, position, subscription));
 640        }
 641    }
 642
 643    fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
 644        let active_session = self.active_session.clone();
 645
 646        Some(
 647            h_flex()
 648                .border_b_1()
 649                .border_color(cx.theme().colors().border)
 650                .p_1()
 651                .justify_between()
 652                .w_full()
 653                .child(
 654                    h_flex().gap_2().w_full().when_some(
 655                        active_session
 656                            .as_ref()
 657                            .and_then(|session| session.read(cx).mode().as_running()),
 658                        |this, running_session| {
 659                            let thread_status = running_session
 660                                .read(cx)
 661                                .thread_status(cx)
 662                                .unwrap_or(project::debugger::session::ThreadStatus::Exited);
 663                            let capabilities = running_session.read(cx).capabilities(cx);
 664                            this.map(|this| {
 665                                if thread_status == ThreadStatus::Running {
 666                                    this.child(
 667                                        IconButton::new("debug-pause", IconName::DebugPause)
 668                                            .icon_size(IconSize::XSmall)
 669                                            .shape(ui::IconButtonShape::Square)
 670                                            .on_click(window.listener_for(
 671                                                &running_session,
 672                                                |this, _, _window, cx| {
 673                                                    this.pause_thread(cx);
 674                                                },
 675                                            ))
 676                                            .tooltip(move |window, cx| {
 677                                                Tooltip::text("Pause program")(window, cx)
 678                                            }),
 679                                    )
 680                                } else {
 681                                    this.child(
 682                                        IconButton::new("debug-continue", IconName::DebugContinue)
 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(move |window, cx| {
 691                                                Tooltip::text("Continue program")(window, cx)
 692                                            }),
 693                                    )
 694                                }
 695                            })
 696                            .child(
 697                                IconButton::new("debug-step-over", IconName::ArrowRight)
 698                                    .icon_size(IconSize::XSmall)
 699                                    .shape(ui::IconButtonShape::Square)
 700                                    .on_click(window.listener_for(
 701                                        &running_session,
 702                                        |this, _, _window, cx| {
 703                                            this.step_over(cx);
 704                                        },
 705                                    ))
 706                                    .disabled(thread_status != ThreadStatus::Stopped)
 707                                    .tooltip(move |window, cx| {
 708                                        Tooltip::text("Step over")(window, cx)
 709                                    }),
 710                            )
 711                            .child(
 712                                IconButton::new("debug-step-out", IconName::ArrowUpRight)
 713                                    .icon_size(IconSize::XSmall)
 714                                    .shape(ui::IconButtonShape::Square)
 715                                    .on_click(window.listener_for(
 716                                        &running_session,
 717                                        |this, _, _window, cx| {
 718                                            this.step_out(cx);
 719                                        },
 720                                    ))
 721                                    .disabled(thread_status != ThreadStatus::Stopped)
 722                                    .tooltip(move |window, cx| {
 723                                        Tooltip::text("Step out")(window, cx)
 724                                    }),
 725                            )
 726                            .child(
 727                                IconButton::new("debug-step-into", IconName::ArrowDownRight)
 728                                    .icon_size(IconSize::XSmall)
 729                                    .shape(ui::IconButtonShape::Square)
 730                                    .on_click(window.listener_for(
 731                                        &running_session,
 732                                        |this, _, _window, cx| {
 733                                            this.step_in(cx);
 734                                        },
 735                                    ))
 736                                    .disabled(thread_status != ThreadStatus::Stopped)
 737                                    .tooltip(move |window, cx| {
 738                                        Tooltip::text("Step in")(window, cx)
 739                                    }),
 740                            )
 741                            .child(Divider::vertical())
 742                            .child(
 743                                IconButton::new(
 744                                    "debug-enable-breakpoint",
 745                                    IconName::DebugDisabledBreakpoint,
 746                                )
 747                                .icon_size(IconSize::XSmall)
 748                                .shape(ui::IconButtonShape::Square)
 749                                .disabled(thread_status != ThreadStatus::Stopped),
 750                            )
 751                            .child(
 752                                IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
 753                                    .icon_size(IconSize::XSmall)
 754                                    .shape(ui::IconButtonShape::Square)
 755                                    .disabled(thread_status != ThreadStatus::Stopped),
 756                            )
 757                            .child(
 758                                IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
 759                                    .icon_size(IconSize::XSmall)
 760                                    .shape(ui::IconButtonShape::Square)
 761                                    .disabled(
 762                                        thread_status == ThreadStatus::Exited
 763                                            || thread_status == ThreadStatus::Ended,
 764                                    )
 765                                    .on_click(window.listener_for(
 766                                        &running_session,
 767                                        |this, _, _window, cx| {
 768                                            this.toggle_ignore_breakpoints(cx);
 769                                        },
 770                                    ))
 771                                    .tooltip(move |window, cx| {
 772                                        Tooltip::text("Disable all breakpoints")(window, cx)
 773                                    }),
 774                            )
 775                            .child(Divider::vertical())
 776                            .child(
 777                                IconButton::new("debug-restart", IconName::DebugRestart)
 778                                    .icon_size(IconSize::XSmall)
 779                                    .on_click(window.listener_for(
 780                                        &running_session,
 781                                        |this, _, _window, cx| {
 782                                            this.restart_session(cx);
 783                                        },
 784                                    ))
 785                                    .tooltip(move |window, cx| {
 786                                        Tooltip::text("Restart")(window, cx)
 787                                    }),
 788                            )
 789                            .child(
 790                                IconButton::new("debug-stop", IconName::Power)
 791                                    .icon_size(IconSize::XSmall)
 792                                    .on_click(window.listener_for(
 793                                        &running_session,
 794                                        |this, _, _window, cx| {
 795                                            this.stop_thread(cx);
 796                                        },
 797                                    ))
 798                                    .disabled(
 799                                        thread_status != ThreadStatus::Stopped
 800                                            && thread_status != ThreadStatus::Running,
 801                                    )
 802                                    .tooltip({
 803                                        let label = if capabilities
 804                                            .supports_terminate_threads_request
 805                                            .unwrap_or_default()
 806                                        {
 807                                            "Terminate Thread"
 808                                        } else {
 809                                            "Terminate all Threads"
 810                                        };
 811                                        move |window, cx| Tooltip::text(label)(window, cx)
 812                                    }),
 813                            )
 814                        },
 815                    ),
 816                )
 817                .child(
 818                    h_flex()
 819                        .gap_2()
 820                        .when_some(
 821                            active_session
 822                                .as_ref()
 823                                .and_then(|session| session.read(cx).mode().as_running())
 824                                .cloned(),
 825                            |this, session| {
 826                                this.child(
 827                                    session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
 828                                )
 829                                .child(Divider::vertical())
 830                            },
 831                        )
 832                        .when_some(active_session.as_ref(), |this, session| {
 833                            let context_menu = self.sessions_drop_down_menu(session, window, cx);
 834                            this.child(context_menu).child(Divider::vertical())
 835                        })
 836                        .child(
 837                            IconButton::new("debug-new-session", IconName::Plus)
 838                                .icon_size(IconSize::Small)
 839                                .on_click({
 840                                    let workspace = self.workspace.clone();
 841                                    let weak_panel = cx.weak_entity();
 842                                    let past_debug_definition = self.past_debug_definition.clone();
 843                                    move |_, window, cx| {
 844                                        let weak_panel = weak_panel.clone();
 845                                        let past_debug_definition = past_debug_definition.clone();
 846
 847                                        let _ = workspace.update(cx, |this, cx| {
 848                                            let workspace = cx.weak_entity();
 849                                            this.toggle_modal(window, cx, |window, cx| {
 850                                                NewSessionModal::new(
 851                                                    past_debug_definition,
 852                                                    weak_panel,
 853                                                    workspace,
 854                                                    window,
 855                                                    cx,
 856                                                )
 857                                            });
 858                                        });
 859                                    }
 860                                })
 861                                .tooltip(|window, cx| {
 862                                    Tooltip::for_action(
 863                                        "New Debug Session",
 864                                        &CreateDebuggingSession,
 865                                        window,
 866                                        cx,
 867                                    )
 868                                }),
 869                        ),
 870                ),
 871        )
 872    }
 873
 874    fn activate_session(
 875        &mut self,
 876        session_item: Entity<DebugSession>,
 877        window: &mut Window,
 878        cx: &mut Context<Self>,
 879    ) {
 880        debug_assert!(self.sessions.contains(&session_item));
 881        session_item.focus_handle(cx).focus(window);
 882        session_item.update(cx, |this, cx| {
 883            if let Some(running) = this.mode().as_running() {
 884                running.update(cx, |this, cx| {
 885                    this.go_to_selected_stack_frame(window, cx);
 886                });
 887            }
 888        });
 889        self.active_session = Some(session_item);
 890        cx.notify();
 891    }
 892}
 893
 894impl EventEmitter<PanelEvent> for DebugPanel {}
 895impl EventEmitter<DebugPanelEvent> for DebugPanel {}
 896impl EventEmitter<project::Event> for DebugPanel {}
 897
 898impl Focusable for DebugPanel {
 899    fn focus_handle(&self, _: &App) -> FocusHandle {
 900        self.focus_handle.clone()
 901    }
 902}
 903
 904impl Panel for DebugPanel {
 905    fn persistent_name() -> &'static str {
 906        "DebugPanel"
 907    }
 908
 909    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
 910        DockPosition::Bottom
 911    }
 912
 913    fn position_is_valid(&self, position: DockPosition) -> bool {
 914        position == DockPosition::Bottom
 915    }
 916
 917    fn set_position(
 918        &mut self,
 919        _position: DockPosition,
 920        _window: &mut Window,
 921        _cx: &mut Context<Self>,
 922    ) {
 923    }
 924
 925    fn size(&self, _window: &Window, _: &App) -> Pixels {
 926        self.size
 927    }
 928
 929    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
 930        self.size = size.unwrap();
 931    }
 932
 933    fn remote_id() -> Option<proto::PanelId> {
 934        Some(proto::PanelId::DebugPanel)
 935    }
 936
 937    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
 938        Some(IconName::Debug)
 939    }
 940
 941    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
 942        if DebuggerSettings::get_global(cx).button {
 943            Some("Debug Panel")
 944        } else {
 945            None
 946        }
 947    }
 948
 949    fn toggle_action(&self) -> Box<dyn Action> {
 950        Box::new(ToggleFocus)
 951    }
 952
 953    fn activation_priority(&self) -> u32 {
 954        9
 955    }
 956    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
 957}
 958
 959impl Render for DebugPanel {
 960    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 961        let has_sessions = self.sessions.len() > 0;
 962        debug_assert_eq!(has_sessions, self.active_session.is_some());
 963
 964        if self
 965            .active_session
 966            .as_ref()
 967            .and_then(|session| session.read(cx).mode().as_running().cloned())
 968            .map(|state| state.read(cx).has_open_context_menu(cx))
 969            .unwrap_or(false)
 970        {
 971            self.context_menu.take();
 972        }
 973
 974        v_flex()
 975            .size_full()
 976            .key_context("DebugPanel")
 977            .child(h_flex().children(self.top_controls_strip(window, cx)))
 978            .track_focus(&self.focus_handle(cx))
 979            .when(self.active_session.is_some(), |this| {
 980                this.on_mouse_down(
 981                    MouseButton::Right,
 982                    cx.listener(|this, event: &MouseDownEvent, window, cx| {
 983                        if this
 984                            .active_session
 985                            .as_ref()
 986                            .and_then(|session| {
 987                                session.read(cx).mode().as_running().map(|state| {
 988                                    state.read(cx).has_pane_at_position(event.position)
 989                                })
 990                            })
 991                            .unwrap_or(false)
 992                        {
 993                            this.deploy_context_menu(event.position, window, cx);
 994                        }
 995                    }),
 996                )
 997                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
 998                    deferred(
 999                        anchored()
1000                            .position(*position)
1001                            .anchor(gpui::Corner::TopLeft)
1002                            .child(menu.clone()),
1003                    )
1004                    .with_priority(1)
1005                }))
1006            })
1007            .map(|this| {
1008                if has_sessions {
1009                    this.children(self.active_session.clone())
1010                } else {
1011                    this.child(
1012                        v_flex()
1013                            .h_full()
1014                            .gap_1()
1015                            .items_center()
1016                            .justify_center()
1017                            .child(
1018                                h_flex().child(
1019                                    Label::new("No Debugging Sessions")
1020                                        .size(LabelSize::Small)
1021                                        .color(Color::Muted),
1022                                ),
1023                            )
1024                            .child(
1025                                h_flex().flex_shrink().child(
1026                                    Button::new("spawn-new-session-empty-state", "New Session")
1027                                        .size(ButtonSize::Large)
1028                                        .on_click(|_, window, cx| {
1029                                            window.dispatch_action(
1030                                                CreateDebuggingSession.boxed_clone(),
1031                                                cx,
1032                                            );
1033                                        }),
1034                                ),
1035                            ),
1036                    )
1037                }
1038            })
1039            .into_any()
1040    }
1041}