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