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                            .tooltip(Tooltip::text(value))
1133                    }
1134                })
1135                .into_any_element()
1136        } else {
1137            Empty.into_any_element()
1138        }
1139    }
1140
1141    fn center_truncate_string(s: &str, mut max_chars: usize) -> String {
1142        const ELLIPSIS: &str = "...";
1143        const MIN_LENGTH: usize = 3;
1144
1145        max_chars = max_chars.max(MIN_LENGTH);
1146
1147        let char_count = s.chars().count();
1148        if char_count <= max_chars {
1149            return s.to_string();
1150        }
1151
1152        if ELLIPSIS.len() + MIN_LENGTH > max_chars {
1153            return s.chars().take(MIN_LENGTH).collect();
1154        }
1155
1156        let available_chars = max_chars - ELLIPSIS.len();
1157
1158        let start_chars = available_chars / 2;
1159        let end_chars = available_chars - start_chars;
1160        let skip_chars = char_count - end_chars;
1161
1162        let mut start_boundary = 0;
1163        let mut end_boundary = s.len();
1164
1165        for (i, (byte_idx, _)) in s.char_indices().enumerate() {
1166            if i == start_chars {
1167                start_boundary = byte_idx.max(MIN_LENGTH);
1168            }
1169
1170            if i == skip_chars {
1171                end_boundary = byte_idx;
1172            }
1173        }
1174
1175        if start_boundary >= end_boundary {
1176            return s.chars().take(MIN_LENGTH).collect();
1177        }
1178
1179        format!("{}{}{}", &s[..start_boundary], ELLIPSIS, &s[end_boundary..])
1180    }
1181
1182    fn render_watcher(
1183        &self,
1184        entry: &ListEntry,
1185        state: EntryState,
1186        _window: &mut Window,
1187        cx: &mut Context<Self>,
1188    ) -> AnyElement {
1189        let Some(watcher) = &entry.as_watcher() else {
1190            debug_panic!("Called render watcher on non watcher variable list entry variant");
1191            return div().into_any_element();
1192        };
1193
1194        let variable_color = self.variable_color(watcher.presentation_hint.as_ref(), cx);
1195
1196        let is_selected = self
1197            .selection
1198            .as_ref()
1199            .is_some_and(|selection| selection == &entry.path);
1200        let var_ref = watcher.variables_reference;
1201
1202        let colors = get_entry_color(cx);
1203        let bg_hover_color = if !is_selected {
1204            colors.hover
1205        } else {
1206            colors.default
1207        };
1208        let border_color = if is_selected {
1209            colors.marked_active
1210        } else {
1211            colors.default
1212        };
1213        let path = entry.path.clone();
1214
1215        let weak = cx.weak_entity();
1216        let focus_handle = self.focus_handle.clone();
1217        let watcher_len = (f32::from(self.list_handle.content_size().width / 12.0).floor()) - 3.0;
1218        let watcher_len = watcher_len as usize;
1219
1220        div()
1221            .id(entry.item_id())
1222            .group("variable_list_entry")
1223            .pl_2()
1224            .border_1()
1225            .border_r_2()
1226            .border_color(border_color)
1227            .flex()
1228            .w_full()
1229            .h_full()
1230            .hover(|style| style.bg(bg_hover_color))
1231            .on_click(cx.listener({
1232                let path = path.clone();
1233                move |this, _, _window, cx| {
1234                    this.selection = Some(path.clone());
1235                    cx.notify();
1236                }
1237            }))
1238            .child(
1239                ListItem::new(SharedString::from(format!(
1240                    "watcher-{}",
1241                    watcher.expression
1242                )))
1243                .selectable(false)
1244                .disabled(self.disabled)
1245                .selectable(false)
1246                .indent_level(state.depth)
1247                .indent_step_size(px(10.))
1248                .always_show_disclosure_icon(true)
1249                .when(var_ref > 0, |list_item| {
1250                    list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1251                        let var_path = entry.path.clone();
1252                        move |this, _, _, cx| {
1253                            this.session.update(cx, |session, cx| {
1254                                session.variables(var_ref, cx);
1255                            });
1256
1257                            this.toggle_entry(&var_path, cx);
1258                        }
1259                    }))
1260                })
1261                .on_secondary_mouse_down(cx.listener({
1262                    let path = path.clone();
1263                    let entry = entry.clone();
1264                    move |this, event: &MouseDownEvent, window, cx| {
1265                        this.selection = Some(path.clone());
1266                        this.deploy_list_entry_context_menu(
1267                            entry.clone(),
1268                            event.position,
1269                            window,
1270                            cx,
1271                        );
1272                        cx.stop_propagation();
1273                    }
1274                }))
1275                .child(
1276                    h_flex()
1277                        .gap_1()
1278                        .text_ui_sm(cx)
1279                        .w_full()
1280                        .child(
1281                            Label::new(&Self::center_truncate_string(
1282                                watcher.expression.as_ref(),
1283                                watcher_len,
1284                            ))
1285                            .when_some(variable_color.name, |this, color| {
1286                                this.color(Color::from(color))
1287                            }),
1288                        )
1289                        .child(self.render_variable_value(
1290                            entry,
1291                            &variable_color,
1292                            watcher.value.to_string(),
1293                            cx,
1294                        )),
1295                )
1296                .end_slot(
1297                    IconButton::new(
1298                        SharedString::from(format!("watcher-{}-remove-button", watcher.expression)),
1299                        IconName::Close,
1300                    )
1301                    .on_click({
1302                        move |_, window, cx| {
1303                            weak.update(cx, |variable_list, cx| {
1304                                variable_list.selection = Some(path.clone());
1305                                variable_list.remove_watcher(&RemoveWatch, window, cx);
1306                            })
1307                            .ok();
1308                        }
1309                    })
1310                    .tooltip(move |_window, cx| {
1311                        Tooltip::for_action_in("Remove Watch", &RemoveWatch, &focus_handle, cx)
1312                    })
1313                    .icon_size(ui::IconSize::Indicator),
1314                ),
1315            )
1316            .into_any()
1317    }
1318
1319    fn render_scope(
1320        &self,
1321        entry: &ListEntry,
1322        state: EntryState,
1323        cx: &mut Context<Self>,
1324    ) -> AnyElement {
1325        let Some(scope) = entry.as_scope() else {
1326            debug_panic!("Called render scope on non scope variable list entry variant");
1327            return div().into_any_element();
1328        };
1329
1330        let var_ref = scope.variables_reference;
1331        let is_selected = self
1332            .selection
1333            .as_ref()
1334            .is_some_and(|selection| selection == &entry.path);
1335
1336        let colors = get_entry_color(cx);
1337        let bg_hover_color = if !is_selected {
1338            colors.hover
1339        } else {
1340            colors.default
1341        };
1342        let border_color = if is_selected {
1343            colors.marked_active
1344        } else {
1345            colors.default
1346        };
1347        let path = entry.path.clone();
1348
1349        div()
1350            .id(var_ref as usize)
1351            .group("variable_list_entry")
1352            .pl_2()
1353            .border_1()
1354            .border_r_2()
1355            .border_color(border_color)
1356            .flex()
1357            .w_full()
1358            .h_full()
1359            .hover(|style| style.bg(bg_hover_color))
1360            .on_click(cx.listener({
1361                move |this, _, _window, cx| {
1362                    this.selection = Some(path.clone());
1363                    cx.notify();
1364                }
1365            }))
1366            .child(
1367                ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
1368                    .selectable(false)
1369                    .disabled(self.disabled)
1370                    .indent_level(state.depth)
1371                    .indent_step_size(px(10.))
1372                    .always_show_disclosure_icon(true)
1373                    .toggle(state.is_expanded)
1374                    .on_toggle({
1375                        let var_path = entry.path.clone();
1376                        cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
1377                    })
1378                    .child(
1379                        div()
1380                            .text_ui(cx)
1381                            .w_full()
1382                            .when(self.disabled, |this| {
1383                                this.text_color(Color::Disabled.color(cx))
1384                            })
1385                            .child(scope.name.clone()),
1386                    ),
1387            )
1388            .into_any()
1389    }
1390
1391    fn render_variable(
1392        &self,
1393        variable: &ListEntry,
1394        state: EntryState,
1395        window: &mut Window,
1396        cx: &mut Context<Self>,
1397    ) -> AnyElement {
1398        let Some(dap) = &variable.as_variable() else {
1399            debug_panic!("Called render variable on non variable variable list entry variant");
1400            return div().into_any_element();
1401        };
1402
1403        let variable_color = self.variable_color(dap.presentation_hint.as_ref(), cx);
1404
1405        let var_ref = dap.variables_reference;
1406        let colors = get_entry_color(cx);
1407        let is_selected = self
1408            .selection
1409            .as_ref()
1410            .is_some_and(|selected_path| *selected_path == variable.path);
1411
1412        let bg_hover_color = if !is_selected {
1413            colors.hover
1414        } else {
1415            colors.default
1416        };
1417        let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
1418            colors.marked_active
1419        } else {
1420            colors.default
1421        };
1422        let path = variable.path.clone();
1423        div()
1424            .id(variable.item_id())
1425            .group("variable_list_entry")
1426            .pl_2()
1427            .border_1()
1428            .border_r_2()
1429            .border_color(border_color)
1430            .h_4()
1431            .size_full()
1432            .hover(|style| style.bg(bg_hover_color))
1433            .on_click(cx.listener({
1434                let path = path.clone();
1435                move |this, _, _window, cx| {
1436                    this.selection = Some(path.clone());
1437                    cx.notify();
1438                }
1439            }))
1440            .child(
1441                ListItem::new(SharedString::from(format!(
1442                    "variable-item-{}-{}",
1443                    dap.name, state.depth
1444                )))
1445                .disabled(self.disabled)
1446                .selectable(false)
1447                .indent_level(state.depth)
1448                .indent_step_size(px(10.))
1449                .always_show_disclosure_icon(true)
1450                .when(var_ref > 0, |list_item| {
1451                    list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1452                        let var_path = variable.path.clone();
1453                        move |this, _, _, cx| {
1454                            this.session.update(cx, |session, cx| {
1455                                session.variables(var_ref, cx);
1456                            });
1457
1458                            this.toggle_entry(&var_path, cx);
1459                        }
1460                    }))
1461                })
1462                .on_secondary_mouse_down(cx.listener({
1463                    let entry = variable.clone();
1464                    move |this, event: &MouseDownEvent, window, cx| {
1465                        this.selection = Some(path.clone());
1466                        this.deploy_list_entry_context_menu(
1467                            entry.clone(),
1468                            event.position,
1469                            window,
1470                            cx,
1471                        );
1472                        cx.stop_propagation();
1473                    }
1474                }))
1475                .child(
1476                    h_flex()
1477                        .gap_1()
1478                        .text_ui_sm(cx)
1479                        .w_full()
1480                        .child(
1481                            Label::new(&dap.name).when_some(variable_color.name, |this, color| {
1482                                this.color(Color::from(color))
1483                            }),
1484                        )
1485                        .child(self.render_variable_value(
1486                            variable,
1487                            &variable_color,
1488                            dap.value.clone(),
1489                            cx,
1490                        )),
1491                ),
1492            )
1493            .into_any()
1494    }
1495}
1496
1497impl Focusable for VariableList {
1498    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
1499        self.focus_handle.clone()
1500    }
1501}
1502
1503impl Render for VariableList {
1504    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1505        v_flex()
1506            .track_focus(&self.focus_handle)
1507            .key_context("VariableList")
1508            .id("variable-list")
1509            .group("variable-list")
1510            .overflow_y_scroll()
1511            .size_full()
1512            .on_action(cx.listener(Self::select_first))
1513            .on_action(cx.listener(Self::select_last))
1514            .on_action(cx.listener(Self::select_prev))
1515            .on_action(cx.listener(Self::select_next))
1516            .on_action(cx.listener(Self::cancel))
1517            .on_action(cx.listener(Self::confirm))
1518            .on_action(cx.listener(Self::expand_selected_entry))
1519            .on_action(cx.listener(Self::collapse_selected_entry))
1520            .on_action(cx.listener(Self::copy_variable_name))
1521            .on_action(cx.listener(Self::copy_variable_value))
1522            .on_action(cx.listener(Self::edit_variable))
1523            .on_action(cx.listener(Self::add_watcher))
1524            .on_action(cx.listener(Self::remove_watcher))
1525            .on_action(cx.listener(Self::toggle_data_breakpoint))
1526            .on_action(cx.listener(Self::jump_to_variable_memory))
1527            .child(
1528                uniform_list(
1529                    "variable-list",
1530                    self.entries.len(),
1531                    cx.processor(move |this, range: Range<usize>, window, cx| {
1532                        this.render_entries(range, window, cx)
1533                    }),
1534                )
1535                .track_scroll(self.list_handle.clone())
1536                .gap_1_5()
1537                .size_full()
1538                .flex_grow(),
1539            )
1540            .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
1541                deferred(
1542                    anchored()
1543                        .position(*position)
1544                        .anchor(gpui::Corner::TopLeft)
1545                        .child(menu.clone()),
1546                )
1547                .with_priority(1)
1548            }))
1549            .vertical_scrollbar_for(self.list_handle.clone(), window, cx)
1550    }
1551}
1552
1553struct EntryColors {
1554    default: Hsla,
1555    hover: Hsla,
1556    marked_active: Hsla,
1557}
1558
1559fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
1560    let colors = cx.theme().colors();
1561
1562    EntryColors {
1563        default: colors.panel_background,
1564        hover: colors.ghost_element_hover,
1565        marked_active: colors.ghost_element_selected,
1566    }
1567}
1568
1569#[cfg(test)]
1570mod tests {
1571    use super::*;
1572
1573    #[test]
1574    fn test_center_truncate_string() {
1575        // Test string shorter than limit - should not be truncated
1576        assert_eq!(VariableList::center_truncate_string("short", 10), "short");
1577
1578        // Test exact length - should not be truncated
1579        assert_eq!(
1580            VariableList::center_truncate_string("exactly_10", 10),
1581            "exactly_10"
1582        );
1583
1584        // Test simple truncation
1585        assert_eq!(
1586            VariableList::center_truncate_string("value->value2->value3->value4", 20),
1587            "value->v...3->value4"
1588        );
1589
1590        // Test with very long expression
1591        assert_eq!(
1592            VariableList::center_truncate_string(
1593                "object->property1->property2->property3->property4->property5",
1594                30
1595            ),
1596            "object->prope...ty4->property5"
1597        );
1598
1599        // Test edge case with limit equal to ellipsis length
1600        assert_eq!(VariableList::center_truncate_string("anything", 3), "any");
1601
1602        // Test edge case with limit less than ellipsis length
1603        assert_eq!(VariableList::center_truncate_string("anything", 2), "any");
1604
1605        // Test with UTF-8 characters
1606        assert_eq!(
1607            VariableList::center_truncate_string("café->résumé->naïve->voilà", 15),
1608            "café->...>voilà"
1609        );
1610
1611        // Test with emoji (multi-byte UTF-8)
1612        assert_eq!(
1613            VariableList::center_truncate_string("😀->happy->face->😎->cool", 15),
1614            "😀->hap...->cool"
1615        );
1616    }
1617}