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