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