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