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).iter().cloned().collect()
 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).len() > 0
 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.clone()),
 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.is_some()
 674            } else {
 675                true
 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(can_toggle_data_breakpoint, |menu| {
 690                                menu.action(
 691                                    "Toggle Data Breakpoint",
 692                                    crate::ToggleDataBreakpoint.boxed_clone(),
 693                                )
 694                            })
 695                    })
 696                    .when(entry.as_watcher().is_some(), |menu| {
 697                        menu.action("Copy Name", CopyVariableName.boxed_clone())
 698                            .action("Copy Value", CopyVariableValue.boxed_clone())
 699                            .when(supports_set_variable, |menu| {
 700                                menu.action("Edit Value", EditVariable.boxed_clone())
 701                            })
 702                            .action("Remove Watch", RemoveWatch.boxed_clone())
 703                    })
 704                    .context(focus_handle.clone())
 705                });
 706
 707                _ = this.update(cx, |this, cx| {
 708                    cx.focus_view(&context_menu, window);
 709                    let subscription = cx.subscribe_in(
 710                        &context_menu,
 711                        window,
 712                        |this, _, _: &DismissEvent, window, cx| {
 713                            if this.open_context_menu.as_ref().is_some_and(|context_menu| {
 714                                context_menu.0.focus_handle(cx).contains_focused(window, cx)
 715                            }) {
 716                                cx.focus_self(window);
 717                            }
 718                            this.open_context_menu.take();
 719                            cx.notify();
 720                        },
 721                    );
 722
 723                    this.open_context_menu = Some((context_menu, position, subscription));
 724                });
 725            })
 726        })
 727        .detach();
 728    }
 729
 730    fn toggle_data_breakpoint(
 731        &mut self,
 732        _: &crate::ToggleDataBreakpoint,
 733        _window: &mut Window,
 734        cx: &mut Context<Self>,
 735    ) {
 736        let Some(entry) = self
 737            .selection
 738            .as_ref()
 739            .and_then(|selection| self.entries.iter().find(|entry| &entry.path == selection))
 740        else {
 741            return;
 742        };
 743
 744        let Some((name, var_ref)) = entry.as_variable().map(|var| &var.name).zip(
 745            self.entry_states
 746                .get(&entry.path)
 747                .map(|state| state.parent_reference),
 748        ) else {
 749            return;
 750        };
 751
 752        let context = Arc::new(DataBreakpointContext::Variable {
 753            variables_reference: var_ref,
 754            name: name.clone(),
 755            bytes: None,
 756        });
 757        let data_breakpoint = self.session.update(cx, |session, cx| {
 758            session.data_breakpoint_info(context.clone(), None, cx)
 759        });
 760
 761        let session = self.session.downgrade();
 762        cx.spawn(async move |_, cx| {
 763            let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else {
 764                return;
 765            };
 766            _ = session.update(cx, |session, cx| {
 767                session.create_data_breakpoint(
 768                    context,
 769                    data_id.clone(),
 770                    dap::DataBreakpoint {
 771                        data_id,
 772                        access_type: None,
 773                        condition: None,
 774                        hit_condition: None,
 775                    },
 776                    cx,
 777                );
 778                cx.notify();
 779            });
 780        })
 781        .detach();
 782    }
 783
 784    fn copy_variable_name(
 785        &mut self,
 786        _: &CopyVariableName,
 787        _window: &mut Window,
 788        cx: &mut Context<Self>,
 789    ) {
 790        let Some(selection) = self.selection.as_ref() else {
 791            return;
 792        };
 793
 794        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 795            return;
 796        };
 797
 798        let variable_name = match &entry.entry {
 799            DapEntry::Variable(dap) => dap.name.clone(),
 800            DapEntry::Watcher(watcher) => watcher.expression.to_string(),
 801            DapEntry::Scope(_) => return,
 802        };
 803
 804        cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
 805    }
 806
 807    fn copy_variable_value(
 808        &mut self,
 809        _: &CopyVariableValue,
 810        _window: &mut Window,
 811        cx: &mut Context<Self>,
 812    ) {
 813        let Some(selection) = self.selection.as_ref() else {
 814            return;
 815        };
 816
 817        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 818            return;
 819        };
 820
 821        let variable_value = match &entry.entry {
 822            DapEntry::Variable(dap) => dap.value.clone(),
 823            DapEntry::Watcher(watcher) => watcher.value.to_string(),
 824            DapEntry::Scope(_) => return,
 825        };
 826
 827        cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
 828    }
 829
 830    fn edit_variable(&mut self, _: &EditVariable, window: &mut Window, cx: &mut Context<Self>) {
 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_value = match &entry.entry {
 840            DapEntry::Watcher(watcher) => watcher.value.to_string(),
 841            DapEntry::Variable(variable) => variable.value.clone(),
 842            DapEntry::Scope(_) => return,
 843        };
 844
 845        let editor = Self::create_variable_editor(&variable_value, window, cx);
 846        self.edited_path = Some((entry.path.clone(), editor));
 847
 848        cx.notify();
 849    }
 850
 851    fn add_watcher(&mut self, _: &AddWatch, _: &mut Window, cx: &mut Context<Self>) {
 852        let Some(selection) = self.selection.as_ref() else {
 853            return;
 854        };
 855
 856        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 857            return;
 858        };
 859
 860        let Some(variable) = entry.as_variable() else {
 861            return;
 862        };
 863
 864        let Some(stack_frame_id) = self.selected_stack_frame_id else {
 865            return;
 866        };
 867
 868        let add_watcher_task = self.session.update(cx, |session, cx| {
 869            let expression = variable
 870                .evaluate_name
 871                .clone()
 872                .unwrap_or_else(|| variable.name.clone());
 873
 874            session.add_watcher(expression.into(), stack_frame_id, cx)
 875        });
 876
 877        cx.spawn(async move |this, cx| {
 878            add_watcher_task.await?;
 879
 880            this.update(cx, |this, cx| {
 881                this.build_entries(cx);
 882            })
 883        })
 884        .detach_and_log_err(cx);
 885    }
 886
 887    fn remove_watcher(&mut self, _: &RemoveWatch, _: &mut Window, cx: &mut Context<Self>) {
 888        let Some(selection) = self.selection.as_ref() else {
 889            return;
 890        };
 891
 892        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 893            return;
 894        };
 895
 896        let Some(watcher) = entry.as_watcher() else {
 897            return;
 898        };
 899
 900        self.session.update(cx, |session, _| {
 901            session.remove_watcher(watcher.expression.clone());
 902        });
 903        self.build_entries(cx);
 904    }
 905
 906    #[track_caller]
 907    #[cfg(test)]
 908    pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
 909        const INDENT: &'static str = "    ";
 910
 911        let entries = &self.entries;
 912        let mut visual_entries = Vec::with_capacity(entries.len());
 913        for entry in entries {
 914            let state = self
 915                .entry_states
 916                .get(&entry.path)
 917                .expect("If there's a variable entry there has to be a state that goes with it");
 918
 919            visual_entries.push(format!(
 920                "{}{} {}{}",
 921                INDENT.repeat(state.depth - 1),
 922                if state.is_expanded { "v" } else { ">" },
 923                entry.entry.name(),
 924                if self.selection.as_ref() == Some(&entry.path) {
 925                    " <=== selected"
 926                } else {
 927                    ""
 928                }
 929            ));
 930        }
 931
 932        pretty_assertions::assert_eq!(expected, visual_entries);
 933    }
 934
 935    #[track_caller]
 936    #[cfg(test)]
 937    pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
 938        self.entries
 939            .iter()
 940            .filter_map(|entry| match &entry.entry {
 941                DapEntry::Scope(scope) => Some(scope),
 942                _ => None,
 943            })
 944            .cloned()
 945            .collect()
 946    }
 947
 948    #[track_caller]
 949    #[cfg(test)]
 950    pub(crate) fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
 951        let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
 952        let mut idx = 0;
 953
 954        for entry in self.entries.iter() {
 955            match &entry.entry {
 956                DapEntry::Watcher { .. } => continue,
 957                DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
 958                DapEntry::Scope(scope) => {
 959                    if scopes.len() > 0 {
 960                        idx += 1;
 961                    }
 962
 963                    scopes.push((scope.clone(), Vec::new()));
 964                }
 965            }
 966        }
 967
 968        scopes
 969    }
 970
 971    #[track_caller]
 972    #[cfg(test)]
 973    pub(crate) fn variables(&self) -> Vec<dap::Variable> {
 974        self.entries
 975            .iter()
 976            .filter_map(|entry| match &entry.entry {
 977                DapEntry::Variable(variable) => Some(variable),
 978                _ => None,
 979            })
 980            .cloned()
 981            .collect()
 982    }
 983
 984    fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> {
 985        let editor = cx.new(|cx| {
 986            let mut editor = Editor::single_line(window, cx);
 987
 988            let refinement = TextStyleRefinement {
 989                font_size: Some(
 990                    TextSize::XSmall
 991                        .rems(cx)
 992                        .to_pixels(window.rem_size())
 993                        .into(),
 994                ),
 995                ..Default::default()
 996            };
 997            editor.set_text_style_refinement(refinement);
 998            editor.set_text(default, window, cx);
 999            editor.select_all(&editor::actions::SelectAll, window, cx);
1000            editor
1001        });
1002        editor.focus_handle(cx).focus(window);
1003        editor
1004    }
1005
1006    fn variable_color(
1007        &self,
1008        presentation_hint: Option<&VariablePresentationHint>,
1009        cx: &Context<Self>,
1010    ) -> VariableColor {
1011        let syntax_color_for = |name| cx.theme().syntax().get(name).color;
1012        let name = if self.disabled {
1013            Some(Color::Disabled.color(cx))
1014        } else {
1015            match presentation_hint
1016                .as_ref()
1017                .and_then(|hint| hint.kind.as_ref())
1018                .unwrap_or(&VariablePresentationHintKind::Unknown)
1019            {
1020                VariablePresentationHintKind::Class
1021                | VariablePresentationHintKind::BaseClass
1022                | VariablePresentationHintKind::InnerClass
1023                | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
1024                VariablePresentationHintKind::Data => syntax_color_for("variable"),
1025                VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
1026            }
1027        };
1028        let value = self
1029            .disabled
1030            .then(|| Color::Disabled.color(cx))
1031            .or_else(|| syntax_color_for("variable.special"));
1032
1033        VariableColor { name, value }
1034    }
1035
1036    fn render_variable_value(
1037        &self,
1038        entry: &ListEntry,
1039        variable_color: &VariableColor,
1040        value: String,
1041        cx: &mut Context<Self>,
1042    ) -> AnyElement {
1043        if !value.is_empty() {
1044            div()
1045                .w_full()
1046                .id(entry.item_value_id())
1047                .map(|this| {
1048                    if let Some((_, editor)) = self
1049                        .edited_path
1050                        .as_ref()
1051                        .filter(|(path, _)| path == &entry.path)
1052                    {
1053                        this.child(div().size_full().px_2().child(editor.clone()))
1054                    } else {
1055                        this.text_color(cx.theme().colors().text_muted)
1056                            .when(
1057                                !self.disabled
1058                                    && self
1059                                        .session
1060                                        .read(cx)
1061                                        .capabilities()
1062                                        .supports_set_variable
1063                                        .unwrap_or_default(),
1064                                |this| {
1065                                    let path = entry.path.clone();
1066                                    let variable_value = value.clone();
1067                                    this.on_click(cx.listener(
1068                                        move |this, click: &ClickEvent, window, cx| {
1069                                            if click.down.click_count < 2 {
1070                                                return;
1071                                            }
1072                                            let editor = Self::create_variable_editor(
1073                                                &variable_value,
1074                                                window,
1075                                                cx,
1076                                            );
1077                                            this.edited_path = Some((path.clone(), editor));
1078
1079                                            cx.notify();
1080                                        },
1081                                    ))
1082                                },
1083                            )
1084                            .child(
1085                                Label::new(format!("=  {}", &value))
1086                                    .single_line()
1087                                    .truncate()
1088                                    .size(LabelSize::Small)
1089                                    .color(Color::Muted)
1090                                    .when_some(variable_color.value, |this, color| {
1091                                        this.color(Color::from(color))
1092                                    }),
1093                            )
1094                    }
1095                })
1096                .into_any_element()
1097        } else {
1098            Empty.into_any_element()
1099        }
1100    }
1101
1102    fn center_truncate_string(s: &str, mut max_chars: usize) -> String {
1103        const ELLIPSIS: &str = "...";
1104        const MIN_LENGTH: usize = 3;
1105
1106        max_chars = max_chars.max(MIN_LENGTH);
1107
1108        let char_count = s.chars().count();
1109        if char_count <= max_chars {
1110            return s.to_string();
1111        }
1112
1113        if ELLIPSIS.len() + MIN_LENGTH > max_chars {
1114            return s.chars().take(MIN_LENGTH).collect();
1115        }
1116
1117        let available_chars = max_chars - ELLIPSIS.len();
1118
1119        let start_chars = available_chars / 2;
1120        let end_chars = available_chars - start_chars;
1121        let skip_chars = char_count - end_chars;
1122
1123        let mut start_boundary = 0;
1124        let mut end_boundary = s.len();
1125
1126        for (i, (byte_idx, _)) in s.char_indices().enumerate() {
1127            if i == start_chars {
1128                start_boundary = byte_idx.max(MIN_LENGTH);
1129            }
1130
1131            if i == skip_chars {
1132                end_boundary = byte_idx;
1133            }
1134        }
1135
1136        if start_boundary >= end_boundary {
1137            return s.chars().take(MIN_LENGTH).collect();
1138        }
1139
1140        format!("{}{}{}", &s[..start_boundary], ELLIPSIS, &s[end_boundary..])
1141    }
1142
1143    fn render_watcher(
1144        &self,
1145        entry: &ListEntry,
1146        state: EntryState,
1147        _window: &mut Window,
1148        cx: &mut Context<Self>,
1149    ) -> AnyElement {
1150        let Some(watcher) = &entry.as_watcher() else {
1151            debug_panic!("Called render watcher on non watcher variable list entry variant");
1152            return div().into_any_element();
1153        };
1154
1155        let variable_color = self.variable_color(watcher.presentation_hint.as_ref(), cx);
1156
1157        let is_selected = self
1158            .selection
1159            .as_ref()
1160            .is_some_and(|selection| selection == &entry.path);
1161        let var_ref = watcher.variables_reference;
1162
1163        let colors = get_entry_color(cx);
1164        let bg_hover_color = if !is_selected {
1165            colors.hover
1166        } else {
1167            colors.default
1168        };
1169        let border_color = if is_selected {
1170            colors.marked_active
1171        } else {
1172            colors.default
1173        };
1174        let path = entry.path.clone();
1175
1176        let weak = cx.weak_entity();
1177        let focus_handle = self.focus_handle.clone();
1178        let watcher_len = (self.list_handle.content_size().width.0 / 12.0).floor() - 3.0;
1179        let watcher_len = watcher_len as usize;
1180
1181        div()
1182            .id(entry.item_id())
1183            .group("variable_list_entry")
1184            .pl_2()
1185            .border_1()
1186            .border_r_2()
1187            .border_color(border_color)
1188            .flex()
1189            .w_full()
1190            .h_full()
1191            .hover(|style| style.bg(bg_hover_color))
1192            .on_click(cx.listener({
1193                let path = path.clone();
1194                move |this, _, _window, cx| {
1195                    this.selection = Some(path.clone());
1196                    cx.notify();
1197                }
1198            }))
1199            .child(
1200                ListItem::new(SharedString::from(format!(
1201                    "watcher-{}",
1202                    watcher.expression
1203                )))
1204                .selectable(false)
1205                .disabled(self.disabled)
1206                .selectable(false)
1207                .indent_level(state.depth)
1208                .indent_step_size(px(10.))
1209                .always_show_disclosure_icon(true)
1210                .when(var_ref > 0, |list_item| {
1211                    list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1212                        let var_path = entry.path.clone();
1213                        move |this, _, _, cx| {
1214                            this.session.update(cx, |session, cx| {
1215                                session.variables(var_ref, cx);
1216                            });
1217
1218                            this.toggle_entry(&var_path, cx);
1219                        }
1220                    }))
1221                })
1222                .on_secondary_mouse_down(cx.listener({
1223                    let path = path.clone();
1224                    let entry = entry.clone();
1225                    move |this, event: &MouseDownEvent, window, cx| {
1226                        this.selection = Some(path.clone());
1227                        this.deploy_list_entry_context_menu(
1228                            entry.clone(),
1229                            event.position,
1230                            window,
1231                            cx,
1232                        );
1233                        cx.stop_propagation();
1234                    }
1235                }))
1236                .child(
1237                    h_flex()
1238                        .gap_1()
1239                        .text_ui_sm(cx)
1240                        .w_full()
1241                        .child(
1242                            Label::new(&Self::center_truncate_string(
1243                                watcher.expression.as_ref(),
1244                                watcher_len,
1245                            ))
1246                            .when_some(variable_color.name, |this, color| {
1247                                this.color(Color::from(color))
1248                            }),
1249                        )
1250                        .child(self.render_variable_value(
1251                            &entry,
1252                            &variable_color,
1253                            watcher.value.to_string(),
1254                            cx,
1255                        )),
1256                )
1257                .end_slot(
1258                    IconButton::new(
1259                        SharedString::from(format!("watcher-{}-remove-button", watcher.expression)),
1260                        IconName::Close,
1261                    )
1262                    .on_click({
1263                        let weak = weak.clone();
1264                        let path = path.clone();
1265                        move |_, window, cx| {
1266                            weak.update(cx, |variable_list, cx| {
1267                                variable_list.selection = Some(path.clone());
1268                                variable_list.remove_watcher(&RemoveWatch, window, cx);
1269                            })
1270                            .ok();
1271                        }
1272                    })
1273                    .tooltip(move |window, cx| {
1274                        Tooltip::for_action_in(
1275                            "Remove Watch",
1276                            &RemoveWatch,
1277                            &focus_handle,
1278                            window,
1279                            cx,
1280                        )
1281                    })
1282                    .icon_size(ui::IconSize::Indicator),
1283                ),
1284            )
1285            .into_any()
1286    }
1287
1288    fn render_scope(
1289        &self,
1290        entry: &ListEntry,
1291        state: EntryState,
1292        cx: &mut Context<Self>,
1293    ) -> AnyElement {
1294        let Some(scope) = entry.as_scope() else {
1295            debug_panic!("Called render scope on non scope variable list entry variant");
1296            return div().into_any_element();
1297        };
1298
1299        let var_ref = scope.variables_reference;
1300        let is_selected = self
1301            .selection
1302            .as_ref()
1303            .is_some_and(|selection| selection == &entry.path);
1304
1305        let colors = get_entry_color(cx);
1306        let bg_hover_color = if !is_selected {
1307            colors.hover
1308        } else {
1309            colors.default
1310        };
1311        let border_color = if is_selected {
1312            colors.marked_active
1313        } else {
1314            colors.default
1315        };
1316        let path = entry.path.clone();
1317
1318        div()
1319            .id(var_ref as usize)
1320            .group("variable_list_entry")
1321            .pl_2()
1322            .border_1()
1323            .border_r_2()
1324            .border_color(border_color)
1325            .flex()
1326            .w_full()
1327            .h_full()
1328            .hover(|style| style.bg(bg_hover_color))
1329            .on_click(cx.listener({
1330                move |this, _, _window, cx| {
1331                    this.selection = Some(path.clone());
1332                    cx.notify();
1333                }
1334            }))
1335            .child(
1336                ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
1337                    .selectable(false)
1338                    .disabled(self.disabled)
1339                    .indent_level(state.depth)
1340                    .indent_step_size(px(10.))
1341                    .always_show_disclosure_icon(true)
1342                    .toggle(state.is_expanded)
1343                    .on_toggle({
1344                        let var_path = entry.path.clone();
1345                        cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
1346                    })
1347                    .child(
1348                        div()
1349                            .text_ui(cx)
1350                            .w_full()
1351                            .when(self.disabled, |this| {
1352                                this.text_color(Color::Disabled.color(cx))
1353                            })
1354                            .child(scope.name.clone()),
1355                    ),
1356            )
1357            .into_any()
1358    }
1359
1360    fn render_variable(
1361        &self,
1362        variable: &ListEntry,
1363        state: EntryState,
1364        window: &mut Window,
1365        cx: &mut Context<Self>,
1366    ) -> AnyElement {
1367        let Some(dap) = &variable.as_variable() else {
1368            debug_panic!("Called render variable on non variable variable list entry variant");
1369            return div().into_any_element();
1370        };
1371
1372        let variable_color = self.variable_color(dap.presentation_hint.as_ref(), cx);
1373
1374        let var_ref = dap.variables_reference;
1375        let colors = get_entry_color(cx);
1376        let is_selected = self
1377            .selection
1378            .as_ref()
1379            .is_some_and(|selected_path| *selected_path == variable.path);
1380
1381        let bg_hover_color = if !is_selected {
1382            colors.hover
1383        } else {
1384            colors.default
1385        };
1386        let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
1387            colors.marked_active
1388        } else {
1389            colors.default
1390        };
1391        let path = variable.path.clone();
1392        div()
1393            .id(variable.item_id())
1394            .group("variable_list_entry")
1395            .pl_2()
1396            .border_1()
1397            .border_r_2()
1398            .border_color(border_color)
1399            .h_4()
1400            .size_full()
1401            .hover(|style| style.bg(bg_hover_color))
1402            .on_click(cx.listener({
1403                let path = path.clone();
1404                move |this, _, _window, cx| {
1405                    this.selection = Some(path.clone());
1406                    cx.notify();
1407                }
1408            }))
1409            .child(
1410                ListItem::new(SharedString::from(format!(
1411                    "variable-item-{}-{}",
1412                    dap.name, state.depth
1413                )))
1414                .disabled(self.disabled)
1415                .selectable(false)
1416                .indent_level(state.depth)
1417                .indent_step_size(px(10.))
1418                .always_show_disclosure_icon(true)
1419                .when(var_ref > 0, |list_item| {
1420                    list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1421                        let var_path = variable.path.clone();
1422                        move |this, _, _, cx| {
1423                            this.session.update(cx, |session, cx| {
1424                                session.variables(var_ref, cx);
1425                            });
1426
1427                            this.toggle_entry(&var_path, cx);
1428                        }
1429                    }))
1430                })
1431                .on_secondary_mouse_down(cx.listener({
1432                    let path = path.clone();
1433                    let entry = variable.clone();
1434                    move |this, event: &MouseDownEvent, window, cx| {
1435                        this.selection = Some(path.clone());
1436                        this.deploy_list_entry_context_menu(
1437                            entry.clone(),
1438                            event.position,
1439                            window,
1440                            cx,
1441                        );
1442                        cx.stop_propagation();
1443                    }
1444                }))
1445                .child(
1446                    h_flex()
1447                        .gap_1()
1448                        .text_ui_sm(cx)
1449                        .w_full()
1450                        .child(
1451                            Label::new(&dap.name).when_some(variable_color.name, |this, color| {
1452                                this.color(Color::from(color))
1453                            }),
1454                        )
1455                        .child(self.render_variable_value(
1456                            &variable,
1457                            &variable_color,
1458                            dap.value.clone(),
1459                            cx,
1460                        )),
1461                ),
1462            )
1463            .into_any()
1464    }
1465
1466    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
1467        div()
1468            .occlude()
1469            .id("variable-list-vertical-scrollbar")
1470            .on_mouse_move(cx.listener(|_, _, _, cx| {
1471                cx.notify();
1472                cx.stop_propagation()
1473            }))
1474            .on_hover(|_, _, cx| {
1475                cx.stop_propagation();
1476            })
1477            .on_any_mouse_down(|_, _, cx| {
1478                cx.stop_propagation();
1479            })
1480            .on_mouse_up(
1481                MouseButton::Left,
1482                cx.listener(|_, _, _, cx| {
1483                    cx.stop_propagation();
1484                }),
1485            )
1486            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
1487                cx.notify();
1488            }))
1489            .h_full()
1490            .absolute()
1491            .right_1()
1492            .top_1()
1493            .bottom_0()
1494            .w(px(12.))
1495            .cursor_default()
1496            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
1497    }
1498}
1499
1500impl Focusable for VariableList {
1501    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
1502        self.focus_handle.clone()
1503    }
1504}
1505
1506impl Render for VariableList {
1507    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1508        v_flex()
1509            .track_focus(&self.focus_handle)
1510            .key_context("VariableList")
1511            .id("variable-list")
1512            .group("variable-list")
1513            .overflow_y_scroll()
1514            .size_full()
1515            .on_action(cx.listener(Self::select_first))
1516            .on_action(cx.listener(Self::select_last))
1517            .on_action(cx.listener(Self::select_prev))
1518            .on_action(cx.listener(Self::select_next))
1519            .on_action(cx.listener(Self::cancel))
1520            .on_action(cx.listener(Self::confirm))
1521            .on_action(cx.listener(Self::expand_selected_entry))
1522            .on_action(cx.listener(Self::collapse_selected_entry))
1523            .on_action(cx.listener(Self::copy_variable_name))
1524            .on_action(cx.listener(Self::copy_variable_value))
1525            .on_action(cx.listener(Self::edit_variable))
1526            .on_action(cx.listener(Self::add_watcher))
1527            .on_action(cx.listener(Self::remove_watcher))
1528            .on_action(cx.listener(Self::toggle_data_breakpoint))
1529            .on_action(cx.listener(Self::jump_to_variable_memory))
1530            .child(
1531                uniform_list(
1532                    "variable-list",
1533                    self.entries.len(),
1534                    cx.processor(move |this, range: Range<usize>, window, cx| {
1535                        this.render_entries(range, window, cx)
1536                    }),
1537                )
1538                .track_scroll(self.list_handle.clone())
1539                .gap_1_5()
1540                .size_full()
1541                .flex_grow(),
1542            )
1543            .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
1544                deferred(
1545                    anchored()
1546                        .position(*position)
1547                        .anchor(gpui::Corner::TopLeft)
1548                        .child(menu.clone()),
1549                )
1550                .with_priority(1)
1551            }))
1552            .child(self.render_vertical_scrollbar(cx))
1553    }
1554}
1555
1556struct EntryColors {
1557    default: Hsla,
1558    hover: Hsla,
1559    marked_active: Hsla,
1560}
1561
1562fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
1563    let colors = cx.theme().colors();
1564
1565    EntryColors {
1566        default: colors.panel_background,
1567        hover: colors.ghost_element_hover,
1568        marked_active: colors.ghost_element_selected,
1569    }
1570}
1571
1572#[cfg(test)]
1573mod tests {
1574    use super::*;
1575
1576    #[test]
1577    fn test_center_truncate_string() {
1578        // Test string shorter than limit - should not be truncated
1579        assert_eq!(VariableList::center_truncate_string("short", 10), "short");
1580
1581        // Test exact length - should not be truncated
1582        assert_eq!(
1583            VariableList::center_truncate_string("exactly_10", 10),
1584            "exactly_10"
1585        );
1586
1587        // Test simple truncation
1588        assert_eq!(
1589            VariableList::center_truncate_string("value->value2->value3->value4", 20),
1590            "value->v...3->value4"
1591        );
1592
1593        // Test with very long expression
1594        assert_eq!(
1595            VariableList::center_truncate_string(
1596                "object->property1->property2->property3->property4->property5",
1597                30
1598            ),
1599            "object->prope...ty4->property5"
1600        );
1601
1602        // Test edge case with limit equal to ellipsis length
1603        assert_eq!(VariableList::center_truncate_string("anything", 3), "any");
1604
1605        // Test edge case with limit less than ellipsis length
1606        assert_eq!(VariableList::center_truncate_string("anything", 2), "any");
1607
1608        // Test with UTF-8 characters
1609        assert_eq!(
1610            VariableList::center_truncate_string("café->résumé->naïve->voilà", 15),
1611            "café->...>voilà"
1612        );
1613
1614        // Test with emoji (multi-byte UTF-8)
1615        assert_eq!(
1616            VariableList::center_truncate_string("😀->happy->face->😎->cool", 15),
1617            "😀->hap...->cool"
1618        );
1619    }
1620}