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