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