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