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