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