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::Result;
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, 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 // TODO: restore once we have proper comment preserving file edits
946 // pub(crate) fn save_scenario(
947 // &self,
948 // scenario: &DebugScenario,
949 // worktree_id: WorktreeId,
950 // window: &mut Window,
951 // cx: &mut App,
952 // ) -> Task<Result<ProjectPath>> {
953 // self.workspace
954 // .update(cx, |workspace, cx| {
955 // let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
956 // return Task::ready(Err(anyhow!("Couldn't get worktree path")));
957 // };
958
959 // let serialized_scenario = serde_json::to_value(scenario);
960
961 // cx.spawn_in(window, async move |workspace, cx| {
962 // let serialized_scenario = serialized_scenario?;
963 // let fs =
964 // workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
965
966 // path.push(paths::local_settings_folder_relative_path());
967 // if !fs.is_dir(path.as_path()).await {
968 // fs.create_dir(path.as_path()).await?;
969 // }
970 // path.pop();
971
972 // path.push(paths::local_debug_file_relative_path());
973 // let path = path.as_path();
974
975 // if !fs.is_file(path).await {
976 // fs.create_file(path, Default::default()).await?;
977 // fs.write(
978 // path,
979 // initial_local_debug_tasks_content().to_string().as_bytes(),
980 // )
981 // .await?;
982 // }
983
984 // let content = fs.load(path).await?;
985 // let mut values =
986 // serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
987 // values.push(serialized_scenario);
988 // fs.save(
989 // path,
990 // &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
991 // Default::default(),
992 // )
993 // .await?;
994
995 // workspace.update(cx, |workspace, cx| {
996 // workspace
997 // .project()
998 // .read(cx)
999 // .project_path_for_absolute_path(&path, cx)
1000 // .context(
1001 // "Couldn't get project path for .zed/debug.json in active worktree",
1002 // )
1003 // })?
1004 // })
1005 // })
1006 // .unwrap_or_else(|err| Task::ready(Err(err)))
1007 // }
1008
1009 pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1010 self.thread_picker_menu_handle.toggle(window, cx);
1011 }
1012
1013 pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1014 self.session_picker_menu_handle.toggle(window, cx);
1015 }
1016}
1017
1018async fn register_session_inner(
1019 this: &WeakEntity<DebugPanel>,
1020 session: Entity<Session>,
1021 cx: &mut AsyncWindowContext,
1022) -> Result<Entity<DebugSession>> {
1023 let adapter_name = session.read_with(cx, |session, _| session.adapter())?;
1024 this.update_in(cx, |_, window, cx| {
1025 cx.subscribe_in(
1026 &session,
1027 window,
1028 move |this, session, event: &SessionStateEvent, window, cx| match event {
1029 SessionStateEvent::Restart => {
1030 this.handle_restart_request(session.clone(), window, cx);
1031 }
1032 SessionStateEvent::SpawnChildSession { request } => {
1033 this.handle_start_debugging_request(request, session.clone(), window, cx);
1034 }
1035 _ => {}
1036 },
1037 )
1038 .detach();
1039 })
1040 .ok();
1041 let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
1042 let debug_session = this.update_in(cx, |this, window, cx| {
1043 this.sessions.retain(|session| {
1044 !session
1045 .read(cx)
1046 .running_state()
1047 .read(cx)
1048 .session()
1049 .read(cx)
1050 .is_terminated()
1051 });
1052
1053 let debug_session = DebugSession::running(
1054 this.project.clone(),
1055 this.workspace.clone(),
1056 session,
1057 cx.weak_entity(),
1058 serialized_layout,
1059 this.position(window, cx).axis(),
1060 window,
1061 cx,
1062 );
1063
1064 // We might want to make this an event subscription and only notify when a new thread is selected
1065 // This is used to filter the command menu correctly
1066 cx.observe(
1067 &debug_session.read(cx).running_state().clone(),
1068 |_, _, cx| cx.notify(),
1069 )
1070 .detach();
1071
1072 this.sessions.push(debug_session.clone());
1073
1074 debug_session
1075 })?;
1076 Ok(debug_session)
1077}
1078
1079impl EventEmitter<PanelEvent> for DebugPanel {}
1080impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1081
1082impl Focusable for DebugPanel {
1083 fn focus_handle(&self, _: &App) -> FocusHandle {
1084 self.focus_handle.clone()
1085 }
1086}
1087
1088impl Panel for DebugPanel {
1089 fn persistent_name() -> &'static str {
1090 "DebugPanel"
1091 }
1092
1093 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1094 match DebuggerSettings::get_global(cx).dock {
1095 DebugPanelDockPosition::Left => DockPosition::Left,
1096 DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1097 DebugPanelDockPosition::Right => DockPosition::Right,
1098 }
1099 }
1100
1101 fn position_is_valid(&self, _: DockPosition) -> bool {
1102 true
1103 }
1104
1105 fn set_position(
1106 &mut self,
1107 position: DockPosition,
1108 window: &mut Window,
1109 cx: &mut Context<Self>,
1110 ) {
1111 if position.axis() != self.position(window, cx).axis() {
1112 self.sessions.iter().for_each(|session_item| {
1113 session_item.update(cx, |item, cx| {
1114 item.running_state()
1115 .update(cx, |state, _| state.invert_axies())
1116 })
1117 });
1118 }
1119
1120 settings::update_settings_file::<DebuggerSettings>(
1121 self.fs.clone(),
1122 cx,
1123 move |settings, _| {
1124 let dock = match position {
1125 DockPosition::Left => DebugPanelDockPosition::Left,
1126 DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1127 DockPosition::Right => DebugPanelDockPosition::Right,
1128 };
1129 settings.dock = dock;
1130 },
1131 );
1132 }
1133
1134 fn size(&self, _window: &Window, _: &App) -> Pixels {
1135 self.size
1136 }
1137
1138 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1139 self.size = size.unwrap_or(px(300.));
1140 }
1141
1142 fn remote_id() -> Option<proto::PanelId> {
1143 Some(proto::PanelId::DebugPanel)
1144 }
1145
1146 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1147 Some(IconName::Debug)
1148 }
1149
1150 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1151 if DebuggerSettings::get_global(cx).button {
1152 Some("Debug Panel")
1153 } else {
1154 None
1155 }
1156 }
1157
1158 fn toggle_action(&self) -> Box<dyn Action> {
1159 Box::new(ToggleFocus)
1160 }
1161
1162 fn pane(&self) -> Option<Entity<Pane>> {
1163 None
1164 }
1165
1166 fn activation_priority(&self) -> u32 {
1167 9
1168 }
1169
1170 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1171}
1172
1173impl Render for DebugPanel {
1174 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1175 let has_sessions = self.sessions.len() > 0;
1176 let this = cx.weak_entity();
1177 debug_assert_eq!(has_sessions, self.active_session.is_some());
1178
1179 if self
1180 .active_session
1181 .as_ref()
1182 .map(|session| session.read(cx).running_state())
1183 .map(|state| state.read(cx).has_open_context_menu(cx))
1184 .unwrap_or(false)
1185 {
1186 self.context_menu.take();
1187 }
1188
1189 v_flex()
1190 .size_full()
1191 .key_context("DebugPanel")
1192 .child(h_flex().children(self.top_controls_strip(window, cx)))
1193 .track_focus(&self.focus_handle(cx))
1194 .on_action({
1195 let this = this.clone();
1196 move |_: &workspace::ActivatePaneLeft, window, cx| {
1197 this.update(cx, |this, cx| {
1198 this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1199 })
1200 .ok();
1201 }
1202 })
1203 .on_action({
1204 let this = this.clone();
1205 move |_: &workspace::ActivatePaneRight, window, cx| {
1206 this.update(cx, |this, cx| {
1207 this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1208 })
1209 .ok();
1210 }
1211 })
1212 .on_action({
1213 let this = this.clone();
1214 move |_: &workspace::ActivatePaneUp, window, cx| {
1215 this.update(cx, |this, cx| {
1216 this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1217 })
1218 .ok();
1219 }
1220 })
1221 .on_action({
1222 let this = this.clone();
1223 move |_: &workspace::ActivatePaneDown, window, cx| {
1224 this.update(cx, |this, cx| {
1225 this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1226 })
1227 .ok();
1228 }
1229 })
1230 .on_action({
1231 let this = this.clone();
1232 move |_: &FocusConsole, window, cx| {
1233 this.update(cx, |this, cx| {
1234 this.activate_item(DebuggerPaneItem::Console, window, cx);
1235 })
1236 .ok();
1237 }
1238 })
1239 .on_action({
1240 let this = this.clone();
1241 move |_: &FocusVariables, window, cx| {
1242 this.update(cx, |this, cx| {
1243 this.activate_item(DebuggerPaneItem::Variables, window, cx);
1244 })
1245 .ok();
1246 }
1247 })
1248 .on_action({
1249 let this = this.clone();
1250 move |_: &FocusBreakpointList, window, cx| {
1251 this.update(cx, |this, cx| {
1252 this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1253 })
1254 .ok();
1255 }
1256 })
1257 .on_action({
1258 let this = this.clone();
1259 move |_: &FocusFrames, window, cx| {
1260 this.update(cx, |this, cx| {
1261 this.activate_item(DebuggerPaneItem::Frames, window, cx);
1262 })
1263 .ok();
1264 }
1265 })
1266 .on_action({
1267 let this = this.clone();
1268 move |_: &FocusModules, window, cx| {
1269 this.update(cx, |this, cx| {
1270 this.activate_item(DebuggerPaneItem::Modules, window, cx);
1271 })
1272 .ok();
1273 }
1274 })
1275 .on_action({
1276 let this = this.clone();
1277 move |_: &FocusLoadedSources, window, cx| {
1278 this.update(cx, |this, cx| {
1279 this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1280 })
1281 .ok();
1282 }
1283 })
1284 .on_action({
1285 let this = this.clone();
1286 move |_: &FocusTerminal, window, cx| {
1287 this.update(cx, |this, cx| {
1288 this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1289 })
1290 .ok();
1291 }
1292 })
1293 .on_action({
1294 let this = this.clone();
1295 move |_: &ToggleThreadPicker, window, cx| {
1296 this.update(cx, |this, cx| {
1297 this.toggle_thread_picker(window, cx);
1298 })
1299 .ok();
1300 }
1301 })
1302 .on_action({
1303 let this = this.clone();
1304 move |_: &ToggleSessionPicker, window, cx| {
1305 this.update(cx, |this, cx| {
1306 this.toggle_session_picker(window, cx);
1307 })
1308 .ok();
1309 }
1310 })
1311 .when(self.active_session.is_some(), |this| {
1312 this.on_mouse_down(
1313 MouseButton::Right,
1314 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1315 if this
1316 .active_session
1317 .as_ref()
1318 .map(|session| {
1319 let state = session.read(cx).running_state();
1320 state.read(cx).has_pane_at_position(event.position)
1321 })
1322 .unwrap_or(false)
1323 {
1324 this.deploy_context_menu(event.position, window, cx);
1325 }
1326 }),
1327 )
1328 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1329 deferred(
1330 anchored()
1331 .position(*position)
1332 .anchor(gpui::Corner::TopLeft)
1333 .child(menu.clone()),
1334 )
1335 .with_priority(1)
1336 }))
1337 })
1338 .map(|this| {
1339 if has_sessions {
1340 this.children(self.active_session.clone())
1341 } else {
1342 this.child(
1343 v_flex()
1344 .h_full()
1345 .gap_1()
1346 .items_center()
1347 .justify_center()
1348 .child(
1349 h_flex().child(
1350 Label::new("No Debugging Sessions")
1351 .size(LabelSize::Small)
1352 .color(Color::Muted),
1353 ),
1354 )
1355 .child(
1356 h_flex().flex_shrink().child(
1357 Button::new("spawn-new-session-empty-state", "New Session")
1358 .size(ButtonSize::Large)
1359 .on_click(|_, window, cx| {
1360 window.dispatch_action(crate::Start.boxed_clone(), cx);
1361 }),
1362 ),
1363 ),
1364 )
1365 }
1366 })
1367 .into_any()
1368 }
1369}
1370
1371struct DebuggerProvider(Entity<DebugPanel>);
1372
1373impl workspace::DebuggerProvider for DebuggerProvider {
1374 fn start_session(
1375 &self,
1376 definition: DebugScenario,
1377 context: TaskContext,
1378 buffer: Option<Entity<Buffer>>,
1379 window: &mut Window,
1380 cx: &mut App,
1381 ) {
1382 self.0.update(cx, |_, cx| {
1383 cx.defer_in(window, |this, window, cx| {
1384 this.start_session(definition, context, buffer, None, window, cx);
1385 })
1386 })
1387 }
1388
1389 fn spawn_task_or_modal(
1390 &self,
1391 workspace: &mut Workspace,
1392 action: &tasks_ui::Spawn,
1393 window: &mut Window,
1394 cx: &mut Context<Workspace>,
1395 ) {
1396 spawn_task_or_modal(workspace, action, window, cx);
1397 }
1398
1399 fn debug_scenario_scheduled(&self, cx: &mut App) {
1400 self.0.update(cx, |this, _| {
1401 this.debug_scenario_scheduled_last = true;
1402 });
1403 }
1404
1405 fn task_scheduled(&self, cx: &mut App) {
1406 self.0.update(cx, |this, _| {
1407 this.debug_scenario_scheduled_last = false;
1408 })
1409 }
1410
1411 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
1412 self.0.read(cx).debug_scenario_scheduled_last
1413 }
1414}