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