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