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::StartDebuggingRequestArguments;
14use dap::adapters::DebugAdapterName;
15use dap::debugger_settings::DebugPanelDockPosition;
16use dap::{
17 ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
18 client::SessionId, debugger_settings::DebuggerSettings,
19};
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::Launch, 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 mut label = parent_session.read(cx).label().clone();
449 if !label.ends_with("(child)") {
450 label = format!("{label} (child)").into();
451 }
452 let adapter = parent_session.read(cx).adapter().clone();
453 let mut binary = parent_session.read(cx).binary().clone();
454 binary.request_args = request.clone();
455 cx.spawn_in(window, async move |this, cx| {
456 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
457 let session =
458 dap_store.new_session(label, adapter, Some(parent_session.clone()), cx);
459
460 let task = session.update(cx, |session, cx| {
461 session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
462 });
463 (session, task)
464 })?;
465 Self::register_session(this, session, false, cx).await?;
466 task.await
467 })
468 .detach_and_log_err(cx);
469 }
470
471 pub(crate) fn close_session(
472 &mut self,
473 entity_id: EntityId,
474 window: &mut Window,
475 cx: &mut Context<Self>,
476 ) {
477 let Some(session) = self
478 .sessions
479 .iter()
480 .find(|other| entity_id == other.entity_id())
481 .cloned()
482 else {
483 return;
484 };
485 session.update(cx, |this, cx| {
486 this.running_state().update(cx, |this, cx| {
487 this.serialize_layout(window, cx);
488 });
489 });
490 let session_id = session.update(cx, |this, cx| this.session_id(cx));
491 let should_prompt = self
492 .project
493 .update(cx, |this, cx| {
494 let session = this.dap_store().read(cx).session_by_id(session_id);
495 session.map(|session| !session.read(cx).is_terminated())
496 })
497 .unwrap_or_default();
498
499 cx.spawn_in(window, async move |this, cx| {
500 if should_prompt {
501 let response = cx.prompt(
502 gpui::PromptLevel::Warning,
503 "This Debug Session is still running. Are you sure you want to terminate it?",
504 None,
505 &["Yes", "No"],
506 );
507 if response.await == Ok(1) {
508 return;
509 }
510 }
511 session.update(cx, |session, cx| session.shutdown(cx)).ok();
512 this.update(cx, |this, cx| {
513 this.sessions.retain(|other| entity_id != other.entity_id());
514
515 if let Some(active_session_id) = this
516 .active_session
517 .as_ref()
518 .map(|session| session.entity_id())
519 {
520 if active_session_id == entity_id {
521 this.active_session = this.sessions.first().cloned();
522 }
523 }
524 cx.notify()
525 })
526 .ok();
527 })
528 .detach();
529 }
530
531 pub(crate) fn deploy_context_menu(
532 &mut self,
533 position: Point<Pixels>,
534 window: &mut Window,
535 cx: &mut Context<Self>,
536 ) {
537 if let Some(running_state) = self
538 .active_session
539 .as_ref()
540 .map(|session| session.read(cx).running_state().clone())
541 {
542 let pane_items_status = running_state.read(cx).pane_items_status(cx);
543 let this = cx.weak_entity();
544
545 let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
546 for (item_kind, is_visible) in pane_items_status.into_iter() {
547 menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
548 let this = this.clone();
549 move |window, cx| {
550 this.update(cx, |this, cx| {
551 if let Some(running_state) = this
552 .active_session
553 .as_ref()
554 .map(|session| session.read(cx).running_state().clone())
555 {
556 running_state.update(cx, |state, cx| {
557 if is_visible {
558 state.remove_pane_item(item_kind, window, cx);
559 } else {
560 state.add_pane_item(item_kind, position, window, cx);
561 }
562 })
563 }
564 })
565 .ok();
566 }
567 });
568 }
569
570 menu
571 });
572
573 window.focus(&context_menu.focus_handle(cx));
574 let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
575 this.context_menu.take();
576 cx.notify();
577 });
578 self.context_menu = Some((context_menu, position, subscription));
579 }
580 }
581
582 pub(crate) fn top_controls_strip(
583 &mut self,
584 window: &mut Window,
585 cx: &mut Context<Self>,
586 ) -> Option<Div> {
587 let active_session = self.active_session.clone();
588 let focus_handle = self.focus_handle.clone();
589 let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
590 let div = if is_side { v_flex() } else { h_flex() };
591
592 let new_session_button = || {
593 IconButton::new("debug-new-session", IconName::Plus)
594 .icon_size(IconSize::Small)
595 .on_click({
596 move |_, window, cx| window.dispatch_action(crate::Start.boxed_clone(), cx)
597 })
598 .tooltip({
599 let focus_handle = focus_handle.clone();
600 move |window, cx| {
601 Tooltip::for_action_in(
602 "Start Debug Session",
603 &crate::Start,
604 &focus_handle,
605 window,
606 cx,
607 )
608 }
609 })
610 };
611
612 Some(
613 div.border_b_1()
614 .border_color(cx.theme().colors().border)
615 .p_1()
616 .justify_between()
617 .w_full()
618 .when(is_side, |this| this.gap_1())
619 .child(
620 h_flex()
621 .child(
622 h_flex().gap_2().w_full().when_some(
623 active_session
624 .as_ref()
625 .map(|session| session.read(cx).running_state()),
626 |this, running_state| {
627 let thread_status =
628 running_state.read(cx).thread_status(cx).unwrap_or(
629 project::debugger::session::ThreadStatus::Exited,
630 );
631 let capabilities = running_state.read(cx).capabilities(cx);
632 this.map(|this| {
633 if thread_status == ThreadStatus::Running {
634 this.child(
635 IconButton::new(
636 "debug-pause",
637 IconName::DebugPause,
638 )
639 .icon_size(IconSize::XSmall)
640 .shape(ui::IconButtonShape::Square)
641 .on_click(window.listener_for(
642 &running_state,
643 |this, _, _window, cx| {
644 this.pause_thread(cx);
645 },
646 ))
647 .tooltip({
648 let focus_handle = focus_handle.clone();
649 move |window, cx| {
650 Tooltip::for_action_in(
651 "Pause program",
652 &Pause,
653 &focus_handle,
654 window,
655 cx,
656 )
657 }
658 }),
659 )
660 } else {
661 this.child(
662 IconButton::new(
663 "debug-continue",
664 IconName::DebugContinue,
665 )
666 .icon_size(IconSize::XSmall)
667 .shape(ui::IconButtonShape::Square)
668 .on_click(window.listener_for(
669 &running_state,
670 |this, _, _window, cx| this.continue_thread(cx),
671 ))
672 .disabled(thread_status != ThreadStatus::Stopped)
673 .tooltip({
674 let focus_handle = focus_handle.clone();
675 move |window, cx| {
676 Tooltip::for_action_in(
677 "Continue program",
678 &Continue,
679 &focus_handle,
680 window,
681 cx,
682 )
683 }
684 }),
685 )
686 }
687 })
688 .child(
689 IconButton::new("debug-step-over", IconName::ArrowRight)
690 .icon_size(IconSize::XSmall)
691 .shape(ui::IconButtonShape::Square)
692 .on_click(window.listener_for(
693 &running_state,
694 |this, _, _window, cx| {
695 this.step_over(cx);
696 },
697 ))
698 .disabled(thread_status != ThreadStatus::Stopped)
699 .tooltip({
700 let focus_handle = focus_handle.clone();
701 move |window, cx| {
702 Tooltip::for_action_in(
703 "Step over",
704 &StepOver,
705 &focus_handle,
706 window,
707 cx,
708 )
709 }
710 }),
711 )
712 .child(
713 IconButton::new("debug-step-out", IconName::ArrowUpRight)
714 .icon_size(IconSize::XSmall)
715 .shape(ui::IconButtonShape::Square)
716 .on_click(window.listener_for(
717 &running_state,
718 |this, _, _window, cx| {
719 this.step_out(cx);
720 },
721 ))
722 .disabled(thread_status != ThreadStatus::Stopped)
723 .tooltip({
724 let focus_handle = focus_handle.clone();
725 move |window, cx| {
726 Tooltip::for_action_in(
727 "Step out",
728 &StepOut,
729 &focus_handle,
730 window,
731 cx,
732 )
733 }
734 }),
735 )
736 .child(
737 IconButton::new(
738 "debug-step-into",
739 IconName::ArrowDownRight,
740 )
741 .icon_size(IconSize::XSmall)
742 .shape(ui::IconButtonShape::Square)
743 .on_click(window.listener_for(
744 &running_state,
745 |this, _, _window, cx| {
746 this.step_in(cx);
747 },
748 ))
749 .disabled(thread_status != ThreadStatus::Stopped)
750 .tooltip({
751 let focus_handle = focus_handle.clone();
752 move |window, cx| {
753 Tooltip::for_action_in(
754 "Step in",
755 &StepInto,
756 &focus_handle,
757 window,
758 cx,
759 )
760 }
761 }),
762 )
763 .child(Divider::vertical())
764 .child(
765 IconButton::new("debug-restart", IconName::DebugRestart)
766 .icon_size(IconSize::XSmall)
767 .on_click(window.listener_for(
768 &running_state,
769 |this, _, _window, cx| {
770 this.restart_session(cx);
771 },
772 ))
773 .tooltip({
774 let focus_handle = focus_handle.clone();
775 move |window, cx| {
776 Tooltip::for_action_in(
777 "Restart",
778 &Restart,
779 &focus_handle,
780 window,
781 cx,
782 )
783 }
784 }),
785 )
786 .child(
787 IconButton::new("debug-stop", IconName::Power)
788 .icon_size(IconSize::XSmall)
789 .on_click(window.listener_for(
790 &running_state,
791 |this, _, _window, cx| {
792 this.stop_thread(cx);
793 },
794 ))
795 .disabled(
796 thread_status != ThreadStatus::Stopped
797 && thread_status != ThreadStatus::Running,
798 )
799 .tooltip({
800 let focus_handle = focus_handle.clone();
801 let label = if capabilities
802 .supports_terminate_threads_request
803 .unwrap_or_default()
804 {
805 "Terminate Thread"
806 } else {
807 "Terminate All Threads"
808 };
809 move |window, cx| {
810 Tooltip::for_action_in(
811 label,
812 &Stop,
813 &focus_handle,
814 window,
815 cx,
816 )
817 }
818 }),
819 )
820 .child(
821 IconButton::new("debug-disconnect", IconName::DebugDetach)
822 .icon_size(IconSize::XSmall)
823 .on_click(window.listener_for(
824 &running_state,
825 |this, _, _, cx| {
826 this.detach_client(cx);
827 },
828 ))
829 .tooltip({
830 let focus_handle = focus_handle.clone();
831 move |window, cx| {
832 Tooltip::for_action_in(
833 "Detach",
834 &Detach,
835 &focus_handle,
836 window,
837 cx,
838 )
839 }
840 }),
841 )
842 },
843 ),
844 )
845 .justify_around()
846 .when(is_side, |this| this.child(new_session_button())),
847 )
848 .child(
849 h_flex()
850 .gap_2()
851 .when(is_side, |this| this.justify_between())
852 .child(
853 h_flex().when_some(
854 active_session
855 .as_ref()
856 .map(|session| session.read(cx).running_state())
857 .cloned(),
858 |this, running_state| {
859 this.children({
860 let running_state = running_state.clone();
861 let threads =
862 running_state.update(cx, |running_state, cx| {
863 let session = running_state.session();
864 session
865 .update(cx, |session, cx| session.threads(cx))
866 });
867
868 self.render_thread_dropdown(
869 &running_state,
870 threads,
871 window,
872 cx,
873 )
874 })
875 .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
876 },
877 ),
878 )
879 .child(
880 h_flex()
881 .children(self.render_session_menu(
882 self.active_session(),
883 self.running_state(cx),
884 window,
885 cx,
886 ))
887 .when(!is_side, |this| this.child(new_session_button())),
888 ),
889 ),
890 )
891 }
892
893 pub(crate) fn activate_pane_in_direction(
894 &mut self,
895 direction: SplitDirection,
896 window: &mut Window,
897 cx: &mut Context<Self>,
898 ) {
899 if let Some(session) = self.active_session() {
900 session.update(cx, |session, cx| {
901 session.running_state().update(cx, |running, cx| {
902 running.activate_pane_in_direction(direction, window, cx);
903 })
904 });
905 }
906 }
907
908 pub(crate) fn activate_item(
909 &mut self,
910 item: DebuggerPaneItem,
911 window: &mut Window,
912 cx: &mut Context<Self>,
913 ) {
914 if let Some(session) = self.active_session() {
915 session.update(cx, |session, cx| {
916 session.running_state().update(cx, |running, cx| {
917 running.activate_item(item, window, cx);
918 });
919 });
920 }
921 }
922
923 pub(crate) fn activate_session_by_id(
924 &mut self,
925 session_id: SessionId,
926 window: &mut Window,
927 cx: &mut Context<Self>,
928 ) {
929 if let Some(session) = self
930 .sessions
931 .iter()
932 .find(|session| session.read(cx).session_id(cx) == session_id)
933 {
934 self.activate_session(session.clone(), window, cx);
935 }
936 }
937
938 pub(crate) fn activate_session(
939 &mut self,
940 session_item: Entity<DebugSession>,
941 window: &mut Window,
942 cx: &mut Context<Self>,
943 ) {
944 debug_assert!(self.sessions.contains(&session_item));
945 session_item.focus_handle(cx).focus(window);
946 session_item.update(cx, |this, cx| {
947 this.running_state().update(cx, |this, cx| {
948 this.go_to_selected_stack_frame(window, cx);
949 });
950 });
951 self.active_session = Some(session_item);
952 cx.notify();
953 }
954
955 // TODO: restore once we have proper comment preserving file edits
956 // pub(crate) fn save_scenario(
957 // &self,
958 // scenario: &DebugScenario,
959 // worktree_id: WorktreeId,
960 // window: &mut Window,
961 // cx: &mut App,
962 // ) -> Task<Result<ProjectPath>> {
963 // self.workspace
964 // .update(cx, |workspace, cx| {
965 // let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
966 // return Task::ready(Err(anyhow!("Couldn't get worktree path")));
967 // };
968
969 // let serialized_scenario = serde_json::to_value(scenario);
970
971 // cx.spawn_in(window, async move |workspace, cx| {
972 // let serialized_scenario = serialized_scenario?;
973 // let fs =
974 // workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
975
976 // path.push(paths::local_settings_folder_relative_path());
977 // if !fs.is_dir(path.as_path()).await {
978 // fs.create_dir(path.as_path()).await?;
979 // }
980 // path.pop();
981
982 // path.push(paths::local_debug_file_relative_path());
983 // let path = path.as_path();
984
985 // if !fs.is_file(path).await {
986 // fs.create_file(path, Default::default()).await?;
987 // fs.write(
988 // path,
989 // initial_local_debug_tasks_content().to_string().as_bytes(),
990 // )
991 // .await?;
992 // }
993
994 // let content = fs.load(path).await?;
995 // let mut values =
996 // serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
997 // values.push(serialized_scenario);
998 // fs.save(
999 // path,
1000 // &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
1001 // Default::default(),
1002 // )
1003 // .await?;
1004
1005 // workspace.update(cx, |workspace, cx| {
1006 // workspace
1007 // .project()
1008 // .read(cx)
1009 // .project_path_for_absolute_path(&path, cx)
1010 // .context(
1011 // "Couldn't get project path for .zed/debug.json in active worktree",
1012 // )
1013 // })?
1014 // })
1015 // })
1016 // .unwrap_or_else(|err| Task::ready(Err(err)))
1017 // }
1018
1019 pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1020 self.thread_picker_menu_handle.toggle(window, cx);
1021 }
1022
1023 pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1024 self.session_picker_menu_handle.toggle(window, cx);
1025 }
1026
1027 fn toggle_zoom(
1028 &mut self,
1029 _: &workspace::ToggleZoom,
1030 window: &mut Window,
1031 cx: &mut Context<Self>,
1032 ) {
1033 if self.is_zoomed {
1034 cx.emit(PanelEvent::ZoomOut);
1035 } else {
1036 if !self.focus_handle(cx).contains_focused(window, cx) {
1037 cx.focus_self(window);
1038 }
1039 cx.emit(PanelEvent::ZoomIn);
1040 }
1041 }
1042}
1043
1044async fn register_session_inner(
1045 this: &WeakEntity<DebugPanel>,
1046 session: Entity<Session>,
1047 cx: &mut AsyncWindowContext,
1048) -> Result<Entity<DebugSession>> {
1049 let adapter_name = session.read_with(cx, |session, _| session.adapter())?;
1050 this.update_in(cx, |_, window, cx| {
1051 cx.subscribe_in(
1052 &session,
1053 window,
1054 move |this, session, event: &SessionStateEvent, window, cx| match event {
1055 SessionStateEvent::Restart => {
1056 this.handle_restart_request(session.clone(), window, cx);
1057 }
1058 SessionStateEvent::SpawnChildSession { request } => {
1059 this.handle_start_debugging_request(request, session.clone(), window, cx);
1060 }
1061 _ => {}
1062 },
1063 )
1064 .detach();
1065 })
1066 .ok();
1067 let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
1068 let debug_session = this.update_in(cx, |this, window, cx| {
1069 this.sessions.retain(|session| {
1070 !session
1071 .read(cx)
1072 .running_state()
1073 .read(cx)
1074 .session()
1075 .read(cx)
1076 .is_terminated()
1077 });
1078
1079 let debug_session = DebugSession::running(
1080 this.project.clone(),
1081 this.workspace.clone(),
1082 session,
1083 cx.weak_entity(),
1084 serialized_layout,
1085 this.position(window, cx).axis(),
1086 window,
1087 cx,
1088 );
1089
1090 // We might want to make this an event subscription and only notify when a new thread is selected
1091 // This is used to filter the command menu correctly
1092 cx.observe(
1093 &debug_session.read(cx).running_state().clone(),
1094 |_, _, cx| cx.notify(),
1095 )
1096 .detach();
1097
1098 this.sessions.push(debug_session.clone());
1099
1100 debug_session
1101 })?;
1102 Ok(debug_session)
1103}
1104
1105impl EventEmitter<PanelEvent> for DebugPanel {}
1106impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1107
1108impl Focusable for DebugPanel {
1109 fn focus_handle(&self, _: &App) -> FocusHandle {
1110 self.focus_handle.clone()
1111 }
1112}
1113
1114impl Panel for DebugPanel {
1115 fn persistent_name() -> &'static str {
1116 "DebugPanel"
1117 }
1118
1119 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1120 match DebuggerSettings::get_global(cx).dock {
1121 DebugPanelDockPosition::Left => DockPosition::Left,
1122 DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1123 DebugPanelDockPosition::Right => DockPosition::Right,
1124 }
1125 }
1126
1127 fn position_is_valid(&self, _: DockPosition) -> bool {
1128 true
1129 }
1130
1131 fn set_position(
1132 &mut self,
1133 position: DockPosition,
1134 window: &mut Window,
1135 cx: &mut Context<Self>,
1136 ) {
1137 if position.axis() != self.position(window, cx).axis() {
1138 self.sessions.iter().for_each(|session_item| {
1139 session_item.update(cx, |item, cx| {
1140 item.running_state()
1141 .update(cx, |state, _| state.invert_axies())
1142 })
1143 });
1144 }
1145
1146 settings::update_settings_file::<DebuggerSettings>(
1147 self.fs.clone(),
1148 cx,
1149 move |settings, _| {
1150 let dock = match position {
1151 DockPosition::Left => DebugPanelDockPosition::Left,
1152 DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1153 DockPosition::Right => DebugPanelDockPosition::Right,
1154 };
1155 settings.dock = dock;
1156 },
1157 );
1158 }
1159
1160 fn size(&self, _window: &Window, _: &App) -> Pixels {
1161 self.size
1162 }
1163
1164 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1165 self.size = size.unwrap_or(px(300.));
1166 }
1167
1168 fn remote_id() -> Option<proto::PanelId> {
1169 Some(proto::PanelId::DebugPanel)
1170 }
1171
1172 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1173 Some(IconName::Debug)
1174 }
1175
1176 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1177 if DebuggerSettings::get_global(cx).button {
1178 Some("Debug Panel")
1179 } else {
1180 None
1181 }
1182 }
1183
1184 fn toggle_action(&self) -> Box<dyn Action> {
1185 Box::new(ToggleFocus)
1186 }
1187
1188 fn pane(&self) -> Option<Entity<Pane>> {
1189 None
1190 }
1191
1192 fn activation_priority(&self) -> u32 {
1193 9
1194 }
1195
1196 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1197
1198 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1199 self.is_zoomed
1200 }
1201
1202 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1203 self.is_zoomed = zoomed;
1204 cx.notify();
1205 }
1206}
1207
1208impl Render for DebugPanel {
1209 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1210 let has_sessions = self.sessions.len() > 0;
1211 let this = cx.weak_entity();
1212 debug_assert_eq!(has_sessions, self.active_session.is_some());
1213
1214 if self
1215 .active_session
1216 .as_ref()
1217 .map(|session| session.read(cx).running_state())
1218 .map(|state| state.read(cx).has_open_context_menu(cx))
1219 .unwrap_or(false)
1220 {
1221 self.context_menu.take();
1222 }
1223
1224 v_flex()
1225 .size_full()
1226 .key_context("DebugPanel")
1227 .child(h_flex().children(self.top_controls_strip(window, cx)))
1228 .track_focus(&self.focus_handle(cx))
1229 .on_action({
1230 let this = this.clone();
1231 move |_: &workspace::ActivatePaneLeft, window, cx| {
1232 this.update(cx, |this, cx| {
1233 this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1234 })
1235 .ok();
1236 }
1237 })
1238 .on_action({
1239 let this = this.clone();
1240 move |_: &workspace::ActivatePaneRight, window, cx| {
1241 this.update(cx, |this, cx| {
1242 this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1243 })
1244 .ok();
1245 }
1246 })
1247 .on_action({
1248 let this = this.clone();
1249 move |_: &workspace::ActivatePaneUp, window, cx| {
1250 this.update(cx, |this, cx| {
1251 this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1252 })
1253 .ok();
1254 }
1255 })
1256 .on_action({
1257 let this = this.clone();
1258 move |_: &workspace::ActivatePaneDown, window, cx| {
1259 this.update(cx, |this, cx| {
1260 this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1261 })
1262 .ok();
1263 }
1264 })
1265 .on_action({
1266 let this = this.clone();
1267 move |_: &FocusConsole, window, cx| {
1268 this.update(cx, |this, cx| {
1269 this.activate_item(DebuggerPaneItem::Console, window, cx);
1270 })
1271 .ok();
1272 }
1273 })
1274 .on_action({
1275 let this = this.clone();
1276 move |_: &FocusVariables, window, cx| {
1277 this.update(cx, |this, cx| {
1278 this.activate_item(DebuggerPaneItem::Variables, window, cx);
1279 })
1280 .ok();
1281 }
1282 })
1283 .on_action({
1284 let this = this.clone();
1285 move |_: &FocusBreakpointList, window, cx| {
1286 this.update(cx, |this, cx| {
1287 this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1288 })
1289 .ok();
1290 }
1291 })
1292 .on_action({
1293 let this = this.clone();
1294 move |_: &FocusFrames, window, cx| {
1295 this.update(cx, |this, cx| {
1296 this.activate_item(DebuggerPaneItem::Frames, window, cx);
1297 })
1298 .ok();
1299 }
1300 })
1301 .on_action({
1302 let this = this.clone();
1303 move |_: &FocusModules, window, cx| {
1304 this.update(cx, |this, cx| {
1305 this.activate_item(DebuggerPaneItem::Modules, window, cx);
1306 })
1307 .ok();
1308 }
1309 })
1310 .on_action({
1311 let this = this.clone();
1312 move |_: &FocusLoadedSources, window, cx| {
1313 this.update(cx, |this, cx| {
1314 this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1315 })
1316 .ok();
1317 }
1318 })
1319 .on_action({
1320 let this = this.clone();
1321 move |_: &FocusTerminal, window, cx| {
1322 this.update(cx, |this, cx| {
1323 this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1324 })
1325 .ok();
1326 }
1327 })
1328 .on_action({
1329 let this = this.clone();
1330 move |_: &ToggleThreadPicker, window, cx| {
1331 this.update(cx, |this, cx| {
1332 this.toggle_thread_picker(window, cx);
1333 })
1334 .ok();
1335 }
1336 })
1337 .on_action({
1338 let this = this.clone();
1339 move |_: &ToggleSessionPicker, window, cx| {
1340 this.update(cx, |this, cx| {
1341 this.toggle_session_picker(window, cx);
1342 })
1343 .ok();
1344 }
1345 })
1346 .on_action(cx.listener(Self::toggle_zoom))
1347 .on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
1348 let Some(session) = panel.active_session() else {
1349 return;
1350 };
1351 let active_pane = session
1352 .read(cx)
1353 .running_state()
1354 .read(cx)
1355 .active_pane()
1356 .clone();
1357 active_pane.update(cx, |pane, cx| {
1358 let is_zoomed = pane.is_zoomed();
1359 pane.set_zoomed(!is_zoomed, cx);
1360 });
1361 cx.notify();
1362 }))
1363 .when(self.active_session.is_some(), |this| {
1364 this.on_mouse_down(
1365 MouseButton::Right,
1366 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1367 if this
1368 .active_session
1369 .as_ref()
1370 .map(|session| {
1371 let state = session.read(cx).running_state();
1372 state.read(cx).has_pane_at_position(event.position)
1373 })
1374 .unwrap_or(false)
1375 {
1376 this.deploy_context_menu(event.position, window, cx);
1377 }
1378 }),
1379 )
1380 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1381 deferred(
1382 anchored()
1383 .position(*position)
1384 .anchor(gpui::Corner::TopLeft)
1385 .child(menu.clone()),
1386 )
1387 .with_priority(1)
1388 }))
1389 })
1390 .map(|this| {
1391 if has_sessions {
1392 this.children(self.active_session.clone())
1393 } else {
1394 this.child(
1395 v_flex()
1396 .h_full()
1397 .gap_1()
1398 .items_center()
1399 .justify_center()
1400 .child(
1401 h_flex().child(
1402 Label::new("No Debugging Sessions")
1403 .size(LabelSize::Small)
1404 .color(Color::Muted),
1405 ),
1406 )
1407 .child(
1408 h_flex().flex_shrink().child(
1409 Button::new("spawn-new-session-empty-state", "New Session")
1410 .size(ButtonSize::Large)
1411 .on_click(|_, window, cx| {
1412 window.dispatch_action(crate::Start.boxed_clone(), cx);
1413 }),
1414 ),
1415 ),
1416 )
1417 }
1418 })
1419 .into_any()
1420 }
1421}
1422
1423struct DebuggerProvider(Entity<DebugPanel>);
1424
1425impl workspace::DebuggerProvider for DebuggerProvider {
1426 fn start_session(
1427 &self,
1428 definition: DebugScenario,
1429 context: TaskContext,
1430 buffer: Option<Entity<Buffer>>,
1431 window: &mut Window,
1432 cx: &mut App,
1433 ) {
1434 self.0.update(cx, |_, cx| {
1435 cx.defer_in(window, |this, window, cx| {
1436 this.start_session(definition, context, buffer, None, window, cx);
1437 })
1438 })
1439 }
1440
1441 fn spawn_task_or_modal(
1442 &self,
1443 workspace: &mut Workspace,
1444 action: &tasks_ui::Spawn,
1445 window: &mut Window,
1446 cx: &mut Context<Workspace>,
1447 ) {
1448 spawn_task_or_modal(workspace, action, window, cx);
1449 }
1450
1451 fn debug_scenario_scheduled(&self, cx: &mut App) {
1452 self.0.update(cx, |this, _| {
1453 this.debug_scenario_scheduled_last = true;
1454 });
1455 }
1456
1457 fn task_scheduled(&self, cx: &mut App) {
1458 self.0.update(cx, |this, _| {
1459 this.debug_scenario_scheduled_last = false;
1460 })
1461 }
1462
1463 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
1464 self.0.read(cx).debug_scenario_scheduled_last
1465 }
1466
1467 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
1468 let session = self.0.read(cx).active_session()?;
1469 let thread = session.read(cx).running_state().read(cx).thread_id()?;
1470 session.read(cx).session(cx).read(cx).thread_state(thread)
1471 }
1472}