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