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