variable_list.rs

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