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