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(
753 terminal.clone(),
754 workspace,
755 None,
756 weak_project,
757 false,
758 window,
759 cx,
760 )
761 })?;
762
763 running.update_in(cx, |running, window, cx| {
764 running.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
765 running.debug_terminal.update(cx, |debug_terminal, cx| {
766 debug_terminal.terminal = Some(terminal_view);
767 cx.notify();
768 });
769 })?;
770
771 terminal.read_with(cx, |terminal, _| {
772 terminal
773 .pty_info
774 .pid()
775 .map(|pid| pid.as_u32())
776 .ok_or_else(|| anyhow!("Terminal was spawned but PID was not available"))
777 })?
778 });
779
780 cx.background_spawn(async move { anyhow::Ok(sender.send(terminal_task.await).await?) })
781 }
782
783 fn create_sub_view(
784 &self,
785 item_kind: DebuggerPaneItem,
786 _pane: &Entity<Pane>,
787 cx: &mut Context<Self>,
788 ) -> Box<dyn ItemHandle> {
789 match item_kind {
790 DebuggerPaneItem::Console => {
791 let weak_console = self.console.clone().downgrade();
792
793 Box::new(SubView::new(
794 self.console.focus_handle(cx),
795 self.console.clone().into(),
796 item_kind,
797 Some(Box::new(move |cx| {
798 weak_console
799 .read_with(cx, |console, cx| console.show_indicator(cx))
800 .unwrap_or_default()
801 })),
802 cx,
803 ))
804 }
805 DebuggerPaneItem::Variables => Box::new(SubView::new(
806 self.variable_list.focus_handle(cx),
807 self.variable_list.clone().into(),
808 item_kind,
809 None,
810 cx,
811 )),
812 DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
813 self.breakpoint_list.focus_handle(cx),
814 self.breakpoint_list.clone().into(),
815 item_kind,
816 None,
817 cx,
818 )),
819 DebuggerPaneItem::Frames => Box::new(SubView::new(
820 self.stack_frame_list.focus_handle(cx),
821 self.stack_frame_list.clone().into(),
822 item_kind,
823 None,
824 cx,
825 )),
826 DebuggerPaneItem::Modules => Box::new(SubView::new(
827 self.module_list.focus_handle(cx),
828 self.module_list.clone().into(),
829 item_kind,
830 None,
831 cx,
832 )),
833 DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
834 self.loaded_sources_list.focus_handle(cx),
835 self.loaded_sources_list.clone().into(),
836 item_kind,
837 None,
838 cx,
839 )),
840 DebuggerPaneItem::Terminal => Box::new(SubView::new(
841 self.debug_terminal.focus_handle(cx),
842 self.debug_terminal.clone().into(),
843 item_kind,
844 None,
845 cx,
846 )),
847 }
848 }
849
850 pub(crate) fn ensure_pane_item(
851 &mut self,
852 item_kind: DebuggerPaneItem,
853 window: &mut Window,
854 cx: &mut Context<Self>,
855 ) {
856 if self.pane_items_status(cx).get(&item_kind) == Some(&true) {
857 return;
858 };
859 let pane = self.panes.last_pane();
860 let sub_view = self.create_sub_view(item_kind, &pane, cx);
861
862 pane.update(cx, |pane, cx| {
863 pane.add_item_inner(sub_view, false, false, false, None, window, cx);
864 })
865 }
866
867 pub(crate) fn add_pane_item(
868 &mut self,
869 item_kind: DebuggerPaneItem,
870 position: Point<Pixels>,
871 window: &mut Window,
872 cx: &mut Context<Self>,
873 ) {
874 debug_assert!(
875 item_kind.is_supported(self.session.read(cx).capabilities()),
876 "We should only allow adding supported item kinds"
877 );
878
879 if let Some(pane) = self.panes.pane_at_pixel_position(position) {
880 let sub_view = self.create_sub_view(item_kind, pane, cx);
881
882 pane.update(cx, |pane, cx| {
883 pane.add_item(sub_view, false, false, None, window, cx);
884 })
885 }
886 }
887
888 pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap<DebuggerPaneItem, bool> {
889 let caps = self.session.read(cx).capabilities();
890 let mut pane_item_status = IndexMap::from_iter(
891 DebuggerPaneItem::all()
892 .iter()
893 .filter(|kind| kind.is_supported(&caps))
894 .map(|kind| (*kind, false)),
895 );
896 self.panes.panes().iter().for_each(|pane| {
897 pane.read(cx)
898 .items()
899 .filter_map(|item| item.act_as::<SubView>(cx))
900 .for_each(|view| {
901 pane_item_status.insert(view.read(cx).kind, true);
902 });
903 });
904
905 pane_item_status
906 }
907
908 pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
909 if self._schedule_serialize.is_none() {
910 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
911 cx.background_executor()
912 .timer(Duration::from_millis(100))
913 .await;
914
915 let Some((adapter_name, pane_group)) = this
916 .update(cx, |this, cx| {
917 let adapter_name = this.session.read(cx).adapter_name();
918 (
919 adapter_name,
920 persistence::build_serialized_pane_layout(&this.panes.root, cx),
921 )
922 })
923 .ok()
924 else {
925 return;
926 };
927
928 persistence::serialize_pane_layout(adapter_name, pane_group)
929 .await
930 .log_err();
931
932 this.update(cx, |this, _| {
933 this._schedule_serialize.take();
934 })
935 .ok();
936 }));
937 }
938 }
939
940 pub(crate) fn handle_pane_event(
941 this: &mut RunningState,
942 source_pane: &Entity<Pane>,
943 event: &Event,
944 window: &mut Window,
945 cx: &mut Context<RunningState>,
946 ) {
947 this.serialize_layout(window, cx);
948 match event {
949 Event::Remove { .. } => {
950 let _did_find_pane = this.panes.remove(&source_pane).is_ok();
951 debug_assert!(_did_find_pane);
952 cx.notify();
953 }
954 Event::Focus => {
955 this.active_pane = Some(source_pane.clone());
956 }
957 Event::ZoomIn => {
958 source_pane.update(cx, |pane, cx| {
959 pane.set_zoomed(true, cx);
960 });
961 cx.notify();
962 }
963 Event::ZoomOut => {
964 source_pane.update(cx, |pane, cx| {
965 pane.set_zoomed(false, cx);
966 });
967 cx.notify();
968 }
969 _ => {}
970 }
971 }
972
973 pub(crate) fn activate_pane_in_direction(
974 &mut self,
975 direction: SplitDirection,
976 window: &mut Window,
977 cx: &mut Context<Self>,
978 ) {
979 if let Some(pane) = self
980 .active_pane
981 .as_ref()
982 .and_then(|pane| self.panes.find_pane_in_direction(pane, direction, cx))
983 {
984 window.focus(&pane.focus_handle(cx));
985 } else {
986 self.workspace
987 .update(cx, |workspace, cx| {
988 workspace.activate_pane_in_direction(direction, window, cx)
989 })
990 .ok();
991 }
992 }
993
994 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
995 if self.thread_id.is_some() {
996 self.stack_frame_list
997 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
998 }
999 }
1000
1001 pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool {
1002 self.variable_list.read(cx).has_open_context_menu()
1003 }
1004
1005 pub fn session(&self) -> &Entity<Session> {
1006 &self.session
1007 }
1008
1009 pub fn session_id(&self) -> SessionId {
1010 self.session_id
1011 }
1012
1013 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
1014 self.stack_frame_list.read(cx).selected_stack_frame_id()
1015 }
1016
1017 #[cfg(test)]
1018 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
1019 &self.stack_frame_list
1020 }
1021
1022 #[cfg(test)]
1023 pub fn console(&self) -> &Entity<Console> {
1024 &self.console
1025 }
1026
1027 #[cfg(test)]
1028 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
1029 &self.module_list
1030 }
1031
1032 pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
1033 let (variable_list_position, pane) = self
1034 .panes
1035 .panes()
1036 .into_iter()
1037 .find_map(|pane| {
1038 pane.read(cx)
1039 .items_of_type::<SubView>()
1040 .position(|view| view.read(cx).view_kind() == item)
1041 .map(|view| (view, pane))
1042 })
1043 .unwrap();
1044 pane.update(cx, |this, cx| {
1045 this.activate_item(variable_list_position, true, true, window, cx);
1046 })
1047 }
1048
1049 #[cfg(test)]
1050 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
1051 &self.variable_list
1052 }
1053
1054 pub fn capabilities(&self, cx: &App) -> Capabilities {
1055 self.session().read(cx).capabilities().clone()
1056 }
1057
1058 pub fn select_current_thread(
1059 &mut self,
1060 threads: &Vec<(Thread, ThreadStatus)>,
1061 window: &mut Window,
1062 cx: &mut Context<Self>,
1063 ) {
1064 let selected_thread = self
1065 .thread_id
1066 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
1067 .or_else(|| threads.first());
1068
1069 let Some((selected_thread, _)) = selected_thread else {
1070 return;
1071 };
1072
1073 if Some(ThreadId(selected_thread.id)) != self.thread_id {
1074 self.select_thread(ThreadId(selected_thread.id), window, cx);
1075 }
1076 }
1077
1078 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
1079 self.thread_id
1080 }
1081
1082 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
1083 self.thread_id
1084 .map(|id| self.session().read(cx).thread_status(id))
1085 }
1086
1087 fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
1088 if self.thread_id.is_some_and(|id| id == thread_id) {
1089 return;
1090 }
1091
1092 self.thread_id = Some(thread_id);
1093
1094 self.stack_frame_list
1095 .update(cx, |list, cx| list.schedule_refresh(true, window, cx));
1096 }
1097
1098 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
1099 let Some(thread_id) = self.thread_id else {
1100 return;
1101 };
1102
1103 self.session().update(cx, |state, cx| {
1104 state.continue_thread(thread_id, cx);
1105 });
1106 }
1107
1108 pub fn step_over(&mut self, cx: &mut Context<Self>) {
1109 let Some(thread_id) = self.thread_id else {
1110 return;
1111 };
1112
1113 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1114
1115 self.session().update(cx, |state, cx| {
1116 state.step_over(thread_id, granularity, cx);
1117 });
1118 }
1119
1120 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
1121 let Some(thread_id) = self.thread_id else {
1122 return;
1123 };
1124
1125 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1126
1127 self.session().update(cx, |state, cx| {
1128 state.step_in(thread_id, granularity, cx);
1129 });
1130 }
1131
1132 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
1133 let Some(thread_id) = self.thread_id else {
1134 return;
1135 };
1136
1137 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1138
1139 self.session().update(cx, |state, cx| {
1140 state.step_out(thread_id, granularity, cx);
1141 });
1142 }
1143
1144 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
1145 let Some(thread_id) = self.thread_id else {
1146 return;
1147 };
1148
1149 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1150
1151 self.session().update(cx, |state, cx| {
1152 state.step_back(thread_id, granularity, cx);
1153 });
1154 }
1155
1156 pub fn restart_session(&self, cx: &mut Context<Self>) {
1157 self.session().update(cx, |state, cx| {
1158 state.restart(None, cx);
1159 });
1160 }
1161
1162 pub fn pause_thread(&self, cx: &mut Context<Self>) {
1163 let Some(thread_id) = self.thread_id else {
1164 return;
1165 };
1166
1167 self.session().update(cx, |state, cx| {
1168 state.pause_thread(thread_id, cx);
1169 });
1170 }
1171
1172 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
1173 self.workspace
1174 .update(cx, |workspace, cx| {
1175 workspace
1176 .project()
1177 .read(cx)
1178 .breakpoint_store()
1179 .update(cx, |store, cx| {
1180 store.remove_active_position(Some(self.session_id), cx)
1181 })
1182 })
1183 .log_err();
1184
1185 self.session.update(cx, |session, cx| {
1186 session.shutdown(cx).detach();
1187 })
1188 }
1189
1190 pub fn stop_thread(&self, cx: &mut Context<Self>) {
1191 let Some(thread_id) = self.thread_id else {
1192 return;
1193 };
1194
1195 self.workspace
1196 .update(cx, |workspace, cx| {
1197 workspace
1198 .project()
1199 .read(cx)
1200 .breakpoint_store()
1201 .update(cx, |store, cx| {
1202 store.remove_active_position(Some(self.session_id), cx)
1203 })
1204 })
1205 .log_err();
1206
1207 self.session().update(cx, |state, cx| {
1208 state.terminate_threads(Some(vec![thread_id; 1]), cx);
1209 });
1210 }
1211
1212 #[expect(
1213 unused,
1214 reason = "Support for disconnecting a client is not wired through yet"
1215 )]
1216 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
1217 self.session().update(cx, |state, cx| {
1218 state.disconnect_client(cx);
1219 });
1220 }
1221
1222 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
1223 self.session.update(cx, |session, cx| {
1224 session.toggle_ignore_breakpoints(cx).detach();
1225 });
1226 }
1227
1228 pub(crate) fn thread_dropdown(
1229 &self,
1230 window: &mut Window,
1231 cx: &mut Context<'_, RunningState>,
1232 ) -> DropdownMenu {
1233 let state = cx.entity();
1234 let threads = self.session.update(cx, |this, cx| this.threads(cx));
1235 let selected_thread_name = threads
1236 .iter()
1237 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
1238 .map(|(thread, _)| thread.name.clone())
1239 .unwrap_or("Threads".to_owned());
1240 DropdownMenu::new(
1241 ("thread-list", self.session_id.0),
1242 selected_thread_name,
1243 ContextMenu::build_eager(window, cx, move |mut this, _, _| {
1244 for (thread, _) in threads {
1245 let state = state.clone();
1246 let thread_id = thread.id;
1247 this = this.entry(thread.name, None, move |window, cx| {
1248 state.update(cx, |state, cx| {
1249 state.select_thread(ThreadId(thread_id), window, cx);
1250 });
1251 });
1252 }
1253 this
1254 }),
1255 )
1256 }
1257
1258 fn default_pane_layout(
1259 project: Entity<Project>,
1260 workspace: &WeakEntity<Workspace>,
1261 stack_frame_list: &Entity<StackFrameList>,
1262 variable_list: &Entity<VariableList>,
1263 module_list: &Entity<ModuleList>,
1264 loaded_source_list: &Entity<LoadedSourceList>,
1265 console: &Entity<Console>,
1266 breakpoints: &Entity<BreakpointList>,
1267 subscriptions: &mut HashMap<EntityId, Subscription>,
1268 window: &mut Window,
1269 cx: &mut Context<'_, RunningState>,
1270 ) -> Member {
1271 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1272 leftmost_pane.update(cx, |this, cx| {
1273 this.add_item(
1274 Box::new(SubView::new(
1275 this.focus_handle(cx),
1276 stack_frame_list.clone().into(),
1277 DebuggerPaneItem::Frames,
1278 None,
1279 cx,
1280 )),
1281 true,
1282 false,
1283 None,
1284 window,
1285 cx,
1286 );
1287 this.add_item(
1288 Box::new(SubView::new(
1289 breakpoints.focus_handle(cx),
1290 breakpoints.clone().into(),
1291 DebuggerPaneItem::BreakpointList,
1292 None,
1293 cx,
1294 )),
1295 true,
1296 false,
1297 None,
1298 window,
1299 cx,
1300 );
1301 this.activate_item(0, false, false, window, cx);
1302 });
1303 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1304
1305 center_pane.update(cx, |this, cx| {
1306 this.add_item(
1307 Box::new(SubView::new(
1308 variable_list.focus_handle(cx),
1309 variable_list.clone().into(),
1310 DebuggerPaneItem::Variables,
1311 None,
1312 cx,
1313 )),
1314 true,
1315 false,
1316 None,
1317 window,
1318 cx,
1319 );
1320 this.add_item(
1321 Box::new(SubView::new(
1322 module_list.focus_handle(cx),
1323 module_list.clone().into(),
1324 DebuggerPaneItem::Modules,
1325 None,
1326 cx,
1327 )),
1328 false,
1329 false,
1330 None,
1331 window,
1332 cx,
1333 );
1334
1335 this.add_item(
1336 Box::new(SubView::new(
1337 loaded_source_list.focus_handle(cx),
1338 loaded_source_list.clone().into(),
1339 DebuggerPaneItem::LoadedSources,
1340 None,
1341 cx,
1342 )),
1343 false,
1344 false,
1345 None,
1346 window,
1347 cx,
1348 );
1349 this.activate_item(0, false, false, window, cx);
1350 });
1351
1352 let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1353 rightmost_pane.update(cx, |this, cx| {
1354 let weak_console = console.downgrade();
1355 this.add_item(
1356 Box::new(SubView::new(
1357 this.focus_handle(cx),
1358 console.clone().into(),
1359 DebuggerPaneItem::Console,
1360 Some(Box::new(move |cx| {
1361 weak_console
1362 .read_with(cx, |console, cx| console.show_indicator(cx))
1363 .unwrap_or_default()
1364 })),
1365 cx,
1366 )),
1367 true,
1368 false,
1369 None,
1370 window,
1371 cx,
1372 );
1373 });
1374
1375 subscriptions.extend(
1376 [&leftmost_pane, ¢er_pane, &rightmost_pane]
1377 .into_iter()
1378 .map(|entity| {
1379 (
1380 entity.entity_id(),
1381 cx.subscribe_in(entity, window, Self::handle_pane_event),
1382 )
1383 }),
1384 );
1385
1386 let group_root = workspace::PaneAxis::new(
1387 gpui::Axis::Horizontal,
1388 [leftmost_pane, center_pane, rightmost_pane]
1389 .into_iter()
1390 .map(workspace::Member::Pane)
1391 .collect(),
1392 );
1393
1394 Member::Axis(group_root)
1395 }
1396}
1397
1398impl EventEmitter<DebugPanelItemEvent> for RunningState {}
1399
1400impl Focusable for RunningState {
1401 fn focus_handle(&self, _: &App) -> FocusHandle {
1402 self.focus_handle.clone()
1403 }
1404}