debugger_panel.rs

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