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
626 Some(
627 div.border_b_1()
628 .border_color(cx.theme().colors().border)
629 .p_1()
630 .justify_between()
631 .w_full()
632 .when(is_side, |this| this.gap_1())
633 .child(
634 h_flex()
635 .child(
636 h_flex().gap_2().w_full().when_some(
637 active_session
638 .as_ref()
639 .map(|session| session.read(cx).running_state()),
640 |this, running_state| {
641 let thread_status =
642 running_state.read(cx).thread_status(cx).unwrap_or(
643 project::debugger::session::ThreadStatus::Exited,
644 );
645 let capabilities = running_state.read(cx).capabilities(cx);
646 let supports_detach =
647 running_state.read(cx).session().read(cx).is_attached();
648 this.map(|this| {
649 if thread_status == ThreadStatus::Running {
650 this.child(
651 IconButton::new(
652 "debug-pause",
653 IconName::DebugPause,
654 )
655 .icon_size(IconSize::XSmall)
656 .shape(ui::IconButtonShape::Square)
657 .on_click(window.listener_for(
658 &running_state,
659 |this, _, _window, cx| {
660 this.pause_thread(cx);
661 },
662 ))
663 .tooltip({
664 let focus_handle = focus_handle.clone();
665 move |window, cx| {
666 Tooltip::for_action_in(
667 "Pause program",
668 &Pause,
669 &focus_handle,
670 window,
671 cx,
672 )
673 }
674 }),
675 )
676 } else {
677 this.child(
678 IconButton::new(
679 "debug-continue",
680 IconName::DebugContinue,
681 )
682 .icon_size(IconSize::XSmall)
683 .shape(ui::IconButtonShape::Square)
684 .on_click(window.listener_for(
685 &running_state,
686 |this, _, _window, cx| this.continue_thread(cx),
687 ))
688 .disabled(thread_status != ThreadStatus::Stopped)
689 .tooltip({
690 let focus_handle = focus_handle.clone();
691 move |window, cx| {
692 Tooltip::for_action_in(
693 "Continue program",
694 &Continue,
695 &focus_handle,
696 window,
697 cx,
698 )
699 }
700 }),
701 )
702 }
703 })
704 .child(
705 IconButton::new("debug-step-over", IconName::ArrowRight)
706 .icon_size(IconSize::XSmall)
707 .shape(ui::IconButtonShape::Square)
708 .on_click(window.listener_for(
709 &running_state,
710 |this, _, _window, cx| {
711 this.step_over(cx);
712 },
713 ))
714 .disabled(thread_status != ThreadStatus::Stopped)
715 .tooltip({
716 let focus_handle = focus_handle.clone();
717 move |window, cx| {
718 Tooltip::for_action_in(
719 "Step over",
720 &StepOver,
721 &focus_handle,
722 window,
723 cx,
724 )
725 }
726 }),
727 )
728 .child(
729 IconButton::new(
730 "debug-step-into",
731 IconName::ArrowDownRight,
732 )
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_in(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 in",
747 &StepInto,
748 &focus_handle,
749 window,
750 cx,
751 )
752 }
753 }),
754 )
755 .child(
756 IconButton::new("debug-step-out", IconName::ArrowUpRight)
757 .icon_size(IconSize::XSmall)
758 .shape(ui::IconButtonShape::Square)
759 .on_click(window.listener_for(
760 &running_state,
761 |this, _, _window, cx| {
762 this.step_out(cx);
763 },
764 ))
765 .disabled(thread_status != ThreadStatus::Stopped)
766 .tooltip({
767 let focus_handle = focus_handle.clone();
768 move |window, cx| {
769 Tooltip::for_action_in(
770 "Step out",
771 &StepOut,
772 &focus_handle,
773 window,
774 cx,
775 )
776 }
777 }),
778 )
779 .child(Divider::vertical())
780 .child(
781 IconButton::new("debug-restart", IconName::DebugRestart)
782 .icon_size(IconSize::XSmall)
783 .on_click(window.listener_for(
784 &running_state,
785 |this, _, window, cx| {
786 this.rerun_session(window, cx);
787 },
788 ))
789 .tooltip({
790 let focus_handle = focus_handle.clone();
791 move |window, cx| {
792 Tooltip::for_action_in(
793 "Rerun Session",
794 &RerunSession,
795 &focus_handle,
796 window,
797 cx,
798 )
799 }
800 }),
801 )
802 .child(
803 IconButton::new("debug-stop", IconName::Power)
804 .icon_size(IconSize::XSmall)
805 .on_click(window.listener_for(
806 &running_state,
807 |this, _, _window, cx| {
808 this.stop_thread(cx);
809 },
810 ))
811 .disabled(
812 thread_status != ThreadStatus::Stopped
813 && thread_status != ThreadStatus::Running,
814 )
815 .tooltip({
816 let focus_handle = focus_handle.clone();
817 let label = if capabilities
818 .supports_terminate_threads_request
819 .unwrap_or_default()
820 {
821 "Terminate Thread"
822 } else {
823 "Terminate All Threads"
824 };
825 move |window, cx| {
826 Tooltip::for_action_in(
827 label,
828 &Stop,
829 &focus_handle,
830 window,
831 cx,
832 )
833 }
834 }),
835 )
836 .when(
837 supports_detach,
838 |div| {
839 div.child(
840 IconButton::new(
841 "debug-disconnect",
842 IconName::DebugDetach,
843 )
844 .disabled(
845 thread_status != ThreadStatus::Stopped
846 && thread_status != ThreadStatus::Running,
847 )
848 .icon_size(IconSize::XSmall)
849 .on_click(window.listener_for(
850 &running_state,
851 |this, _, _, cx| {
852 this.detach_client(cx);
853 },
854 ))
855 .tooltip({
856 let focus_handle = focus_handle.clone();
857 move |window, cx| {
858 Tooltip::for_action_in(
859 "Detach",
860 &Detach,
861 &focus_handle,
862 window,
863 cx,
864 )
865 }
866 }),
867 )
868 },
869 )
870 },
871 ),
872 )
873 .justify_around()
874 .when(is_side, |this| {
875 this.child(new_session_button())
876 .child(documentation_button())
877 }),
878 )
879 .child(
880 h_flex()
881 .gap_2()
882 .when(is_side, |this| this.justify_between())
883 .child(
884 h_flex().when_some(
885 active_session
886 .as_ref()
887 .map(|session| session.read(cx).running_state())
888 .cloned(),
889 |this, running_state| {
890 this.children({
891 let running_state = running_state.clone();
892 let threads =
893 running_state.update(cx, |running_state, cx| {
894 let session = running_state.session();
895 session.read(cx).is_started().then(|| {
896 session.update(cx, |session, cx| {
897 session.threads(cx)
898 })
899 })
900 });
901
902 threads.and_then(|threads| {
903 self.render_thread_dropdown(
904 &running_state,
905 threads,
906 window,
907 cx,
908 )
909 })
910 })
911 .when(!is_side, |this| this.gap_2().child(Divider::vertical()))
912 },
913 ),
914 )
915 .child(
916 h_flex()
917 .children(self.render_session_menu(
918 self.active_session(),
919 self.running_state(cx),
920 window,
921 cx,
922 ))
923 .when(!is_side, |this| {
924 this.child(new_session_button())
925 .child(documentation_button())
926 }),
927 ),
928 ),
929 )
930 }
931
932 pub(crate) fn activate_pane_in_direction(
933 &mut self,
934 direction: SplitDirection,
935 window: &mut Window,
936 cx: &mut Context<Self>,
937 ) {
938 if let Some(session) = self.active_session() {
939 session.update(cx, |session, cx| {
940 session.running_state().update(cx, |running, cx| {
941 running.activate_pane_in_direction(direction, window, cx);
942 })
943 });
944 }
945 }
946
947 pub(crate) fn activate_item(
948 &mut self,
949 item: DebuggerPaneItem,
950 window: &mut Window,
951 cx: &mut Context<Self>,
952 ) {
953 if let Some(session) = self.active_session() {
954 session.update(cx, |session, cx| {
955 session.running_state().update(cx, |running, cx| {
956 running.activate_item(item, window, cx);
957 });
958 });
959 }
960 }
961
962 pub(crate) fn activate_session_by_id(
963 &mut self,
964 session_id: SessionId,
965 window: &mut Window,
966 cx: &mut Context<Self>,
967 ) {
968 if let Some(session) = self
969 .sessions
970 .iter()
971 .find(|session| session.read(cx).session_id(cx) == session_id)
972 {
973 self.activate_session(session.clone(), window, cx);
974 }
975 }
976
977 pub(crate) fn activate_session(
978 &mut self,
979 session_item: Entity<DebugSession>,
980 window: &mut Window,
981 cx: &mut Context<Self>,
982 ) {
983 debug_assert!(self.sessions.contains(&session_item));
984 session_item.focus_handle(cx).focus(window);
985 session_item.update(cx, |this, cx| {
986 this.running_state().update(cx, |this, cx| {
987 this.go_to_selected_stack_frame(window, cx);
988 });
989 });
990 self.active_session = Some(session_item);
991 cx.notify();
992 }
993
994 pub(crate) fn go_to_scenario_definition(
995 &self,
996 kind: TaskSourceKind,
997 scenario: DebugScenario,
998 worktree_id: WorktreeId,
999 window: &mut Window,
1000 cx: &mut Context<Self>,
1001 ) -> Task<Result<()>> {
1002 let Some(workspace) = self.workspace.upgrade() else {
1003 return Task::ready(Ok(()));
1004 };
1005 let project_path = match kind {
1006 TaskSourceKind::AbsPath { abs_path, .. } => {
1007 let Some(project_path) = workspace
1008 .read(cx)
1009 .project()
1010 .read(cx)
1011 .project_path_for_absolute_path(&abs_path, cx)
1012 else {
1013 return Task::ready(Err(anyhow!("no abs path")));
1014 };
1015
1016 project_path
1017 }
1018 TaskSourceKind::Worktree {
1019 id,
1020 directory_in_worktree: dir,
1021 ..
1022 } => {
1023 let relative_path = if dir.ends_with(".vscode") {
1024 dir.join("launch.json")
1025 } else {
1026 dir.join("debug.json")
1027 };
1028 ProjectPath {
1029 worktree_id: id,
1030 path: Arc::from(relative_path),
1031 }
1032 }
1033 _ => return self.save_scenario(scenario, worktree_id, window, cx),
1034 };
1035
1036 let editor = workspace.update(cx, |workspace, cx| {
1037 workspace.open_path(project_path, None, true, window, cx)
1038 });
1039 cx.spawn_in(window, async move |_, cx| {
1040 let editor = editor.await?;
1041 let editor = cx
1042 .update(|_, cx| editor.act_as::<Editor>(cx))?
1043 .context("expected editor")?;
1044
1045 // unfortunately debug tasks don't have an easy way to globally
1046 // identify them. to jump to the one that you just created or an
1047 // 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
1048 editor.update_in(cx, |editor, window, cx| {
1049 let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| {
1050 if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") {
1051 Some(row)
1052 } else {
1053 None
1054 }
1055 });
1056 if let Some(row) = row {
1057 editor.go_to_singleton_buffer_point(
1058 text::Point::new(row as u32, 4),
1059 window,
1060 cx,
1061 );
1062 }
1063 })?;
1064
1065 Ok(())
1066 })
1067 }
1068
1069 pub(crate) fn save_scenario(
1070 &self,
1071 scenario: DebugScenario,
1072 worktree_id: WorktreeId,
1073 window: &mut Window,
1074 cx: &mut Context<Self>,
1075 ) -> Task<Result<()>> {
1076 let this = cx.weak_entity();
1077 let project = self.project.clone();
1078 self.workspace
1079 .update(cx, |workspace, cx| {
1080 let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
1081 return Task::ready(Err(anyhow!("Couldn't get worktree path")));
1082 };
1083
1084 let serialized_scenario = serde_json::to_value(scenario);
1085
1086 cx.spawn_in(window, async move |workspace, cx| {
1087 let serialized_scenario = serialized_scenario?;
1088 let fs =
1089 workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
1090
1091 path.push(paths::local_settings_folder_relative_path());
1092 if !fs.is_dir(path.as_path()).await {
1093 fs.create_dir(path.as_path()).await?;
1094 }
1095 path.pop();
1096
1097 path.push(paths::local_debug_file_relative_path());
1098 let path = path.as_path();
1099
1100 if !fs.is_file(path).await {
1101 fs.create_file(path, Default::default()).await?;
1102 fs.write(
1103 path,
1104 settings::initial_local_debug_tasks_content()
1105 .to_string()
1106 .as_bytes(),
1107 )
1108 .await?;
1109 }
1110 let project_path = workspace.update(cx, |workspace, cx| {
1111 workspace
1112 .project()
1113 .read(cx)
1114 .project_path_for_absolute_path(&path, cx)
1115 .context(
1116 "Couldn't get project path for .zed/debug.json in active worktree",
1117 )
1118 })??;
1119
1120 let editor = this
1121 .update_in(cx, |this, window, cx| {
1122 this.workspace.update(cx, |workspace, cx| {
1123 workspace.open_path(project_path, None, true, window, cx)
1124 })
1125 })??
1126 .await?;
1127 let editor = cx
1128 .update(|_, cx| editor.act_as::<Editor>(cx))?
1129 .context("expected editor")?;
1130
1131 let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
1132 .lines()
1133 .map(|l| format!(" {l}"))
1134 .join("\n");
1135
1136 editor
1137 .update_in(cx, |editor, window, cx| {
1138 Self::insert_task_into_editor(editor, new_scenario, project, window, cx)
1139 })??
1140 .await
1141 })
1142 })
1143 .unwrap_or_else(|err| Task::ready(Err(err)))
1144 }
1145
1146 pub fn insert_task_into_editor(
1147 editor: &mut Editor,
1148 new_scenario: String,
1149 project: Entity<Project>,
1150 window: &mut Window,
1151 cx: &mut Context<Editor>,
1152 ) -> Result<Task<Result<()>>> {
1153 static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
1154 Query::new(
1155 &tree_sitter_json::LANGUAGE.into(),
1156 "(document (array (object) @object))", // TODO: use "." anchor to only match last object
1157 )
1158 .expect("Failed to create LAST_ITEM_QUERY")
1159 });
1160 static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
1161 Query::new(
1162 &tree_sitter_json::LANGUAGE.into(),
1163 "(document (array) @array)",
1164 )
1165 .expect("Failed to create EMPTY_ARRAY_QUERY")
1166 });
1167
1168 let content = editor.text(cx);
1169 let mut parser = tree_sitter::Parser::new();
1170 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
1171 let mut cursor = tree_sitter::QueryCursor::new();
1172 let syntax_tree = parser
1173 .parse(&content, None)
1174 .context("could not parse debug.json")?;
1175 let mut matches = cursor.matches(
1176 &LAST_ITEM_QUERY,
1177 syntax_tree.root_node(),
1178 content.as_bytes(),
1179 );
1180
1181 let mut last_offset = None;
1182 while let Some(mat) = matches.next() {
1183 if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
1184 last_offset = Some(pos)
1185 }
1186 }
1187 let mut edits = Vec::new();
1188 let mut cursor_position = 0;
1189
1190 if let Some(pos) = last_offset {
1191 edits.push((pos..pos, format!(",\n{new_scenario}")));
1192 cursor_position = pos + ",\n ".len();
1193 } else {
1194 let mut matches = cursor.matches(
1195 &EMPTY_ARRAY_QUERY,
1196 syntax_tree.root_node(),
1197 content.as_bytes(),
1198 );
1199
1200 if let Some(mat) = matches.next() {
1201 if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
1202 edits.push((pos..pos, format!("\n{new_scenario}\n")));
1203 cursor_position = pos + "\n ".len();
1204 }
1205 } else {
1206 edits.push((0..0, format!("[\n{}\n]", new_scenario)));
1207 cursor_position = "[\n ".len();
1208 }
1209 }
1210 editor.transact(window, cx, |editor, window, cx| {
1211 editor.edit(edits, cx);
1212 let snapshot = editor
1213 .buffer()
1214 .read(cx)
1215 .as_singleton()
1216 .unwrap()
1217 .read(cx)
1218 .snapshot();
1219 let point = cursor_position.to_point(&snapshot);
1220 editor.go_to_singleton_buffer_point(point, window, cx);
1221 });
1222 Ok(editor.save(SaveOptions::default(), project, window, cx))
1223 }
1224
1225 pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1226 self.thread_picker_menu_handle.toggle(window, cx);
1227 }
1228
1229 pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1230 self.session_picker_menu_handle.toggle(window, cx);
1231 }
1232
1233 fn toggle_zoom(
1234 &mut self,
1235 _: &workspace::ToggleZoom,
1236 window: &mut Window,
1237 cx: &mut Context<Self>,
1238 ) {
1239 if self.is_zoomed {
1240 cx.emit(PanelEvent::ZoomOut);
1241 } else {
1242 if !self.focus_handle(cx).contains_focused(window, cx) {
1243 cx.focus_self(window);
1244 }
1245 cx.emit(PanelEvent::ZoomIn);
1246 }
1247 }
1248
1249 fn label_for_child_session(
1250 &self,
1251 parent_session: &Entity<Session>,
1252 request: &StartDebuggingRequestArguments,
1253 cx: &mut Context<'_, Self>,
1254 ) -> SharedString {
1255 let adapter = parent_session.read(cx).adapter();
1256 if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
1257 if let Some(label) = adapter.label_for_child_session(request) {
1258 return label.into();
1259 }
1260 }
1261 let mut label = parent_session.read(cx).label().clone();
1262 if !label.ends_with("(child)") {
1263 label = format!("{label} (child)").into();
1264 }
1265 label
1266 }
1267}
1268
1269async fn register_session_inner(
1270 this: &WeakEntity<DebugPanel>,
1271 session: Entity<Session>,
1272 cx: &mut AsyncWindowContext,
1273) -> Result<Entity<DebugSession>> {
1274 let adapter_name = session.read_with(cx, |session, _| session.adapter())?;
1275 this.update_in(cx, |_, window, cx| {
1276 cx.subscribe_in(
1277 &session,
1278 window,
1279 move |this, session, event: &SessionStateEvent, window, cx| match event {
1280 SessionStateEvent::Restart => {
1281 this.handle_restart_request(session.clone(), window, cx);
1282 }
1283 SessionStateEvent::SpawnChildSession { request } => {
1284 this.handle_start_debugging_request(request, session.clone(), window, cx);
1285 }
1286 _ => {}
1287 },
1288 )
1289 .detach();
1290 })
1291 .ok();
1292 let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
1293 let debug_session = this.update_in(cx, |this, window, cx| {
1294 let parent_session = this
1295 .sessions
1296 .iter()
1297 .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
1298 .cloned();
1299 this.sessions.retain(|session| {
1300 !session
1301 .read(cx)
1302 .running_state()
1303 .read(cx)
1304 .session()
1305 .read(cx)
1306 .is_terminated()
1307 });
1308
1309 let debug_session = DebugSession::running(
1310 this.project.clone(),
1311 this.workspace.clone(),
1312 parent_session
1313 .as_ref()
1314 .map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
1315 session,
1316 serialized_layout,
1317 this.position(window, cx).axis(),
1318 window,
1319 cx,
1320 );
1321
1322 // We might want to make this an event subscription and only notify when a new thread is selected
1323 // This is used to filter the command menu correctly
1324 cx.observe(
1325 &debug_session.read(cx).running_state().clone(),
1326 |_, _, cx| cx.notify(),
1327 )
1328 .detach();
1329 let insert_position = this
1330 .sessions
1331 .iter()
1332 .position(|session| Some(session) == parent_session.as_ref())
1333 .map(|position| position + 1)
1334 .unwrap_or(this.sessions.len());
1335 // Maintain topological sort order of sessions
1336 this.sessions.insert(insert_position, debug_session.clone());
1337
1338 debug_session
1339 })?;
1340 Ok(debug_session)
1341}
1342
1343impl EventEmitter<PanelEvent> for DebugPanel {}
1344impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1345
1346impl Focusable for DebugPanel {
1347 fn focus_handle(&self, _: &App) -> FocusHandle {
1348 self.focus_handle.clone()
1349 }
1350}
1351
1352impl Panel for DebugPanel {
1353 fn persistent_name() -> &'static str {
1354 "DebugPanel"
1355 }
1356
1357 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1358 match DebuggerSettings::get_global(cx).dock {
1359 DebugPanelDockPosition::Left => DockPosition::Left,
1360 DebugPanelDockPosition::Bottom => DockPosition::Bottom,
1361 DebugPanelDockPosition::Right => DockPosition::Right,
1362 }
1363 }
1364
1365 fn position_is_valid(&self, _: DockPosition) -> bool {
1366 true
1367 }
1368
1369 fn set_position(
1370 &mut self,
1371 position: DockPosition,
1372 window: &mut Window,
1373 cx: &mut Context<Self>,
1374 ) {
1375 if position.axis() != self.position(window, cx).axis() {
1376 self.sessions.iter().for_each(|session_item| {
1377 session_item.update(cx, |item, cx| {
1378 item.running_state()
1379 .update(cx, |state, _| state.invert_axies())
1380 })
1381 });
1382 }
1383
1384 settings::update_settings_file::<DebuggerSettings>(
1385 self.fs.clone(),
1386 cx,
1387 move |settings, _| {
1388 let dock = match position {
1389 DockPosition::Left => DebugPanelDockPosition::Left,
1390 DockPosition::Bottom => DebugPanelDockPosition::Bottom,
1391 DockPosition::Right => DebugPanelDockPosition::Right,
1392 };
1393 settings.dock = dock;
1394 },
1395 );
1396 }
1397
1398 fn size(&self, _window: &Window, _: &App) -> Pixels {
1399 self.size
1400 }
1401
1402 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1403 self.size = size.unwrap_or(px(300.));
1404 }
1405
1406 fn remote_id() -> Option<proto::PanelId> {
1407 Some(proto::PanelId::DebugPanel)
1408 }
1409
1410 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1411 Some(IconName::Debug)
1412 }
1413
1414 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1415 if DebuggerSettings::get_global(cx).button {
1416 Some("Debug Panel")
1417 } else {
1418 None
1419 }
1420 }
1421
1422 fn toggle_action(&self) -> Box<dyn Action> {
1423 Box::new(ToggleFocus)
1424 }
1425
1426 fn pane(&self) -> Option<Entity<Pane>> {
1427 None
1428 }
1429
1430 fn activation_priority(&self) -> u32 {
1431 9
1432 }
1433
1434 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1435
1436 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1437 self.is_zoomed
1438 }
1439
1440 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1441 self.is_zoomed = zoomed;
1442 cx.notify();
1443 }
1444}
1445
1446impl Render for DebugPanel {
1447 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1448 let this = cx.weak_entity();
1449
1450 if self
1451 .active_session
1452 .as_ref()
1453 .map(|session| session.read(cx).running_state())
1454 .map(|state| state.read(cx).has_open_context_menu(cx))
1455 .unwrap_or(false)
1456 {
1457 self.context_menu.take();
1458 }
1459
1460 v_flex()
1461 .when(!self.is_zoomed, |this| {
1462 this.when_else(
1463 self.position(window, cx) == DockPosition::Bottom,
1464 |this| this.max_h(self.size),
1465 |this| this.max_w(self.size),
1466 )
1467 })
1468 .size_full()
1469 .key_context("DebugPanel")
1470 .child(h_flex().children(self.top_controls_strip(window, cx)))
1471 .track_focus(&self.focus_handle(cx))
1472 .on_action({
1473 let this = this.clone();
1474 move |_: &workspace::ActivatePaneLeft, window, cx| {
1475 this.update(cx, |this, cx| {
1476 this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1477 })
1478 .ok();
1479 }
1480 })
1481 .on_action({
1482 let this = this.clone();
1483 move |_: &workspace::ActivatePaneRight, window, cx| {
1484 this.update(cx, |this, cx| {
1485 this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1486 })
1487 .ok();
1488 }
1489 })
1490 .on_action({
1491 let this = this.clone();
1492 move |_: &workspace::ActivatePaneUp, window, cx| {
1493 this.update(cx, |this, cx| {
1494 this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1495 })
1496 .ok();
1497 }
1498 })
1499 .on_action({
1500 let this = this.clone();
1501 move |_: &workspace::ActivatePaneDown, window, cx| {
1502 this.update(cx, |this, cx| {
1503 this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1504 })
1505 .ok();
1506 }
1507 })
1508 .on_action({
1509 let this = this.clone();
1510 move |_: &FocusConsole, window, cx| {
1511 this.update(cx, |this, cx| {
1512 this.activate_item(DebuggerPaneItem::Console, window, cx);
1513 })
1514 .ok();
1515 }
1516 })
1517 .on_action({
1518 let this = this.clone();
1519 move |_: &FocusVariables, window, cx| {
1520 this.update(cx, |this, cx| {
1521 this.activate_item(DebuggerPaneItem::Variables, window, cx);
1522 })
1523 .ok();
1524 }
1525 })
1526 .on_action({
1527 let this = this.clone();
1528 move |_: &FocusBreakpointList, window, cx| {
1529 this.update(cx, |this, cx| {
1530 this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1531 })
1532 .ok();
1533 }
1534 })
1535 .on_action({
1536 let this = this.clone();
1537 move |_: &FocusFrames, window, cx| {
1538 this.update(cx, |this, cx| {
1539 this.activate_item(DebuggerPaneItem::Frames, window, cx);
1540 })
1541 .ok();
1542 }
1543 })
1544 .on_action({
1545 let this = this.clone();
1546 move |_: &FocusModules, window, cx| {
1547 this.update(cx, |this, cx| {
1548 this.activate_item(DebuggerPaneItem::Modules, window, cx);
1549 })
1550 .ok();
1551 }
1552 })
1553 .on_action({
1554 let this = this.clone();
1555 move |_: &FocusLoadedSources, window, cx| {
1556 this.update(cx, |this, cx| {
1557 this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1558 })
1559 .ok();
1560 }
1561 })
1562 .on_action({
1563 let this = this.clone();
1564 move |_: &FocusTerminal, window, cx| {
1565 this.update(cx, |this, cx| {
1566 this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1567 })
1568 .ok();
1569 }
1570 })
1571 .on_action({
1572 let this = this.clone();
1573 move |_: &ToggleThreadPicker, window, cx| {
1574 this.update(cx, |this, cx| {
1575 this.toggle_thread_picker(window, cx);
1576 })
1577 .ok();
1578 }
1579 })
1580 .on_action({
1581 let this = this.clone();
1582 move |_: &ToggleSessionPicker, window, cx| {
1583 this.update(cx, |this, cx| {
1584 this.toggle_session_picker(window, cx);
1585 })
1586 .ok();
1587 }
1588 })
1589 .on_action(cx.listener(Self::toggle_zoom))
1590 .on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
1591 let Some(session) = panel.active_session() else {
1592 return;
1593 };
1594 let active_pane = session
1595 .read(cx)
1596 .running_state()
1597 .read(cx)
1598 .active_pane()
1599 .clone();
1600 active_pane.update(cx, |pane, cx| {
1601 let is_zoomed = pane.is_zoomed();
1602 pane.set_zoomed(!is_zoomed, cx);
1603 });
1604 cx.notify();
1605 }))
1606 .on_action(cx.listener(Self::copy_debug_adapter_arguments))
1607 .when(self.active_session.is_some(), |this| {
1608 this.on_mouse_down(
1609 MouseButton::Right,
1610 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1611 if this
1612 .active_session
1613 .as_ref()
1614 .map(|session| {
1615 let state = session.read(cx).running_state();
1616 state.read(cx).has_pane_at_position(event.position)
1617 })
1618 .unwrap_or(false)
1619 {
1620 this.deploy_context_menu(event.position, window, cx);
1621 }
1622 }),
1623 )
1624 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1625 deferred(
1626 anchored()
1627 .position(*position)
1628 .anchor(gpui::Corner::TopLeft)
1629 .child(menu.clone()),
1630 )
1631 .with_priority(1)
1632 }))
1633 })
1634 .map(|this| {
1635 if let Some(active_session) = self.active_session.clone() {
1636 this.child(active_session)
1637 } else {
1638 let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
1639 let welcome_experience = v_flex()
1640 .when_else(
1641 docked_to_bottom,
1642 |this| this.w_2_3().h_full().pr_8(),
1643 |this| this.w_full().h_1_3(),
1644 )
1645 .items_center()
1646 .justify_center()
1647 .gap_2()
1648 .child(
1649 Button::new("spawn-new-session-empty-state", "New Session")
1650 .icon(IconName::Plus)
1651 .icon_size(IconSize::XSmall)
1652 .icon_color(Color::Muted)
1653 .icon_position(IconPosition::Start)
1654 .on_click(|_, window, cx| {
1655 window.dispatch_action(crate::Start.boxed_clone(), cx);
1656 }),
1657 )
1658 .child(
1659 Button::new("edit-debug-settings", "Edit debug.json")
1660 .icon(IconName::Code)
1661 .icon_size(IconSize::XSmall)
1662 .color(Color::Muted)
1663 .icon_color(Color::Muted)
1664 .icon_position(IconPosition::Start)
1665 .on_click(|_, window, cx| {
1666 window.dispatch_action(
1667 zed_actions::OpenProjectDebugTasks.boxed_clone(),
1668 cx,
1669 );
1670 }),
1671 )
1672 .child(
1673 Button::new("open-debugger-docs", "Debugger Docs")
1674 .icon(IconName::Book)
1675 .color(Color::Muted)
1676 .icon_size(IconSize::XSmall)
1677 .icon_color(Color::Muted)
1678 .icon_position(IconPosition::Start)
1679 .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
1680 )
1681 .child(
1682 Button::new(
1683 "spawn-new-session-install-extensions",
1684 "Debugger Extensions",
1685 )
1686 .icon(IconName::Blocks)
1687 .color(Color::Muted)
1688 .icon_size(IconSize::XSmall)
1689 .icon_color(Color::Muted)
1690 .icon_position(IconPosition::Start)
1691 .on_click(|_, window, cx| {
1692 window.dispatch_action(
1693 zed_actions::Extensions {
1694 category_filter: Some(
1695 zed_actions::ExtensionCategoryFilter::DebugAdapters,
1696 ),
1697 }
1698 .boxed_clone(),
1699 cx,
1700 );
1701 }),
1702 );
1703 let breakpoint_list =
1704 v_flex()
1705 .group("base-breakpoint-list")
1706 .items_start()
1707 .when_else(
1708 docked_to_bottom,
1709 |this| this.min_w_1_3().h_full(),
1710 |this| this.w_full().h_2_3(),
1711 )
1712 .p_1()
1713 .child(
1714 h_flex()
1715 .pl_1()
1716 .w_full()
1717 .justify_between()
1718 .child(Label::new("Breakpoints").size(LabelSize::Small))
1719 .child(h_flex().visible_on_hover("base-breakpoint-list").child(
1720 self.breakpoint_list.read(cx).render_control_strip(),
1721 ))
1722 .track_focus(&self.breakpoint_list.focus_handle(cx)),
1723 )
1724 .child(Divider::horizontal())
1725 .child(self.breakpoint_list.clone());
1726 this.child(
1727 v_flex()
1728 .h_full()
1729 .gap_1()
1730 .items_center()
1731 .justify_center()
1732 .child(
1733 div()
1734 .when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
1735 .size_full()
1736 .map(|this| {
1737 if docked_to_bottom {
1738 this.items_start()
1739 .child(breakpoint_list)
1740 .child(Divider::vertical())
1741 .child(welcome_experience)
1742 } else {
1743 this.items_end()
1744 .child(welcome_experience)
1745 .child(Divider::horizontal())
1746 .child(breakpoint_list)
1747 }
1748 }),
1749 ),
1750 )
1751 }
1752 })
1753 .into_any()
1754 }
1755}
1756
1757struct DebuggerProvider(Entity<DebugPanel>);
1758
1759impl workspace::DebuggerProvider for DebuggerProvider {
1760 fn start_session(
1761 &self,
1762 definition: DebugScenario,
1763 context: TaskContext,
1764 buffer: Option<Entity<Buffer>>,
1765 worktree_id: Option<WorktreeId>,
1766 window: &mut Window,
1767 cx: &mut App,
1768 ) {
1769 self.0.update(cx, |_, cx| {
1770 cx.defer_in(window, move |this, window, cx| {
1771 this.start_session(definition, context, buffer, worktree_id, window, cx);
1772 })
1773 })
1774 }
1775
1776 fn spawn_task_or_modal(
1777 &self,
1778 workspace: &mut Workspace,
1779 action: &tasks_ui::Spawn,
1780 window: &mut Window,
1781 cx: &mut Context<Workspace>,
1782 ) {
1783 spawn_task_or_modal(workspace, action, window, cx);
1784 }
1785
1786 fn debug_scenario_scheduled(&self, cx: &mut App) {
1787 self.0.update(cx, |this, _| {
1788 this.debug_scenario_scheduled_last = true;
1789 });
1790 }
1791
1792 fn task_scheduled(&self, cx: &mut App) {
1793 self.0.update(cx, |this, _| {
1794 this.debug_scenario_scheduled_last = false;
1795 })
1796 }
1797
1798 fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
1799 self.0.read(cx).debug_scenario_scheduled_last
1800 }
1801
1802 fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
1803 let session = self.0.read(cx).active_session()?;
1804 let thread = session.read(cx).running_state().read(cx).thread_id()?;
1805 session.read(cx).session(cx).read(cx).thread_state(thread)
1806 }
1807}