running.rs

   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, &center_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}