1pub(crate) mod breakpoint_list;
2pub(crate) mod console;
3pub(crate) mod loaded_source_list;
4pub(crate) mod memory_view;
5pub(crate) mod module_list;
6pub mod stack_frame_list;
7pub mod variable_list;
8use std::{
9 any::Any,
10 path::PathBuf,
11 sync::{Arc, LazyLock},
12 time::Duration,
13};
14
15use crate::{
16 ToggleExpandItem,
17 attach_modal::{AttachModal, ModalIntent},
18 new_process_modal::resolve_path,
19 persistence::{self, DebuggerPaneItem, SerializedLayout},
20 session::running::memory_view::MemoryView,
21};
22
23use anyhow::{Context as _, Result, anyhow, bail};
24use breakpoint_list::BreakpointList;
25use collections::{HashMap, IndexMap};
26use console::Console;
27use dap::{
28 Capabilities, DapRegistry, RunInTerminalRequestArguments, Thread,
29 adapters::{DebugAdapterName, DebugTaskDefinition},
30 client::SessionId,
31 debugger_settings::DebuggerSettings,
32};
33use futures::{SinkExt, channel::mpsc};
34use gpui::{
35 Action as _, AnyView, AppContext, Axis, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
36 NoAction, Pixels, Point, Subscription, Task, WeakEntity,
37};
38use language::Buffer;
39use loaded_source_list::LoadedSourceList;
40use module_list::ModuleList;
41use project::{
42 DebugScenarioContext, Project, WorktreeId,
43 debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
44};
45use rpc::proto::ViewId;
46use serde_json::Value;
47use settings::Settings;
48use stack_frame_list::StackFrameList;
49use task::{
50 BuildTaskDefinition, DebugScenario, SharedTaskContext, Shell, ShellBuilder, SpawnInTerminal,
51 TaskContext, ZedDebugConfig, substitute_variables_in_str,
52};
53use terminal_view::TerminalView;
54use ui::{
55 FluentBuilder, IntoElement, Render, StatefulInteractiveElement, Tab, Tooltip, VisibleOnHover,
56 VisualContext, prelude::*,
57};
58use util::ResultExt;
59use variable_list::VariableList;
60use workspace::{
61 ActivePaneDecorator, DraggedTab, Item, ItemHandle, Member, Pane, PaneGroup, SplitDirection,
62 Workspace, item::TabContentParams, move_item, pane::Event,
63};
64
65static PROCESS_ID_PLACEHOLDER: LazyLock<String> =
66 LazyLock::new(|| task::VariableName::PickProcessId.template_value());
67
68pub struct RunningState {
69 session: Entity<Session>,
70 thread_id: Option<ThreadId>,
71 focus_handle: FocusHandle,
72 _remote_id: Option<ViewId>,
73 workspace: WeakEntity<Workspace>,
74 project: WeakEntity<Project>,
75 session_id: SessionId,
76 variable_list: Entity<variable_list::VariableList>,
77 _subscriptions: Vec<Subscription>,
78 stack_frame_list: Entity<stack_frame_list::StackFrameList>,
79 loaded_sources_list: Entity<LoadedSourceList>,
80 pub debug_terminal: Entity<DebugTerminal>,
81 module_list: Entity<module_list::ModuleList>,
82 console: Entity<Console>,
83 breakpoint_list: Entity<BreakpointList>,
84 panes: PaneGroup,
85 active_pane: Entity<Pane>,
86 pane_close_subscriptions: HashMap<EntityId, Subscription>,
87 dock_axis: Axis,
88 _schedule_serialize: Option<Task<()>>,
89 pub(crate) scenario: Option<DebugScenario>,
90 pub(crate) scenario_context: Option<DebugScenarioContext>,
91 memory_view: Entity<MemoryView>,
92}
93
94impl RunningState {
95 pub(crate) fn thread_id(&self) -> Option<ThreadId> {
96 self.thread_id
97 }
98
99 pub(crate) fn active_pane(&self) -> &Entity<Pane> {
100 &self.active_pane
101 }
102}
103
104impl Render for RunningState {
105 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
106 let zoomed_pane = self
107 .panes
108 .panes()
109 .into_iter()
110 .find(|pane| pane.read(cx).is_zoomed());
111
112 let active = self.panes.panes().into_iter().next();
113 let pane = if let Some(zoomed_pane) = zoomed_pane {
114 zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
115 } else if let Some(active) = active {
116 self.panes
117 .render(
118 None,
119 &ActivePaneDecorator::new(active, &self.workspace),
120 window,
121 cx,
122 )
123 .into_any_element()
124 } else {
125 div().into_any_element()
126 };
127 let thread_status = self
128 .thread_id
129 .map(|thread_id| self.session.read(cx).thread_status(thread_id))
130 .unwrap_or(ThreadStatus::Exited);
131
132 self.variable_list.update(cx, |this, cx| {
133 this.disabled(thread_status != ThreadStatus::Stopped, cx);
134 });
135 v_flex()
136 .size_full()
137 .key_context("DebugSessionItem")
138 .track_focus(&self.focus_handle(cx))
139 .child(h_flex().flex_1().child(pane))
140 }
141}
142
143pub(crate) struct SubView {
144 inner: AnyView,
145 item_focus_handle: FocusHandle,
146 kind: DebuggerPaneItem,
147 running_state: WeakEntity<RunningState>,
148 host_pane: WeakEntity<Pane>,
149 show_indicator: Box<dyn Fn(&App) -> bool>,
150 actions: Option<Box<dyn FnMut(&mut Window, &mut App) -> AnyElement>>,
151 hovered: bool,
152}
153
154impl SubView {
155 pub(crate) fn new(
156 item_focus_handle: FocusHandle,
157 view: AnyView,
158 kind: DebuggerPaneItem,
159 running_state: WeakEntity<RunningState>,
160 host_pane: WeakEntity<Pane>,
161 cx: &mut App,
162 ) -> Entity<Self> {
163 cx.new(|_| Self {
164 kind,
165 inner: view,
166 item_focus_handle,
167 running_state,
168 host_pane,
169 show_indicator: Box::new(|_| false),
170 actions: None,
171 hovered: false,
172 })
173 }
174
175 pub(crate) fn stack_frame_list(
176 stack_frame_list: Entity<StackFrameList>,
177 running_state: WeakEntity<RunningState>,
178 host_pane: WeakEntity<Pane>,
179 cx: &mut App,
180 ) -> Entity<Self> {
181 let weak_list = stack_frame_list.downgrade();
182 let this = Self::new(
183 stack_frame_list.focus_handle(cx),
184 stack_frame_list.into(),
185 DebuggerPaneItem::Frames,
186 running_state,
187 host_pane,
188 cx,
189 );
190
191 this.update(cx, |this, _| {
192 this.with_actions(Box::new(move |_, cx| {
193 weak_list
194 .update(cx, |this, _| this.render_control_strip())
195 .unwrap_or_else(|_| div().into_any_element())
196 }));
197 });
198
199 this
200 }
201
202 pub(crate) fn console(
203 console: Entity<Console>,
204 running_state: WeakEntity<RunningState>,
205 host_pane: WeakEntity<Pane>,
206 cx: &mut App,
207 ) -> Entity<Self> {
208 let weak_console = console.downgrade();
209 let this = Self::new(
210 console.focus_handle(cx),
211 console.into(),
212 DebuggerPaneItem::Console,
213 running_state,
214 host_pane,
215 cx,
216 );
217 this.update(cx, |this, _| {
218 this.with_indicator(Box::new(move |cx| {
219 weak_console
220 .read_with(cx, |console, cx| console.show_indicator(cx))
221 .unwrap_or_default()
222 }))
223 });
224 this
225 }
226
227 pub(crate) fn breakpoint_list(
228 list: Entity<BreakpointList>,
229 running_state: WeakEntity<RunningState>,
230 host_pane: WeakEntity<Pane>,
231 cx: &mut App,
232 ) -> Entity<Self> {
233 let weak_list = list.downgrade();
234 let focus_handle = list.focus_handle(cx);
235 let this = Self::new(
236 focus_handle,
237 list.into(),
238 DebuggerPaneItem::BreakpointList,
239 running_state,
240 host_pane,
241 cx,
242 );
243
244 this.update(cx, |this, _| {
245 this.with_actions(Box::new(move |_, cx| {
246 weak_list
247 .update(cx, |this, _| this.render_control_strip())
248 .unwrap_or_else(|_| div().into_any_element())
249 }));
250 });
251 this
252 }
253
254 pub(crate) fn view_kind(&self) -> DebuggerPaneItem {
255 self.kind
256 }
257 pub(crate) fn with_indicator(&mut self, indicator: Box<dyn Fn(&App) -> bool>) {
258 self.show_indicator = indicator;
259 }
260 pub(crate) fn with_actions(
261 &mut self,
262 actions: Box<dyn FnMut(&mut Window, &mut App) -> AnyElement>,
263 ) {
264 self.actions = Some(actions);
265 }
266
267 fn set_host_pane(&mut self, host_pane: WeakEntity<Pane>) {
268 self.host_pane = host_pane;
269 }
270}
271impl Focusable for SubView {
272 fn focus_handle(&self, _: &App) -> FocusHandle {
273 self.item_focus_handle.clone()
274 }
275}
276impl EventEmitter<()> for SubView {}
277impl Item for SubView {
278 type Event = ();
279
280 /// This is used to serialize debugger pane layouts
281 /// A SharedString gets converted to a enum and back during serialization/deserialization.
282 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
283 self.kind.to_shared_string()
284 }
285
286 fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
287 Some(self.kind.tab_tooltip())
288 }
289
290 fn tab_content(
291 &self,
292 params: workspace::item::TabContentParams,
293 _: &Window,
294 cx: &App,
295 ) -> AnyElement {
296 let label = Label::new(self.kind.to_shared_string())
297 .size(ui::LabelSize::Small)
298 .color(params.text_color())
299 .line_height_style(ui::LineHeightStyle::UiLabel);
300
301 if !params.selected && self.show_indicator.as_ref()(cx) {
302 return h_flex()
303 .justify_between()
304 .child(ui::Indicator::dot())
305 .gap_2()
306 .child(label)
307 .into_any_element();
308 }
309
310 label.into_any_element()
311 }
312
313 fn handle_drop(
314 &self,
315 active_pane: &Pane,
316 dropped: &dyn Any,
317 window: &mut Window,
318 cx: &mut App,
319 ) -> bool {
320 let Some(tab) = dropped.downcast_ref::<DraggedTab>() else {
321 return true;
322 };
323 let Some(this_pane) = self.host_pane.upgrade() else {
324 return true;
325 };
326 let item = if tab.pane == this_pane {
327 active_pane.item_for_index(tab.ix)
328 } else {
329 tab.pane.read(cx).item_for_index(tab.ix)
330 };
331 let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
332 return true;
333 };
334 let Some(split_direction) = active_pane.drag_split_direction() else {
335 return false;
336 };
337
338 let source = tab.pane.clone();
339 let item_id_to_move = item.item_id();
340 let weak_running = self.running_state.clone();
341
342 // Source pane may be the one currently updated, so defer the move.
343 window.defer(cx, move |window, cx| {
344 let new_pane = weak_running.update(cx, |running, cx| {
345 let Some(project) = running.project.upgrade() else {
346 return Err(anyhow!("Debugger project has been dropped"));
347 };
348
349 let new_pane = new_debugger_pane(running.workspace.clone(), project, window, cx);
350 let _previous_subscription = running.pane_close_subscriptions.insert(
351 new_pane.entity_id(),
352 cx.subscribe_in(&new_pane, window, RunningState::handle_pane_event),
353 );
354 debug_assert!(_previous_subscription.is_none());
355 running
356 .panes
357 .split(&this_pane, &new_pane, split_direction, cx);
358 anyhow::Ok(new_pane)
359 });
360
361 match new_pane.and_then(|result| result) {
362 Ok(new_pane) => {
363 move_item(
364 &source,
365 &new_pane,
366 item_id_to_move,
367 new_pane.read(cx).active_item_index(),
368 true,
369 window,
370 cx,
371 );
372 }
373 Err(err) => {
374 log::error!("{err:?}");
375 }
376 }
377 });
378
379 true
380 }
381}
382
383impl Render for SubView {
384 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
385 v_flex()
386 .id(format!(
387 "subview-container-{}",
388 self.kind.to_shared_string()
389 ))
390 .on_hover(cx.listener(|this, hovered, _, cx| {
391 this.hovered = *hovered;
392 cx.notify();
393 }))
394 .size_full()
395 // Add border unconditionally to prevent layout shifts on focus changes.
396 .border_1()
397 .when(self.item_focus_handle.contains_focused(window, cx), |el| {
398 el.border_color(cx.theme().colors().pane_focused_border)
399 })
400 .child(self.inner.clone())
401 }
402}
403
404pub(crate) fn new_debugger_pane(
405 workspace: WeakEntity<Workspace>,
406 project: Entity<Project>,
407 window: &mut Window,
408 cx: &mut Context<RunningState>,
409) -> Entity<Pane> {
410 let weak_running = cx.weak_entity();
411
412 cx.new(move |cx| {
413 let can_drop_predicate: Arc<dyn Fn(&dyn Any, &mut Window, &mut App) -> bool> =
414 Arc::new(|any, _window, _cx| {
415 any.downcast_ref::<DraggedTab>()
416 .is_some_and(|dragged_tab| dragged_tab.item.downcast::<SubView>().is_some())
417 });
418 let mut pane = Pane::new(
419 workspace.clone(),
420 project.clone(),
421 Default::default(),
422 Some(can_drop_predicate),
423 NoAction.boxed_clone(),
424 true,
425 window,
426 cx,
427 );
428 let focus_handle = pane.focus_handle(cx);
429 pane.set_can_split(Some(Arc::new({
430 let weak_running = weak_running.clone();
431 move |pane, dragged_item, _window, cx| {
432 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
433 let is_current_pane = tab.pane == cx.entity();
434 let Some(can_drag_away) = weak_running
435 .read_with(cx, |running_state, _| {
436 let current_panes = running_state.panes.panes();
437 !current_panes.contains(&&tab.pane)
438 || current_panes.len() > 1
439 || (!is_current_pane || pane.items_len() > 1)
440 })
441 .ok()
442 else {
443 return false;
444 };
445 if can_drag_away {
446 let item = if is_current_pane {
447 pane.item_for_index(tab.ix)
448 } else {
449 tab.pane.read(cx).item_for_index(tab.ix)
450 };
451 if let Some(item) = item {
452 return item.downcast::<SubView>().is_some();
453 }
454 }
455 }
456 false
457 }
458 })));
459 pane.set_can_toggle_zoom(false, cx);
460 pane.display_nav_history_buttons(None);
461 pane.set_should_display_tab_bar(|_, _| true);
462 pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
463 pane.set_render_tab_bar(cx, {
464 move |pane, window, cx| {
465 let active_pane_item = pane.active_item();
466 let pane_group_id: SharedString =
467 format!("pane-zoom-button-hover-{}", cx.entity_id()).into();
468 let as_subview = active_pane_item
469 .as_ref()
470 .and_then(|item| item.downcast::<SubView>());
471 let is_hovered = as_subview
472 .as_ref()
473 .is_some_and(|item| item.read(cx).hovered);
474
475 h_flex()
476 .track_focus(&focus_handle)
477 .group(pane_group_id.clone())
478 .pl_1p5()
479 .pr_1()
480 .justify_between()
481 .border_b_1()
482 .border_color(cx.theme().colors().border)
483 .bg(cx.theme().colors().tab_bar_background)
484 .on_action(|_: &menu::Cancel, window, cx| {
485 if cx.stop_active_drag(window) {
486 } else {
487 cx.propagate();
488 }
489 })
490 .child(
491 h_flex()
492 .w_full()
493 .gap_1()
494 .h(Tab::container_height(cx))
495 .drag_over::<DraggedTab>(|bar, _, _, cx| {
496 bar.bg(cx.theme().colors().drop_target_background)
497 })
498 .on_drop(cx.listener(
499 move |this, dragged_tab: &DraggedTab, window, cx| {
500 if dragged_tab.item.downcast::<SubView>().is_none() {
501 return;
502 }
503 this.drag_split_direction = None;
504 this.handle_tab_drop(
505 dragged_tab,
506 this.items_len(),
507 false,
508 window,
509 cx,
510 )
511 },
512 ))
513 .children(pane.items().enumerate().map(|(ix, item)| {
514 let selected = active_pane_item
515 .as_ref()
516 .is_some_and(|active| active.item_id() == item.item_id());
517 let deemphasized = !pane.has_focus(window, cx);
518 let item_ = item.boxed_clone();
519 div()
520 .id(format!("debugger_tab_{}", item.item_id().as_u64()))
521 .p_1()
522 .rounded_md()
523 .cursor_pointer()
524 .when_some(item.tab_tooltip_text(cx), |this, tooltip| {
525 this.tooltip(Tooltip::text(tooltip))
526 })
527 .map(|this| {
528 let theme = cx.theme();
529 if selected {
530 let color = theme.colors().tab_active_background;
531 let color = if deemphasized {
532 color.opacity(0.5)
533 } else {
534 color
535 };
536 this.bg(color)
537 } else {
538 let hover_color = theme.colors().element_hover;
539 this.hover(|style| style.bg(hover_color))
540 }
541 })
542 .on_click(cx.listener(move |this, _, window, cx| {
543 let index = this.index_for_item(&*item_);
544 if let Some(index) = index {
545 this.activate_item(index, true, true, window, cx);
546 }
547 }))
548 .child(item.tab_content(
549 TabContentParams {
550 selected,
551 deemphasized,
552 ..Default::default()
553 },
554 window,
555 cx,
556 ))
557 .on_drop(cx.listener(
558 move |this, dragged_tab: &DraggedTab, window, cx| {
559 if dragged_tab.item.downcast::<SubView>().is_none() {
560 return;
561 }
562 this.drag_split_direction = None;
563 this.handle_tab_drop(dragged_tab, ix, false, window, cx)
564 },
565 ))
566 .on_drag(
567 DraggedTab {
568 item: item.boxed_clone(),
569 pane: cx.entity(),
570 detail: 0,
571 is_active: selected,
572 ix,
573 },
574 |tab, _, _, cx| cx.new(|_| tab.clone()),
575 )
576 })),
577 )
578 .child({
579 let zoomed = pane.is_zoomed();
580
581 h_flex()
582 .visible_on_hover(pane_group_id)
583 .when(is_hovered, |this| this.visible())
584 .when_some(as_subview.as_ref(), |this, subview| {
585 subview.update(cx, |view, cx| {
586 let Some(additional_actions) = view.actions.as_mut() else {
587 return this;
588 };
589 this.child(additional_actions(window, cx))
590 })
591 })
592 .child(
593 IconButton::new(
594 SharedString::from(format!(
595 "debug-toggle-zoom-{}",
596 cx.entity_id()
597 )),
598 if zoomed {
599 IconName::Minimize
600 } else {
601 IconName::Maximize
602 },
603 )
604 .icon_size(IconSize::Small)
605 .on_click(cx.listener(move |pane, _, _, cx| {
606 let is_zoomed = pane.is_zoomed();
607 pane.set_zoomed(!is_zoomed, cx);
608 cx.notify();
609 }))
610 .tooltip({
611 let focus_handle = focus_handle.clone();
612 move |_window, cx| {
613 let zoomed_text =
614 if zoomed { "Minimize" } else { "Expand" };
615 Tooltip::for_action_in(
616 zoomed_text,
617 &ToggleExpandItem,
618 &focus_handle,
619 cx,
620 )
621 }
622 }),
623 )
624 })
625 .into_any_element()
626 }
627 });
628 pane
629 })
630}
631
632pub struct DebugTerminal {
633 pub terminal: Option<Entity<TerminalView>>,
634 focus_handle: FocusHandle,
635 _subscriptions: [Subscription; 1],
636}
637
638impl DebugTerminal {
639 fn empty(window: &mut Window, cx: &mut Context<Self>) -> Self {
640 let focus_handle = cx.focus_handle();
641 let focus_subscription = cx.on_focus(&focus_handle, window, |this, window, cx| {
642 if let Some(terminal) = this.terminal.as_ref() {
643 terminal.focus_handle(cx).focus(window, cx);
644 }
645 });
646
647 Self {
648 terminal: None,
649 focus_handle,
650 _subscriptions: [focus_subscription],
651 }
652 }
653}
654
655impl gpui::Render for DebugTerminal {
656 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
657 div()
658 .track_focus(&self.focus_handle)
659 .size_full()
660 .bg(cx.theme().colors().editor_background)
661 .children(self.terminal.clone())
662 }
663}
664impl Focusable for DebugTerminal {
665 fn focus_handle(&self, _cx: &App) -> FocusHandle {
666 self.focus_handle.clone()
667 }
668}
669
670impl RunningState {
671 // todo(debugger) move this to util and make it so you pass a closure to it that converts a string
672 pub(crate) fn substitute_variables_in_config(
673 config: &mut serde_json::Value,
674 context: &TaskContext,
675 ) {
676 match config {
677 serde_json::Value::Object(obj) => {
678 obj.values_mut()
679 .for_each(|value| Self::substitute_variables_in_config(value, context));
680 }
681 serde_json::Value::Array(array) => {
682 array
683 .iter_mut()
684 .for_each(|value| Self::substitute_variables_in_config(value, context));
685 }
686 serde_json::Value::String(s) => {
687 // Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
688 if s.starts_with("\"$ZED_") && s.ends_with('"') {
689 *s = s[1..s.len() - 1].to_string();
690 }
691 if let Some(substituted) = substitute_variables_in_str(s, context) {
692 *s = substituted;
693 }
694 }
695 _ => {}
696 }
697 }
698
699 pub(crate) fn contains_substring(config: &serde_json::Value, substring: &str) -> bool {
700 match config {
701 serde_json::Value::Object(obj) => obj
702 .values()
703 .any(|value| Self::contains_substring(value, substring)),
704 serde_json::Value::Array(array) => array
705 .iter()
706 .any(|value| Self::contains_substring(value, substring)),
707 serde_json::Value::String(s) => s.contains(substring),
708 _ => false,
709 }
710 }
711
712 pub(crate) fn substitute_process_id_in_config(config: &mut serde_json::Value, process_id: i32) {
713 match config {
714 serde_json::Value::Object(obj) => {
715 obj.values_mut().for_each(|value| {
716 Self::substitute_process_id_in_config(value, process_id);
717 });
718 }
719 serde_json::Value::Array(array) => {
720 array.iter_mut().for_each(|value| {
721 Self::substitute_process_id_in_config(value, process_id);
722 });
723 }
724 serde_json::Value::String(s) => {
725 if s.contains(PROCESS_ID_PLACEHOLDER.as_str()) {
726 *s = s.replace(PROCESS_ID_PLACEHOLDER.as_str(), &process_id.to_string());
727 }
728 }
729 _ => {}
730 }
731 }
732
733 pub(crate) fn relativize_paths(
734 key: Option<&str>,
735 config: &mut serde_json::Value,
736 context: &TaskContext,
737 ) {
738 match config {
739 serde_json::Value::Object(obj) => {
740 obj.iter_mut()
741 .for_each(|(key, value)| Self::relativize_paths(Some(key), value, context));
742 }
743 serde_json::Value::Array(array) => {
744 array
745 .iter_mut()
746 .for_each(|value| Self::relativize_paths(None, value, context));
747 }
748 serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
749 // Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
750 if s.starts_with("\"$ZED_") && s.ends_with('"') {
751 *s = s[1..s.len() - 1].to_string();
752 }
753 resolve_path(s);
754
755 if let Some(substituted) = substitute_variables_in_str(s, context) {
756 *s = substituted;
757 }
758 }
759 _ => {}
760 }
761 }
762
763 pub(crate) fn new(
764 session: Entity<Session>,
765 project: Entity<Project>,
766 workspace: WeakEntity<Workspace>,
767 parent_terminal: Option<Entity<DebugTerminal>>,
768 serialized_pane_layout: Option<SerializedLayout>,
769 dock_axis: Axis,
770 window: &mut Window,
771 cx: &mut Context<Self>,
772 ) -> Self {
773 let focus_handle = cx.focus_handle();
774 let session_id = session.read(cx).session_id();
775 let weak_project = project.downgrade();
776 let weak_state = cx.weak_entity();
777 let stack_frame_list = cx.new(|cx| {
778 StackFrameList::new(
779 workspace.clone(),
780 session.clone(),
781 weak_state.clone(),
782 window,
783 cx,
784 )
785 });
786
787 let debug_terminal =
788 parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
789 let memory_view = cx.new(|cx| {
790 MemoryView::new(
791 session.clone(),
792 workspace.clone(),
793 stack_frame_list.downgrade(),
794 window,
795 cx,
796 )
797 });
798 let variable_list = cx.new(|cx| {
799 VariableList::new(
800 session.clone(),
801 stack_frame_list.clone(),
802 memory_view.clone(),
803 weak_state.clone(),
804 window,
805 cx,
806 )
807 });
808
809 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
810
811 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
812
813 let console = cx.new(|cx| {
814 Console::new(
815 session.clone(),
816 stack_frame_list.clone(),
817 variable_list.clone(),
818 window,
819 cx,
820 )
821 });
822
823 let breakpoint_list = BreakpointList::new(
824 Some(session.clone()),
825 workspace.clone(),
826 &project,
827 window,
828 cx,
829 );
830
831 let _subscriptions = vec![
832 cx.on_app_quit(move |this, cx| {
833 let shutdown = this
834 .session
835 .update(cx, |session, cx| session.on_app_quit(cx));
836 let terminal = this.debug_terminal.clone();
837 async move {
838 shutdown.await;
839 drop(terminal)
840 }
841 }),
842 cx.observe(&module_list, |_, _, cx| cx.notify()),
843 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
844 match event {
845 SessionEvent::Stopped(thread_id) => {
846 let panel = this
847 .workspace
848 .update(cx, |workspace, cx| {
849 workspace.open_panel::<crate::DebugPanel>(window, cx);
850 workspace.panel::<crate::DebugPanel>(cx)
851 })
852 .log_err()
853 .flatten();
854
855 if let Some(thread_id) = thread_id {
856 this.select_thread(*thread_id, window, cx);
857 }
858 if let Some(panel) = panel {
859 let id = this.session_id;
860 window.defer(cx, move |window, cx| {
861 panel.update(cx, |this, cx| {
862 this.activate_session_by_id(id, window, cx);
863 })
864 })
865 }
866 }
867 SessionEvent::Threads => {
868 let threads = this.session.update(cx, |this, cx| this.threads(cx));
869 this.select_current_thread(&threads, window, cx);
870 }
871 SessionEvent::CapabilitiesLoaded => {
872 let capabilities = this.capabilities(cx);
873 if !capabilities.supports_modules_request.unwrap_or(false) {
874 this.remove_pane_item(DebuggerPaneItem::Modules, window, cx);
875 }
876 if !capabilities
877 .supports_loaded_sources_request
878 .unwrap_or(false)
879 {
880 this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
881 }
882 }
883 SessionEvent::RunInTerminal { request, sender } => this
884 .handle_run_in_terminal(request, sender.clone(), window, cx)
885 .detach_and_log_err(cx),
886
887 _ => {}
888 }
889 cx.notify()
890 }),
891 cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
892 this.serialize_layout(window, cx);
893 }),
894 cx.subscribe(
895 &session,
896 |this, session, event: &SessionStateEvent, cx| match event {
897 SessionStateEvent::Shutdown if session.read(cx).is_building() => {
898 this.shutdown(cx);
899 }
900 _ => {}
901 },
902 ),
903 ];
904
905 let mut pane_close_subscriptions = HashMap::default();
906 let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| {
907 persistence::deserialize_pane_layout(
908 serialized_layout.panes,
909 dock_axis != serialized_layout.dock_axis,
910 &workspace,
911 &project,
912 &stack_frame_list,
913 &variable_list,
914 &module_list,
915 &console,
916 &breakpoint_list,
917 &loaded_source_list,
918 &debug_terminal,
919 &memory_view,
920 &mut pane_close_subscriptions,
921 window,
922 cx,
923 )
924 }) {
925 workspace::PaneGroup::with_root(root)
926 } else {
927 pane_close_subscriptions.clear();
928
929 let root = Self::default_pane_layout(
930 project,
931 &workspace,
932 &stack_frame_list,
933 &variable_list,
934 &console,
935 &breakpoint_list,
936 &debug_terminal,
937 dock_axis,
938 &mut pane_close_subscriptions,
939 window,
940 cx,
941 );
942
943 workspace::PaneGroup::with_root(root)
944 };
945 let active_pane = panes.first_pane();
946
947 Self {
948 memory_view,
949 session,
950 workspace,
951 project: weak_project,
952 focus_handle,
953 variable_list,
954 _subscriptions,
955 thread_id: None,
956 _remote_id: None,
957 stack_frame_list,
958 session_id,
959 panes,
960 active_pane,
961 module_list,
962 console,
963 breakpoint_list,
964 loaded_sources_list: loaded_source_list,
965 pane_close_subscriptions,
966 debug_terminal,
967 dock_axis,
968 _schedule_serialize: None,
969 scenario: None,
970 scenario_context: None,
971 }
972 }
973
974 pub(crate) fn remove_pane_item(
975 &mut self,
976 item_kind: DebuggerPaneItem,
977 window: &mut Window,
978 cx: &mut Context<Self>,
979 ) {
980 if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
981 Some(pane).zip(
982 pane.read(cx)
983 .items()
984 .find(|item| {
985 item.act_as::<SubView>(cx)
986 .is_some_and(|view| view.read(cx).kind == item_kind)
987 })
988 .map(|item| item.item_id()),
989 )
990 }) {
991 pane.update(cx, |pane, cx| {
992 pane.remove_item(item_id, false, true, window, cx)
993 })
994 }
995 }
996
997 pub(crate) fn has_pane_at_position(&self, position: Point<Pixels>) -> bool {
998 self.panes.pane_at_pixel_position(position).is_some()
999 }
1000
1001 pub(crate) fn resolve_scenario(
1002 &self,
1003 scenario: DebugScenario,
1004 task_context: SharedTaskContext,
1005 buffer: Option<Entity<Buffer>>,
1006 worktree_id: Option<WorktreeId>,
1007 window: &Window,
1008 cx: &mut Context<Self>,
1009 ) -> Task<Result<DebugTaskDefinition>> {
1010 let Some(workspace) = self.workspace.upgrade() else {
1011 return Task::ready(Err(anyhow!("no workspace")));
1012 };
1013 let project = workspace.read(cx).project().clone();
1014 let dap_store = project.read(cx).dap_store().downgrade();
1015 let dap_registry = cx.global::<DapRegistry>().clone();
1016 let task_store = project.read(cx).task_store().downgrade();
1017 let weak_project = project.downgrade();
1018 let weak_workspace = workspace.downgrade();
1019 let is_windows = project.read(cx).path_style(cx).is_windows();
1020 let remote_shell = project
1021 .read(cx)
1022 .remote_client()
1023 .as_ref()
1024 .and_then(|remote| remote.read(cx).shell());
1025
1026 cx.spawn_in(window, async move |this, cx| {
1027 let DebugScenario {
1028 adapter,
1029 label,
1030 build,
1031 mut config,
1032 tcp_connection,
1033 } = scenario;
1034 Self::relativize_paths(None, &mut config, &task_context);
1035 Self::substitute_variables_in_config(&mut config, &task_context);
1036
1037 if Self::contains_substring(&config, PROCESS_ID_PLACEHOLDER.as_str()) || label.as_ref().contains(PROCESS_ID_PLACEHOLDER.as_str()) {
1038 let (tx, rx) = futures::channel::oneshot::channel::<Option<i32>>();
1039
1040 let weak_workspace_clone = weak_workspace.clone();
1041 weak_workspace.update_in(cx, |workspace, window, cx| {
1042 let project = workspace.project().clone();
1043 workspace.toggle_modal(window, cx, |window, cx| {
1044 AttachModal::new(
1045 ModalIntent::ResolveProcessId(Some(tx)),
1046 weak_workspace_clone,
1047 project,
1048 true,
1049 window,
1050 cx,
1051 )
1052 });
1053 }).ok();
1054
1055 let Some(process_id) = rx.await.ok().flatten() else {
1056 bail!("No process selected with config that contains {}", PROCESS_ID_PLACEHOLDER.as_str())
1057 };
1058
1059 Self::substitute_process_id_in_config(&mut config, process_id);
1060 }
1061
1062 let request_type = match dap_registry
1063 .adapter(&adapter)
1064 .with_context(|| format!("{}: is not a valid adapter name", &adapter)) {
1065 Ok(adapter) => adapter.request_kind(&config).await,
1066 Err(e) => Err(e)
1067 };
1068
1069
1070 let config_is_valid = request_type.is_ok();
1071 let mut extra_config = Value::Null;
1072 let build_output = if let Some(build) = build {
1073 let (task_template, locator_name) = match build {
1074 BuildTaskDefinition::Template {
1075 task_template,
1076 locator_name,
1077 } => (task_template, locator_name),
1078 BuildTaskDefinition::ByName(ref label) => {
1079 let task = task_store.update(cx, |this, cx| {
1080 this.task_inventory().map(|inventory| {
1081 inventory.read(cx).task_template_by_label(
1082 buffer,
1083 worktree_id,
1084 label,
1085 cx,
1086 )
1087 })
1088 })?;
1089 let task = match task {
1090 Some(task) => task.await,
1091 None => None,
1092 }.with_context(|| format!("Couldn't find task template for {build:?}"))?;
1093 (task, None)
1094 }
1095 };
1096 let Some(mut task) = task_template.resolve_task("debug-build-task", &task_context) else {
1097 anyhow::bail!("Could not resolve task variables within a debug scenario");
1098 };
1099
1100 let locator_name = if let Some(locator_name) = locator_name {
1101 extra_config = config.clone();
1102 debug_assert!(!config_is_valid);
1103 Some(locator_name)
1104 } else if !config_is_valid {
1105 let task = dap_store
1106 .update(cx, |this, cx| {
1107 this.debug_scenario_for_build_task(
1108 task.original_task().clone(),
1109 adapter.clone().into(),
1110 task.display_label().to_owned().into(),
1111 cx,
1112 )
1113
1114 });
1115 if let Ok(t) = task {
1116 t.await.and_then(|scenario| {
1117 extra_config = scenario.config;
1118 match scenario.build {
1119 Some(BuildTaskDefinition::Template {
1120 locator_name, ..
1121 }) => locator_name,
1122 _ => None,
1123 }
1124 })
1125 } else {
1126 None
1127 }
1128
1129 } else {
1130 None
1131 };
1132
1133 if let Some(remote_shell) = remote_shell && task.resolved.shell == Shell::System {
1134 task.resolved.shell = Shell::Program(remote_shell);
1135 }
1136
1137 let builder = ShellBuilder::new(&task.resolved.shell, is_windows);
1138 let command_label = builder.command_label(task.resolved.command.as_deref().unwrap_or(""));
1139 let (command, args) =
1140 builder.build(task.resolved.command.clone(), &task.resolved.args);
1141
1142 let task_with_shell = SpawnInTerminal {
1143 command_label,
1144 command: Some(command),
1145 args,
1146 ..task.resolved.clone()
1147 };
1148 let terminal = project
1149 .update(cx, |project, cx| {
1150 project.create_terminal_task(
1151 task_with_shell.clone(),
1152 cx,
1153 )
1154 }).await?;
1155
1156 let terminal_view = cx.new_window_entity(|window, cx| {
1157 TerminalView::new(
1158 terminal.clone(),
1159 weak_workspace,
1160 None,
1161 weak_project,
1162 window,
1163 cx,
1164 )
1165 })?;
1166
1167 this.update_in(cx, |this, window, cx| {
1168 this.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
1169 this.debug_terminal.update(cx, |debug_terminal, cx| {
1170 debug_terminal.terminal = Some(terminal_view);
1171 cx.notify();
1172 });
1173 })?;
1174
1175 let exit_status = terminal
1176 .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))
1177 .await
1178 .context("Failed to wait for completed task")?;
1179
1180 if !exit_status.success() {
1181 anyhow::bail!("Build failed");
1182 }
1183 Some((task.resolved.clone(), locator_name, extra_config))
1184 } else {
1185 None
1186 };
1187
1188 if config_is_valid {
1189 } else if let Some((task, locator_name, extra_config)) = build_output {
1190 let locator_name =
1191 locator_name.with_context(|| {
1192 format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
1193 .map(|err| err.to_string())
1194 .unwrap_or_default())
1195 })?;
1196 let request = dap_store
1197 .update(cx, |this, cx| {
1198 this.run_debug_locator(&locator_name, task, cx)
1199 })?
1200 .await?;
1201
1202 let zed_config = ZedDebugConfig {
1203 label: label.clone(),
1204 adapter: adapter.clone(),
1205 request,
1206 stop_on_entry: None,
1207 };
1208
1209 let scenario = dap_registry
1210 .adapter(&adapter)
1211 .with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
1212 .await?;
1213 config = scenario.config;
1214 util::merge_non_null_json_value_into(extra_config, &mut config);
1215
1216 Self::substitute_variables_in_config(&mut config, &task_context);
1217 } else {
1218 let Err(e) = request_type else {
1219 unreachable!();
1220 };
1221 anyhow::bail!("Zed cannot determine how to run this debug scenario. `build` field was not provided and Debug Adapter won't accept provided configuration because: {e}");
1222 };
1223
1224 Ok(DebugTaskDefinition {
1225 label,
1226 adapter: DebugAdapterName(adapter),
1227 config,
1228 tcp_connection,
1229 })
1230 })
1231 }
1232
1233 fn handle_run_in_terminal(
1234 &self,
1235 request: &RunInTerminalRequestArguments,
1236 mut sender: mpsc::Sender<Result<u32>>,
1237 window: &mut Window,
1238 cx: &mut Context<Self>,
1239 ) -> Task<Result<()>> {
1240 let running = cx.entity();
1241 let Ok(project) = self
1242 .workspace
1243 .read_with(cx, |workspace, _| workspace.project().clone())
1244 else {
1245 return Task::ready(Err(anyhow!("no workspace")));
1246 };
1247 let session = self.session.read(cx);
1248
1249 let cwd = (!request.cwd.is_empty())
1250 .then(|| PathBuf::from(&request.cwd))
1251 .or_else(|| session.binary().unwrap().cwd.clone());
1252
1253 let mut envs: HashMap<String, String> =
1254 self.session.read(cx).task_context().project_env.clone();
1255 if let Some(Value::Object(env)) = &request.env {
1256 for (key, value) in env {
1257 let value_str = match (key.as_str(), value) {
1258 (_, Value::String(value)) => value,
1259 _ => continue,
1260 };
1261
1262 envs.insert(key.clone(), value_str.clone());
1263 }
1264 }
1265
1266 let mut args = request.args.clone();
1267 let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") {
1268 // Handle special case for NodeJS debug adapter
1269 // If the Node binary path is provided (possibly with arguments like --experimental-network-inspection),
1270 // we set the command to None
1271 // This prevents the NodeJS REPL from appearing, which is not the desired behavior
1272 // The expected usage is for users to provide their own Node command, e.g., `node test.js`
1273 // This allows the NodeJS debug client to attach correctly
1274 if args
1275 .iter()
1276 .filter(|arg| !arg.starts_with("--"))
1277 .collect::<Vec<_>>()
1278 .len()
1279 > 1
1280 {
1281 Some(args.remove(0))
1282 } else {
1283 None
1284 }
1285 } else if !args.is_empty() {
1286 Some(args.remove(0))
1287 } else {
1288 None
1289 };
1290
1291 let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
1292 let title = request
1293 .title
1294 .clone()
1295 .filter(|title| !title.is_empty())
1296 .or_else(|| command.clone())
1297 .unwrap_or_else(|| "Debug terminal".to_string());
1298 let kind = task::SpawnInTerminal {
1299 id: task::TaskId("debug".to_string()),
1300 full_label: title.clone(),
1301 label: title.clone(),
1302 command,
1303 args,
1304 command_label: title,
1305 cwd,
1306 env: envs,
1307 use_new_terminal: true,
1308 allow_concurrent_runs: true,
1309 reveal: task::RevealStrategy::NoFocus,
1310 reveal_target: task::RevealTarget::Dock,
1311 hide: task::HideStrategy::Never,
1312 shell,
1313 show_summary: false,
1314 show_command: false,
1315 show_rerun: false,
1316 save: task::SaveStrategy::default(),
1317 };
1318
1319 let workspace = self.workspace.clone();
1320 let weak_project = project.downgrade();
1321
1322 let terminal_task =
1323 project.update(cx, |project, cx| project.create_terminal_task(kind, cx));
1324 let terminal_task = cx.spawn_in(window, async move |_, cx| {
1325 let terminal = terminal_task.await?;
1326
1327 let terminal_view = cx.new_window_entity(|window, cx| {
1328 TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx)
1329 })?;
1330
1331 running.update_in(cx, |running, window, cx| {
1332 running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
1333 running.debug_terminal.update(cx, |debug_terminal, cx| {
1334 debug_terminal.terminal = Some(terminal_view);
1335 cx.notify();
1336 });
1337 })?;
1338
1339 terminal.read_with(cx, |terminal, _| {
1340 terminal
1341 .pid()
1342 .map(|pid| pid.as_u32())
1343 .context("Terminal was spawned but PID was not available")
1344 })
1345 });
1346
1347 cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) })
1348 }
1349
1350 fn create_sub_view(
1351 &self,
1352 item_kind: DebuggerPaneItem,
1353 pane: &Entity<Pane>,
1354 cx: &mut Context<Self>,
1355 ) -> Box<dyn ItemHandle> {
1356 let running_state = cx.weak_entity();
1357 let host_pane = pane.downgrade();
1358
1359 match item_kind {
1360 DebuggerPaneItem::Console => Box::new(SubView::console(
1361 self.console.clone(),
1362 running_state,
1363 host_pane,
1364 cx,
1365 )),
1366 DebuggerPaneItem::Variables => Box::new(SubView::new(
1367 self.variable_list.focus_handle(cx),
1368 self.variable_list.clone().into(),
1369 item_kind,
1370 running_state,
1371 host_pane,
1372 cx,
1373 )),
1374 DebuggerPaneItem::BreakpointList => Box::new(SubView::breakpoint_list(
1375 self.breakpoint_list.clone(),
1376 running_state,
1377 host_pane,
1378 cx,
1379 )),
1380 DebuggerPaneItem::Frames => Box::new(SubView::new(
1381 self.stack_frame_list.focus_handle(cx),
1382 self.stack_frame_list.clone().into(),
1383 item_kind,
1384 running_state,
1385 host_pane,
1386 cx,
1387 )),
1388 DebuggerPaneItem::Modules => Box::new(SubView::new(
1389 self.module_list.focus_handle(cx),
1390 self.module_list.clone().into(),
1391 item_kind,
1392 running_state,
1393 host_pane,
1394 cx,
1395 )),
1396 DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
1397 self.loaded_sources_list.focus_handle(cx),
1398 self.loaded_sources_list.clone().into(),
1399 item_kind,
1400 running_state,
1401 host_pane,
1402 cx,
1403 )),
1404 DebuggerPaneItem::Terminal => Box::new(SubView::new(
1405 self.debug_terminal.focus_handle(cx),
1406 self.debug_terminal.clone().into(),
1407 item_kind,
1408 running_state,
1409 host_pane,
1410 cx,
1411 )),
1412 DebuggerPaneItem::MemoryView => Box::new(SubView::new(
1413 self.memory_view.focus_handle(cx),
1414 self.memory_view.clone().into(),
1415 item_kind,
1416 running_state,
1417 host_pane,
1418 cx,
1419 )),
1420 }
1421 }
1422
1423 pub(crate) fn ensure_pane_item(
1424 &mut self,
1425 item_kind: DebuggerPaneItem,
1426 window: &mut Window,
1427 cx: &mut Context<Self>,
1428 ) {
1429 if self.pane_items_status(cx).get(&item_kind) == Some(&true) {
1430 return;
1431 };
1432 let pane = self.panes.last_pane();
1433 let sub_view = self.create_sub_view(item_kind, &pane, cx);
1434
1435 pane.update(cx, |pane, cx| {
1436 pane.add_item_inner(sub_view, false, false, false, None, window, cx);
1437 })
1438 }
1439
1440 pub(crate) fn add_pane_item(
1441 &mut self,
1442 item_kind: DebuggerPaneItem,
1443 position: Point<Pixels>,
1444 window: &mut Window,
1445 cx: &mut Context<Self>,
1446 ) {
1447 debug_assert!(
1448 item_kind.is_supported(self.session.read(cx).capabilities()),
1449 "We should only allow adding supported item kinds"
1450 );
1451
1452 if let Some(pane) = self.panes.pane_at_pixel_position(position) {
1453 let sub_view = self.create_sub_view(item_kind, pane, cx);
1454
1455 pane.update(cx, |pane, cx| {
1456 pane.add_item(sub_view, false, false, None, window, cx);
1457 })
1458 }
1459 }
1460
1461 pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap<DebuggerPaneItem, bool> {
1462 let caps = self.session.read(cx).capabilities();
1463 let mut pane_item_status = IndexMap::from_iter(
1464 DebuggerPaneItem::all()
1465 .iter()
1466 .filter(|kind| kind.is_supported(caps))
1467 .map(|kind| (*kind, false)),
1468 );
1469 self.panes.panes().iter().for_each(|pane| {
1470 pane.read(cx)
1471 .items()
1472 .filter_map(|item| item.act_as::<SubView>(cx))
1473 .for_each(|view| {
1474 pane_item_status.insert(view.read(cx).kind, true);
1475 });
1476 });
1477
1478 pane_item_status
1479 }
1480
1481 pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1482 if self._schedule_serialize.is_none() {
1483 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
1484 cx.background_executor()
1485 .timer(Duration::from_millis(100))
1486 .await;
1487
1488 let Some((adapter_name, pane_layout)) = this
1489 .read_with(cx, |this, cx| {
1490 let adapter_name = this.session.read(cx).adapter();
1491 (
1492 adapter_name,
1493 persistence::build_serialized_layout(
1494 &this.panes.root,
1495 this.dock_axis,
1496 cx,
1497 ),
1498 )
1499 })
1500 .ok()
1501 else {
1502 return;
1503 };
1504
1505 let kvp = this
1506 .read_with(cx, |_, cx| db::kvp::KeyValueStore::global(cx))
1507 .ok();
1508 if let Some(kvp) = kvp {
1509 persistence::serialize_pane_layout(adapter_name, pane_layout, kvp)
1510 .await
1511 .log_err();
1512 }
1513
1514 this.update(cx, |this, _| {
1515 this._schedule_serialize.take();
1516 })
1517 .ok();
1518 }));
1519 }
1520 }
1521
1522 pub(crate) fn handle_pane_event(
1523 this: &mut RunningState,
1524 source_pane: &Entity<Pane>,
1525 event: &Event,
1526 window: &mut Window,
1527 cx: &mut Context<RunningState>,
1528 ) {
1529 this.serialize_layout(window, cx);
1530 match event {
1531 Event::AddItem { item } => {
1532 if let Some(sub_view) = item.downcast::<SubView>() {
1533 sub_view.update(cx, |sub_view, _| {
1534 sub_view.set_host_pane(source_pane.downgrade());
1535 });
1536 }
1537 }
1538 Event::Remove { .. } => {
1539 let _did_find_pane = this.panes.remove(source_pane, cx).is_ok();
1540 debug_assert!(_did_find_pane);
1541 cx.notify();
1542 }
1543 Event::Focus => {
1544 this.active_pane = source_pane.clone();
1545 }
1546 _ => {}
1547 }
1548 }
1549
1550 pub(crate) fn activate_pane_in_direction(
1551 &mut self,
1552 direction: SplitDirection,
1553 window: &mut Window,
1554 cx: &mut Context<Self>,
1555 ) {
1556 let active_pane = self.active_pane.clone();
1557 if let Some(pane) = self
1558 .panes
1559 .find_pane_in_direction(&active_pane, direction, cx)
1560 {
1561 pane.update(cx, |pane, cx| {
1562 pane.focus_active_item(window, cx);
1563 })
1564 } else {
1565 self.workspace
1566 .update(cx, |workspace, cx| {
1567 workspace.activate_pane_in_direction(direction, window, cx)
1568 })
1569 .ok();
1570 }
1571 }
1572
1573 pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
1574 if self.thread_id.is_some() {
1575 self.stack_frame_list
1576 .update(cx, |list, cx| {
1577 let Some(stack_frame_id) = list.opened_stack_frame_id() else {
1578 return Task::ready(Ok(()));
1579 };
1580 list.go_to_stack_frame(stack_frame_id, window, cx)
1581 })
1582 .detach();
1583 }
1584 }
1585
1586 pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool {
1587 self.variable_list.read(cx).has_open_context_menu()
1588 }
1589
1590 pub fn session(&self) -> &Entity<Session> {
1591 &self.session
1592 }
1593
1594 pub fn session_id(&self) -> SessionId {
1595 self.session_id
1596 }
1597
1598 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
1599 self.stack_frame_list.read(cx).opened_stack_frame_id()
1600 }
1601
1602 pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
1603 &self.stack_frame_list
1604 }
1605
1606 #[cfg(test)]
1607 pub fn console(&self) -> &Entity<Console> {
1608 &self.console
1609 }
1610
1611 #[cfg(test)]
1612 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
1613 &self.module_list
1614 }
1615
1616 pub(crate) fn activate_item(
1617 &mut self,
1618 item: DebuggerPaneItem,
1619 window: &mut Window,
1620 cx: &mut Context<Self>,
1621 ) {
1622 self.ensure_pane_item(item, window, cx);
1623
1624 let (variable_list_position, pane) = self
1625 .panes
1626 .panes()
1627 .into_iter()
1628 .find_map(|pane| {
1629 pane.read(cx)
1630 .items_of_type::<SubView>()
1631 .position(|view| view.read(cx).view_kind() == item)
1632 .map(|view| (view, pane))
1633 })
1634 .unwrap();
1635
1636 pane.update(cx, |this, cx| {
1637 this.activate_item(variable_list_position, true, true, window, cx);
1638 });
1639 }
1640
1641 #[cfg(test)]
1642 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
1643 &self.variable_list
1644 }
1645
1646 #[cfg(test)]
1647 pub(crate) fn serialized_layout(&self, cx: &App) -> SerializedLayout {
1648 persistence::build_serialized_layout(&self.panes.root, self.dock_axis, cx)
1649 }
1650
1651 pub fn capabilities(&self, cx: &App) -> Capabilities {
1652 self.session().read(cx).capabilities().clone()
1653 }
1654
1655 pub fn select_current_thread(
1656 &mut self,
1657 threads: &Vec<(Thread, ThreadStatus)>,
1658 window: &mut Window,
1659 cx: &mut Context<Self>,
1660 ) {
1661 let selected_thread = self
1662 .thread_id
1663 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
1664 .or_else(|| threads.first());
1665
1666 let Some((selected_thread, _)) = selected_thread else {
1667 return;
1668 };
1669
1670 if Some(ThreadId(selected_thread.id)) != self.thread_id {
1671 self.select_thread(ThreadId(selected_thread.id), window, cx);
1672 }
1673 }
1674
1675 pub fn selected_thread_id(&self) -> Option<ThreadId> {
1676 self.thread_id
1677 }
1678
1679 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
1680 self.thread_id
1681 .map(|id| self.session().read(cx).thread_status(id))
1682 }
1683
1684 pub(crate) fn select_thread(
1685 &mut self,
1686 thread_id: ThreadId,
1687 window: &mut Window,
1688 cx: &mut Context<Self>,
1689 ) {
1690 if self.thread_id.is_some_and(|id| id == thread_id) {
1691 return;
1692 }
1693
1694 self.thread_id = Some(thread_id);
1695
1696 self.stack_frame_list
1697 .update(cx, |list, cx| list.schedule_refresh(true, window, cx));
1698 }
1699
1700 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
1701 let Some(thread_id) = self.thread_id else {
1702 return;
1703 };
1704
1705 self.session().update(cx, |state, cx| {
1706 state.continue_thread(thread_id, cx);
1707 });
1708 }
1709
1710 pub fn step_over(&mut self, cx: &mut Context<Self>) {
1711 let Some(thread_id) = self.thread_id else {
1712 return;
1713 };
1714
1715 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1716
1717 self.session().update(cx, |state, cx| {
1718 state.step_over(thread_id, granularity, cx);
1719 });
1720 }
1721
1722 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
1723 let Some(thread_id) = self.thread_id else {
1724 return;
1725 };
1726
1727 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1728
1729 self.session().update(cx, |state, cx| {
1730 state.step_in(thread_id, granularity, cx);
1731 });
1732 }
1733
1734 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
1735 let Some(thread_id) = self.thread_id else {
1736 return;
1737 };
1738
1739 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1740
1741 self.session().update(cx, |state, cx| {
1742 state.step_out(thread_id, granularity, cx);
1743 });
1744 }
1745
1746 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
1747 let Some(thread_id) = self.thread_id else {
1748 return;
1749 };
1750
1751 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1752
1753 self.session().update(cx, |state, cx| {
1754 state.step_back(thread_id, granularity, cx);
1755 });
1756 }
1757
1758 pub fn rerun_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1759 if let Some((scenario, context)) = self.scenario.take().zip(self.scenario_context.take())
1760 && scenario.build.is_some()
1761 {
1762 let DebugScenarioContext {
1763 task_context,
1764 active_buffer,
1765 worktree_id,
1766 } = context;
1767 let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
1768
1769 self.workspace
1770 .update(cx, |workspace, cx| {
1771 workspace.start_debug_session(
1772 scenario,
1773 task_context,
1774 active_buffer,
1775 worktree_id,
1776 window,
1777 cx,
1778 )
1779 })
1780 .ok();
1781 } else {
1782 self.restart_session(cx);
1783 }
1784 }
1785
1786 pub fn restart_session(&self, cx: &mut Context<Self>) {
1787 self.session().update(cx, |state, cx| {
1788 state.restart(None, cx);
1789 });
1790 }
1791
1792 pub fn pause_thread(&self, cx: &mut Context<Self>) {
1793 let Some(thread_id) = self.thread_id else {
1794 return;
1795 };
1796
1797 self.session().update(cx, |state, cx| {
1798 state.pause_thread(thread_id, cx);
1799 });
1800 }
1801
1802 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
1803 self.workspace
1804 .update(cx, |workspace, cx| {
1805 workspace
1806 .project()
1807 .read(cx)
1808 .breakpoint_store()
1809 .update(cx, |store, cx| {
1810 store.remove_active_position(Some(self.session_id), cx)
1811 })
1812 })
1813 .log_err();
1814
1815 let is_building = self.session.update(cx, |session, cx| {
1816 session.shutdown(cx).detach();
1817 matches!(session.state, session::SessionState::Booting(_))
1818 });
1819
1820 if is_building {
1821 self.debug_terminal.update(cx, |terminal, cx| {
1822 if let Some(view) = terminal.terminal.as_ref() {
1823 view.update(cx, |view, cx| {
1824 view.terminal()
1825 .update(cx, |terminal, _| terminal.kill_active_task())
1826 })
1827 }
1828 })
1829 }
1830 }
1831
1832 pub fn stop_thread(&self, cx: &mut Context<Self>) {
1833 let Some(thread_id) = self.thread_id else {
1834 return;
1835 };
1836
1837 self.workspace
1838 .update(cx, |workspace, cx| {
1839 workspace
1840 .project()
1841 .read(cx)
1842 .breakpoint_store()
1843 .update(cx, |store, cx| {
1844 store.remove_active_position(Some(self.session_id), cx)
1845 })
1846 })
1847 .log_err();
1848
1849 self.session().update(cx, |state, cx| {
1850 state.terminate_threads(Some(vec![thread_id; 1]), cx);
1851 });
1852 }
1853
1854 pub fn detach_client(&self, cx: &mut Context<Self>) {
1855 self.session().update(cx, |state, cx| {
1856 state.disconnect_client(cx);
1857 });
1858 }
1859
1860 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
1861 self.session.update(cx, |session, cx| {
1862 session.toggle_ignore_breakpoints(cx).detach();
1863 });
1864 }
1865
1866 fn default_pane_layout(
1867 project: Entity<Project>,
1868 workspace: &WeakEntity<Workspace>,
1869 stack_frame_list: &Entity<StackFrameList>,
1870 variable_list: &Entity<VariableList>,
1871 console: &Entity<Console>,
1872 breakpoints: &Entity<BreakpointList>,
1873 debug_terminal: &Entity<DebugTerminal>,
1874 dock_axis: Axis,
1875 subscriptions: &mut HashMap<EntityId, Subscription>,
1876 window: &mut Window,
1877 cx: &mut Context<'_, RunningState>,
1878 ) -> Member {
1879 let running_state = cx.weak_entity();
1880
1881 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1882 let leftmost_pane_handle = leftmost_pane.downgrade();
1883 let leftmost_frames = SubView::new(
1884 stack_frame_list.focus_handle(cx),
1885 stack_frame_list.clone().into(),
1886 DebuggerPaneItem::Frames,
1887 running_state.clone(),
1888 leftmost_pane_handle.clone(),
1889 cx,
1890 );
1891 let leftmost_breakpoints = SubView::breakpoint_list(
1892 breakpoints.clone(),
1893 running_state.clone(),
1894 leftmost_pane_handle,
1895 cx,
1896 );
1897 leftmost_pane.update(cx, |this, cx| {
1898 this.add_item(Box::new(leftmost_frames), true, false, None, window, cx);
1899 this.add_item(
1900 Box::new(leftmost_breakpoints),
1901 true,
1902 false,
1903 None,
1904 window,
1905 cx,
1906 );
1907 this.activate_item(0, false, false, window, cx);
1908 });
1909
1910 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1911 let center_pane_handle = center_pane.downgrade();
1912 let center_console = SubView::console(
1913 console.clone(),
1914 running_state.clone(),
1915 center_pane_handle.clone(),
1916 cx,
1917 );
1918 let center_variables = SubView::new(
1919 variable_list.focus_handle(cx),
1920 variable_list.clone().into(),
1921 DebuggerPaneItem::Variables,
1922 running_state.clone(),
1923 center_pane_handle,
1924 cx,
1925 );
1926
1927 center_pane.update(cx, |this, cx| {
1928 this.add_item(Box::new(center_console), true, false, None, window, cx);
1929
1930 this.add_item(Box::new(center_variables), true, false, None, window, cx);
1931 this.activate_item(0, false, false, window, cx);
1932 });
1933
1934 let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx);
1935 let rightmost_terminal = SubView::new(
1936 debug_terminal.focus_handle(cx),
1937 debug_terminal.clone().into(),
1938 DebuggerPaneItem::Terminal,
1939 running_state,
1940 rightmost_pane.downgrade(),
1941 cx,
1942 );
1943 rightmost_pane.update(cx, |this, cx| {
1944 this.add_item(Box::new(rightmost_terminal), false, false, None, window, cx);
1945 });
1946
1947 subscriptions.extend(
1948 [&leftmost_pane, ¢er_pane, &rightmost_pane]
1949 .into_iter()
1950 .map(|entity| {
1951 (
1952 entity.entity_id(),
1953 cx.subscribe_in(entity, window, Self::handle_pane_event),
1954 )
1955 }),
1956 );
1957
1958 let group_root = workspace::PaneAxis::new(
1959 dock_axis.invert(),
1960 [leftmost_pane, center_pane, rightmost_pane]
1961 .into_iter()
1962 .map(workspace::Member::Pane)
1963 .collect(),
1964 );
1965
1966 Member::Axis(group_root)
1967 }
1968
1969 pub(crate) fn invert_axies(&mut self, cx: &mut App) {
1970 self.dock_axis = self.dock_axis.invert();
1971 self.panes.invert_axies(cx);
1972 }
1973}
1974
1975impl Focusable for RunningState {
1976 fn focus_handle(&self, _: &App) -> FocusHandle {
1977 self.focus_handle.clone()
1978 }
1979}