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