Skip to repository content1924 lines · 79.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:30:21.141Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
debugger_panel.rs
1use crate::persistence::DebuggerPaneItem;
2use crate::session::DebugSession;
3use crate::session::running::RunningState;
4use crate::session::running::breakpoint_list::BreakpointList;
5
6use crate::{
7 ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
8 FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
9 NewProcessModal, NewProcessMode, Pause, RerunSession, StepInto, StepOut, StepOver, Stop,
10 ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
11};
12use anyhow::{Context as _, Result, anyhow};
13use collections::IndexMap;
14use dap::adapters::DebugAdapterName;
15use dap::{DapRegistry, StartDebuggingRequestArguments};
16use dap::{client::SessionId, debugger_settings::DebuggerSettings};
17use editor::Editor;
18use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
19use gpui::{
20 Action, Anchor, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity,
21 EntityId, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point,
22 Subscription, Task, TaskExt, WeakEntity, anchored, deferred,
23};
24
25use itertools::Itertools as _;
26use language::Buffer;
27use project::debugger::session::{Session, SessionQuirks, SessionState, SessionStateEvent};
28use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
29use project::{Project, debugger::session::ThreadStatus};
30use rpc::proto::{self};
31use settings::Settings;
32use std::sync::Arc;
33use task::{DebugScenario, SharedTaskContext};
34
35use ui::{
36 ButtonLike, ContextMenu, Divider, ElevationIndex, PopoverMenu, PopoverMenuHandle, SplitButton,
37 Tab, TintColor, Tooltip, prelude::*,
38};
39use util::redact::redact_command;
40use util::rel_path::RelPath;
41use util::{ResultExt, debug_panic, maybe};
42use workspace::SplitDirection;
43use workspace::item::SaveOptions;
44use workspace::{
45 Item, Pane, Workspace,
46 dock::{DockPosition, Panel, PanelEvent},
47};
48use zed_actions::debug_panel::ToggleFocus;
49
50pub struct DebuggerHistoryFeatureFlag;
51
52impl FeatureFlag for DebuggerHistoryFeatureFlag {
53 const NAME: &'static str = "debugger-history";
54 type Value = PresenceFlag;
55}
56register_feature_flag!(DebuggerHistoryFeatureFlag);
57
58const DEBUG_PANEL_KEY: &str = "DebugPanel";
59
60pub struct DebugPanel {
61 active_session: Option<Entity<DebugSession>>,
62 project: Entity<Project>,
63 workspace: WeakEntity<Workspace>,
64 focus_handle: FocusHandle,
65 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
66 debug_scenario_scheduled_last: bool,
67 pub(crate) sessions_with_children:
68 IndexMap<Entity<DebugSession>, Vec<WeakEntity<DebugSession>>>,
69 pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
70 pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
71 fs: Arc<dyn Fs>,
72 is_zoomed: bool,
73 _subscriptions: [Subscription; 1],
74 breakpoint_list: Entity<BreakpointList>,
75}
76
77impl DebugPanel {
78 pub fn new(
79 workspace: &Workspace,
80 window: &mut Window,
81 cx: &mut Context<Workspace>,
82 ) -> Entity<Self> {
83 cx.new(|cx| {
84 let project = workspace.project().clone();
85 let focus_handle = cx.focus_handle();
86 let thread_picker_menu_handle = PopoverMenuHandle::default();
87 let session_picker_menu_handle = PopoverMenuHandle::default();
88
89 let focus_subscription = cx.on_focus(
90 &focus_handle,
91 window,
92 |this: &mut DebugPanel, window, cx| {
93 this.focus_active_item(window, cx);
94 },
95 );
96
97 Self {
98 sessions_with_children: Default::default(),
99 active_session: None,
100 focus_handle,
101 breakpoint_list: BreakpointList::new(
102 None,
103 workspace.weak_handle(),
104 &project,
105 window,
106 cx,
107 ),
108 project,
109 workspace: workspace.weak_handle(),
110 context_menu: None,
111 fs: workspace.app_state().fs.clone(),
112 thread_picker_menu_handle,
113 session_picker_menu_handle,
114 is_zoomed: false,
115 _subscriptions: [focus_subscription],
116 debug_scenario_scheduled_last: true,
117 }
118 })
119 }
120
121 pub(crate) fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context<Self>) {
122 let Some(session) = self.active_session.clone() else {
123 return;
124 };
125 let active_pane = session
126 .read(cx)
127 .running_state()
128 .read(cx)
129 .active_pane()
130 .clone();
131 active_pane.update(cx, |pane, cx| {
132 pane.focus_active_item(window, cx);
133 });
134 }
135
136 #[cfg(test)]
137 pub(crate) fn sessions(&self) -> impl Iterator<Item = Entity<DebugSession>> {
138 self.sessions_with_children.keys().cloned()
139 }
140
141 pub fn active_session(&self) -> Option<Entity<DebugSession>> {
142 self.active_session.clone()
143 }
144
145 pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
146 self.active_session()
147 .map(|session| session.read(cx).running_state().clone())
148 }
149
150 pub fn project(&self) -> &Entity<Project> {
151 &self.project
152 }
153
154 pub fn load(
155 workspace: WeakEntity<Workspace>,
156 cx: &mut AsyncWindowContext,
157 ) -> Task<Result<Entity<Self>>> {
158 cx.spawn(async move |cx| {
159 workspace.update_in(cx, |workspace, window, cx| {
160 let debug_panel = DebugPanel::new(workspace, window, cx);
161
162 workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
163 workspace.project().read(cx).breakpoint_store().update(
164 cx,
165 |breakpoint_store, cx| {
166 breakpoint_store.clear_breakpoints(cx);
167 },
168 )
169 });
170
171 workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
172
173 debug_panel
174 })
175 })
176 }
177
178 pub fn start_session(
179 &mut self,
180 scenario: DebugScenario,
181 task_context: SharedTaskContext,
182 active_buffer: Option<Entity<Buffer>>,
183 worktree_id: Option<WorktreeId>,
184 window: &mut Window,
185 cx: &mut Context<Self>,
186 ) {
187 let dap_store = self.project.read(cx).dap_store();
188 let Some(adapter) = DapRegistry::global(cx).adapter(&scenario.adapter) else {
189 return;
190 };
191 let quirks = SessionQuirks {
192 compact: adapter.compact_child_session(),
193 prefer_thread_name: adapter.prefer_thread_name(),
194 };
195 let session = dap_store.update(cx, |dap_store, cx| {
196 dap_store.new_session(
197 Some(scenario.label.clone()),
198 DebugAdapterName(scenario.adapter.clone()),
199 task_context.clone(),
200 None,
201 quirks,
202 cx,
203 )
204 });
205 let worktree = worktree_id.or_else(|| {
206 active_buffer
207 .as_ref()
208 .and_then(|buffer| buffer.read(cx).file())
209 .map(|f| f.worktree_id(cx))
210 });
211
212 let Some(worktree) = worktree
213 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
214 .or_else(|| self.project.read(cx).visible_worktrees(cx).next())
215 else {
216 log::debug!("Could not find a worktree to spawn the debug session in");
217 return;
218 };
219
220 self.debug_scenario_scheduled_last = true;
221 if let Some(inventory) = self
222 .project
223 .read(cx)
224 .task_store()
225 .read(cx)
226 .task_inventory()
227 .cloned()
228 {
229 inventory.update(cx, |inventory, _| {
230 inventory.scenario_scheduled(
231 scenario.clone(),
232 task_context.clone(),
233 worktree_id,
234 active_buffer.as_ref().map(|buffer| buffer.downgrade()),
235 );
236 })
237 }
238 let task = cx.spawn_in(window, {
239 let session = session.clone();
240 async move |this, cx| {
241 let debug_session =
242 Self::register_session(this.clone(), session.clone(), true, cx).await?;
243 let definition = debug_session
244 .update_in(cx, |debug_session, window, cx| {
245 debug_session.running_state().update(cx, |running, cx| {
246 if scenario.build.is_some() {
247 running.scenario = Some(scenario.clone());
248 running.scenario_context = Some(DebugScenarioContext {
249 active_buffer: active_buffer
250 .as_ref()
251 .map(|entity| entity.downgrade()),
252 task_context: task_context.clone(),
253 worktree_id,
254 });
255 };
256 running.resolve_scenario(
257 scenario,
258 task_context,
259 active_buffer,
260 worktree_id,
261 window,
262 cx,
263 )
264 })
265 })?
266 .await?;
267 dap_store
268 .update(cx, |dap_store, cx| {
269 dap_store.boot_session(session.clone(), definition, worktree, cx)
270 })
271 .await
272 }
273 });
274
275 let boot_task = cx.spawn({
276 let session = session.clone();
277
278 async move |_, cx| {
279 if let Err(error) = task.await {
280 let redacted_error = redact_command(&format!("{error:#}"));
281 log::error!("{redacted_error}");
282 session
283 .update(cx, |session, cx| {
284 session
285 .console_output(cx)
286 .unbounded_send(format!("error: {:#}", redacted_error))
287 .ok();
288 session.shutdown(cx)
289 })
290 .await;
291 }
292 anyhow::Ok(())
293 }
294 });
295
296 session.update(cx, |session, _| match &mut session.state {
297 SessionState::Booting(state_task) => {
298 *state_task = Some(boot_task);
299 }
300 SessionState::Running(_) => {
301 debug_panic!("Session state should be in building because we are just starting it");
302 }
303 });
304 }
305
306 pub(crate) fn rerun_last_session(
307 &mut self,
308 workspace: &mut Workspace,
309 window: &mut Window,
310 cx: &mut Context<Self>,
311 ) {
312 let task_store = workspace.project().read(cx).task_store().clone();
313 let Some(task_inventory) = task_store.read(cx).task_inventory() else {
314 return;
315 };
316 let workspace = self.workspace.clone();
317 let Some((scenario, context)) = task_inventory.read(cx).last_scheduled_scenario().cloned()
318 else {
319 window.defer(cx, move |window, cx| {
320 workspace
321 .update(cx, |workspace, cx| {
322 NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
323 })
324 .ok();
325 });
326 return;
327 };
328
329 let DebugScenarioContext {
330 task_context,
331 worktree_id,
332 active_buffer,
333 } = context;
334
335 let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
336
337 self.start_session(
338 scenario,
339 task_context,
340 active_buffer,
341 worktree_id,
342 window,
343 cx,
344 );
345 }
346
347 pub(crate) async fn register_session(
348 this: WeakEntity<Self>,
349 session: Entity<Session>,
350 focus: bool,
351 cx: &mut AsyncWindowContext,
352 ) -> Result<Entity<DebugSession>> {
353 let debug_session = register_session_inner(&this, session, cx).await?;
354
355 let workspace = this.update_in(cx, |this, window, cx| {
356 if focus {
357 this.activate_session(debug_session.clone(), window, cx);
358 }
359
360 this.workspace.clone()
361 })?;
362 workspace.update_in(cx, |workspace, window, cx| {
363 workspace.focus_panel::<Self>(window, cx);
364 })?;
365 Ok(debug_session)
366 }
367
368 pub(crate) fn handle_restart_request(
369 &mut self,
370 mut curr_session: Entity<Session>,
371 window: &mut Window,
372 cx: &mut Context<Self>,
373 ) {
374 while let Some(parent_session) = curr_session.read(cx).parent_session().cloned() {
375 curr_session = parent_session;
376 }
377
378 let Some(worktree) = curr_session.read(cx).worktree() else {
379 log::error!("Attempted to restart a non-running session");
380 return;
381 };
382
383 let dap_store_handle = self.project.read(cx).dap_store();
384 let label = curr_session.read(cx).label();
385 let quirks = curr_session.read(cx).quirks();
386 let adapter = curr_session.read(cx).adapter();
387 let binary = curr_session.read(cx).binary().cloned().unwrap();
388 let task_context = curr_session.read(cx).task_context().clone();
389
390 let curr_session_id = curr_session.read(cx).session_id();
391 self.sessions_with_children
392 .retain(|session, _| session.read(cx).session_id(cx) != curr_session_id);
393 let task = dap_store_handle.update(cx, |dap_store, cx| {
394 dap_store.shutdown_session(curr_session_id, cx)
395 });
396
397 cx.spawn_in(window, async move |this, cx| {
398 task.await.log_err();
399
400 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
401 let session = dap_store.new_session(label, adapter, task_context, None, quirks, cx);
402
403 let task = session.update(cx, |session, cx| {
404 session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
405 });
406 (session, task)
407 });
408 Self::register_session(this.clone(), session.clone(), true, cx).await?;
409
410 if let Err(error) = task.await {
411 session
412 .update(cx, |session, cx| {
413 session
414 .console_output(cx)
415 .unbounded_send(format!(
416 "Session failed to restart with error: {}",
417 error
418 ))
419 .ok();
420 session.shutdown(cx)
421 })
422 .await;
423
424 return Err(error);
425 };
426
427 Ok(())
428 })
429 .detach_and_log_err(cx);
430 }
431
432 pub fn handle_start_debugging_request(
433 &mut self,
434 request: &StartDebuggingRequestArguments,
435 parent_session: Entity<Session>,
436 window: &mut Window,
437 cx: &mut Context<Self>,
438 ) {
439 let Some(worktree) = parent_session.read(cx).worktree() else {
440 log::error!("Attempted to start a child-session from a non-running session");
441 return;
442 };
443
444 let dap_store_handle = self.project.read(cx).dap_store();
445 let label = self.label_for_child_session(&parent_session, request, cx);
446 let adapter = parent_session.read(cx).adapter();
447 let quirks = parent_session.read(cx).quirks();
448 let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
449 log::error!("Attempted to start a child-session without a binary");
450 return;
451 };
452 let task_context = parent_session.read(cx).task_context().clone();
453 binary.request_args = request.clone();
454 cx.spawn_in(window, async move |this, cx| {
455 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
456 let session = dap_store.new_session(
457 label,
458 adapter,
459 task_context,
460 Some(parent_session.clone()),
461 quirks,
462 cx,
463 );
464
465 let task = session.update(cx, |session, cx| {
466 session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
467 });
468 (session, task)
469 });
470 // Focus child sessions if the parent has never emitted a stopped event;
471 // this improves our JavaScript experience, as it always spawns a "main" session that then spawns subsessions.
472 let parent_ever_stopped = parent_session.update(cx, |this, _| this.has_ever_stopped());
473 Self::register_session(this, session, !parent_ever_stopped, cx).await?;
474 task.await
475 })
476 .detach_and_log_err(cx);
477 }
478
479 pub(crate) fn close_session(
480 &mut self,
481 entity_id: EntityId,
482 window: &mut Window,
483 cx: &mut Context<Self>,
484 ) {
485 let Some(session) = self
486 .sessions_with_children
487 .keys()
488 .find(|other| entity_id == other.entity_id())
489 .cloned()
490 else {
491 return;
492 };
493 session.update(cx, |this, cx| {
494 this.running_state().update(cx, |this, cx| {
495 this.serialize_layout(window, cx);
496 });
497 });
498 cx.spawn_in(window, async move |this, cx| {
499 session.update(cx, |session, cx| session.shutdown(cx));
500 this.update(cx, |this, cx| {
501 this.retain_sessions(&|other: &Entity<DebugSession>| {
502 entity_id != other.entity_id()
503 });
504 if let Some(active_session_id) = this
505 .active_session
506 .as_ref()
507 .map(|session| session.entity_id())
508 && active_session_id == entity_id
509 {
510 this.active_session = this.sessions_with_children.keys().next().cloned();
511 }
512 cx.notify()
513 })
514 .ok();
515 })
516 .detach();
517 }
518
519 pub(crate) fn deploy_context_menu(
520 &mut self,
521 position: Point<Pixels>,
522 window: &mut Window,
523 cx: &mut Context<Self>,
524 ) {
525 if let Some(running_state) = self
526 .active_session
527 .as_ref()
528 .map(|session| session.read(cx).running_state().clone())
529 {
530 let pane_items_status = running_state.read(cx).pane_items_status(cx);
531 let this = cx.weak_entity();
532
533 let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
534 for (item_kind, is_visible) in pane_items_status.into_iter() {
535 menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
536 let this = this.clone();
537 move |window, cx| {
538 this.update(cx, |this, cx| {
539 if let Some(running_state) = this
540 .active_session
541 .as_ref()
542 .map(|session| session.read(cx).running_state().clone())
543 {
544 running_state.update(cx, |state, cx| {
545 if is_visible {
546 state.remove_pane_item(item_kind, window, cx);
547 } else {
548 state.add_pane_item(item_kind, position, window, cx);
549 }
550 })
551 }
552 })
553 .ok();
554 }
555 });
556 }
557
558 menu
559 });
560
561 window.focus(&context_menu.focus_handle(cx), cx);
562 let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
563 this.context_menu.take();
564 cx.notify();
565 });
566 self.context_menu = Some((context_menu, position, subscription));
567 }
568 }
569
570 fn copy_debug_adapter_arguments(
571 &mut self,
572 _: &CopyDebugAdapterArguments,
573 _window: &mut Window,
574 cx: &mut Context<Self>,
575 ) {
576 let content = maybe!({
577 let mut session = self.active_session()?.read(cx).session(cx);
578 while let Some(parent) = session.read(cx).parent_session().cloned() {
579 session = parent;
580 }
581 let binary = session.read(cx).binary()?;
582 let content = serde_json::to_string_pretty(&binary).ok()?;
583 Some(content)
584 });
585 if let Some(content) = content {
586 cx.write_to_clipboard(ClipboardItem::new_string(content));
587 }
588 }
589
590 pub(crate) fn top_controls_strip(
591 &mut self,
592 window: &mut Window,
593 cx: &mut Context<Self>,
594 ) -> Option<Div> {
595 let active_session = self.active_session.clone();
596 let focus_handle = self.focus_handle.clone();
597 let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
598 let div = if is_side { v_flex() } else { h_flex() };
599
600 let new_session_button = || {
601 IconButton::new("debug-new-session", IconName::Plus)
602 .icon_size(IconSize::Small)
603 .on_click({
604 move |_, window, cx| window.dispatch_action(crate::Start.boxed_clone(), cx)
605 })
606 .tooltip({
607 let focus_handle = focus_handle.clone();
608 move |_window, cx| {
609 Tooltip::for_action_in(
610 "Start Debug Session",
611 &crate::Start,
612 &focus_handle,
613 cx,
614 )
615 }
616 })
617 };
618
619 let edit_debug_json_button = || {
620 IconButton::new("debug-edit-debug-json", IconName::Code)
621 .icon_size(IconSize::Small)
622 .on_click(|_, window, cx| {
623 window.dispatch_action(zed_actions::OpenProjectDebugTasks.boxed_clone(), cx);
624 })
625 .tooltip(Tooltip::text("Edit debug.json"))
626 };
627
628 let documentation_button = || {
629 IconButton::new("debug-open-documentation", IconName::CircleHelp)
630 .icon_size(IconSize::Small)
631 .on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
632 .tooltip(Tooltip::text("Open Documentation"))
633 };
634
635 let logs_button = || {
636 IconButton::new("debug-open-logs", IconName::Notepad)
637 .icon_size(IconSize::Small)
638 .on_click(move |_, window, cx| {
639 window.dispatch_action(debugger_tools::OpenDebugAdapterLogs.boxed_clone(), cx)
640 })
641 .tooltip(Tooltip::text("Open Debug Adapter Logs"))
642 };
643
644 let close_bottom_panel_button = {
645 h_flex().pl_0p5().gap_1().child(Divider::vertical()).child(
646 IconButton::new("debug-close-panel", IconName::Close)
647 .icon_size(IconSize::Small)
648 .on_click(move |_, window, cx| {
649 window.dispatch_action(workspace::ToggleBottomDock.boxed_clone(), cx)
650 })
651 .tooltip(Tooltip::text("Close Panel")),
652 )
653 };
654
655 let thread_status = active_session
656 .as_ref()
657 .map(|session| session.read(cx).running_state())
658 .and_then(|state| state.read(cx).thread_status(cx))
659 .unwrap_or(project::debugger::session::ThreadStatus::Exited);
660
661 Some(
662 div.w_full()
663 .py_1()
664 .px_1p5()
665 .justify_between()
666 .border_b_1()
667 .border_color(cx.theme().colors().border)
668 .when(is_side, |this| this.gap_1().h(Tab::container_height(cx)))
669 .child(
670 h_flex()
671 .justify_between()
672 .child(
673 h_flex().gap_1().w_full().when_some(
674 active_session
675 .as_ref()
676 .map(|session| session.read(cx).running_state()),
677 |this, running_state| {
678 let capabilities = running_state.read(cx).capabilities(cx);
679 let supports_detach =
680 running_state.read(cx).session().read(cx).is_attached();
681
682 this.map(|this| {
683 if thread_status == ThreadStatus::Running {
684 this.child(
685 IconButton::new(
686 "debug-pause",
687 IconName::DebugPause,
688 )
689 .icon_size(IconSize::Small)
690 .on_click(window.listener_for(
691 running_state,
692 |this, _, _window, cx| {
693 this.pause_thread(cx);
694 },
695 ))
696 .tooltip({
697 let focus_handle = focus_handle.clone();
698 move |_window, cx| {
699 Tooltip::for_action_in(
700 "Pause Program",
701 &Pause,
702 &focus_handle,
703 cx,
704 )
705 }
706 }),
707 )
708 } else {
709 this.child(
710 IconButton::new(
711 "debug-continue",
712 IconName::DebugContinue,
713 )
714 .icon_size(IconSize::Small)
715 .disabled(thread_status != ThreadStatus::Stopped)
716 .on_click(window.listener_for(
717 running_state,
718 |this, _, _window, cx| this.continue_thread(cx),
719 ))
720 .tooltip({
721 let focus_handle = focus_handle.clone();
722 move |_window, cx| {
723 Tooltip::for_action_in(
724 "Continue Program",
725 &Continue,
726 &focus_handle,
727 cx,
728 )
729 }
730 }),
731 )
732 }
733 })
734 .child(
735 IconButton::new("step-over", IconName::DebugStepOver)
736 .icon_size(IconSize::Small)
737 .on_click(window.listener_for(
738 running_state,
739 |this, _, _window, cx| {
740 this.step_over(cx);
741 },
742 ))
743 .disabled(thread_status != ThreadStatus::Stopped)
744 .tooltip({
745 let focus_handle = focus_handle.clone();
746 move |_window, cx| {
747 Tooltip::for_action_in(
748 "Step Over",
749 &StepOver,
750 &focus_handle,
751 cx,
752 )
753 }
754 }),
755 )
756 .child(
757 IconButton::new("step-into", IconName::DebugStepInto)
758 .icon_size(IconSize::Small)
759 .on_click(window.listener_for(
760 running_state,
761 |this, _, _window, cx| {
762 this.step_in(cx);
763 },
764 ))
765 .disabled(thread_status != ThreadStatus::Stopped)
766 .tooltip({
767 let focus_handle = focus_handle.clone();
768 move |_window, cx| {
769 Tooltip::for_action_in(
770 "Step In",
771 &StepInto,
772 &focus_handle,
773 cx,
774 )
775 }
776 }),
777 )
778 .child(
779 IconButton::new("step-out", IconName::DebugStepOut)
780 .icon_size(IconSize::Small)
781 .on_click(window.listener_for(
782 running_state,
783 |this, _, _window, cx| {
784 this.step_out(cx);
785 },
786 ))
787 .disabled(thread_status != ThreadStatus::Stopped)
788 .tooltip({
789 let focus_handle = focus_handle.clone();
790 move |_window, cx| {
791 Tooltip::for_action_in(
792 "Step Out",
793 &StepOut,
794 &focus_handle,
795 cx,
796 )
797 }
798 }),
799 )
800 .child(Divider::vertical())
801 .child(
802 IconButton::new("debug-restart", IconName::RotateCcw)
803 .icon_size(IconSize::Small)
804 .on_click(window.listener_for(
805 running_state,
806 |this, _, window, cx| {
807 this.rerun_session(window, cx);
808 },
809 ))
810 .tooltip({
811 let focus_handle = focus_handle.clone();
812 move |_window, cx| {
813 Tooltip::for_action_in(
814 "Rerun Session",
815 &RerunSession,
816 &focus_handle,
817 cx,
818 )
819 }
820 }),
821 )
822 .child(
823 IconButton::new("debug-stop", IconName::Power)
824 .icon_size(IconSize::Small)
825 .on_click(window.listener_for(
826 running_state,
827 |this, _, _window, cx| {
828 if this.session().read(cx).is_building() {
829 this.session().update(cx, |session, cx| {
830 session.shutdown(cx).detach()
831 });
832 } else {
833 this.stop_thread(cx);
834 }
835 },
836 ))
837 .disabled(active_session.as_ref().is_none_or(
838 |session| {
839 session
840 .read(cx)
841 .session(cx)
842 .read(cx)
843 .is_terminated()
844 },
845 ))
846 .tooltip({
847 let focus_handle = focus_handle.clone();
848 let label = if capabilities
849 .supports_terminate_threads_request
850 .unwrap_or_default()
851 {
852 "Terminate Thread"
853 } else {
854 "Terminate All Threads"
855 };
856 move |_window, cx| {
857 Tooltip::for_action_in(
858 label,
859 &Stop,
860 &focus_handle,
861 cx,
862 )
863 }
864 }),
865 )
866 .when(supports_detach, |div| {
867 div.child(
868 IconButton::new(
869 "debug-disconnect",
870 IconName::DebugDetach,
871 )
872 .disabled(
873 thread_status != ThreadStatus::Stopped
874 && thread_status != ThreadStatus::Running,
875 )
876 .icon_size(IconSize::Small)
877 .on_click(window.listener_for(
878 running_state,
879 |this, _, _, cx| {
880 this.detach_client(cx);
881 },
882 ))
883 .tooltip({
884 let focus_handle = focus_handle.clone();
885 move |_window, cx| {
886 Tooltip::for_action_in(
887 "Detach",
888 &Detach,
889 &focus_handle,
890 cx,
891 )
892 }
893 }),
894 )
895 })
896 .when(
897 cx.has_flag::<DebuggerHistoryFeatureFlag>(),
898 |this| {
899 this.child(Divider::vertical()).child(
900 SplitButton::new(
901 self.render_history_button(
902 &running_state,
903 thread_status,
904 window,
905 ),
906 self.render_history_toggle_button(
907 thread_status,
908 &running_state,
909 )
910 .into_any_element(),
911 )
912 .style(ui::SplitButtonStyle::Outlined),
913 )
914 },
915 )
916 },
917 ),
918 )
919 .when(is_side, |this| {
920 this.child(new_session_button())
921 .child(edit_debug_json_button())
922 .child(documentation_button())
923 .child(logs_button())
924 }),
925 )
926 .child(
927 h_flex()
928 .gap_0p5()
929 .when(is_side, |this| this.justify_between())
930 .child(
931 h_flex().when_some(
932 active_session
933 .as_ref()
934 .map(|session| session.read(cx).running_state())
935 .cloned(),
936 |this, running_state| {
937 let threads = running_state.update(cx, |running_state, cx| {
938 let session = running_state.session();
939 session.read(cx).is_started().then(|| {
940 session.update(cx, |session, cx| session.threads(cx))
941 })
942 });
943
944 let thread_dropdown = threads.and_then(|threads| {
945 self.render_thread_dropdown(
946 &running_state,
947 threads,
948 window,
949 cx,
950 )
951 });
952
953 this.when_some(thread_dropdown, |this, dropdown| {
954 this.child(dropdown).when(!is_side, |this| {
955 this.gap_0p5().child(Divider::vertical())
956 })
957 })
958 },
959 ),
960 )
961 .child(
962 h_flex()
963 .gap_0p5()
964 .children(self.render_session_menu(
965 self.active_session(),
966 self.running_state(cx),
967 window,
968 cx,
969 ))
970 .when(!is_side, |this| {
971 this.child(new_session_button())
972 .child(edit_debug_json_button())
973 .child(documentation_button())
974 .child(logs_button())
975 .child(close_bottom_panel_button)
976 }),
977 ),
978 ),
979 )
980 }
981
982 pub(crate) fn activate_pane_in_direction(
983 &mut self,
984 direction: SplitDirection,
985 window: &mut Window,
986 cx: &mut Context<Self>,
987 ) {
988 if let Some(session) = self.active_session() {
989 session.update(cx, |session, cx| {
990 session.running_state().update(cx, |running, cx| {
991 running.activate_pane_in_direction(direction, window, cx);
992 })
993 });
994 }
995 }
996
997 pub(crate) fn activate_item(
998 &mut self,
999 item: DebuggerPaneItem,
1000 window: &mut Window,
1001 cx: &mut Context<Self>,
1002 ) {
1003 if let Some(session) = self.active_session() {
1004 session.update(cx, |session, cx| {
1005 session.running_state().update(cx, |running, cx| {
1006 running.activate_item(item, window, cx);
1007 });
1008 });
1009 }
1010 }
1011
1012 pub(crate) fn activate_session_by_id(
1013 &mut self,
1014 session_id: SessionId,
1015 window: &mut Window,
1016 cx: &mut Context<Self>,
1017 ) {
1018 if let Some(session) = self
1019 .sessions_with_children
1020 .keys()
1021 .find(|session| session.read(cx).session_id(cx) == session_id)
1022 {
1023 self.activate_session(session.clone(), window, cx);
1024 }
1025 }
1026
1027 pub(crate) fn activate_session(
1028 &mut self,
1029 session_item: Entity<DebugSession>,
1030 window: &mut Window,
1031 cx: &mut Context<Self>,
1032 ) {
1033 debug_assert!(self.sessions_with_children.contains_key(&session_item));
1034 session_item.focus_handle(cx).focus(window, cx);
1035 session_item.update(cx, |this, cx| {
1036 this.running_state().update(cx, |this, cx| {
1037 this.go_to_selected_stack_frame(window, cx);
1038 });
1039 });
1040 self.active_session = Some(session_item);
1041 cx.notify();
1042 }
1043
1044 pub(crate) fn go_to_scenario_definition(
1045 &self,
1046 kind: TaskSourceKind,
1047 scenario: DebugScenario,
1048 worktree_id: WorktreeId,
1049 window: &mut Window,
1050 cx: &mut Context<Self>,
1051 ) -> Task<Result<()>> {
1052 let Some(workspace) = self.workspace.upgrade() else {
1053 return Task::ready(Ok(()));
1054 };
1055 let project_path = match kind {
1056 TaskSourceKind::AbsPath { abs_path, .. } => {
1057 let Some(project_path) = workspace
1058 .read(cx)
1059 .project()
1060 .read(cx)
1061 .project_path_for_absolute_path(&abs_path, cx)
1062 else {
1063 return Task::ready(Err(anyhow!("no abs path")));
1064 };
1065
1066 project_path
1067 }
1068 TaskSourceKind::Worktree {
1069 id,
1070 directory_in_worktree: dir,
1071 ..
1072 } => {
1073 let relative_path = if dir.ends_with(RelPath::from_unix_str(".vscode").unwrap()) {
1074 dir.join(RelPath::from_unix_str("launch.json").unwrap())
1075 } else {
1076 dir.join(RelPath::from_unix_str("debug.json").unwrap())
1077 };
1078 ProjectPath {
1079 worktree_id: id,
1080 path: relative_path.into(),
1081 }
1082 }
1083 _ => return self.save_scenario(scenario, worktree_id, window, cx),
1084 };
1085
1086 let editor = workspace.update(cx, |workspace, cx| {
1087 workspace.open_path(project_path, None, true, window, cx)
1088 });
1089 cx.spawn_in(window, async move |_, cx| {
1090 let editor = editor.await?;
1091 let editor = cx
1092 .update(|_, cx| editor.act_as::<Editor>(cx))?
1093 .context("expected editor")?;
1094
1095 // unfortunately debug tasks don't have an easy way to globally
1096 // identify them. to jump to the one that you just created or an
1097 // old one that you're choosing to edit we use a heuristic of searching for a line with `label: <your label>` from the end rather than the start so we bias towards more renctly
1098 editor.update_in(cx, |editor, window, cx| {
1099 let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| {
1100 if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") {
1101 Some(row)
1102 } else {
1103 None
1104 }
1105 });
1106 if let Some(row) = row {
1107 editor.go_to_singleton_buffer_point(
1108 text::Point::new(row as u32, 4),
1109 window,
1110 cx,
1111 );
1112 }
1113 })?;
1114
1115 Ok(())
1116 })
1117 }
1118
1119 pub(crate) fn save_scenario(
1120 &self,
1121 scenario: DebugScenario,
1122 worktree_id: WorktreeId,
1123 window: &mut Window,
1124 cx: &mut Context<Self>,
1125 ) -> Task<Result<()>> {
1126 let this = cx.weak_entity();
1127 let project = self.project.clone();
1128 self.workspace
1129 .update(cx, |workspace, cx| {
1130 let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
1131 return Task::ready(Err(anyhow!("Couldn't get worktree path")));
1132 };
1133
1134 let serialized_scenario = serde_json::to_value(scenario);
1135
1136 cx.spawn_in(window, async move |workspace, cx| {
1137 let serialized_scenario = serialized_scenario?;
1138 let fs =
1139 workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
1140
1141 path.push(paths::local_settings_folder_name());
1142 if !fs.is_dir(path.as_path()).await {
1143 fs.create_dir(path.as_path()).await?;
1144 }
1145 path.pop();
1146
1147 path.push(paths::local_debug_file_relative_path().as_std_path());
1148 let path = path.as_path();
1149
1150 if !fs.is_file(path).await {
1151 fs.create_file(path, Default::default()).await?;
1152 fs.write(
1153 path,
1154 settings::initial_local_debug_tasks_content()
1155 .to_string()
1156 .as_bytes(),
1157 )
1158 .await?;
1159 }
1160 let project_path = workspace.update(cx, |workspace, cx| {
1161 workspace
1162 .project()
1163 .read(cx)
1164 .project_path_for_absolute_path(path, cx)
1165 .context(
1166 "Couldn't get project path for .zed/debug.json in active worktree",
1167 )
1168 })??;
1169
1170 let editor = this
1171 .update_in(cx, |this, window, cx| {
1172 this.workspace.update(cx, |workspace, cx| {
1173 workspace.open_path(project_path, None, true, window, cx)
1174 })
1175 })??
1176 .await?;
1177 let editor = cx
1178 .update(|_, cx| editor.act_as::<Editor>(cx))?
1179 .context("expected editor")?;
1180
1181 let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
1182 .lines()
1183 .map(|l| format!(" {l}"))
1184 .join("\n");
1185
1186 editor
1187 .update_in(cx, |editor, window, cx| {
1188 Self::insert_task_into_editor(editor, new_scenario, project, window, cx)
1189 })??
1190 .await
1191 })
1192 })
1193 .unwrap_or_else(|err| Task::ready(Err(err)))
1194 }
1195
1196 pub fn insert_task_into_editor(
1197 editor: &mut Editor,
1198 new_scenario: String,
1199 project: Entity<Project>,
1200 window: &mut Window,
1201 cx: &mut Context<Editor>,
1202 ) -> Result<Task<Result<()>>> {
1203 tasks_ui::insert_task_json_into_editor(editor, new_scenario, window, cx)?;
1204 Ok(editor.save(SaveOptions::default(), project, window, cx))
1205 }
1206
1207 pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1208 self.thread_picker_menu_handle.toggle(window, cx);
1209 }
1210
1211 pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1212 self.session_picker_menu_handle.toggle(window, cx);
1213 }
1214
1215 fn toggle_zoom(
1216 &mut self,
1217 _: &workspace::ToggleZoom,
1218 window: &mut Window,
1219 cx: &mut Context<Self>,
1220 ) {
1221 if self.is_zoomed {
1222 cx.emit(PanelEvent::ZoomOut);
1223 } else {
1224 if !self.focus_handle(cx).contains_focused(window, cx) {
1225 cx.focus_self(window);
1226 }
1227 cx.emit(PanelEvent::ZoomIn);
1228 }
1229 }
1230
1231 fn label_for_child_session(
1232 &self,
1233 parent_session: &Entity<Session>,
1234 request: &StartDebuggingRequestArguments,
1235 cx: &mut Context<'_, Self>,
1236 ) -> Option<SharedString> {
1237 let adapter = parent_session.read(cx).adapter();
1238 if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter)
1239 && let Some(label) = adapter.label_for_child_session(request)
1240 {
1241 return Some(label.into());
1242 }
1243 None
1244 }
1245
1246 fn retain_sessions(&mut self, keep: &dyn Fn(&Entity<DebugSession>) -> bool) {
1247 self.sessions_with_children
1248 .retain(|session, _| keep(session));
1249 for children in self.sessions_with_children.values_mut() {
1250 children.retain(|child| {
1251 let Some(child) = child.upgrade() else {
1252 return false;
1253 };
1254 keep(&child)
1255 });
1256 }
1257 }
1258
1259 fn render_history_button(
1260 &self,
1261 running_state: &Entity<RunningState>,
1262 thread_status: ThreadStatus,
1263 window: &mut Window,
1264 ) -> ButtonLike {
1265 ButtonLike::new_rounded_left("debug-back-in-history")
1266 .layer(ElevationIndex::ModalSurface)
1267 .child(Icon::new(IconName::HistoryRerun).size(IconSize::Small))
1268 .disabled(
1269 thread_status == ThreadStatus::Running || thread_status == ThreadStatus::Stepping,
1270 )
1271 .tooltip(Tooltip::text("Step Back in Session History"))
1272 .on_click(window.listener_for(running_state, |this, _, _window, cx| {
1273 this.session().update(cx, |session, cx| {
1274 let ix = session
1275 .active_snapshot_index()
1276 .unwrap_or_else(|| session.historic_snapshots().len());
1277
1278 session.select_historic_snapshot(Some(ix.saturating_sub(1)), cx);
1279 })
1280 }))
1281 }
1282
1283 fn render_history_toggle_button(
1284 &self,
1285 thread_status: ThreadStatus,
1286 running_state: &Entity<RunningState>,
1287 ) -> impl IntoElement {
1288 let chevron_button_size = rems_from_px(20.);
1289 PopoverMenu::new("debug-back-in-history-menu")
1290 .trigger(
1291 ButtonLike::new_rounded_right("debug-back-in-history-menu-trigger")
1292 .layer(ElevationIndex::ModalSurface)
1293 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1294 .disabled(
1295 thread_status == ThreadStatus::Running
1296 || thread_status == ThreadStatus::Stepping,
1297 )
1298 .width(chevron_button_size)
1299 .height(chevron_button_size.into())
1300 .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
1301 )
1302 .menu({
1303 let running_state = running_state.clone();
1304 move |window, cx| {
1305 let handler =
1306 |ix: Option<usize>, running_state: Entity<RunningState>, cx: &mut App| {
1307 running_state.update(cx, |state, cx| {
1308 state.session().update(cx, |session, cx| {
1309 session.select_historic_snapshot(ix, cx);
1310 })
1311 })
1312 };
1313
1314 let running_state = running_state.clone();
1315 Some(ContextMenu::build(
1316 window,
1317 cx,
1318 move |mut context_menu, _window, cx| {
1319 let history = running_state
1320 .read(cx)
1321 .session()
1322 .read(cx)
1323 .historic_snapshots();
1324
1325 context_menu = context_menu.entry("Current State", None, {
1326 let running_state = running_state.clone();
1327 move |_window, cx| {
1328 handler(None, running_state.clone(), cx);
1329 }
1330 });
1331
1332 if !history.is_empty() {
1333 context_menu = context_menu.separator();
1334 }
1335
1336 for (ix, _) in history.iter().enumerate().rev() {
1337 context_menu =
1338 context_menu.entry(format!("history-{}", ix + 1), None, {
1339 let running_state = running_state.clone();
1340 move |_window, cx| {
1341 handler(Some(ix), running_state.clone(), cx);
1342 }
1343 });
1344 }
1345
1346 context_menu
1347 },
1348 ))
1349 }
1350 })
1351 .anchor(Anchor::TopRight)
1352 }
1353}
1354
1355async fn register_session_inner(
1356 this: &WeakEntity<DebugPanel>,
1357 session: Entity<Session>,
1358 cx: &mut AsyncWindowContext,
1359) -> Result<Entity<DebugSession>> {
1360 let adapter_name = session.read_with(cx, |session, _| session.adapter());
1361 this.update_in(cx, |_, window, cx| {
1362 cx.subscribe_in(
1363 &session,
1364 window,
1365 move |this, session, event: &SessionStateEvent, window, cx| match event {
1366 SessionStateEvent::Restart => {
1367 this.handle_restart_request(session.clone(), window, cx);
1368 }
1369 SessionStateEvent::SpawnChildSession { request } => {
1370 this.handle_start_debugging_request(request, session.clone(), window, cx);
1371 }
1372 _ => {}
1373 },
1374 )
1375 .detach();
1376 })
1377 .ok();
1378 let serialized_layout = this
1379 .update(cx, |_, cx| {
1380 persistence::get_serialized_layout(&adapter_name, &db::kvp::KeyValueStore::global(cx))
1381 })
1382 .ok()
1383 .flatten();
1384 let debug_session = this.update_in(cx, |this, window, cx| {
1385 let parent_session = this
1386 .sessions_with_children
1387 .keys()
1388 .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
1389 .cloned();
1390 this.retain_sessions(&|session: &Entity<DebugSession>| {
1391 !session
1392 .read(cx)
1393 .running_state()
1394 .read(cx)
1395 .session()
1396 .read(cx)
1397 .is_terminated()
1398 });
1399
1400 let debug_session = DebugSession::running(
1401 this.project.clone(),
1402 this.workspace.clone(),
1403 parent_session
1404 .as_ref()
1405 .map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
1406 session,
1407 serialized_layout,
1408 this.position(window, cx).axis(),
1409 window,
1410 cx,
1411 );
1412
1413 // We might want to make this an event subscription and only notify when a new thread is selected
1414 // This is used to filter the command menu correctly
1415 cx.observe(
1416 &debug_session.read(cx).running_state().clone(),
1417 |_, _, cx| cx.notify(),
1418 )
1419 .detach();
1420 let insert_position = this
1421 .sessions_with_children
1422 .keys()
1423 .position(|session| Some(session) == parent_session.as_ref())
1424 .map(|position| position + 1)
1425 .unwrap_or(this.sessions_with_children.len());
1426 // Maintain topological sort order of sessions
1427 let (_, old) = this.sessions_with_children.insert_before(
1428 insert_position,
1429 debug_session.clone(),
1430 Default::default(),
1431 );
1432 debug_assert!(old.is_none());
1433 if let Some(parent_session) = parent_session {
1434 this.sessions_with_children
1435 .entry(parent_session)
1436 .and_modify(|children| children.push(debug_session.downgrade()));
1437 }
1438
1439 debug_session
1440 })?;
1441 Ok(debug_session)
1442}
1443
1444impl EventEmitter<PanelEvent> for DebugPanel {}
1445
1446impl Focusable for DebugPanel {
1447 fn focus_handle(&self, _: &App) -> FocusHandle {
1448 self.focus_handle.clone()
1449 }
1450}
1451
1452impl Panel for DebugPanel {
1453 fn persistent_name() -> &'static str {
1454 "DebugPanel"
1455 }
1456
1457 fn panel_key() -> &'static str {
1458 DEBUG_PANEL_KEY
1459 }
1460
1461 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1462 DebuggerSettings::get_global(cx).dock.into()
1463 }
1464
1465 fn position_is_valid(&self, _: DockPosition) -> bool {
1466 true
1467 }
1468
1469 fn set_position(
1470 &mut self,
1471 position: DockPosition,
1472 window: &mut Window,
1473 cx: &mut Context<Self>,
1474 ) {
1475 if position.axis() != self.position(window, cx).axis() {
1476 self.sessions_with_children.keys().for_each(|session_item| {
1477 session_item.update(cx, |item, cx| {
1478 item.running_state()
1479 .update(cx, |state, cx| state.invert_axies(cx))
1480 })
1481 });
1482 }
1483
1484 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1485 settings.debugger.get_or_insert_default().dock = Some(position.into());
1486 });
1487 }
1488
1489 fn default_size(&self, _window: &Window, _: &App) -> Pixels {
1490 px(300.)
1491 }
1492
1493 fn remote_id() -> Option<proto::PanelId> {
1494 Some(proto::PanelId::DebugPanel)
1495 }
1496
1497 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1498 DebuggerSettings::get_global(cx)
1499 .button
1500 .then_some(IconName::Debug)
1501 }
1502
1503 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1504 if DebuggerSettings::get_global(cx).button {
1505 Some("Debug Panel")
1506 } else {
1507 None
1508 }
1509 }
1510
1511 fn toggle_action(&self) -> Box<dyn Action> {
1512 Box::new(ToggleFocus)
1513 }
1514
1515 fn pane(&self) -> Option<Entity<Pane>> {
1516 None
1517 }
1518
1519 fn activation_priority(&self) -> u32 {
1520 7
1521 }
1522
1523 fn hide_button_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
1524 Some(workspace::HideStatusItem::new(|settings| {
1525 settings.debugger.get_or_insert_default().button = Some(false);
1526 }))
1527 }
1528
1529 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1530
1531 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1532 self.is_zoomed
1533 }
1534
1535 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1536 self.is_zoomed = zoomed;
1537 cx.notify();
1538 }
1539}
1540
1541impl Render for DebugPanel {
1542 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1543 let this = cx.weak_entity();
1544
1545 if self
1546 .active_session
1547 .as_ref()
1548 .map(|session| session.read(cx).running_state())
1549 .map(|state| state.read(cx).has_open_context_menu(cx))
1550 .unwrap_or(false)
1551 {
1552 self.context_menu.take();
1553 }
1554
1555 v_flex()
1556 .size_full()
1557 .key_context("DebugPanel")
1558 .child(h_flex().children(self.top_controls_strip(window, cx)))
1559 .track_focus(&self.focus_handle(cx))
1560 .on_action({
1561 let this = this.clone();
1562 move |_: &workspace::ActivatePaneLeft, window, cx| {
1563 this.update(cx, |this, cx| {
1564 this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1565 })
1566 .ok();
1567 }
1568 })
1569 .on_action({
1570 let this = this.clone();
1571 move |_: &workspace::ActivatePaneRight, window, cx| {
1572 this.update(cx, |this, cx| {
1573 this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1574 })
1575 .ok();
1576 }
1577 })
1578 .on_action({
1579 let this = this.clone();
1580 move |_: &workspace::ActivatePaneUp, window, cx| {
1581 this.update(cx, |this, cx| {
1582 this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1583 })
1584 .ok();
1585 }
1586 })
1587 .on_action({
1588 let this = this.clone();
1589 move |_: &workspace::ActivatePaneDown, window, cx| {
1590 this.update(cx, |this, cx| {
1591 this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1592 })
1593 .ok();
1594 }
1595 })
1596 .on_action({
1597 let this = this.clone();
1598 move |_: &FocusConsole, window, cx| {
1599 this.update(cx, |this, cx| {
1600 this.activate_item(DebuggerPaneItem::Console, window, cx);
1601 })
1602 .ok();
1603 }
1604 })
1605 .on_action({
1606 let this = this.clone();
1607 move |_: &FocusVariables, window, cx| {
1608 this.update(cx, |this, cx| {
1609 this.activate_item(DebuggerPaneItem::Variables, window, cx);
1610 })
1611 .ok();
1612 }
1613 })
1614 .on_action({
1615 let this = this.clone();
1616 move |_: &FocusBreakpointList, window, cx| {
1617 this.update(cx, |this, cx| {
1618 this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1619 })
1620 .ok();
1621 }
1622 })
1623 .on_action({
1624 let this = this.clone();
1625 move |_: &FocusFrames, window, cx| {
1626 this.update(cx, |this, cx| {
1627 this.activate_item(DebuggerPaneItem::Frames, window, cx);
1628 })
1629 .ok();
1630 }
1631 })
1632 .on_action({
1633 let this = this.clone();
1634 move |_: &FocusModules, window, cx| {
1635 this.update(cx, |this, cx| {
1636 this.activate_item(DebuggerPaneItem::Modules, window, cx);
1637 })
1638 .ok();
1639 }
1640 })
1641 .on_action({
1642 let this = this.clone();
1643 move |_: &FocusLoadedSources, window, cx| {
1644 this.update(cx, |this, cx| {
1645 this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1646 })
1647 .ok();
1648 }
1649 })
1650 .on_action({
1651 let this = this.clone();
1652 move |_: &FocusTerminal, window, cx| {
1653 this.update(cx, |this, cx| {
1654 this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1655 })
1656 .ok();
1657 }
1658 })
1659 .on_action({
1660 let this = this.clone();
1661 move |_: &ToggleThreadPicker, window, cx| {
1662 this.update(cx, |this, cx| {
1663 this.toggle_thread_picker(window, cx);
1664 })
1665 .ok();
1666 }
1667 })
1668 .on_action({
1669 move |_: &ToggleSessionPicker, window, cx| {
1670 this.update(cx, |this, cx| {
1671 this.toggle_session_picker(window, cx);
1672 })
1673 .ok();
1674 }
1675 })
1676 .on_action(cx.listener(Self::toggle_zoom))
1677 .on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
1678 let Some(session) = panel.active_session() else {
1679 return;
1680 };
1681 let active_pane = session
1682 .read(cx)
1683 .running_state()
1684 .read(cx)
1685 .active_pane()
1686 .clone();
1687 active_pane.update(cx, |pane, cx| {
1688 let is_zoomed = pane.is_zoomed();
1689 pane.set_zoomed(!is_zoomed, cx);
1690 });
1691 cx.notify();
1692 }))
1693 .on_action(cx.listener(Self::copy_debug_adapter_arguments))
1694 .when(self.active_session.is_some(), |this| {
1695 this.on_mouse_down(
1696 MouseButton::Right,
1697 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1698 if this
1699 .active_session
1700 .as_ref()
1701 .map(|session| {
1702 let state = session.read(cx).running_state();
1703 state.read(cx).has_pane_at_position(event.position)
1704 })
1705 .unwrap_or(false)
1706 {
1707 this.deploy_context_menu(event.position, window, cx);
1708 }
1709 }),
1710 )
1711 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1712 deferred(
1713 anchored()
1714 .position(*position)
1715 .anchor(gpui::Anchor::TopLeft)
1716 .child(menu.clone()),
1717 )
1718 .with_priority(1)
1719 }))
1720 })
1721 .map(|this| {
1722 if let Some(active_session) = self.active_session.clone() {
1723 this.child(active_session)
1724 } else {
1725 let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
1726
1727 let welcome_experience = v_flex()
1728 .when_else(
1729 docked_to_bottom,
1730 |this| this.w_2_3().h_full().pr_8(),
1731 |this| this.w_full().h_1_3(),
1732 )
1733 .items_center()
1734 .justify_center()
1735 .gap_2()
1736 .child(
1737 Button::new("spawn-new-session-empty-state", "New Session")
1738 .start_icon(
1739 Icon::new(IconName::Plus)
1740 .size(IconSize::Small)
1741 .color(Color::Muted),
1742 )
1743 .on_click(|_, window, cx| {
1744 window.dispatch_action(crate::Start.boxed_clone(), cx);
1745 }),
1746 )
1747 .child(
1748 Button::new("edit-debug-settings", "Edit debug.json")
1749 .start_icon(
1750 Icon::new(IconName::Code)
1751 .size(IconSize::Small)
1752 .color(Color::Muted),
1753 )
1754 .on_click(|_, window, cx| {
1755 window.dispatch_action(
1756 zed_actions::OpenProjectDebugTasks.boxed_clone(),
1757 cx,
1758 );
1759 }),
1760 )
1761 .child(
1762 Button::new("open-debugger-docs", "Debugger Docs")
1763 .start_icon(
1764 Icon::new(IconName::Book)
1765 .size(IconSize::Small)
1766 .color(Color::Muted),
1767 )
1768 .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
1769 )
1770 .child(
1771 Button::new(
1772 "spawn-new-session-install-extensions",
1773 "Debugger Extensions",
1774 )
1775 .start_icon(
1776 Icon::new(IconName::Blocks)
1777 .size(IconSize::Small)
1778 .color(Color::Muted),
1779 )
1780 .on_click(|_, window, cx| {
1781 window.dispatch_action(
1782 zed_actions::Extensions {
1783 category_filter: Some(
1784 zed_actions::ExtensionCategoryFilter::DebugAdapters,
1785 ),
1786 id: None,
1787 }
1788 .boxed_clone(),
1789 cx,
1790 );
1791 }),
1792 );
1793
1794 let has_breakpoints = self
1795 .project
1796 .read(cx)
1797 .breakpoint_store()
1798 .read(cx)
1799 .all_source_breakpoints(cx)
1800 .values()
1801 .any(|breakpoints| !breakpoints.is_empty());
1802
1803 let breakpoint_list = v_flex()
1804 .group("base-breakpoint-list")
1805 .when_else(
1806 docked_to_bottom,
1807 |this| this.min_w_1_3().h_full(),
1808 |this| this.size_full().h_2_3(),
1809 )
1810 .child(
1811 h_flex()
1812 .track_focus(&self.breakpoint_list.focus_handle(cx))
1813 .h(Tab::container_height(cx))
1814 .p_1p5()
1815 .w_full()
1816 .justify_between()
1817 .border_b_1()
1818 .border_color(cx.theme().colors().border_variant)
1819 .child(Label::new("Breakpoints").size(LabelSize::Small))
1820 .child(
1821 h_flex().visible_on_hover("base-breakpoint-list").child(
1822 self.breakpoint_list.read(cx).render_control_strip(),
1823 ),
1824 ),
1825 )
1826 .when(has_breakpoints, |this| {
1827 this.child(self.breakpoint_list.clone())
1828 })
1829 .when(!has_breakpoints, |this| {
1830 this.child(
1831 v_flex().size_full().items_center().justify_center().child(
1832 Label::new("No Breakpoints Set")
1833 .size(LabelSize::Small)
1834 .color(Color::Muted),
1835 ),
1836 )
1837 });
1838
1839 this.child(
1840 v_flex()
1841 .size_full()
1842 .overflow_hidden()
1843 .gap_1()
1844 .items_center()
1845 .justify_center()
1846 .map(|this| {
1847 if docked_to_bottom {
1848 this.child(
1849 h_flex()
1850 .size_full()
1851 .child(breakpoint_list)
1852 .child(Divider::vertical().h_full())
1853 .child(welcome_experience)
1854 .child(Divider::vertical().h_full()),
1855 )
1856 } else {
1857 this.child(
1858 v_flex()
1859 .size_full()
1860 .child(welcome_experience)
1861 .child(Divider::horizontal())
1862 .child(breakpoint_list),
1863 )
1864 }
1865 }),
1866 )
1867 }
1868 })
1869 .into_any()
1870 }
1871}
1872
1873struct DebuggerProvider(Entity<DebugPanel>);
1874
1875impl workspace::DebuggerProvider for DebuggerProvider {
1876 fn start_session(
1877 &self,
1878 definition: DebugScenario,
1879 context: SharedTaskContext,
1880 buffer: Option<Entity<Buffer>>,
1881 worktree_id: Option<WorktreeId>,
1882 window: &mut Window,
1883 cx: &mut App,
1884 ) {
1885 self.0.update(cx, |_, cx| {
1886 cx.defer_in(window, move |this, window, cx| {
1887 this.start_session(definition, context, buffer, worktree_id, window, cx);
1888 })
1889 })
1890 }
1891
1892 fn spawn_task_or_modal(
1893 &self,
1894 workspace: &mut Workspace,
1895 action: &tasks_ui::Spawn,
1896 window: &mut Window,
1897 cx: &mut Context<Workspace>,
1898 ) {
1899 spawn_task_or_modal(workspace, action, window, cx);
1900 }
1901
1902 fn debug_scenario_scheduled(&self, cx: &mut App) {
1903 self.0.update(cx, |this, _| {
1904 this.debug_scenario_scheduled_last = true;
1905 });
1906 }
1907
1908 fn task_scheduled(&self, cx: &mut App) {
1909 self.0.update(cx, |this, _| {
1910 this.debug_scenario_scheduled_last = false;
1911 })
1912 }
1913
1914 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
1915 self.0.read(cx).debug_scenario_scheduled_last
1916 }
1917
1918 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
1919 let session = self.0.read(cx).active_session()?;
1920 let thread = session.read(cx).running_state().read(cx).thread_id()?;
1921 session.read(cx).session(cx).read(cx).thread_state(thread)
1922 }
1923}
1924