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