debugger_panel.rs

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