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