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