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