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