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