variable_list.rs

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