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