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