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