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