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