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 };
1317
1318 let workspace = self.workspace.clone();
1319 let weak_project = project.downgrade();
1320
1321 let terminal_task =
1322 project.update(cx, |project, cx| project.create_terminal_task(kind, cx));
1323 let terminal_task = cx.spawn_in(window, async move |_, cx| {
1324 let terminal = terminal_task.await?;
1325
1326 let terminal_view = cx.new_window_entity(|window, cx| {
1327 TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx)
1328 })?;
1329
1330 running.update_in(cx, |running, window, cx| {
1331 running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
1332 running.debug_terminal.update(cx, |debug_terminal, cx| {
1333 debug_terminal.terminal = Some(terminal_view);
1334 cx.notify();
1335 });
1336 })?;
1337
1338 terminal.read_with(cx, |terminal, _| {
1339 terminal
1340 .pid()
1341 .map(|pid| pid.as_u32())
1342 .context("Terminal was spawned but PID was not available")
1343 })
1344 });
1345
1346 cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) })
1347 }
1348
1349 fn create_sub_view(
1350 &self,
1351 item_kind: DebuggerPaneItem,
1352 pane: &Entity<Pane>,
1353 cx: &mut Context<Self>,
1354 ) -> Box<dyn ItemHandle> {
1355 let running_state = cx.weak_entity();
1356 let host_pane = pane.downgrade();
1357
1358 match item_kind {
1359 DebuggerPaneItem::Console => Box::new(SubView::console(
1360 self.console.clone(),
1361 running_state,
1362 host_pane,
1363 cx,
1364 )),
1365 DebuggerPaneItem::Variables => Box::new(SubView::new(
1366 self.variable_list.focus_handle(cx),
1367 self.variable_list.clone().into(),
1368 item_kind,
1369 running_state,
1370 host_pane,
1371 cx,
1372 )),
1373 DebuggerPaneItem::BreakpointList => Box::new(SubView::breakpoint_list(
1374 self.breakpoint_list.clone(),
1375 running_state,
1376 host_pane,
1377 cx,
1378 )),
1379 DebuggerPaneItem::Frames => Box::new(SubView::new(
1380 self.stack_frame_list.focus_handle(cx),
1381 self.stack_frame_list.clone().into(),
1382 item_kind,
1383 running_state,
1384 host_pane,
1385 cx,
1386 )),
1387 DebuggerPaneItem::Modules => Box::new(SubView::new(
1388 self.module_list.focus_handle(cx),
1389 self.module_list.clone().into(),
1390 item_kind,
1391 running_state,
1392 host_pane,
1393 cx,
1394 )),
1395 DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
1396 self.loaded_sources_list.focus_handle(cx),
1397 self.loaded_sources_list.clone().into(),
1398 item_kind,
1399 running_state,
1400 host_pane,
1401 cx,
1402 )),
1403 DebuggerPaneItem::Terminal => Box::new(SubView::new(
1404 self.debug_terminal.focus_handle(cx),
1405 self.debug_terminal.clone().into(),
1406 item_kind,
1407 running_state,
1408 host_pane,
1409 cx,
1410 )),
1411 DebuggerPaneItem::MemoryView => Box::new(SubView::new(
1412 self.memory_view.focus_handle(cx),
1413 self.memory_view.clone().into(),
1414 item_kind,
1415 running_state,
1416 host_pane,
1417 cx,
1418 )),
1419 }
1420 }
1421
1422 pub(crate) fn ensure_pane_item(
1423 &mut self,
1424 item_kind: DebuggerPaneItem,
1425 window: &mut Window,
1426 cx: &mut Context<Self>,
1427 ) {
1428 if self.pane_items_status(cx).get(&item_kind) == Some(&true) {
1429 return;
1430 };
1431 let pane = self.panes.last_pane();
1432 let sub_view = self.create_sub_view(item_kind, &pane, cx);
1433
1434 pane.update(cx, |pane, cx| {
1435 pane.add_item_inner(sub_view, false, false, false, None, window, cx);
1436 })
1437 }
1438
1439 pub(crate) fn add_pane_item(
1440 &mut self,
1441 item_kind: DebuggerPaneItem,
1442 position: Point<Pixels>,
1443 window: &mut Window,
1444 cx: &mut Context<Self>,
1445 ) {
1446 debug_assert!(
1447 item_kind.is_supported(self.session.read(cx).capabilities()),
1448 "We should only allow adding supported item kinds"
1449 );
1450
1451 if let Some(pane) = self.panes.pane_at_pixel_position(position) {
1452 let sub_view = self.create_sub_view(item_kind, pane, cx);
1453
1454 pane.update(cx, |pane, cx| {
1455 pane.add_item(sub_view, false, false, None, window, cx);
1456 })
1457 }
1458 }
1459
1460 pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap<DebuggerPaneItem, bool> {
1461 let caps = self.session.read(cx).capabilities();
1462 let mut pane_item_status = IndexMap::from_iter(
1463 DebuggerPaneItem::all()
1464 .iter()
1465 .filter(|kind| kind.is_supported(caps))
1466 .map(|kind| (*kind, false)),
1467 );
1468 self.panes.panes().iter().for_each(|pane| {
1469 pane.read(cx)
1470 .items()
1471 .filter_map(|item| item.act_as::<SubView>(cx))
1472 .for_each(|view| {
1473 pane_item_status.insert(view.read(cx).kind, true);
1474 });
1475 });
1476
1477 pane_item_status
1478 }
1479
1480 pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1481 if self._schedule_serialize.is_none() {
1482 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
1483 cx.background_executor()
1484 .timer(Duration::from_millis(100))
1485 .await;
1486
1487 let Some((adapter_name, pane_layout)) = this
1488 .read_with(cx, |this, cx| {
1489 let adapter_name = this.session.read(cx).adapter();
1490 (
1491 adapter_name,
1492 persistence::build_serialized_layout(
1493 &this.panes.root,
1494 this.dock_axis,
1495 cx,
1496 ),
1497 )
1498 })
1499 .ok()
1500 else {
1501 return;
1502 };
1503
1504 persistence::serialize_pane_layout(adapter_name, pane_layout)
1505 .await
1506 .log_err();
1507
1508 this.update(cx, |this, _| {
1509 this._schedule_serialize.take();
1510 })
1511 .ok();
1512 }));
1513 }
1514 }
1515
1516 pub(crate) fn handle_pane_event(
1517 this: &mut RunningState,
1518 source_pane: &Entity<Pane>,
1519 event: &Event,
1520 window: &mut Window,
1521 cx: &mut Context<RunningState>,
1522 ) {
1523 this.serialize_layout(window, cx);
1524 match event {
1525 Event::AddItem { item } => {
1526 if let Some(sub_view) = item.downcast::<SubView>() {
1527 sub_view.update(cx, |sub_view, _| {
1528 sub_view.set_host_pane(source_pane.downgrade());
1529 });
1530 }
1531 }
1532 Event::Remove { .. } => {
1533 let _did_find_pane = this.panes.remove(source_pane, cx).is_ok();
1534 debug_assert!(_did_find_pane);
1535 cx.notify();
1536 }
1537 Event::Focus => {
1538 this.active_pane = source_pane.clone();
1539 }
1540 _ => {}
1541 }
1542 }
1543
1544 pub(crate) fn activate_pane_in_direction(
1545 &mut self,
1546 direction: SplitDirection,
1547 window: &mut Window,
1548 cx: &mut Context<Self>,
1549 ) {
1550 let active_pane = self.active_pane.clone();
1551 if let Some(pane) = self
1552 .panes
1553 .find_pane_in_direction(&active_pane, direction, cx)
1554 {
1555 pane.update(cx, |pane, cx| {
1556 pane.focus_active_item(window, cx);
1557 })
1558 } else {
1559 self.workspace
1560 .update(cx, |workspace, cx| {
1561 workspace.activate_pane_in_direction(direction, window, cx)
1562 })
1563 .ok();
1564 }
1565 }
1566
1567 pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
1568 if self.thread_id.is_some() {
1569 self.stack_frame_list
1570 .update(cx, |list, cx| {
1571 let Some(stack_frame_id) = list.opened_stack_frame_id() else {
1572 return Task::ready(Ok(()));
1573 };
1574 list.go_to_stack_frame(stack_frame_id, window, cx)
1575 })
1576 .detach();
1577 }
1578 }
1579
1580 pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool {
1581 self.variable_list.read(cx).has_open_context_menu()
1582 }
1583
1584 pub fn session(&self) -> &Entity<Session> {
1585 &self.session
1586 }
1587
1588 pub fn session_id(&self) -> SessionId {
1589 self.session_id
1590 }
1591
1592 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
1593 self.stack_frame_list.read(cx).opened_stack_frame_id()
1594 }
1595
1596 pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
1597 &self.stack_frame_list
1598 }
1599
1600 #[cfg(test)]
1601 pub fn console(&self) -> &Entity<Console> {
1602 &self.console
1603 }
1604
1605 #[cfg(test)]
1606 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
1607 &self.module_list
1608 }
1609
1610 pub(crate) fn activate_item(
1611 &mut self,
1612 item: DebuggerPaneItem,
1613 window: &mut Window,
1614 cx: &mut Context<Self>,
1615 ) {
1616 self.ensure_pane_item(item, window, cx);
1617
1618 let (variable_list_position, pane) = self
1619 .panes
1620 .panes()
1621 .into_iter()
1622 .find_map(|pane| {
1623 pane.read(cx)
1624 .items_of_type::<SubView>()
1625 .position(|view| view.read(cx).view_kind() == item)
1626 .map(|view| (view, pane))
1627 })
1628 .unwrap();
1629
1630 pane.update(cx, |this, cx| {
1631 this.activate_item(variable_list_position, true, true, window, cx);
1632 });
1633 }
1634
1635 #[cfg(test)]
1636 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
1637 &self.variable_list
1638 }
1639
1640 #[cfg(test)]
1641 pub(crate) fn serialized_layout(&self, cx: &App) -> SerializedLayout {
1642 persistence::build_serialized_layout(&self.panes.root, self.dock_axis, cx)
1643 }
1644
1645 pub fn capabilities(&self, cx: &App) -> Capabilities {
1646 self.session().read(cx).capabilities().clone()
1647 }
1648
1649 pub fn select_current_thread(
1650 &mut self,
1651 threads: &Vec<(Thread, ThreadStatus)>,
1652 window: &mut Window,
1653 cx: &mut Context<Self>,
1654 ) {
1655 let selected_thread = self
1656 .thread_id
1657 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
1658 .or_else(|| threads.first());
1659
1660 let Some((selected_thread, _)) = selected_thread else {
1661 return;
1662 };
1663
1664 if Some(ThreadId(selected_thread.id)) != self.thread_id {
1665 self.select_thread(ThreadId(selected_thread.id), window, cx);
1666 }
1667 }
1668
1669 pub fn selected_thread_id(&self) -> Option<ThreadId> {
1670 self.thread_id
1671 }
1672
1673 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
1674 self.thread_id
1675 .map(|id| self.session().read(cx).thread_status(id))
1676 }
1677
1678 pub(crate) fn select_thread(
1679 &mut self,
1680 thread_id: ThreadId,
1681 window: &mut Window,
1682 cx: &mut Context<Self>,
1683 ) {
1684 if self.thread_id.is_some_and(|id| id == thread_id) {
1685 return;
1686 }
1687
1688 self.thread_id = Some(thread_id);
1689
1690 self.stack_frame_list
1691 .update(cx, |list, cx| list.schedule_refresh(true, window, cx));
1692 }
1693
1694 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
1695 let Some(thread_id) = self.thread_id else {
1696 return;
1697 };
1698
1699 self.session().update(cx, |state, cx| {
1700 state.continue_thread(thread_id, cx);
1701 });
1702 }
1703
1704 pub fn step_over(&mut self, cx: &mut Context<Self>) {
1705 let Some(thread_id) = self.thread_id else {
1706 return;
1707 };
1708
1709 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1710
1711 self.session().update(cx, |state, cx| {
1712 state.step_over(thread_id, granularity, cx);
1713 });
1714 }
1715
1716 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
1717 let Some(thread_id) = self.thread_id else {
1718 return;
1719 };
1720
1721 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1722
1723 self.session().update(cx, |state, cx| {
1724 state.step_in(thread_id, granularity, cx);
1725 });
1726 }
1727
1728 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
1729 let Some(thread_id) = self.thread_id else {
1730 return;
1731 };
1732
1733 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1734
1735 self.session().update(cx, |state, cx| {
1736 state.step_out(thread_id, granularity, cx);
1737 });
1738 }
1739
1740 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
1741 let Some(thread_id) = self.thread_id else {
1742 return;
1743 };
1744
1745 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1746
1747 self.session().update(cx, |state, cx| {
1748 state.step_back(thread_id, granularity, cx);
1749 });
1750 }
1751
1752 pub fn rerun_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1753 if let Some((scenario, context)) = self.scenario.take().zip(self.scenario_context.take())
1754 && scenario.build.is_some()
1755 {
1756 let DebugScenarioContext {
1757 task_context,
1758 active_buffer,
1759 worktree_id,
1760 } = context;
1761 let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
1762
1763 self.workspace
1764 .update(cx, |workspace, cx| {
1765 workspace.start_debug_session(
1766 scenario,
1767 task_context,
1768 active_buffer,
1769 worktree_id,
1770 window,
1771 cx,
1772 )
1773 })
1774 .ok();
1775 } else {
1776 self.restart_session(cx);
1777 }
1778 }
1779
1780 pub fn restart_session(&self, cx: &mut Context<Self>) {
1781 self.session().update(cx, |state, cx| {
1782 state.restart(None, cx);
1783 });
1784 }
1785
1786 pub fn pause_thread(&self, cx: &mut Context<Self>) {
1787 let Some(thread_id) = self.thread_id else {
1788 return;
1789 };
1790
1791 self.session().update(cx, |state, cx| {
1792 state.pause_thread(thread_id, cx);
1793 });
1794 }
1795
1796 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
1797 self.workspace
1798 .update(cx, |workspace, cx| {
1799 workspace
1800 .project()
1801 .read(cx)
1802 .breakpoint_store()
1803 .update(cx, |store, cx| {
1804 store.remove_active_position(Some(self.session_id), cx)
1805 })
1806 })
1807 .log_err();
1808
1809 let is_building = self.session.update(cx, |session, cx| {
1810 session.shutdown(cx).detach();
1811 matches!(session.state, session::SessionState::Booting(_))
1812 });
1813
1814 if is_building {
1815 self.debug_terminal.update(cx, |terminal, cx| {
1816 if let Some(view) = terminal.terminal.as_ref() {
1817 view.update(cx, |view, cx| {
1818 view.terminal()
1819 .update(cx, |terminal, _| terminal.kill_active_task())
1820 })
1821 }
1822 })
1823 }
1824 }
1825
1826 pub fn stop_thread(&self, cx: &mut Context<Self>) {
1827 let Some(thread_id) = self.thread_id else {
1828 return;
1829 };
1830
1831 self.workspace
1832 .update(cx, |workspace, cx| {
1833 workspace
1834 .project()
1835 .read(cx)
1836 .breakpoint_store()
1837 .update(cx, |store, cx| {
1838 store.remove_active_position(Some(self.session_id), cx)
1839 })
1840 })
1841 .log_err();
1842
1843 self.session().update(cx, |state, cx| {
1844 state.terminate_threads(Some(vec![thread_id; 1]), cx);
1845 });
1846 }
1847
1848 pub fn detach_client(&self, cx: &mut Context<Self>) {
1849 self.session().update(cx, |state, cx| {
1850 state.disconnect_client(cx);
1851 });
1852 }
1853
1854 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
1855 self.session.update(cx, |session, cx| {
1856 session.toggle_ignore_breakpoints(cx).detach();
1857 });
1858 }
1859
1860 fn default_pane_layout(
1861 project: Entity<Project>,
1862 workspace: &WeakEntity<Workspace>,
1863 stack_frame_list: &Entity<StackFrameList>,
1864 variable_list: &Entity<VariableList>,
1865 console: &Entity<Console>,
1866 breakpoints: &Entity<BreakpointList>,
1867 debug_terminal: &Entity<DebugTerminal>,
1868 dock_axis: Axis,
1869 subscriptions: &mut HashMap<EntityId, Subscription>,
1870 window: &mut Window,
1871 cx: &mut Context<'_, RunningState>,
1872 ) -> Member {
1873 let running_state = cx.weak_entity();
1874
1875 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1876 let leftmost_pane_handle = leftmost_pane.downgrade();
1877 let leftmost_frames = SubView::new(
1878 stack_frame_list.focus_handle(cx),
1879 stack_frame_list.clone().into(),
1880 DebuggerPaneItem::Frames,
1881 running_state.clone(),
1882 leftmost_pane_handle.clone(),
1883 cx,
1884 );
1885 let leftmost_breakpoints = SubView::breakpoint_list(
1886 breakpoints.clone(),
1887 running_state.clone(),
1888 leftmost_pane_handle,
1889 cx,
1890 );
1891 leftmost_pane.update(cx, |this, cx| {
1892 this.add_item(Box::new(leftmost_frames), true, false, None, window, cx);
1893 this.add_item(
1894 Box::new(leftmost_breakpoints),
1895 true,
1896 false,
1897 None,
1898 window,
1899 cx,
1900 );
1901 this.activate_item(0, false, false, window, cx);
1902 });
1903
1904 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1905 let center_pane_handle = center_pane.downgrade();
1906 let center_console = SubView::console(
1907 console.clone(),
1908 running_state.clone(),
1909 center_pane_handle.clone(),
1910 cx,
1911 );
1912 let center_variables = SubView::new(
1913 variable_list.focus_handle(cx),
1914 variable_list.clone().into(),
1915 DebuggerPaneItem::Variables,
1916 running_state.clone(),
1917 center_pane_handle,
1918 cx,
1919 );
1920
1921 center_pane.update(cx, |this, cx| {
1922 this.add_item(Box::new(center_console), true, false, None, window, cx);
1923
1924 this.add_item(Box::new(center_variables), true, false, None, window, cx);
1925 this.activate_item(0, false, false, window, cx);
1926 });
1927
1928 let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx);
1929 let rightmost_terminal = SubView::new(
1930 debug_terminal.focus_handle(cx),
1931 debug_terminal.clone().into(),
1932 DebuggerPaneItem::Terminal,
1933 running_state,
1934 rightmost_pane.downgrade(),
1935 cx,
1936 );
1937 rightmost_pane.update(cx, |this, cx| {
1938 this.add_item(Box::new(rightmost_terminal), false, false, None, window, cx);
1939 });
1940
1941 subscriptions.extend(
1942 [&leftmost_pane, ¢er_pane, &rightmost_pane]
1943 .into_iter()
1944 .map(|entity| {
1945 (
1946 entity.entity_id(),
1947 cx.subscribe_in(entity, window, Self::handle_pane_event),
1948 )
1949 }),
1950 );
1951
1952 let group_root = workspace::PaneAxis::new(
1953 dock_axis.invert(),
1954 [leftmost_pane, center_pane, rightmost_pane]
1955 .into_iter()
1956 .map(workspace::Member::Pane)
1957 .collect(),
1958 );
1959
1960 Member::Axis(group_root)
1961 }
1962
1963 pub(crate) fn invert_axies(&mut self, cx: &mut App) {
1964 self.dock_axis = self.dock_axis.invert();
1965 self.panes.invert_axies(cx);
1966 }
1967}
1968
1969impl Focusable for RunningState {
1970 fn focus_handle(&self, _: &App) -> FocusHandle {
1971 self.focus_handle.clone()
1972 }
1973}