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