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