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