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::{Context as _, 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
 551                        .send(Ok(pid.as_u32()))
 552                        .await
 553                        .context("task cancelled")?,
 554                    Ok(None) => {
 555                        sender
 556                            .send(Err(anyhow!(
 557                                "Terminal was spawned but PID was not available"
 558                            )))
 559                            .await?
 560                    }
 561                    Err(error) => sender.send(Err(anyhow!(error))).await?,
 562                },
 563                Err(error) => sender.send(Err(anyhow!(error))).await?,
 564            };
 565
 566            Ok(())
 567        })
 568    }
 569
 570    fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
 571        let Some(session) = self
 572            .sessions
 573            .iter()
 574            .find(|other| entity_id == other.entity_id())
 575            .cloned()
 576        else {
 577            return;
 578        };
 579        session.update(cx, |this, cx| {
 580            if let Some(running) = this.mode().as_running() {
 581                running.update(cx, |this, cx| {
 582                    this.serialize_layout(window, cx);
 583                });
 584            }
 585        });
 586        let session_id = session.update(cx, |this, cx| this.session_id(cx));
 587        let should_prompt = self
 588            .project
 589            .update(cx, |this, cx| {
 590                let session = this.dap_store().read(cx).session_by_id(session_id);
 591                session.map(|session| !session.read(cx).is_terminated())
 592            })
 593            .unwrap_or_default();
 594
 595        cx.spawn_in(window, async move |this, cx| {
 596            if should_prompt {
 597                let response = cx.prompt(
 598                    gpui::PromptLevel::Warning,
 599                    "This Debug Session is still running. Are you sure you want to terminate it?",
 600                    None,
 601                    &["Yes", "No"],
 602                );
 603                if response.await == Ok(1) {
 604                    return;
 605                }
 606            }
 607            session.update(cx, |session, cx| session.shutdown(cx)).ok();
 608            this.update(cx, |this, cx| {
 609                this.sessions.retain(|other| entity_id != other.entity_id());
 610
 611                if let Some(active_session_id) = this
 612                    .active_session
 613                    .as_ref()
 614                    .map(|session| session.entity_id())
 615                {
 616                    if active_session_id == entity_id {
 617                        this.active_session = this.sessions.first().cloned();
 618                    }
 619                }
 620                cx.notify()
 621            })
 622            .ok();
 623        })
 624        .detach();
 625    }
 626    fn sessions_drop_down_menu(
 627        &self,
 628        active_session: &Entity<DebugSession>,
 629        window: &mut Window,
 630        cx: &mut Context<Self>,
 631    ) -> DropdownMenu {
 632        let sessions = self.sessions.clone();
 633        let weak = cx.weak_entity();
 634        let label = active_session.read(cx).label_element(cx);
 635
 636        DropdownMenu::new_with_element(
 637            "debugger-session-list",
 638            label,
 639            ContextMenu::build(window, cx, move |mut this, _, cx| {
 640                let context_menu = cx.weak_entity();
 641                for session in sessions.into_iter() {
 642                    let weak_session = session.downgrade();
 643                    let weak_session_id = weak_session.entity_id();
 644
 645                    this = this.custom_entry(
 646                        {
 647                            let weak = weak.clone();
 648                            let context_menu = context_menu.clone();
 649                            move |_, cx| {
 650                                weak_session
 651                                    .read_with(cx, |session, cx| {
 652                                        let context_menu = context_menu.clone();
 653                                        let id: SharedString =
 654                                            format!("debug-session-{}", session.session_id(cx).0)
 655                                                .into();
 656                                        h_flex()
 657                                            .w_full()
 658                                            .group(id.clone())
 659                                            .justify_between()
 660                                            .child(session.label_element(cx))
 661                                            .child(
 662                                                IconButton::new(
 663                                                    "close-debug-session",
 664                                                    IconName::Close,
 665                                                )
 666                                                .visible_on_hover(id.clone())
 667                                                .icon_size(IconSize::Small)
 668                                                .on_click({
 669                                                    let weak = weak.clone();
 670                                                    move |_, window, cx| {
 671                                                        weak.update(cx, |panel, cx| {
 672                                                            panel.close_session(
 673                                                                weak_session_id,
 674                                                                window,
 675                                                                cx,
 676                                                            );
 677                                                        })
 678                                                        .ok();
 679                                                        context_menu
 680                                                            .update(cx, |this, cx| {
 681                                                                this.cancel(
 682                                                                    &Default::default(),
 683                                                                    window,
 684                                                                    cx,
 685                                                                );
 686                                                            })
 687                                                            .ok();
 688                                                    }
 689                                                }),
 690                                            )
 691                                            .into_any_element()
 692                                    })
 693                                    .unwrap_or_else(|_| div().into_any_element())
 694                            }
 695                        },
 696                        {
 697                            let weak = weak.clone();
 698                            move |window, cx| {
 699                                weak.update(cx, |panel, cx| {
 700                                    panel.activate_session(session.clone(), window, cx);
 701                                })
 702                                .ok();
 703                            }
 704                        },
 705                    );
 706                }
 707                this
 708            }),
 709        )
 710    }
 711
 712    fn deploy_context_menu(
 713        &mut self,
 714        position: Point<Pixels>,
 715        window: &mut Window,
 716        cx: &mut Context<Self>,
 717    ) {
 718        if let Some(running_state) = self
 719            .active_session
 720            .as_ref()
 721            .and_then(|session| session.read(cx).mode().as_running().cloned())
 722        {
 723            let pane_items_status = running_state.read(cx).pane_items_status(cx);
 724            let this = cx.weak_entity();
 725
 726            let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
 727                for (item_kind, is_visible) in pane_items_status.into_iter() {
 728                    menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
 729                        let this = this.clone();
 730                        move |window, cx| {
 731                            this.update(cx, |this, cx| {
 732                                if let Some(running_state) =
 733                                    this.active_session.as_ref().and_then(|session| {
 734                                        session.read(cx).mode().as_running().cloned()
 735                                    })
 736                                {
 737                                    running_state.update(cx, |state, cx| {
 738                                        if is_visible {
 739                                            state.remove_pane_item(item_kind, window, cx);
 740                                        } else {
 741                                            state.add_pane_item(item_kind, position, window, cx);
 742                                        }
 743                                    })
 744                                }
 745                            })
 746                            .ok();
 747                        }
 748                    });
 749                }
 750
 751                menu
 752            });
 753
 754            window.focus(&context_menu.focus_handle(cx));
 755            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
 756                this.context_menu.take();
 757                cx.notify();
 758            });
 759            self.context_menu = Some((context_menu, position, subscription));
 760        }
 761    }
 762
 763    fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
 764        let active_session = self.active_session.clone();
 765
 766        Some(
 767            h_flex()
 768                .border_b_1()
 769                .border_color(cx.theme().colors().border)
 770                .p_1()
 771                .justify_between()
 772                .w_full()
 773                .child(
 774                    h_flex().gap_2().w_full().when_some(
 775                        active_session
 776                            .as_ref()
 777                            .and_then(|session| session.read(cx).mode().as_running()),
 778                        |this, running_session| {
 779                            let thread_status = running_session
 780                                .read(cx)
 781                                .thread_status(cx)
 782                                .unwrap_or(project::debugger::session::ThreadStatus::Exited);
 783                            let capabilities = running_session.read(cx).capabilities(cx);
 784                            this.map(|this| {
 785                                if thread_status == ThreadStatus::Running {
 786                                    this.child(
 787                                        IconButton::new("debug-pause", IconName::DebugPause)
 788                                            .icon_size(IconSize::XSmall)
 789                                            .shape(ui::IconButtonShape::Square)
 790                                            .on_click(window.listener_for(
 791                                                &running_session,
 792                                                |this, _, _window, cx| {
 793                                                    this.pause_thread(cx);
 794                                                },
 795                                            ))
 796                                            .tooltip(move |window, cx| {
 797                                                Tooltip::text("Pause program")(window, cx)
 798                                            }),
 799                                    )
 800                                } else {
 801                                    this.child(
 802                                        IconButton::new("debug-continue", IconName::DebugContinue)
 803                                            .icon_size(IconSize::XSmall)
 804                                            .shape(ui::IconButtonShape::Square)
 805                                            .on_click(window.listener_for(
 806                                                &running_session,
 807                                                |this, _, _window, cx| this.continue_thread(cx),
 808                                            ))
 809                                            .disabled(thread_status != ThreadStatus::Stopped)
 810                                            .tooltip(move |window, cx| {
 811                                                Tooltip::text("Continue program")(window, cx)
 812                                            }),
 813                                    )
 814                                }
 815                            })
 816                            .child(
 817                                IconButton::new("debug-step-over", IconName::ArrowRight)
 818                                    .icon_size(IconSize::XSmall)
 819                                    .shape(ui::IconButtonShape::Square)
 820                                    .on_click(window.listener_for(
 821                                        &running_session,
 822                                        |this, _, _window, cx| {
 823                                            this.step_over(cx);
 824                                        },
 825                                    ))
 826                                    .disabled(thread_status != ThreadStatus::Stopped)
 827                                    .tooltip(move |window, cx| {
 828                                        Tooltip::text("Step over")(window, cx)
 829                                    }),
 830                            )
 831                            .child(
 832                                IconButton::new("debug-step-out", IconName::ArrowUpRight)
 833                                    .icon_size(IconSize::XSmall)
 834                                    .shape(ui::IconButtonShape::Square)
 835                                    .on_click(window.listener_for(
 836                                        &running_session,
 837                                        |this, _, _window, cx| {
 838                                            this.step_out(cx);
 839                                        },
 840                                    ))
 841                                    .disabled(thread_status != ThreadStatus::Stopped)
 842                                    .tooltip(move |window, cx| {
 843                                        Tooltip::text("Step out")(window, cx)
 844                                    }),
 845                            )
 846                            .child(
 847                                IconButton::new("debug-step-into", IconName::ArrowDownRight)
 848                                    .icon_size(IconSize::XSmall)
 849                                    .shape(ui::IconButtonShape::Square)
 850                                    .on_click(window.listener_for(
 851                                        &running_session,
 852                                        |this, _, _window, cx| {
 853                                            this.step_in(cx);
 854                                        },
 855                                    ))
 856                                    .disabled(thread_status != ThreadStatus::Stopped)
 857                                    .tooltip(move |window, cx| {
 858                                        Tooltip::text("Step in")(window, cx)
 859                                    }),
 860                            )
 861                            .child(Divider::vertical())
 862                            .child(
 863                                IconButton::new(
 864                                    "debug-enable-breakpoint",
 865                                    IconName::DebugDisabledBreakpoint,
 866                                )
 867                                .icon_size(IconSize::XSmall)
 868                                .shape(ui::IconButtonShape::Square)
 869                                .disabled(thread_status != ThreadStatus::Stopped),
 870                            )
 871                            .child(
 872                                IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
 873                                    .icon_size(IconSize::XSmall)
 874                                    .shape(ui::IconButtonShape::Square)
 875                                    .disabled(thread_status != ThreadStatus::Stopped),
 876                            )
 877                            .child(
 878                                IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
 879                                    .icon_size(IconSize::XSmall)
 880                                    .shape(ui::IconButtonShape::Square)
 881                                    .disabled(
 882                                        thread_status == ThreadStatus::Exited
 883                                            || thread_status == ThreadStatus::Ended,
 884                                    )
 885                                    .on_click(window.listener_for(
 886                                        &running_session,
 887                                        |this, _, _window, cx| {
 888                                            this.toggle_ignore_breakpoints(cx);
 889                                        },
 890                                    ))
 891                                    .tooltip(move |window, cx| {
 892                                        Tooltip::text("Disable all breakpoints")(window, cx)
 893                                    }),
 894                            )
 895                            .child(Divider::vertical())
 896                            .child(
 897                                IconButton::new("debug-restart", IconName::DebugRestart)
 898                                    .icon_size(IconSize::XSmall)
 899                                    .on_click(window.listener_for(
 900                                        &running_session,
 901                                        |this, _, _window, cx| {
 902                                            this.restart_session(cx);
 903                                        },
 904                                    ))
 905                                    .tooltip(move |window, cx| {
 906                                        Tooltip::text("Restart")(window, cx)
 907                                    }),
 908                            )
 909                            .child(
 910                                IconButton::new("debug-stop", IconName::Power)
 911                                    .icon_size(IconSize::XSmall)
 912                                    .on_click(window.listener_for(
 913                                        &running_session,
 914                                        |this, _, _window, cx| {
 915                                            this.stop_thread(cx);
 916                                        },
 917                                    ))
 918                                    .disabled(
 919                                        thread_status != ThreadStatus::Stopped
 920                                            && thread_status != ThreadStatus::Running,
 921                                    )
 922                                    .tooltip({
 923                                        let label = if capabilities
 924                                            .supports_terminate_threads_request
 925                                            .unwrap_or_default()
 926                                        {
 927                                            "Terminate Thread"
 928                                        } else {
 929                                            "Terminate all Threads"
 930                                        };
 931                                        move |window, cx| Tooltip::text(label)(window, cx)
 932                                    }),
 933                            )
 934                        },
 935                    ),
 936                )
 937                .child(
 938                    h_flex()
 939                        .gap_2()
 940                        .when_some(
 941                            active_session
 942                                .as_ref()
 943                                .and_then(|session| session.read(cx).mode().as_running())
 944                                .cloned(),
 945                            |this, session| {
 946                                this.child(
 947                                    session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
 948                                )
 949                                .child(Divider::vertical())
 950                            },
 951                        )
 952                        .when_some(active_session.as_ref(), |this, session| {
 953                            let context_menu = self.sessions_drop_down_menu(session, window, cx);
 954                            this.child(context_menu).child(Divider::vertical())
 955                        })
 956                        .child(
 957                            IconButton::new("debug-new-session", IconName::Plus)
 958                                .icon_size(IconSize::Small)
 959                                .on_click({
 960                                    let workspace = self.workspace.clone();
 961                                    let weak_panel = cx.weak_entity();
 962                                    let past_debug_definition = self.past_debug_definition.clone();
 963                                    move |_, window, cx| {
 964                                        let weak_panel = weak_panel.clone();
 965                                        let past_debug_definition = past_debug_definition.clone();
 966
 967                                        let _ = workspace.update(cx, |this, cx| {
 968                                            let workspace = cx.weak_entity();
 969                                            this.toggle_modal(window, cx, |window, cx| {
 970                                                NewSessionModal::new(
 971                                                    past_debug_definition,
 972                                                    weak_panel,
 973                                                    workspace,
 974                                                    window,
 975                                                    cx,
 976                                                )
 977                                            });
 978                                        });
 979                                    }
 980                                })
 981                                .tooltip(|window, cx| {
 982                                    Tooltip::for_action(
 983                                        "New Debug Session",
 984                                        &CreateDebuggingSession,
 985                                        window,
 986                                        cx,
 987                                    )
 988                                }),
 989                        ),
 990                ),
 991        )
 992    }
 993
 994    fn activate_session(
 995        &mut self,
 996        session_item: Entity<DebugSession>,
 997        window: &mut Window,
 998        cx: &mut Context<Self>,
 999    ) {
1000        debug_assert!(self.sessions.contains(&session_item));
1001        session_item.focus_handle(cx).focus(window);
1002        session_item.update(cx, |this, cx| {
1003            if let Some(running) = this.mode().as_running() {
1004                running.update(cx, |this, cx| {
1005                    this.go_to_selected_stack_frame(window, cx);
1006                });
1007            }
1008        });
1009        self.active_session = Some(session_item);
1010        cx.notify();
1011    }
1012}
1013
1014impl EventEmitter<PanelEvent> for DebugPanel {}
1015impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1016
1017impl Focusable for DebugPanel {
1018    fn focus_handle(&self, _: &App) -> FocusHandle {
1019        self.focus_handle.clone()
1020    }
1021}
1022
1023impl Panel for DebugPanel {
1024    fn persistent_name() -> &'static str {
1025        "DebugPanel"
1026    }
1027
1028    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
1029        DockPosition::Bottom
1030    }
1031
1032    fn position_is_valid(&self, position: DockPosition) -> bool {
1033        position == DockPosition::Bottom
1034    }
1035
1036    fn set_position(
1037        &mut self,
1038        _position: DockPosition,
1039        _window: &mut Window,
1040        _cx: &mut Context<Self>,
1041    ) {
1042    }
1043
1044    fn size(&self, _window: &Window, _: &App) -> Pixels {
1045        self.size
1046    }
1047
1048    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1049        self.size = size.unwrap();
1050    }
1051
1052    fn remote_id() -> Option<proto::PanelId> {
1053        Some(proto::PanelId::DebugPanel)
1054    }
1055
1056    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1057        Some(IconName::Debug)
1058    }
1059
1060    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1061        if DebuggerSettings::get_global(cx).button {
1062            Some("Debug Panel")
1063        } else {
1064            None
1065        }
1066    }
1067
1068    fn toggle_action(&self) -> Box<dyn Action> {
1069        Box::new(ToggleFocus)
1070    }
1071
1072    fn activation_priority(&self) -> u32 {
1073        9
1074    }
1075    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1076}
1077
1078impl Render for DebugPanel {
1079    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1080        let has_sessions = self.sessions.len() > 0;
1081        debug_assert_eq!(has_sessions, self.active_session.is_some());
1082
1083        if self
1084            .active_session
1085            .as_ref()
1086            .and_then(|session| session.read(cx).mode().as_running().cloned())
1087            .map(|state| state.read(cx).has_open_context_menu(cx))
1088            .unwrap_or(false)
1089        {
1090            self.context_menu.take();
1091        }
1092
1093        v_flex()
1094            .size_full()
1095            .key_context("DebugPanel")
1096            .child(h_flex().children(self.top_controls_strip(window, cx)))
1097            .track_focus(&self.focus_handle(cx))
1098            .when(self.active_session.is_some(), |this| {
1099                this.on_mouse_down(
1100                    MouseButton::Right,
1101                    cx.listener(|this, event: &MouseDownEvent, window, cx| {
1102                        if this
1103                            .active_session
1104                            .as_ref()
1105                            .and_then(|session| {
1106                                session.read(cx).mode().as_running().map(|state| {
1107                                    state.read(cx).has_pane_at_position(event.position)
1108                                })
1109                            })
1110                            .unwrap_or(false)
1111                        {
1112                            this.deploy_context_menu(event.position, window, cx);
1113                        }
1114                    }),
1115                )
1116                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1117                    deferred(
1118                        anchored()
1119                            .position(*position)
1120                            .anchor(gpui::Corner::TopLeft)
1121                            .child(menu.clone()),
1122                    )
1123                    .with_priority(1)
1124                }))
1125            })
1126            .map(|this| {
1127                if has_sessions {
1128                    this.children(self.active_session.clone())
1129                } else {
1130                    this.child(
1131                        v_flex()
1132                            .h_full()
1133                            .gap_1()
1134                            .items_center()
1135                            .justify_center()
1136                            .child(
1137                                h_flex().child(
1138                                    Label::new("No Debugging Sessions")
1139                                        .size(LabelSize::Small)
1140                                        .color(Color::Muted),
1141                                ),
1142                            )
1143                            .child(
1144                                h_flex().flex_shrink().child(
1145                                    Button::new("spawn-new-session-empty-state", "New Session")
1146                                        .size(ButtonSize::Large)
1147                                        .on_click(|_, window, cx| {
1148                                            window.dispatch_action(
1149                                                CreateDebuggingSession.boxed_clone(),
1150                                                cx,
1151                                            );
1152                                        }),
1153                                ),
1154                            ),
1155                    )
1156                }
1157            })
1158            .into_any()
1159    }
1160}
1161
1162struct DebuggerProvider(Entity<DebugPanel>);
1163
1164impl workspace::DebuggerProvider for DebuggerProvider {
1165    fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
1166        self.0.update(cx, |_, cx| {
1167            cx.defer_in(window, |this, window, cx| {
1168                this.start_session(definition, window, cx);
1169            })
1170        })
1171    }
1172}