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