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