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