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