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