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