1pub(crate) mod breakpoint_list;
2pub(crate) mod console;
3pub(crate) mod loaded_source_list;
4pub(crate) mod module_list;
5pub mod stack_frame_list;
6pub mod variable_list;
7
8use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
9
10use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
11
12use super::DebugPanelItemEvent;
13use anyhow::{Result, anyhow};
14use breakpoint_list::BreakpointList;
15use collections::{HashMap, IndexMap};
16use console::Console;
17use dap::{
18 Capabilities, RunInTerminalRequestArguments, Thread, client::SessionId,
19 debugger_settings::DebuggerSettings,
20};
21use futures::{SinkExt, channel::mpsc};
22use gpui::{
23 Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
24 NoAction, Pixels, Point, Subscription, Task, WeakEntity,
25};
26use loaded_source_list::LoadedSourceList;
27use module_list::ModuleList;
28use project::{
29 Project,
30 debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
31 terminals::TerminalKind,
32};
33use rpc::proto::ViewId;
34use serde_json::Value;
35use settings::Settings;
36use stack_frame_list::StackFrameList;
37use terminal_view::TerminalView;
38use ui::{
39 ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
40 DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IntoElement,
41 Label, LabelCommon as _, ParentElement, Render, SharedString, StatefulInteractiveElement,
42 Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
43};
44use util::ResultExt;
45use variable_list::VariableList;
46use workspace::{
47 ActivePaneDecorator, DraggedTab, Item, ItemHandle, Member, Pane, PaneGroup, SplitDirection,
48 Workspace, item::TabContentParams, move_item, pane::Event,
49};
50
51pub struct RunningState {
52 session: Entity<Session>,
53 thread_id: Option<ThreadId>,
54 focus_handle: FocusHandle,
55 _remote_id: Option<ViewId>,
56 workspace: WeakEntity<Workspace>,
57 session_id: SessionId,
58 variable_list: Entity<variable_list::VariableList>,
59 _subscriptions: Vec<Subscription>,
60 stack_frame_list: Entity<stack_frame_list::StackFrameList>,
61 loaded_sources_list: Entity<LoadedSourceList>,
62 pub debug_terminal: Entity<DebugTerminal>,
63 module_list: Entity<module_list::ModuleList>,
64 console: Entity<Console>,
65 breakpoint_list: Entity<BreakpointList>,
66 panes: PaneGroup,
67 active_pane: Option<Entity<Pane>>,
68 pane_close_subscriptions: HashMap<EntityId, Subscription>,
69 _schedule_serialize: Option<Task<()>>,
70}
71
72impl Render for RunningState {
73 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
74 let zoomed_pane = self
75 .panes
76 .panes()
77 .into_iter()
78 .find(|pane| pane.read(cx).is_zoomed());
79
80 let active = self.panes.panes().into_iter().next();
81 let x = if let Some(ref zoomed_pane) = zoomed_pane {
82 zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
83 } else if let Some(active) = active {
84 self.panes
85 .render(
86 None,
87 &ActivePaneDecorator::new(active, &self.workspace),
88 window,
89 cx,
90 )
91 .into_any_element()
92 } else {
93 div().into_any_element()
94 };
95 let thread_status = self
96 .thread_id
97 .map(|thread_id| self.session.read(cx).thread_status(thread_id))
98 .unwrap_or(ThreadStatus::Exited);
99
100 self.variable_list.update(cx, |this, cx| {
101 this.disabled(thread_status != ThreadStatus::Stopped, cx);
102 });
103 v_flex()
104 .size_full()
105 .key_context("DebugSessionItem")
106 .track_focus(&self.focus_handle(cx))
107 .child(h_flex().flex_1().child(x))
108 }
109}
110
111pub(crate) struct SubView {
112 inner: AnyView,
113 pane_focus_handle: FocusHandle,
114 kind: DebuggerPaneItem,
115 show_indicator: Box<dyn Fn(&App) -> bool>,
116 hovered: bool,
117}
118
119impl SubView {
120 pub(crate) fn new(
121 pane_focus_handle: FocusHandle,
122 view: AnyView,
123 kind: DebuggerPaneItem,
124 show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
125 cx: &mut App,
126 ) -> Entity<Self> {
127 cx.new(|_| Self {
128 kind,
129 inner: view,
130 pane_focus_handle,
131 show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
132 hovered: false,
133 })
134 }
135
136 pub(crate) fn view_kind(&self) -> DebuggerPaneItem {
137 self.kind
138 }
139}
140impl Focusable for SubView {
141 fn focus_handle(&self, _: &App) -> FocusHandle {
142 self.pane_focus_handle.clone()
143 }
144}
145impl EventEmitter<()> for SubView {}
146impl Item for SubView {
147 type Event = ();
148
149 /// This is used to serialize debugger pane layouts
150 /// A SharedString gets converted to a enum and back during serialization/deserialization.
151 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
152 self.kind.to_shared_string()
153 }
154
155 fn tab_content(
156 &self,
157 params: workspace::item::TabContentParams,
158 _: &Window,
159 cx: &App,
160 ) -> AnyElement {
161 let label = Label::new(self.kind.to_shared_string())
162 .size(ui::LabelSize::Small)
163 .color(params.text_color())
164 .line_height_style(ui::LineHeightStyle::UiLabel);
165
166 if !params.selected && self.show_indicator.as_ref()(cx) {
167 return h_flex()
168 .justify_between()
169 .child(ui::Indicator::dot())
170 .gap_2()
171 .child(label)
172 .into_any_element();
173 }
174
175 label.into_any_element()
176 }
177}
178
179impl Render for SubView {
180 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
181 v_flex()
182 .id(SharedString::from(format!(
183 "subview-container-{}",
184 self.kind.to_shared_string()
185 )))
186 .on_hover(cx.listener(|this, hovered, _, cx| {
187 this.hovered = *hovered;
188 cx.notify();
189 }))
190 .size_full()
191 // Add border unconditionally to prevent layout shifts on focus changes.
192 .border_1()
193 .when(self.pane_focus_handle.contains_focused(window, cx), |el| {
194 el.border_color(cx.theme().colors().pane_focused_border)
195 })
196 .child(self.inner.clone())
197 }
198}
199
200pub(crate) fn new_debugger_pane(
201 workspace: WeakEntity<Workspace>,
202 project: Entity<Project>,
203 window: &mut Window,
204 cx: &mut Context<RunningState>,
205) -> Entity<Pane> {
206 let weak_running = cx.weak_entity();
207 let custom_drop_handle = {
208 let workspace = workspace.clone();
209 let project = project.downgrade();
210 let weak_running = weak_running.clone();
211 move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context<Pane>| {
212 let Some(tab) = any.downcast_ref::<DraggedTab>() else {
213 return ControlFlow::Break(());
214 };
215 let Some(project) = project.upgrade() else {
216 return ControlFlow::Break(());
217 };
218 let this_pane = cx.entity().clone();
219 let item = if tab.pane == this_pane {
220 pane.item_for_index(tab.ix)
221 } else {
222 tab.pane.read(cx).item_for_index(tab.ix)
223 };
224 let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
225 return ControlFlow::Break(());
226 };
227
228 let source = tab.pane.clone();
229 let item_id_to_move = item.item_id();
230
231 let Ok(new_split_pane) = pane
232 .drag_split_direction()
233 .map(|split_direction| {
234 weak_running.update(cx, |running, cx| {
235 let new_pane =
236 new_debugger_pane(workspace.clone(), project.clone(), window, cx);
237 let _previous_subscription = running.pane_close_subscriptions.insert(
238 new_pane.entity_id(),
239 cx.subscribe_in(&new_pane, window, RunningState::handle_pane_event),
240 );
241 debug_assert!(_previous_subscription.is_none());
242 running
243 .panes
244 .split(&this_pane, &new_pane, split_direction)?;
245 anyhow::Ok(new_pane)
246 })
247 })
248 .transpose()
249 else {
250 return ControlFlow::Break(());
251 };
252
253 match new_split_pane.transpose() {
254 // Source pane may be the one currently updated, so defer the move.
255 Ok(Some(new_pane)) => cx
256 .spawn_in(window, async move |_, cx| {
257 cx.update(|window, cx| {
258 move_item(
259 &source,
260 &new_pane,
261 item_id_to_move,
262 new_pane.read(cx).active_item_index(),
263 window,
264 cx,
265 );
266 })
267 .ok();
268 })
269 .detach(),
270 // If we drop into existing pane or current pane,
271 // regular pane drop handler will take care of it,
272 // using the right tab index for the operation.
273 Ok(None) => return ControlFlow::Continue(()),
274 err @ Err(_) => {
275 err.log_err();
276 return ControlFlow::Break(());
277 }
278 };
279
280 ControlFlow::Break(())
281 }
282 };
283
284 let ret = cx.new(move |cx| {
285 let mut pane = Pane::new(
286 workspace.clone(),
287 project.clone(),
288 Default::default(),
289 None,
290 NoAction.boxed_clone(),
291 window,
292 cx,
293 );
294 let focus_handle = pane.focus_handle(cx);
295 pane.set_can_split(Some(Arc::new({
296 let weak_running = weak_running.clone();
297 move |pane, dragged_item, _window, cx| {
298 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
299 let is_current_pane = tab.pane == cx.entity();
300 let Some(can_drag_away) = weak_running
301 .update(cx, |running_state, _| {
302 let current_panes = running_state.panes.panes();
303 !current_panes.contains(&&tab.pane)
304 || current_panes.len() > 1
305 || (!is_current_pane || pane.items_len() > 1)
306 })
307 .ok()
308 else {
309 return false;
310 };
311 if can_drag_away {
312 let item = if is_current_pane {
313 pane.item_for_index(tab.ix)
314 } else {
315 tab.pane.read(cx).item_for_index(tab.ix)
316 };
317 if let Some(item) = item {
318 return item.downcast::<SubView>().is_some();
319 }
320 }
321 }
322 false
323 }
324 })));
325 pane.display_nav_history_buttons(None);
326 pane.set_custom_drop_handle(cx, custom_drop_handle);
327 pane.set_should_display_tab_bar(|_, _| true);
328 pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
329 pane.set_render_tab_bar(cx, {
330 move |pane, window, cx| {
331 let active_pane_item = pane.active_item();
332 let pane_group_id: SharedString =
333 format!("pane-zoom-button-hover-{}", cx.entity_id()).into();
334 let is_hovered = active_pane_item.as_ref().map_or(false, |item| {
335 item.downcast::<SubView>()
336 .map_or(false, |this| this.read(cx).hovered)
337 });
338 h_flex()
339 .group(pane_group_id.clone())
340 .justify_between()
341 .bg(cx.theme().colors().tab_bar_background)
342 .border_b_1()
343 .px_2()
344 .border_color(cx.theme().colors().border)
345 .track_focus(&focus_handle)
346 .child(
347 h_flex()
348 .w_full()
349 .gap_1()
350 .h(Tab::container_height(cx))
351 .drag_over::<DraggedTab>(|bar, _, _, cx| {
352 bar.bg(cx.theme().colors().drop_target_background)
353 })
354 .on_drop(cx.listener(
355 move |this, dragged_tab: &DraggedTab, window, cx| {
356 this.drag_split_direction = None;
357 this.handle_tab_drop(dragged_tab, this.items_len(), window, cx)
358 },
359 ))
360 .children(pane.items().enumerate().map(|(ix, item)| {
361 let selected = active_pane_item
362 .as_ref()
363 .map_or(false, |active| active.item_id() == item.item_id());
364 let deemphasized = !pane.has_focus(window, cx);
365 let item_ = item.boxed_clone();
366 div()
367 .id(SharedString::from(format!(
368 "debugger_tab_{}",
369 item.item_id().as_u64()
370 )))
371 .p_1()
372 .rounded_md()
373 .cursor_pointer()
374 .map(|this| {
375 let theme = cx.theme();
376 if selected {
377 let color = theme.colors().tab_active_background;
378 let color = if deemphasized {
379 color.opacity(0.5)
380 } else {
381 color
382 };
383 this.bg(color)
384 } else {
385 let hover_color = theme.colors().element_hover;
386 this.hover(|style| style.bg(hover_color))
387 }
388 })
389 .on_click(cx.listener(move |this, _, window, cx| {
390 let index = this.index_for_item(&*item_);
391 if let Some(index) = index {
392 this.activate_item(index, true, true, window, cx);
393 }
394 }))
395 .child(item.tab_content(
396 TabContentParams {
397 selected,
398 deemphasized,
399 ..Default::default()
400 },
401 window,
402 cx,
403 ))
404 .on_drop(cx.listener(
405 move |this, dragged_tab: &DraggedTab, window, cx| {
406 this.drag_split_direction = None;
407 this.handle_tab_drop(dragged_tab, ix, window, cx)
408 },
409 ))
410 .on_drag(
411 DraggedTab {
412 item: item.boxed_clone(),
413 pane: cx.entity().clone(),
414 detail: 0,
415 is_active: selected,
416 ix,
417 },
418 |tab, _, _, cx| cx.new(|_| tab.clone()),
419 )
420 })),
421 )
422 .child({
423 let zoomed = pane.is_zoomed();
424 div()
425 .visible_on_hover(pane_group_id)
426 .when(is_hovered, |this| this.visible())
427 .child(
428 IconButton::new(
429 SharedString::from(format!(
430 "debug-toggle-zoom-{}",
431 cx.entity_id()
432 )),
433 if zoomed {
434 IconName::Minimize
435 } else {
436 IconName::Maximize
437 },
438 )
439 .icon_size(IconSize::XSmall)
440 .on_click(cx.listener(move |pane, _, window, cx| {
441 pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
442 }))
443 .tooltip({
444 let focus_handle = focus_handle.clone();
445 move |window, cx| {
446 let zoomed_text =
447 if zoomed { "Zoom Out" } else { "Zoom In" };
448 Tooltip::for_action_in(
449 zoomed_text,
450 &workspace::ToggleZoom,
451 &focus_handle,
452 window,
453 cx,
454 )
455 }
456 }),
457 )
458 })
459 .into_any_element()
460 }
461 });
462 pane
463 });
464
465 ret
466}
467
468pub struct DebugTerminal {
469 pub terminal: Option<Entity<TerminalView>>,
470 focus_handle: FocusHandle,
471}
472
473impl DebugTerminal {
474 fn empty(cx: &mut Context<Self>) -> Self {
475 Self {
476 terminal: None,
477 focus_handle: cx.focus_handle(),
478 }
479 }
480}
481
482impl gpui::Render for DebugTerminal {
483 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
484 if let Some(terminal) = self.terminal.clone() {
485 terminal.into_any_element()
486 } else {
487 div().track_focus(&self.focus_handle).into_any_element()
488 }
489 }
490}
491impl Focusable for DebugTerminal {
492 fn focus_handle(&self, cx: &App) -> FocusHandle {
493 if let Some(terminal) = self.terminal.as_ref() {
494 return terminal.focus_handle(cx);
495 } else {
496 self.focus_handle.clone()
497 }
498 }
499}
500
501impl RunningState {
502 pub fn new(
503 session: Entity<Session>,
504 project: Entity<Project>,
505 workspace: WeakEntity<Workspace>,
506 serialized_pane_layout: Option<SerializedPaneLayout>,
507 window: &mut Window,
508 cx: &mut Context<Self>,
509 ) -> Self {
510 let focus_handle = cx.focus_handle();
511 let session_id = session.read(cx).session_id();
512 let weak_state = cx.weak_entity();
513 let stack_frame_list = cx.new(|cx| {
514 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
515 });
516
517 let debug_terminal = cx.new(DebugTerminal::empty);
518
519 let variable_list =
520 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
521
522 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
523
524 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
525
526 let console = cx.new(|cx| {
527 Console::new(
528 session.clone(),
529 stack_frame_list.clone(),
530 variable_list.clone(),
531 window,
532 cx,
533 )
534 });
535
536 let breakpoint_list = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
537
538 let _subscriptions = vec![
539 cx.observe(&module_list, |_, _, cx| cx.notify()),
540 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
541 match event {
542 SessionEvent::Stopped(thread_id) => {
543 this.workspace
544 .update(cx, |workspace, cx| {
545 workspace.open_panel::<crate::DebugPanel>(window, cx);
546 })
547 .log_err();
548
549 if let Some(thread_id) = thread_id {
550 this.select_thread(*thread_id, window, cx);
551 }
552 }
553 SessionEvent::Threads => {
554 let threads = this.session.update(cx, |this, cx| this.threads(cx));
555 this.select_current_thread(&threads, window, cx);
556 }
557 SessionEvent::CapabilitiesLoaded => {
558 let capabilities = this.capabilities(cx);
559 if !capabilities.supports_modules_request.unwrap_or(false) {
560 this.remove_pane_item(DebuggerPaneItem::Modules, window, cx);
561 }
562 if !capabilities
563 .supports_loaded_sources_request
564 .unwrap_or(false)
565 {
566 this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
567 }
568 }
569 SessionEvent::RunInTerminal { request, sender } => this
570 .handle_run_in_terminal(request, sender.clone(), window, cx)
571 .detach_and_log_err(cx),
572
573 _ => {}
574 }
575 cx.notify()
576 }),
577 cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
578 this.serialize_layout(window, cx);
579 }),
580 ];
581
582 let mut pane_close_subscriptions = HashMap::default();
583 let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| {
584 persistence::deserialize_pane_layout(
585 serialized_layout,
586 &workspace,
587 &project,
588 &stack_frame_list,
589 &variable_list,
590 &module_list,
591 &console,
592 &breakpoint_list,
593 &loaded_source_list,
594 &debug_terminal,
595 &mut pane_close_subscriptions,
596 window,
597 cx,
598 )
599 }) {
600 workspace::PaneGroup::with_root(root)
601 } else {
602 pane_close_subscriptions.clear();
603
604 let root = Self::default_pane_layout(
605 project,
606 &workspace,
607 &stack_frame_list,
608 &variable_list,
609 &module_list,
610 &loaded_source_list,
611 &console,
612 &breakpoint_list,
613 &mut pane_close_subscriptions,
614 window,
615 cx,
616 );
617
618 workspace::PaneGroup::with_root(root)
619 };
620
621 Self {
622 session,
623 workspace,
624 focus_handle,
625 variable_list,
626 _subscriptions,
627 thread_id: None,
628 _remote_id: None,
629 stack_frame_list,
630 session_id,
631 panes,
632 active_pane: None,
633 module_list,
634 console,
635 breakpoint_list,
636 loaded_sources_list: loaded_source_list,
637 pane_close_subscriptions,
638 debug_terminal,
639 _schedule_serialize: None,
640 }
641 }
642
643 pub(crate) fn remove_pane_item(
644 &mut self,
645 item_kind: DebuggerPaneItem,
646 window: &mut Window,
647 cx: &mut Context<Self>,
648 ) {
649 if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
650 Some(pane).zip(
651 pane.read(cx)
652 .items()
653 .find(|item| {
654 item.act_as::<SubView>(cx)
655 .is_some_and(|view| view.read(cx).kind == item_kind)
656 })
657 .map(|item| item.item_id()),
658 )
659 }) {
660 pane.update(cx, |pane, cx| {
661 pane.remove_item(item_id, false, true, window, cx)
662 })
663 }
664 }
665
666 pub(crate) fn has_pane_at_position(&self, position: Point<Pixels>) -> bool {
667 self.panes.pane_at_pixel_position(position).is_some()
668 }
669
670 fn handle_run_in_terminal(
671 &self,
672 request: &RunInTerminalRequestArguments,
673 mut sender: mpsc::Sender<Result<u32>>,
674 window: &mut Window,
675 cx: &mut Context<Self>,
676 ) -> Task<Result<()>> {
677 let running = cx.entity();
678 let Ok(project) = self
679 .workspace
680 .update(cx, |workspace, _| workspace.project().clone())
681 else {
682 return Task::ready(Err(anyhow!("no workspace")));
683 };
684 let session = self.session.read(cx);
685
686 let cwd = Some(&request.cwd)
687 .filter(|cwd| cwd.len() > 0)
688 .map(PathBuf::from)
689 .or_else(|| session.binary().cwd.clone());
690
691 let mut args = request.args.clone();
692
693 // Handle special case for NodeJS debug adapter
694 // If only the Node binary path is provided, we set the command to None
695 // This prevents the NodeJS REPL from appearing, which is not the desired behavior
696 // The expected usage is for users to provide their own Node command, e.g., `node test.js`
697 // This allows the NodeJS debug client to attach correctly
698 let command = if args.len() > 1 {
699 Some(args.remove(0))
700 } else {
701 None
702 };
703
704 let mut envs: HashMap<String, String> = Default::default();
705 if let Some(Value::Object(env)) = &request.env {
706 for (key, value) in env {
707 let value_str = match (key.as_str(), value) {
708 (_, Value::String(value)) => value,
709 _ => continue,
710 };
711
712 envs.insert(key.clone(), value_str.clone());
713 }
714 }
715
716 let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
717 let kind = if let Some(command) = command {
718 let title = request.title.clone().unwrap_or(command.clone());
719 TerminalKind::Task(task::SpawnInTerminal {
720 id: task::TaskId("debug".to_string()),
721 full_label: title.clone(),
722 label: title.clone(),
723 command: command.clone(),
724 args,
725 command_label: title.clone(),
726 cwd,
727 env: envs,
728 use_new_terminal: true,
729 allow_concurrent_runs: true,
730 reveal: task::RevealStrategy::NoFocus,
731 reveal_target: task::RevealTarget::Dock,
732 hide: task::HideStrategy::Never,
733 shell,
734 show_summary: false,
735 show_command: false,
736 show_rerun: false,
737 })
738 } else {
739 TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
740 };
741
742 let workspace = self.workspace.clone();
743 let weak_project = project.downgrade();
744
745 let terminal_task = project.update(cx, |project, cx| {
746 project.create_terminal(kind, window.window_handle(), cx)
747 });
748 let terminal_task = cx.spawn_in(window, async move |_, cx| {
749 let terminal = terminal_task.await?;
750
751 let terminal_view = cx.new_window_entity(|window, cx| {
752 TerminalView::new(terminal.clone(), workspace, None, weak_project, window, cx)
753 })?;
754
755 running.update_in(cx, |running, window, cx| {
756 running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
757 running.debug_terminal.update(cx, |debug_terminal, cx| {
758 debug_terminal.terminal = Some(terminal_view);
759 cx.notify();
760 });
761 })?;
762
763 terminal.read_with(cx, |terminal, _| {
764 terminal
765 .pty_info
766 .pid()
767 .map(|pid| pid.as_u32())
768 .ok_or_else(|| anyhow!("Terminal was spawned but PID was not available"))
769 })?
770 });
771
772 cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) })
773 }
774
775 fn create_sub_view(
776 &self,
777 item_kind: DebuggerPaneItem,
778 _pane: &Entity<Pane>,
779 cx: &mut Context<Self>,
780 ) -> Box<dyn ItemHandle> {
781 match item_kind {
782 DebuggerPaneItem::Console => {
783 let weak_console = self.console.clone().downgrade();
784
785 Box::new(SubView::new(
786 self.console.focus_handle(cx),
787 self.console.clone().into(),
788 item_kind,
789 Some(Box::new(move |cx| {
790 weak_console
791 .read_with(cx, |console, cx| console.show_indicator(cx))
792 .unwrap_or_default()
793 })),
794 cx,
795 ))
796 }
797 DebuggerPaneItem::Variables => Box::new(SubView::new(
798 self.variable_list.focus_handle(cx),
799 self.variable_list.clone().into(),
800 item_kind,
801 None,
802 cx,
803 )),
804 DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
805 self.breakpoint_list.focus_handle(cx),
806 self.breakpoint_list.clone().into(),
807 item_kind,
808 None,
809 cx,
810 )),
811 DebuggerPaneItem::Frames => Box::new(SubView::new(
812 self.stack_frame_list.focus_handle(cx),
813 self.stack_frame_list.clone().into(),
814 item_kind,
815 None,
816 cx,
817 )),
818 DebuggerPaneItem::Modules => Box::new(SubView::new(
819 self.module_list.focus_handle(cx),
820 self.module_list.clone().into(),
821 item_kind,
822 None,
823 cx,
824 )),
825 DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
826 self.loaded_sources_list.focus_handle(cx),
827 self.loaded_sources_list.clone().into(),
828 item_kind,
829 None,
830 cx,
831 )),
832 DebuggerPaneItem::Terminal => Box::new(SubView::new(
833 self.debug_terminal.focus_handle(cx),
834 self.debug_terminal.clone().into(),
835 item_kind,
836 None,
837 cx,
838 )),
839 }
840 }
841
842 pub(crate) fn ensure_pane_item(
843 &mut self,
844 item_kind: DebuggerPaneItem,
845 window: &mut Window,
846 cx: &mut Context<Self>,
847 ) {
848 if self.pane_items_status(cx).get(&item_kind) == Some(&true) {
849 return;
850 };
851 let pane = self.panes.last_pane();
852 let sub_view = self.create_sub_view(item_kind, &pane, cx);
853
854 pane.update(cx, |pane, cx| {
855 pane.add_item_inner(sub_view, false, false, false, None, window, cx);
856 })
857 }
858
859 pub(crate) fn add_pane_item(
860 &mut self,
861 item_kind: DebuggerPaneItem,
862 position: Point<Pixels>,
863 window: &mut Window,
864 cx: &mut Context<Self>,
865 ) {
866 debug_assert!(
867 item_kind.is_supported(self.session.read(cx).capabilities()),
868 "We should only allow adding supported item kinds"
869 );
870
871 if let Some(pane) = self.panes.pane_at_pixel_position(position) {
872 let sub_view = self.create_sub_view(item_kind, pane, cx);
873
874 pane.update(cx, |pane, cx| {
875 pane.add_item(sub_view, false, false, None, window, cx);
876 })
877 }
878 }
879
880 pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap<DebuggerPaneItem, bool> {
881 let caps = self.session.read(cx).capabilities();
882 let mut pane_item_status = IndexMap::from_iter(
883 DebuggerPaneItem::all()
884 .iter()
885 .filter(|kind| kind.is_supported(&caps))
886 .map(|kind| (*kind, false)),
887 );
888 self.panes.panes().iter().for_each(|pane| {
889 pane.read(cx)
890 .items()
891 .filter_map(|item| item.act_as::<SubView>(cx))
892 .for_each(|view| {
893 pane_item_status.insert(view.read(cx).kind, true);
894 });
895 });
896
897 pane_item_status
898 }
899
900 pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
901 if self._schedule_serialize.is_none() {
902 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
903 cx.background_executor()
904 .timer(Duration::from_millis(100))
905 .await;
906
907 let Some((adapter_name, pane_group)) = this
908 .update(cx, |this, cx| {
909 let adapter_name = this.session.read(cx).adapter_name();
910 (
911 adapter_name,
912 persistence::build_serialized_pane_layout(&this.panes.root, cx),
913 )
914 })
915 .ok()
916 else {
917 return;
918 };
919
920 persistence::serialize_pane_layout(adapter_name, pane_group)
921 .await
922 .log_err();
923
924 this.update(cx, |this, _| {
925 this._schedule_serialize.take();
926 })
927 .ok();
928 }));
929 }
930 }
931
932 pub(crate) fn handle_pane_event(
933 this: &mut RunningState,
934 source_pane: &Entity<Pane>,
935 event: &Event,
936 window: &mut Window,
937 cx: &mut Context<RunningState>,
938 ) {
939 this.serialize_layout(window, cx);
940 match event {
941 Event::Remove { .. } => {
942 let _did_find_pane = this.panes.remove(&source_pane).is_ok();
943 debug_assert!(_did_find_pane);
944 cx.notify();
945 }
946 Event::Focus => {
947 this.active_pane = Some(source_pane.clone());
948 }
949 Event::ZoomIn => {
950 source_pane.update(cx, |pane, cx| {
951 pane.set_zoomed(true, cx);
952 });
953 cx.notify();
954 }
955 Event::ZoomOut => {
956 source_pane.update(cx, |pane, cx| {
957 pane.set_zoomed(false, cx);
958 });
959 cx.notify();
960 }
961 _ => {}
962 }
963 }
964
965 pub(crate) fn activate_pane_in_direction(
966 &mut self,
967 direction: SplitDirection,
968 window: &mut Window,
969 cx: &mut Context<Self>,
970 ) {
971 if let Some(pane) = self
972 .active_pane
973 .as_ref()
974 .and_then(|pane| self.panes.find_pane_in_direction(pane, direction, cx))
975 {
976 window.focus(&pane.focus_handle(cx));
977 } else {
978 self.workspace
979 .update(cx, |workspace, cx| {
980 workspace.activate_pane_in_direction(direction, window, cx)
981 })
982 .ok();
983 }
984 }
985
986 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
987 if self.thread_id.is_some() {
988 self.stack_frame_list
989 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
990 }
991 }
992
993 pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool {
994 self.variable_list.read(cx).has_open_context_menu()
995 }
996
997 pub fn session(&self) -> &Entity<Session> {
998 &self.session
999 }
1000
1001 pub fn session_id(&self) -> SessionId {
1002 self.session_id
1003 }
1004
1005 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
1006 self.stack_frame_list.read(cx).selected_stack_frame_id()
1007 }
1008
1009 #[cfg(test)]
1010 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
1011 &self.stack_frame_list
1012 }
1013
1014 #[cfg(test)]
1015 pub fn console(&self) -> &Entity<Console> {
1016 &self.console
1017 }
1018
1019 #[cfg(test)]
1020 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
1021 &self.module_list
1022 }
1023
1024 pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
1025 let (variable_list_position, pane) = self
1026 .panes
1027 .panes()
1028 .into_iter()
1029 .find_map(|pane| {
1030 pane.read(cx)
1031 .items_of_type::<SubView>()
1032 .position(|view| view.read(cx).view_kind() == item)
1033 .map(|view| (view, pane))
1034 })
1035 .unwrap();
1036 pane.update(cx, |this, cx| {
1037 this.activate_item(variable_list_position, true, true, window, cx);
1038 })
1039 }
1040
1041 #[cfg(test)]
1042 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
1043 &self.variable_list
1044 }
1045
1046 pub fn capabilities(&self, cx: &App) -> Capabilities {
1047 self.session().read(cx).capabilities().clone()
1048 }
1049
1050 pub fn select_current_thread(
1051 &mut self,
1052 threads: &Vec<(Thread, ThreadStatus)>,
1053 window: &mut Window,
1054 cx: &mut Context<Self>,
1055 ) {
1056 let selected_thread = self
1057 .thread_id
1058 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
1059 .or_else(|| threads.first());
1060
1061 let Some((selected_thread, _)) = selected_thread else {
1062 return;
1063 };
1064
1065 if Some(ThreadId(selected_thread.id)) != self.thread_id {
1066 self.select_thread(ThreadId(selected_thread.id), window, cx);
1067 }
1068 }
1069
1070 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
1071 self.thread_id
1072 }
1073
1074 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
1075 self.thread_id
1076 .map(|id| self.session().read(cx).thread_status(id))
1077 }
1078
1079 fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
1080 if self.thread_id.is_some_and(|id| id == thread_id) {
1081 return;
1082 }
1083
1084 self.thread_id = Some(thread_id);
1085
1086 self.stack_frame_list
1087 .update(cx, |list, cx| list.schedule_refresh(true, window, cx));
1088 }
1089
1090 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
1091 let Some(thread_id) = self.thread_id else {
1092 return;
1093 };
1094
1095 self.session().update(cx, |state, cx| {
1096 state.continue_thread(thread_id, cx);
1097 });
1098 }
1099
1100 pub fn step_over(&mut self, cx: &mut Context<Self>) {
1101 let Some(thread_id) = self.thread_id else {
1102 return;
1103 };
1104
1105 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1106
1107 self.session().update(cx, |state, cx| {
1108 state.step_over(thread_id, granularity, cx);
1109 });
1110 }
1111
1112 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
1113 let Some(thread_id) = self.thread_id else {
1114 return;
1115 };
1116
1117 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1118
1119 self.session().update(cx, |state, cx| {
1120 state.step_in(thread_id, granularity, cx);
1121 });
1122 }
1123
1124 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
1125 let Some(thread_id) = self.thread_id else {
1126 return;
1127 };
1128
1129 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1130
1131 self.session().update(cx, |state, cx| {
1132 state.step_out(thread_id, granularity, cx);
1133 });
1134 }
1135
1136 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
1137 let Some(thread_id) = self.thread_id else {
1138 return;
1139 };
1140
1141 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1142
1143 self.session().update(cx, |state, cx| {
1144 state.step_back(thread_id, granularity, cx);
1145 });
1146 }
1147
1148 pub fn restart_session(&self, cx: &mut Context<Self>) {
1149 self.session().update(cx, |state, cx| {
1150 state.restart(None, cx);
1151 });
1152 }
1153
1154 pub fn pause_thread(&self, cx: &mut Context<Self>) {
1155 let Some(thread_id) = self.thread_id else {
1156 return;
1157 };
1158
1159 self.session().update(cx, |state, cx| {
1160 state.pause_thread(thread_id, cx);
1161 });
1162 }
1163
1164 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
1165 self.workspace
1166 .update(cx, |workspace, cx| {
1167 workspace
1168 .project()
1169 .read(cx)
1170 .breakpoint_store()
1171 .update(cx, |store, cx| {
1172 store.remove_active_position(Some(self.session_id), cx)
1173 })
1174 })
1175 .log_err();
1176
1177 self.session.update(cx, |session, cx| {
1178 session.shutdown(cx).detach();
1179 })
1180 }
1181
1182 pub fn stop_thread(&self, cx: &mut Context<Self>) {
1183 let Some(thread_id) = self.thread_id else {
1184 return;
1185 };
1186
1187 self.workspace
1188 .update(cx, |workspace, cx| {
1189 workspace
1190 .project()
1191 .read(cx)
1192 .breakpoint_store()
1193 .update(cx, |store, cx| {
1194 store.remove_active_position(Some(self.session_id), cx)
1195 })
1196 })
1197 .log_err();
1198
1199 self.session().update(cx, |state, cx| {
1200 state.terminate_threads(Some(vec![thread_id; 1]), cx);
1201 });
1202 }
1203
1204 #[expect(
1205 unused,
1206 reason = "Support for disconnecting a client is not wired through yet"
1207 )]
1208 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
1209 self.session().update(cx, |state, cx| {
1210 state.disconnect_client(cx);
1211 });
1212 }
1213
1214 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
1215 self.session.update(cx, |session, cx| {
1216 session.toggle_ignore_breakpoints(cx).detach();
1217 });
1218 }
1219
1220 pub(crate) fn thread_dropdown(
1221 &self,
1222 window: &mut Window,
1223 cx: &mut Context<'_, RunningState>,
1224 ) -> DropdownMenu {
1225 let state = cx.entity();
1226 let threads = self.session.update(cx, |this, cx| this.threads(cx));
1227 let selected_thread_name = threads
1228 .iter()
1229 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
1230 .map(|(thread, _)| thread.name.clone())
1231 .unwrap_or("Threads".to_owned());
1232 DropdownMenu::new(
1233 ("thread-list", self.session_id.0),
1234 selected_thread_name,
1235 ContextMenu::build_eager(window, cx, move |mut this, _, _| {
1236 for (thread, _) in threads {
1237 let state = state.clone();
1238 let thread_id = thread.id;
1239 this = this.entry(thread.name, None, move |window, cx| {
1240 state.update(cx, |state, cx| {
1241 state.select_thread(ThreadId(thread_id), window, cx);
1242 });
1243 });
1244 }
1245 this
1246 }),
1247 )
1248 }
1249
1250 fn default_pane_layout(
1251 project: Entity<Project>,
1252 workspace: &WeakEntity<Workspace>,
1253 stack_frame_list: &Entity<StackFrameList>,
1254 variable_list: &Entity<VariableList>,
1255 module_list: &Entity<ModuleList>,
1256 loaded_source_list: &Entity<LoadedSourceList>,
1257 console: &Entity<Console>,
1258 breakpoints: &Entity<BreakpointList>,
1259 subscriptions: &mut HashMap<EntityId, Subscription>,
1260 window: &mut Window,
1261 cx: &mut Context<'_, RunningState>,
1262 ) -> Member {
1263 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1264 leftmost_pane.update(cx, |this, cx| {
1265 this.add_item(
1266 Box::new(SubView::new(
1267 this.focus_handle(cx),
1268 stack_frame_list.clone().into(),
1269 DebuggerPaneItem::Frames,
1270 None,
1271 cx,
1272 )),
1273 true,
1274 false,
1275 None,
1276 window,
1277 cx,
1278 );
1279 this.add_item(
1280 Box::new(SubView::new(
1281 breakpoints.focus_handle(cx),
1282 breakpoints.clone().into(),
1283 DebuggerPaneItem::BreakpointList,
1284 None,
1285 cx,
1286 )),
1287 true,
1288 false,
1289 None,
1290 window,
1291 cx,
1292 );
1293 this.activate_item(0, false, false, window, cx);
1294 });
1295 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1296
1297 center_pane.update(cx, |this, cx| {
1298 this.add_item(
1299 Box::new(SubView::new(
1300 variable_list.focus_handle(cx),
1301 variable_list.clone().into(),
1302 DebuggerPaneItem::Variables,
1303 None,
1304 cx,
1305 )),
1306 true,
1307 false,
1308 None,
1309 window,
1310 cx,
1311 );
1312 this.add_item(
1313 Box::new(SubView::new(
1314 module_list.focus_handle(cx),
1315 module_list.clone().into(),
1316 DebuggerPaneItem::Modules,
1317 None,
1318 cx,
1319 )),
1320 false,
1321 false,
1322 None,
1323 window,
1324 cx,
1325 );
1326
1327 this.add_item(
1328 Box::new(SubView::new(
1329 loaded_source_list.focus_handle(cx),
1330 loaded_source_list.clone().into(),
1331 DebuggerPaneItem::LoadedSources,
1332 None,
1333 cx,
1334 )),
1335 false,
1336 false,
1337 None,
1338 window,
1339 cx,
1340 );
1341 this.activate_item(0, false, false, window, cx);
1342 });
1343
1344 let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1345 rightmost_pane.update(cx, |this, cx| {
1346 let weak_console = console.downgrade();
1347 this.add_item(
1348 Box::new(SubView::new(
1349 this.focus_handle(cx),
1350 console.clone().into(),
1351 DebuggerPaneItem::Console,
1352 Some(Box::new(move |cx| {
1353 weak_console
1354 .read_with(cx, |console, cx| console.show_indicator(cx))
1355 .unwrap_or_default()
1356 })),
1357 cx,
1358 )),
1359 true,
1360 false,
1361 None,
1362 window,
1363 cx,
1364 );
1365 });
1366
1367 subscriptions.extend(
1368 [&leftmost_pane, ¢er_pane, &rightmost_pane]
1369 .into_iter()
1370 .map(|entity| {
1371 (
1372 entity.entity_id(),
1373 cx.subscribe_in(entity, window, Self::handle_pane_event),
1374 )
1375 }),
1376 );
1377
1378 let group_root = workspace::PaneAxis::new(
1379 gpui::Axis::Horizontal,
1380 [leftmost_pane, center_pane, rightmost_pane]
1381 .into_iter()
1382 .map(workspace::Member::Pane)
1383 .collect(),
1384 );
1385
1386 Member::Axis(group_root)
1387 }
1388}
1389
1390impl EventEmitter<DebugPanelItemEvent> for RunningState {}
1391
1392impl Focusable for RunningState {
1393 fn focus_handle(&self, _: &App) -> FocusHandle {
1394 self.focus_handle.clone()
1395 }
1396}