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::{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 if let Some(project_path) = workspace
1025 .project()
1026 .read(cx)
1027 .project_path_for_absolute_path(&path, cx)
1028 {
1029 Ok(project_path)
1030 } else {
1031 Err(anyhow!(
1032 "Couldn't get project path for .zed/debug.json in active worktree"
1033 ))
1034 }
1035 })?
1036 })
1037 })
1038 .unwrap_or_else(|err| Task::ready(Err(err)))
1039 }
1040}
1041
1042impl EventEmitter<PanelEvent> for DebugPanel {}
1043impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1044
1045impl Focusable for DebugPanel {
1046 fn focus_handle(&self, _: &App) -> FocusHandle {
1047 self.focus_handle.clone()
1048 }
1049}
1050
1051impl Panel for DebugPanel {
1052 fn persistent_name() -> &'static str {
1053 "DebugPanel"
1054 }
1055
1056 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1057 match DebuggerSettings::get_global(cx).dock {
1058 DebugPanelDockPosition::Left => DockPosition::Left,
1059 DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1060 DebugPanelDockPosition::Right => DockPosition::Right,
1061 }
1062 }
1063
1064 fn position_is_valid(&self, _: DockPosition) -> bool {
1065 true
1066 }
1067
1068 fn set_position(
1069 &mut self,
1070 position: DockPosition,
1071 window: &mut Window,
1072 cx: &mut Context<Self>,
1073 ) {
1074 if position.axis() != self.position(window, cx).axis() {
1075 self.sessions.iter().for_each(|session_item| {
1076 session_item.update(cx, |item, cx| {
1077 item.running_state()
1078 .update(cx, |state, _| state.invert_axies())
1079 })
1080 });
1081 }
1082
1083 settings::update_settings_file::<DebuggerSettings>(
1084 self.fs.clone(),
1085 cx,
1086 move |settings, _| {
1087 let dock = match position {
1088 DockPosition::Left => DebugPanelDockPosition::Left,
1089 DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1090 DockPosition::Right => DebugPanelDockPosition::Right,
1091 };
1092 settings.dock = dock;
1093 },
1094 );
1095 }
1096
1097 fn size(&self, _window: &Window, _: &App) -> Pixels {
1098 self.size
1099 }
1100
1101 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1102 self.size = size.unwrap_or(px(300.));
1103 }
1104
1105 fn remote_id() -> Option<proto::PanelId> {
1106 Some(proto::PanelId::DebugPanel)
1107 }
1108
1109 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1110 Some(IconName::Debug)
1111 }
1112
1113 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1114 if DebuggerSettings::get_global(cx).button {
1115 Some("Debug Panel")
1116 } else {
1117 None
1118 }
1119 }
1120
1121 fn toggle_action(&self) -> Box<dyn Action> {
1122 Box::new(ToggleFocus)
1123 }
1124
1125 fn pane(&self) -> Option<Entity<Pane>> {
1126 None
1127 }
1128
1129 fn activation_priority(&self) -> u32 {
1130 9
1131 }
1132
1133 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1134}
1135
1136impl Render for DebugPanel {
1137 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1138 let has_sessions = self.sessions.len() > 0;
1139 let this = cx.weak_entity();
1140 debug_assert_eq!(has_sessions, self.active_session.is_some());
1141
1142 if self
1143 .active_session
1144 .as_ref()
1145 .map(|session| session.read(cx).running_state())
1146 .map(|state| state.read(cx).has_open_context_menu(cx))
1147 .unwrap_or(false)
1148 {
1149 self.context_menu.take();
1150 }
1151
1152 v_flex()
1153 .size_full()
1154 .key_context("DebugPanel")
1155 .child(h_flex().children(self.top_controls_strip(window, cx)))
1156 .track_focus(&self.focus_handle(cx))
1157 .on_action({
1158 let this = this.clone();
1159 move |_: &workspace::ActivatePaneLeft, window, cx| {
1160 this.update(cx, |this, cx| {
1161 this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1162 })
1163 .ok();
1164 }
1165 })
1166 .on_action({
1167 let this = this.clone();
1168 move |_: &workspace::ActivatePaneRight, window, cx| {
1169 this.update(cx, |this, cx| {
1170 this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1171 })
1172 .ok();
1173 }
1174 })
1175 .on_action({
1176 let this = this.clone();
1177 move |_: &workspace::ActivatePaneUp, window, cx| {
1178 this.update(cx, |this, cx| {
1179 this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1180 })
1181 .ok();
1182 }
1183 })
1184 .on_action({
1185 let this = this.clone();
1186 move |_: &workspace::ActivatePaneDown, window, cx| {
1187 this.update(cx, |this, cx| {
1188 this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1189 })
1190 .ok();
1191 }
1192 })
1193 .on_action({
1194 let this = this.clone();
1195 move |_: &FocusConsole, window, cx| {
1196 this.update(cx, |this, cx| {
1197 this.activate_item(DebuggerPaneItem::Console, window, cx);
1198 })
1199 .ok();
1200 }
1201 })
1202 .on_action({
1203 let this = this.clone();
1204 move |_: &FocusVariables, window, cx| {
1205 this.update(cx, |this, cx| {
1206 this.activate_item(DebuggerPaneItem::Variables, window, cx);
1207 })
1208 .ok();
1209 }
1210 })
1211 .on_action({
1212 let this = this.clone();
1213 move |_: &FocusBreakpointList, window, cx| {
1214 this.update(cx, |this, cx| {
1215 this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1216 })
1217 .ok();
1218 }
1219 })
1220 .on_action({
1221 let this = this.clone();
1222 move |_: &FocusFrames, window, cx| {
1223 this.update(cx, |this, cx| {
1224 this.activate_item(DebuggerPaneItem::Frames, window, cx);
1225 })
1226 .ok();
1227 }
1228 })
1229 .on_action({
1230 let this = this.clone();
1231 move |_: &FocusModules, window, cx| {
1232 this.update(cx, |this, cx| {
1233 this.activate_item(DebuggerPaneItem::Modules, window, cx);
1234 })
1235 .ok();
1236 }
1237 })
1238 .on_action({
1239 let this = this.clone();
1240 move |_: &FocusLoadedSources, window, cx| {
1241 this.update(cx, |this, cx| {
1242 this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1243 })
1244 .ok();
1245 }
1246 })
1247 .on_action({
1248 let this = this.clone();
1249 move |_: &FocusTerminal, window, cx| {
1250 this.update(cx, |this, cx| {
1251 this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1252 })
1253 .ok();
1254 }
1255 })
1256 .when(self.active_session.is_some(), |this| {
1257 this.on_mouse_down(
1258 MouseButton::Right,
1259 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1260 if this
1261 .active_session
1262 .as_ref()
1263 .map(|session| {
1264 let state = session.read(cx).running_state();
1265 state.read(cx).has_pane_at_position(event.position)
1266 })
1267 .unwrap_or(false)
1268 {
1269 this.deploy_context_menu(event.position, window, cx);
1270 }
1271 }),
1272 )
1273 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1274 deferred(
1275 anchored()
1276 .position(*position)
1277 .anchor(gpui::Corner::TopLeft)
1278 .child(menu.clone()),
1279 )
1280 .with_priority(1)
1281 }))
1282 })
1283 .map(|this| {
1284 if has_sessions {
1285 this.children(self.active_session.clone())
1286 } else {
1287 this.child(
1288 v_flex()
1289 .h_full()
1290 .gap_1()
1291 .items_center()
1292 .justify_center()
1293 .child(
1294 h_flex().child(
1295 Label::new("No Debugging Sessions")
1296 .size(LabelSize::Small)
1297 .color(Color::Muted),
1298 ),
1299 )
1300 .child(
1301 h_flex().flex_shrink().child(
1302 Button::new("spawn-new-session-empty-state", "New Session")
1303 .size(ButtonSize::Large)
1304 .on_click(|_, window, cx| {
1305 window.dispatch_action(crate::Start.boxed_clone(), cx);
1306 }),
1307 ),
1308 ),
1309 )
1310 }
1311 })
1312 .into_any()
1313 }
1314}
1315
1316struct DebuggerProvider(Entity<DebugPanel>);
1317
1318impl workspace::DebuggerProvider for DebuggerProvider {
1319 fn start_session(
1320 &self,
1321 definition: DebugScenario,
1322 context: TaskContext,
1323 buffer: Option<Entity<Buffer>>,
1324 window: &mut Window,
1325 cx: &mut App,
1326 ) {
1327 self.0.update(cx, |_, cx| {
1328 cx.defer_in(window, |this, window, cx| {
1329 this.start_session(definition, context, buffer, None, window, cx);
1330 })
1331 })
1332 }
1333}