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