variable_list.rs

   1use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
   2use dap::{
   3    ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
   4    VariableReference,
   5};
   6use editor::Editor;
   7use gpui::{
   8    Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
   9    FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
  10    TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
  11};
  12use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
  13use project::debugger::session::{Session, SessionEvent, Watcher};
  14use std::{collections::HashMap, ops::Range, sync::Arc};
  15use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
  16use util::debug_panic;
  17
  18actions!(
  19    variable_list,
  20    [
  21        /// Expands the selected variable entry to show its children.
  22        ExpandSelectedEntry,
  23        /// Collapses the selected variable entry to hide its children.
  24        CollapseSelectedEntry,
  25        /// Copies the variable name to the clipboard.
  26        CopyVariableName,
  27        /// Copies the variable value to the clipboard.
  28        CopyVariableValue,
  29        /// Edits the value of the selected variable.
  30        EditVariable,
  31        /// Adds the selected variable to the watch list.
  32        AddWatch,
  33        /// Removes the selected variable from the watch list.
  34        RemoveWatch,
  35    ]
  36);
  37
  38#[derive(Debug, Copy, Clone, PartialEq, Eq)]
  39pub(crate) struct EntryState {
  40    depth: usize,
  41    is_expanded: bool,
  42    has_children: bool,
  43    parent_reference: VariableReference,
  44}
  45
  46#[derive(Debug, PartialEq, Eq, Hash, Clone)]
  47pub(crate) struct EntryPath {
  48    pub leaf_name: Option<SharedString>,
  49    pub indices: Arc<[SharedString]>,
  50}
  51
  52impl EntryPath {
  53    fn for_watcher(expression: impl Into<SharedString>) -> Self {
  54        Self {
  55            leaf_name: Some(expression.into()),
  56            indices: Arc::new([]),
  57        }
  58    }
  59
  60    fn for_scope(scope_name: impl Into<SharedString>) -> Self {
  61        Self {
  62            leaf_name: Some(scope_name.into()),
  63            indices: Arc::new([]),
  64        }
  65    }
  66
  67    fn with_name(&self, name: SharedString) -> Self {
  68        Self {
  69            leaf_name: Some(name),
  70            indices: self.indices.clone(),
  71        }
  72    }
  73
  74    /// Create a new child of this variable path
  75    fn with_child(&self, name: SharedString) -> Self {
  76        Self {
  77            leaf_name: None,
  78            indices: self
  79                .indices
  80                .iter()
  81                .cloned()
  82                .chain(std::iter::once(name))
  83                .collect(),
  84        }
  85    }
  86}
  87
  88#[derive(Debug, Clone, PartialEq)]
  89enum EntryKind {
  90    Watcher(Watcher),
  91    Variable(dap::Variable),
  92    Scope(dap::Scope),
  93}
  94
  95impl EntryKind {
  96    fn as_watcher(&self) -> Option<&Watcher> {
  97        match self {
  98            EntryKind::Watcher(watcher) => Some(watcher),
  99            _ => None,
 100        }
 101    }
 102
 103    fn as_variable(&self) -> Option<&dap::Variable> {
 104        match self {
 105            EntryKind::Variable(dap) => Some(dap),
 106            _ => None,
 107        }
 108    }
 109
 110    fn as_scope(&self) -> Option<&dap::Scope> {
 111        match self {
 112            EntryKind::Scope(dap) => Some(dap),
 113            _ => None,
 114        }
 115    }
 116
 117    #[cfg(test)]
 118    fn name(&self) -> &str {
 119        match self {
 120            EntryKind::Watcher(watcher) => &watcher.expression,
 121            EntryKind::Variable(dap) => &dap.name,
 122            EntryKind::Scope(dap) => &dap.name,
 123        }
 124    }
 125}
 126
 127#[derive(Debug, Clone, PartialEq)]
 128struct ListEntry {
 129    dap_kind: EntryKind,
 130    path: EntryPath,
 131}
 132
 133impl ListEntry {
 134    fn as_watcher(&self) -> Option<&Watcher> {
 135        self.dap_kind.as_watcher()
 136    }
 137
 138    fn as_variable(&self) -> Option<&dap::Variable> {
 139        self.dap_kind.as_variable()
 140    }
 141
 142    fn as_scope(&self) -> Option<&dap::Scope> {
 143        self.dap_kind.as_scope()
 144    }
 145
 146    fn item_id(&self) -> ElementId {
 147        use std::fmt::Write;
 148        let mut id = match &self.dap_kind {
 149            EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
 150            EntryKind::Variable(dap) => format!("variable-{}", dap.name),
 151            EntryKind::Scope(dap) => format!("scope-{}", dap.name),
 152        };
 153        for name in self.path.indices.iter() {
 154            _ = write!(id, "-{}", name);
 155        }
 156        SharedString::from(id).into()
 157    }
 158
 159    fn item_value_id(&self) -> ElementId {
 160        use std::fmt::Write;
 161        let mut id = match &self.dap_kind {
 162            EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
 163            EntryKind::Variable(dap) => format!("variable-{}", dap.name),
 164            EntryKind::Scope(dap) => format!("scope-{}", dap.name),
 165        };
 166        for name in self.path.indices.iter() {
 167            _ = write!(id, "-{}", name);
 168        }
 169        _ = write!(id, "-value");
 170        SharedString::from(id).into()
 171    }
 172}
 173
 174struct VariableColor {
 175    name: Option<Hsla>,
 176    value: Option<Hsla>,
 177}
 178
 179pub struct VariableList {
 180    entries: Vec<ListEntry>,
 181    entry_states: HashMap<EntryPath, EntryState>,
 182    selected_stack_frame_id: Option<StackFrameId>,
 183    list_handle: UniformListScrollHandle,
 184    scrollbar_state: ScrollbarState,
 185    session: Entity<Session>,
 186    selection: Option<EntryPath>,
 187    open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 188    focus_handle: FocusHandle,
 189    edited_path: Option<(EntryPath, Entity<Editor>)>,
 190    disabled: bool,
 191    _subscriptions: Vec<Subscription>,
 192}
 193
 194impl VariableList {
 195    pub fn new(
 196        session: Entity<Session>,
 197        stack_frame_list: Entity<StackFrameList>,
 198        window: &mut Window,
 199        cx: &mut Context<Self>,
 200    ) -> Self {
 201        let focus_handle = cx.focus_handle();
 202
 203        let _subscriptions = vec![
 204            cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
 205            cx.subscribe(&session, |this, _, event, cx| match event {
 206                SessionEvent::Stopped(_) => {
 207                    this.selection.take();
 208                    this.edited_path.take();
 209                    this.selected_stack_frame_id.take();
 210                }
 211                SessionEvent::Variables | SessionEvent::Watchers => {
 212                    this.build_entries(cx);
 213                }
 214                _ => {}
 215            }),
 216            cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
 217                this.edited_path.take();
 218                cx.notify();
 219            }),
 220        ];
 221
 222        let list_state = UniformListScrollHandle::default();
 223
 224        Self {
 225            scrollbar_state: ScrollbarState::new(list_state.clone()),
 226            list_handle: list_state,
 227            session,
 228            focus_handle,
 229            _subscriptions,
 230            selected_stack_frame_id: None,
 231            selection: None,
 232            open_context_menu: None,
 233            disabled: false,
 234            edited_path: None,
 235            entries: Default::default(),
 236            entry_states: Default::default(),
 237        }
 238    }
 239
 240    pub(super) fn disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
 241        let old_disabled = std::mem::take(&mut self.disabled);
 242        self.disabled = disabled;
 243        if old_disabled != disabled {
 244            cx.notify();
 245        }
 246    }
 247
 248    pub(super) fn has_open_context_menu(&self) -> bool {
 249        self.open_context_menu.is_some()
 250    }
 251
 252    fn build_entries(&mut self, cx: &mut Context<Self>) {
 253        let Some(stack_frame_id) = self.selected_stack_frame_id else {
 254            return;
 255        };
 256
 257        let mut entries = vec![];
 258
 259        let scopes: Vec<_> = self.session.update(cx, |session, cx| {
 260            session.scopes(stack_frame_id, cx).iter().cloned().collect()
 261        });
 262
 263        let mut contains_local_scope = false;
 264
 265        let mut stack = scopes
 266            .into_iter()
 267            .rev()
 268            .filter(|scope| {
 269                if scope
 270                    .presentation_hint
 271                    .as_ref()
 272                    .map(|hint| *hint == ScopePresentationHint::Locals)
 273                    .unwrap_or(scope.name.to_lowercase().starts_with("local"))
 274                {
 275                    contains_local_scope = true;
 276                }
 277
 278                self.session.update(cx, |session, cx| {
 279                    session.variables(scope.variables_reference, cx).len() > 0
 280                })
 281            })
 282            .map(|scope| {
 283                (
 284                    scope.variables_reference,
 285                    scope.variables_reference,
 286                    EntryPath::for_scope(&scope.name),
 287                    EntryKind::Scope(scope),
 288                )
 289            })
 290            .collect::<Vec<_>>();
 291
 292        let watches = self.session.read(cx).watchers().clone();
 293        stack.extend(
 294            watches
 295                .into_values()
 296                .map(|watcher| {
 297                    (
 298                        watcher.variables_reference,
 299                        watcher.variables_reference,
 300                        EntryPath::for_watcher(watcher.expression.clone()),
 301                        EntryKind::Watcher(watcher.clone()),
 302                    )
 303                })
 304                .collect::<Vec<_>>(),
 305        );
 306
 307        let scopes_count = stack.len();
 308
 309        while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
 310        {
 311            match &dap_kind {
 312                EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
 313                EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
 314                EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
 315            }
 316
 317            let var_state = self
 318                .entry_states
 319                .entry(path.clone())
 320                .and_modify(|state| {
 321                    state.parent_reference = container_reference;
 322                    state.has_children = variables_reference != 0;
 323                })
 324                .or_insert(EntryState {
 325                    depth: path.indices.len(),
 326                    is_expanded: dap_kind.as_scope().is_some_and(|scope| {
 327                        (scopes_count == 1 && !contains_local_scope)
 328                            || scope
 329                                .presentation_hint
 330                                .as_ref()
 331                                .map(|hint| *hint == ScopePresentationHint::Locals)
 332                                .unwrap_or(scope.name.to_lowercase().starts_with("local"))
 333                    }),
 334                    parent_reference: container_reference,
 335                    has_children: variables_reference != 0,
 336                });
 337
 338            entries.push(ListEntry {
 339                dap_kind,
 340                path: path.clone(),
 341            });
 342
 343            if var_state.is_expanded {
 344                let children = self
 345                    .session
 346                    .update(cx, |session, cx| session.variables(variables_reference, cx));
 347                stack.extend(children.into_iter().rev().map(|child| {
 348                    (
 349                        variables_reference,
 350                        child.variables_reference,
 351                        path.with_child(child.name.clone().into()),
 352                        EntryKind::Variable(child),
 353                    )
 354                }));
 355            }
 356        }
 357
 358        self.entries = entries;
 359        cx.notify();
 360    }
 361
 362    fn handle_stack_frame_list_events(
 363        &mut self,
 364        _: Entity<StackFrameList>,
 365        event: &StackFrameListEvent,
 366        cx: &mut Context<Self>,
 367    ) {
 368        match event {
 369            StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
 370                self.selected_stack_frame_id = Some(*stack_frame_id);
 371                self.session.update(cx, |session, cx| {
 372                    session.refresh_watchers(*stack_frame_id, cx);
 373                });
 374                self.build_entries(cx);
 375            }
 376            StackFrameListEvent::BuiltEntries => {}
 377        }
 378    }
 379
 380    pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
 381        self.entries
 382            .iter()
 383            .filter_map(|entry| match &entry.dap_kind {
 384                EntryKind::Variable(dap) => Some(dap.clone()),
 385                EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
 386            })
 387            .collect()
 388    }
 389
 390    fn render_entries(
 391        &mut self,
 392        ix: Range<usize>,
 393        window: &mut Window,
 394        cx: &mut Context<Self>,
 395    ) -> Vec<AnyElement> {
 396        ix.into_iter()
 397            .filter_map(|ix| {
 398                let (entry, state) = self
 399                    .entries
 400                    .get(ix)
 401                    .and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
 402
 403                match &entry.dap_kind {
 404                    EntryKind::Watcher { .. } => {
 405                        Some(self.render_watcher(entry, *state, window, cx))
 406                    }
 407                    EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
 408                    EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
 409                }
 410            })
 411            .collect()
 412    }
 413
 414    pub(crate) fn toggle_entry(&mut self, var_path: &EntryPath, cx: &mut Context<Self>) {
 415        let Some(entry) = self.entry_states.get_mut(var_path) else {
 416            log::error!("Could not find variable list entry state to toggle");
 417            return;
 418        };
 419
 420        entry.is_expanded = !entry.is_expanded;
 421        self.build_entries(cx);
 422    }
 423
 424    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
 425        self.cancel(&Default::default(), window, cx);
 426        if let Some(variable) = self.entries.first() {
 427            self.selection = Some(variable.path.clone());
 428            self.build_entries(cx);
 429        }
 430    }
 431
 432    fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
 433        self.cancel(&Default::default(), window, cx);
 434        if let Some(variable) = self.entries.last() {
 435            self.selection = Some(variable.path.clone());
 436            self.build_entries(cx);
 437        }
 438    }
 439
 440    fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
 441        self.cancel(&Default::default(), window, cx);
 442        if let Some(selection) = &self.selection {
 443            let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
 444                if &var.path == selection && ix > 0 {
 445                    Some(ix.saturating_sub(1))
 446                } else {
 447                    None
 448                }
 449            });
 450
 451            if let Some(new_selection) =
 452                index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
 453            {
 454                self.selection = Some(new_selection);
 455                self.build_entries(cx);
 456            } else {
 457                self.select_last(&SelectLast, window, cx);
 458            }
 459        } else {
 460            self.select_last(&SelectLast, window, cx);
 461        }
 462    }
 463
 464    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 465        self.cancel(&Default::default(), window, cx);
 466        if let Some(selection) = &self.selection {
 467            let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
 468                if &var.path == selection {
 469                    Some(ix.saturating_add(1))
 470                } else {
 471                    None
 472                }
 473            });
 474
 475            if let Some(new_selection) =
 476                index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
 477            {
 478                self.selection = Some(new_selection);
 479                self.build_entries(cx);
 480            } else {
 481                self.select_first(&SelectFirst, window, cx);
 482            }
 483        } else {
 484            self.select_first(&SelectFirst, window, cx);
 485        }
 486    }
 487
 488    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 489        self.edited_path.take();
 490        self.focus_handle.focus(window);
 491        cx.notify();
 492    }
 493
 494    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 495        if let Some((var_path, editor)) = self.edited_path.take() {
 496            let Some(state) = self.entry_states.get(&var_path) else {
 497                return;
 498            };
 499
 500            let variables_reference = state.parent_reference;
 501            let Some(name) = var_path.leaf_name else {
 502                return;
 503            };
 504
 505            let Some(stack_frame_id) = self.selected_stack_frame_id else {
 506                return;
 507            };
 508
 509            let value = editor.read(cx).text(cx);
 510
 511            self.session.update(cx, |session, cx| {
 512                session.set_variable_value(
 513                    stack_frame_id,
 514                    variables_reference,
 515                    name.into(),
 516                    value,
 517                    cx,
 518                )
 519            });
 520        }
 521    }
 522
 523    fn collapse_selected_entry(
 524        &mut self,
 525        _: &CollapseSelectedEntry,
 526        window: &mut Window,
 527        cx: &mut Context<Self>,
 528    ) {
 529        if let Some(ref selected_entry) = self.selection {
 530            let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
 531                debug_panic!("Trying to toggle variable in variable list that has an no state");
 532                return;
 533            };
 534
 535            if !entry_state.is_expanded || !entry_state.has_children {
 536                self.select_prev(&SelectPrevious, window, cx);
 537            } else {
 538                entry_state.is_expanded = false;
 539                self.build_entries(cx);
 540            }
 541        }
 542    }
 543
 544    fn expand_selected_entry(
 545        &mut self,
 546        _: &ExpandSelectedEntry,
 547        window: &mut Window,
 548        cx: &mut Context<Self>,
 549    ) {
 550        if let Some(selected_entry) = &self.selection {
 551            let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
 552                debug_panic!("Trying to toggle variable in variable list that has an no state");
 553                return;
 554            };
 555
 556            if entry_state.is_expanded || !entry_state.has_children {
 557                self.select_next(&SelectNext, window, cx);
 558            } else {
 559                entry_state.is_expanded = true;
 560                self.build_entries(cx);
 561            }
 562        }
 563    }
 564
 565    fn deploy_list_entry_context_menu(
 566        &mut self,
 567        entry: ListEntry,
 568        position: Point<Pixels>,
 569        window: &mut Window,
 570        cx: &mut Context<Self>,
 571    ) {
 572        let supports_set_variable = self
 573            .session
 574            .read(cx)
 575            .capabilities()
 576            .supports_set_variable
 577            .unwrap_or_default();
 578
 579        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
 580            menu.when(entry.as_variable().is_some(), |menu| {
 581                menu.action("Copy Name", CopyVariableName.boxed_clone())
 582                    .action("Copy Value", CopyVariableValue.boxed_clone())
 583                    .when(supports_set_variable, |menu| {
 584                        menu.action("Edit Value", EditVariable.boxed_clone())
 585                    })
 586                    .action("Watch Variable", AddWatch.boxed_clone())
 587            })
 588            .when(entry.as_watcher().is_some(), |menu| {
 589                menu.action("Copy Name", CopyVariableName.boxed_clone())
 590                    .action("Copy Value", CopyVariableValue.boxed_clone())
 591                    .when(supports_set_variable, |menu| {
 592                        menu.action("Edit Value", EditVariable.boxed_clone())
 593                    })
 594                    .action("Remove Watch", RemoveWatch.boxed_clone())
 595            })
 596            .context(self.focus_handle.clone())
 597        });
 598
 599        cx.focus_view(&context_menu, window);
 600        let subscription = cx.subscribe_in(
 601            &context_menu,
 602            window,
 603            |this, _, _: &DismissEvent, window, cx| {
 604                if this.open_context_menu.as_ref().is_some_and(|context_menu| {
 605                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
 606                }) {
 607                    cx.focus_self(window);
 608                }
 609                this.open_context_menu.take();
 610                cx.notify();
 611            },
 612        );
 613
 614        self.open_context_menu = Some((context_menu, position, subscription));
 615    }
 616
 617    fn copy_variable_name(
 618        &mut self,
 619        _: &CopyVariableName,
 620        _window: &mut Window,
 621        cx: &mut Context<Self>,
 622    ) {
 623        let Some(selection) = self.selection.as_ref() else {
 624            return;
 625        };
 626
 627        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 628            return;
 629        };
 630
 631        let variable_name = match &entry.dap_kind {
 632            EntryKind::Variable(dap) => dap.name.clone(),
 633            EntryKind::Watcher(watcher) => watcher.expression.to_string(),
 634            EntryKind::Scope(_) => return,
 635        };
 636
 637        cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
 638    }
 639
 640    fn copy_variable_value(
 641        &mut self,
 642        _: &CopyVariableValue,
 643        _window: &mut Window,
 644        cx: &mut Context<Self>,
 645    ) {
 646        let Some(selection) = self.selection.as_ref() else {
 647            return;
 648        };
 649
 650        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 651            return;
 652        };
 653
 654        let variable_value = match &entry.dap_kind {
 655            EntryKind::Variable(dap) => dap.value.clone(),
 656            EntryKind::Watcher(watcher) => watcher.value.to_string(),
 657            EntryKind::Scope(_) => return,
 658        };
 659
 660        cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
 661    }
 662
 663    fn edit_variable(&mut self, _: &EditVariable, window: &mut Window, cx: &mut Context<Self>) {
 664        let Some(selection) = self.selection.as_ref() else {
 665            return;
 666        };
 667
 668        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 669            return;
 670        };
 671
 672        let variable_value = match &entry.dap_kind {
 673            EntryKind::Watcher(watcher) => watcher.value.to_string(),
 674            EntryKind::Variable(variable) => variable.value.clone(),
 675            EntryKind::Scope(_) => return,
 676        };
 677
 678        let editor = Self::create_variable_editor(&variable_value, window, cx);
 679        self.edited_path = Some((entry.path.clone(), editor));
 680
 681        cx.notify();
 682    }
 683
 684    fn add_watcher(&mut self, _: &AddWatch, _: &mut Window, cx: &mut Context<Self>) {
 685        let Some(selection) = self.selection.as_ref() else {
 686            return;
 687        };
 688
 689        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 690            return;
 691        };
 692
 693        let Some(variable) = entry.as_variable() else {
 694            return;
 695        };
 696
 697        let Some(stack_frame_id) = self.selected_stack_frame_id else {
 698            return;
 699        };
 700
 701        let add_watcher_task = self.session.update(cx, |session, cx| {
 702            let expression = variable
 703                .evaluate_name
 704                .clone()
 705                .unwrap_or_else(|| variable.name.clone());
 706
 707            session.add_watcher(expression.into(), stack_frame_id, cx)
 708        });
 709
 710        cx.spawn(async move |this, cx| {
 711            add_watcher_task.await?;
 712
 713            this.update(cx, |this, cx| {
 714                this.build_entries(cx);
 715            })
 716        })
 717        .detach_and_log_err(cx);
 718    }
 719
 720    fn remove_watcher(&mut self, _: &RemoveWatch, _: &mut Window, cx: &mut Context<Self>) {
 721        let Some(selection) = self.selection.as_ref() else {
 722            return;
 723        };
 724
 725        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 726            return;
 727        };
 728
 729        let Some(watcher) = entry.as_watcher() else {
 730            return;
 731        };
 732
 733        self.session.update(cx, |session, _| {
 734            session.remove_watcher(watcher.expression.clone());
 735        });
 736        self.build_entries(cx);
 737    }
 738
 739    #[track_caller]
 740    #[cfg(test)]
 741    pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
 742        const INDENT: &'static str = "    ";
 743
 744        let entries = &self.entries;
 745        let mut visual_entries = Vec::with_capacity(entries.len());
 746        for entry in entries {
 747            let state = self
 748                .entry_states
 749                .get(&entry.path)
 750                .expect("If there's a variable entry there has to be a state that goes with it");
 751
 752            visual_entries.push(format!(
 753                "{}{} {}{}",
 754                INDENT.repeat(state.depth - 1),
 755                if state.is_expanded { "v" } else { ">" },
 756                entry.dap_kind.name(),
 757                if self.selection.as_ref() == Some(&entry.path) {
 758                    " <=== selected"
 759                } else {
 760                    ""
 761                }
 762            ));
 763        }
 764
 765        pretty_assertions::assert_eq!(expected, visual_entries);
 766    }
 767
 768    #[track_caller]
 769    #[cfg(test)]
 770    pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
 771        self.entries
 772            .iter()
 773            .filter_map(|entry| match &entry.dap_kind {
 774                EntryKind::Scope(scope) => Some(scope),
 775                _ => None,
 776            })
 777            .cloned()
 778            .collect()
 779    }
 780
 781    #[track_caller]
 782    #[cfg(test)]
 783    pub(crate) fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
 784        let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
 785        let mut idx = 0;
 786
 787        for entry in self.entries.iter() {
 788            match &entry.dap_kind {
 789                EntryKind::Watcher { .. } => continue,
 790                EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
 791                EntryKind::Scope(scope) => {
 792                    if scopes.len() > 0 {
 793                        idx += 1;
 794                    }
 795
 796                    scopes.push((scope.clone(), Vec::new()));
 797                }
 798            }
 799        }
 800
 801        scopes
 802    }
 803
 804    #[track_caller]
 805    #[cfg(test)]
 806    pub(crate) fn variables(&self) -> Vec<dap::Variable> {
 807        self.entries
 808            .iter()
 809            .filter_map(|entry| match &entry.dap_kind {
 810                EntryKind::Variable(variable) => Some(variable),
 811                _ => None,
 812            })
 813            .cloned()
 814            .collect()
 815    }
 816
 817    fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> {
 818        let editor = cx.new(|cx| {
 819            let mut editor = Editor::single_line(window, cx);
 820
 821            let refinement = TextStyleRefinement {
 822                font_size: Some(
 823                    TextSize::XSmall
 824                        .rems(cx)
 825                        .to_pixels(window.rem_size())
 826                        .into(),
 827                ),
 828                ..Default::default()
 829            };
 830            editor.set_text_style_refinement(refinement);
 831            editor.set_text(default, window, cx);
 832            editor.select_all(&editor::actions::SelectAll, window, cx);
 833            editor
 834        });
 835        editor.focus_handle(cx).focus(window);
 836        editor
 837    }
 838
 839    fn variable_color(
 840        &self,
 841        presentation_hint: Option<&VariablePresentationHint>,
 842        cx: &Context<Self>,
 843    ) -> VariableColor {
 844        let syntax_color_for = |name| cx.theme().syntax().get(name).color;
 845        let name = if self.disabled {
 846            Some(Color::Disabled.color(cx))
 847        } else {
 848            match presentation_hint
 849                .as_ref()
 850                .and_then(|hint| hint.kind.as_ref())
 851                .unwrap_or(&VariablePresentationHintKind::Unknown)
 852            {
 853                VariablePresentationHintKind::Class
 854                | VariablePresentationHintKind::BaseClass
 855                | VariablePresentationHintKind::InnerClass
 856                | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
 857                VariablePresentationHintKind::Data => syntax_color_for("variable"),
 858                VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
 859            }
 860        };
 861        let value = self
 862            .disabled
 863            .then(|| Color::Disabled.color(cx))
 864            .or_else(|| syntax_color_for("variable.special"));
 865
 866        VariableColor { name, value }
 867    }
 868
 869    fn render_variable_value(
 870        &self,
 871        entry: &ListEntry,
 872        variable_color: &VariableColor,
 873        value: String,
 874        cx: &mut Context<Self>,
 875    ) -> AnyElement {
 876        if !value.is_empty() {
 877            div()
 878                .w_full()
 879                .id(entry.item_value_id())
 880                .map(|this| {
 881                    if let Some((_, editor)) = self
 882                        .edited_path
 883                        .as_ref()
 884                        .filter(|(path, _)| path == &entry.path)
 885                    {
 886                        this.child(div().size_full().px_2().child(editor.clone()))
 887                    } else {
 888                        this.text_color(cx.theme().colors().text_muted)
 889                            .when(
 890                                !self.disabled
 891                                    && self
 892                                        .session
 893                                        .read(cx)
 894                                        .capabilities()
 895                                        .supports_set_variable
 896                                        .unwrap_or_default(),
 897                                |this| {
 898                                    let path = entry.path.clone();
 899                                    let variable_value = value.clone();
 900                                    this.on_click(cx.listener(
 901                                        move |this, click: &ClickEvent, window, cx| {
 902                                            if click.down.click_count < 2 {
 903                                                return;
 904                                            }
 905                                            let editor = Self::create_variable_editor(
 906                                                &variable_value,
 907                                                window,
 908                                                cx,
 909                                            );
 910                                            this.edited_path = Some((path.clone(), editor));
 911
 912                                            cx.notify();
 913                                        },
 914                                    ))
 915                                },
 916                            )
 917                            .child(
 918                                Label::new(format!("=  {}", &value))
 919                                    .single_line()
 920                                    .truncate()
 921                                    .size(LabelSize::Small)
 922                                    .color(Color::Muted)
 923                                    .when_some(variable_color.value, |this, color| {
 924                                        this.color(Color::from(color))
 925                                    }),
 926                            )
 927                    }
 928                })
 929                .into_any_element()
 930        } else {
 931            Empty.into_any_element()
 932        }
 933    }
 934
 935    fn center_truncate_string(s: &str, mut max_chars: usize) -> String {
 936        const ELLIPSIS: &str = "...";
 937        const MIN_LENGTH: usize = 3;
 938
 939        max_chars = max_chars.max(MIN_LENGTH);
 940
 941        let char_count = s.chars().count();
 942        if char_count <= max_chars {
 943            return s.to_string();
 944        }
 945
 946        if ELLIPSIS.len() + MIN_LENGTH > max_chars {
 947            return s.chars().take(MIN_LENGTH).collect();
 948        }
 949
 950        let available_chars = max_chars - ELLIPSIS.len();
 951
 952        let start_chars = available_chars / 2;
 953        let end_chars = available_chars - start_chars;
 954        let skip_chars = char_count - end_chars;
 955
 956        let mut start_boundary = 0;
 957        let mut end_boundary = s.len();
 958
 959        for (i, (byte_idx, _)) in s.char_indices().enumerate() {
 960            if i == start_chars {
 961                start_boundary = byte_idx.max(MIN_LENGTH);
 962            }
 963
 964            if i == skip_chars {
 965                end_boundary = byte_idx;
 966            }
 967        }
 968
 969        if start_boundary >= end_boundary {
 970            return s.chars().take(MIN_LENGTH).collect();
 971        }
 972
 973        format!("{}{}{}", &s[..start_boundary], ELLIPSIS, &s[end_boundary..])
 974    }
 975
 976    fn render_watcher(
 977        &self,
 978        entry: &ListEntry,
 979        state: EntryState,
 980        _window: &mut Window,
 981        cx: &mut Context<Self>,
 982    ) -> AnyElement {
 983        let Some(watcher) = &entry.as_watcher() else {
 984            debug_panic!("Called render watcher on non watcher variable list entry variant");
 985            return div().into_any_element();
 986        };
 987
 988        let variable_color = self.variable_color(watcher.presentation_hint.as_ref(), cx);
 989
 990        let is_selected = self
 991            .selection
 992            .as_ref()
 993            .is_some_and(|selection| selection == &entry.path);
 994        let var_ref = watcher.variables_reference;
 995
 996        let colors = get_entry_color(cx);
 997        let bg_hover_color = if !is_selected {
 998            colors.hover
 999        } else {
1000            colors.default
1001        };
1002        let border_color = if is_selected {
1003            colors.marked_active
1004        } else {
1005            colors.default
1006        };
1007        let path = entry.path.clone();
1008
1009        let weak = cx.weak_entity();
1010        let focus_handle = self.focus_handle.clone();
1011        let watcher_len = (self.list_handle.content_size().width.0 / 12.0).floor() - 3.0;
1012        let watcher_len = watcher_len as usize;
1013
1014        div()
1015            .id(entry.item_id())
1016            .group("variable_list_entry")
1017            .pl_2()
1018            .border_1()
1019            .border_r_2()
1020            .border_color(border_color)
1021            .flex()
1022            .w_full()
1023            .h_full()
1024            .hover(|style| style.bg(bg_hover_color))
1025            .on_click(cx.listener({
1026                let path = path.clone();
1027                move |this, _, _window, cx| {
1028                    this.selection = Some(path.clone());
1029                    cx.notify();
1030                }
1031            }))
1032            .child(
1033                ListItem::new(SharedString::from(format!(
1034                    "watcher-{}",
1035                    watcher.expression
1036                )))
1037                .selectable(false)
1038                .disabled(self.disabled)
1039                .selectable(false)
1040                .indent_level(state.depth)
1041                .indent_step_size(px(10.))
1042                .always_show_disclosure_icon(true)
1043                .when(var_ref > 0, |list_item| {
1044                    list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1045                        let var_path = entry.path.clone();
1046                        move |this, _, _, cx| {
1047                            this.session.update(cx, |session, cx| {
1048                                session.variables(var_ref, cx);
1049                            });
1050
1051                            this.toggle_entry(&var_path, cx);
1052                        }
1053                    }))
1054                })
1055                .on_secondary_mouse_down(cx.listener({
1056                    let path = path.clone();
1057                    let entry = entry.clone();
1058                    move |this, event: &MouseDownEvent, window, cx| {
1059                        this.selection = Some(path.clone());
1060                        this.deploy_list_entry_context_menu(
1061                            entry.clone(),
1062                            event.position,
1063                            window,
1064                            cx,
1065                        );
1066                        cx.stop_propagation();
1067                    }
1068                }))
1069                .child(
1070                    h_flex()
1071                        .gap_1()
1072                        .text_ui_sm(cx)
1073                        .w_full()
1074                        .child(
1075                            Label::new(&Self::center_truncate_string(
1076                                watcher.expression.as_ref(),
1077                                watcher_len,
1078                            ))
1079                            .when_some(variable_color.name, |this, color| {
1080                                this.color(Color::from(color))
1081                            }),
1082                        )
1083                        .child(self.render_variable_value(
1084                            &entry,
1085                            &variable_color,
1086                            watcher.value.to_string(),
1087                            cx,
1088                        )),
1089                )
1090                .end_slot(
1091                    IconButton::new(
1092                        SharedString::from(format!("watcher-{}-remove-button", watcher.expression)),
1093                        IconName::Close,
1094                    )
1095                    .on_click({
1096                        let weak = weak.clone();
1097                        let path = path.clone();
1098                        move |_, window, cx| {
1099                            weak.update(cx, |variable_list, cx| {
1100                                variable_list.selection = Some(path.clone());
1101                                variable_list.remove_watcher(&RemoveWatch, window, cx);
1102                            })
1103                            .ok();
1104                        }
1105                    })
1106                    .tooltip(move |window, cx| {
1107                        Tooltip::for_action_in(
1108                            "Remove Watch",
1109                            &RemoveWatch,
1110                            &focus_handle,
1111                            window,
1112                            cx,
1113                        )
1114                    })
1115                    .icon_size(ui::IconSize::Indicator),
1116                ),
1117            )
1118            .into_any()
1119    }
1120
1121    fn render_scope(
1122        &self,
1123        entry: &ListEntry,
1124        state: EntryState,
1125        cx: &mut Context<Self>,
1126    ) -> AnyElement {
1127        let Some(scope) = entry.as_scope() else {
1128            debug_panic!("Called render scope on non scope variable list entry variant");
1129            return div().into_any_element();
1130        };
1131
1132        let var_ref = scope.variables_reference;
1133        let is_selected = self
1134            .selection
1135            .as_ref()
1136            .is_some_and(|selection| selection == &entry.path);
1137
1138        let colors = get_entry_color(cx);
1139        let bg_hover_color = if !is_selected {
1140            colors.hover
1141        } else {
1142            colors.default
1143        };
1144        let border_color = if is_selected {
1145            colors.marked_active
1146        } else {
1147            colors.default
1148        };
1149        let path = entry.path.clone();
1150
1151        div()
1152            .id(var_ref as usize)
1153            .group("variable_list_entry")
1154            .pl_2()
1155            .border_1()
1156            .border_r_2()
1157            .border_color(border_color)
1158            .flex()
1159            .w_full()
1160            .h_full()
1161            .hover(|style| style.bg(bg_hover_color))
1162            .on_click(cx.listener({
1163                move |this, _, _window, cx| {
1164                    this.selection = Some(path.clone());
1165                    cx.notify();
1166                }
1167            }))
1168            .child(
1169                ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
1170                    .selectable(false)
1171                    .disabled(self.disabled)
1172                    .indent_level(state.depth)
1173                    .indent_step_size(px(10.))
1174                    .always_show_disclosure_icon(true)
1175                    .toggle(state.is_expanded)
1176                    .on_toggle({
1177                        let var_path = entry.path.clone();
1178                        cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
1179                    })
1180                    .child(
1181                        div()
1182                            .text_ui(cx)
1183                            .w_full()
1184                            .when(self.disabled, |this| {
1185                                this.text_color(Color::Disabled.color(cx))
1186                            })
1187                            .child(scope.name.clone()),
1188                    ),
1189            )
1190            .into_any()
1191    }
1192
1193    fn render_variable(
1194        &self,
1195        variable: &ListEntry,
1196        state: EntryState,
1197        window: &mut Window,
1198        cx: &mut Context<Self>,
1199    ) -> AnyElement {
1200        let Some(dap) = &variable.as_variable() else {
1201            debug_panic!("Called render variable on non variable variable list entry variant");
1202            return div().into_any_element();
1203        };
1204
1205        let variable_color = self.variable_color(dap.presentation_hint.as_ref(), cx);
1206
1207        let var_ref = dap.variables_reference;
1208        let colors = get_entry_color(cx);
1209        let is_selected = self
1210            .selection
1211            .as_ref()
1212            .is_some_and(|selected_path| *selected_path == variable.path);
1213
1214        let bg_hover_color = if !is_selected {
1215            colors.hover
1216        } else {
1217            colors.default
1218        };
1219        let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
1220            colors.marked_active
1221        } else {
1222            colors.default
1223        };
1224        let path = variable.path.clone();
1225        div()
1226            .id(variable.item_id())
1227            .group("variable_list_entry")
1228            .pl_2()
1229            .border_1()
1230            .border_r_2()
1231            .border_color(border_color)
1232            .h_4()
1233            .size_full()
1234            .hover(|style| style.bg(bg_hover_color))
1235            .on_click(cx.listener({
1236                let path = path.clone();
1237                move |this, _, _window, cx| {
1238                    this.selection = Some(path.clone());
1239                    cx.notify();
1240                }
1241            }))
1242            .child(
1243                ListItem::new(SharedString::from(format!(
1244                    "variable-item-{}-{}",
1245                    dap.name, state.depth
1246                )))
1247                .disabled(self.disabled)
1248                .selectable(false)
1249                .indent_level(state.depth)
1250                .indent_step_size(px(10.))
1251                .always_show_disclosure_icon(true)
1252                .when(var_ref > 0, |list_item| {
1253                    list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1254                        let var_path = variable.path.clone();
1255                        move |this, _, _, cx| {
1256                            this.session.update(cx, |session, cx| {
1257                                session.variables(var_ref, cx);
1258                            });
1259
1260                            this.toggle_entry(&var_path, cx);
1261                        }
1262                    }))
1263                })
1264                .on_secondary_mouse_down(cx.listener({
1265                    let path = path.clone();
1266                    let entry = variable.clone();
1267                    move |this, event: &MouseDownEvent, window, cx| {
1268                        this.selection = Some(path.clone());
1269                        this.deploy_list_entry_context_menu(
1270                            entry.clone(),
1271                            event.position,
1272                            window,
1273                            cx,
1274                        );
1275                        cx.stop_propagation();
1276                    }
1277                }))
1278                .child(
1279                    h_flex()
1280                        .gap_1()
1281                        .text_ui_sm(cx)
1282                        .w_full()
1283                        .child(
1284                            Label::new(&dap.name).when_some(variable_color.name, |this, color| {
1285                                this.color(Color::from(color))
1286                            }),
1287                        )
1288                        .child(self.render_variable_value(
1289                            &variable,
1290                            &variable_color,
1291                            dap.value.clone(),
1292                            cx,
1293                        )),
1294                ),
1295            )
1296            .into_any()
1297    }
1298
1299    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
1300        div()
1301            .occlude()
1302            .id("variable-list-vertical-scrollbar")
1303            .on_mouse_move(cx.listener(|_, _, _, cx| {
1304                cx.notify();
1305                cx.stop_propagation()
1306            }))
1307            .on_hover(|_, _, cx| {
1308                cx.stop_propagation();
1309            })
1310            .on_any_mouse_down(|_, _, cx| {
1311                cx.stop_propagation();
1312            })
1313            .on_mouse_up(
1314                MouseButton::Left,
1315                cx.listener(|_, _, _, cx| {
1316                    cx.stop_propagation();
1317                }),
1318            )
1319            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
1320                cx.notify();
1321            }))
1322            .h_full()
1323            .absolute()
1324            .right_1()
1325            .top_1()
1326            .bottom_0()
1327            .w(px(12.))
1328            .cursor_default()
1329            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
1330    }
1331}
1332
1333impl Focusable for VariableList {
1334    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
1335        self.focus_handle.clone()
1336    }
1337}
1338
1339impl Render for VariableList {
1340    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1341        v_flex()
1342            .track_focus(&self.focus_handle)
1343            .key_context("VariableList")
1344            .id("variable-list")
1345            .group("variable-list")
1346            .overflow_y_scroll()
1347            .size_full()
1348            .on_action(cx.listener(Self::select_first))
1349            .on_action(cx.listener(Self::select_last))
1350            .on_action(cx.listener(Self::select_prev))
1351            .on_action(cx.listener(Self::select_next))
1352            .on_action(cx.listener(Self::cancel))
1353            .on_action(cx.listener(Self::confirm))
1354            .on_action(cx.listener(Self::expand_selected_entry))
1355            .on_action(cx.listener(Self::collapse_selected_entry))
1356            .on_action(cx.listener(Self::copy_variable_name))
1357            .on_action(cx.listener(Self::copy_variable_value))
1358            .on_action(cx.listener(Self::edit_variable))
1359            .on_action(cx.listener(Self::add_watcher))
1360            .on_action(cx.listener(Self::remove_watcher))
1361            .child(
1362                uniform_list(
1363                    "variable-list",
1364                    self.entries.len(),
1365                    cx.processor(move |this, range: Range<usize>, window, cx| {
1366                        this.render_entries(range, window, cx)
1367                    }),
1368                )
1369                .track_scroll(self.list_handle.clone())
1370                .gap_1_5()
1371                .size_full()
1372                .flex_grow(),
1373            )
1374            .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
1375                deferred(
1376                    anchored()
1377                        .position(*position)
1378                        .anchor(gpui::Corner::TopLeft)
1379                        .child(menu.clone()),
1380                )
1381                .with_priority(1)
1382            }))
1383            .child(self.render_vertical_scrollbar(cx))
1384    }
1385}
1386
1387struct EntryColors {
1388    default: Hsla,
1389    hover: Hsla,
1390    marked_active: Hsla,
1391}
1392
1393fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
1394    let colors = cx.theme().colors();
1395
1396    EntryColors {
1397        default: colors.panel_background,
1398        hover: colors.ghost_element_hover,
1399        marked_active: colors.ghost_element_selected,
1400    }
1401}
1402
1403#[cfg(test)]
1404mod tests {
1405    use super::*;
1406
1407    #[test]
1408    fn test_center_truncate_string() {
1409        // Test string shorter than limit - should not be truncated
1410        assert_eq!(VariableList::center_truncate_string("short", 10), "short");
1411
1412        // Test exact length - should not be truncated
1413        assert_eq!(
1414            VariableList::center_truncate_string("exactly_10", 10),
1415            "exactly_10"
1416        );
1417
1418        // Test simple truncation
1419        assert_eq!(
1420            VariableList::center_truncate_string("value->value2->value3->value4", 20),
1421            "value->v...3->value4"
1422        );
1423
1424        // Test with very long expression
1425        assert_eq!(
1426            VariableList::center_truncate_string(
1427                "object->property1->property2->property3->property4->property5",
1428                30
1429            ),
1430            "object->prope...ty4->property5"
1431        );
1432
1433        // Test edge case with limit equal to ellipsis length
1434        assert_eq!(VariableList::center_truncate_string("anything", 3), "any");
1435
1436        // Test edge case with limit less than ellipsis length
1437        assert_eq!(VariableList::center_truncate_string("anything", 2), "any");
1438
1439        // Test with UTF-8 characters
1440        assert_eq!(
1441            VariableList::center_truncate_string("café->résumé->naïve->voilà", 15),
1442            "café->...>voilà"
1443        );
1444
1445        // Test with emoji (multi-byte UTF-8)
1446        assert_eq!(
1447            VariableList::center_truncate_string("😀->happy->face->😎->cool", 15),
1448            "😀->hap...->cool"
1449        );
1450    }
1451}