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}