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