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