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