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