variable_list.rs

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