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