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