debugger_panel.rs

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