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