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