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