variable_list.rs

   1use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
   2use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
   3use editor::Editor;
   4use gpui::{
   5    Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, FocusHandle,
   6    Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
   7    TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
   8};
   9use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
  10use project::debugger::session::{Session, SessionEvent};
  11use std::{collections::HashMap, ops::Range, sync::Arc};
  12use ui::{ContextMenu, ListItem, Scrollbar, ScrollbarState, prelude::*};
  13use util::debug_panic;
  14
  15actions!(
  16    variable_list,
  17    [
  18        ExpandSelectedEntry,
  19        CollapseSelectedEntry,
  20        CopyVariableName,
  21        CopyVariableValue,
  22        EditVariable
  23    ]
  24);
  25
  26#[derive(Debug, Copy, Clone, PartialEq, Eq)]
  27pub(crate) struct EntryState {
  28    depth: usize,
  29    is_expanded: bool,
  30    has_children: bool,
  31    parent_reference: VariableReference,
  32}
  33
  34#[derive(Debug, PartialEq, Eq, Hash, Clone)]
  35pub(crate) struct EntryPath {
  36    pub leaf_name: Option<SharedString>,
  37    pub indices: Arc<[SharedString]>,
  38}
  39
  40impl EntryPath {
  41    fn for_scope(scope_name: impl Into<SharedString>) -> Self {
  42        Self {
  43            leaf_name: Some(scope_name.into()),
  44            indices: Arc::new([]),
  45        }
  46    }
  47
  48    fn with_name(&self, name: SharedString) -> Self {
  49        Self {
  50            leaf_name: Some(name),
  51            indices: self.indices.clone(),
  52        }
  53    }
  54
  55    /// Create a new child of this variable path
  56    fn with_child(&self, name: SharedString) -> Self {
  57        Self {
  58            leaf_name: None,
  59            indices: self
  60                .indices
  61                .iter()
  62                .cloned()
  63                .chain(std::iter::once(name))
  64                .collect(),
  65        }
  66    }
  67}
  68
  69#[derive(Debug, Clone, PartialEq)]
  70enum EntryKind {
  71    Variable(dap::Variable),
  72    Scope(dap::Scope),
  73}
  74
  75impl EntryKind {
  76    fn as_variable(&self) -> Option<&dap::Variable> {
  77        match self {
  78            EntryKind::Variable(dap) => Some(dap),
  79            _ => None,
  80        }
  81    }
  82
  83    fn as_scope(&self) -> Option<&dap::Scope> {
  84        match self {
  85            EntryKind::Scope(dap) => Some(dap),
  86            _ => None,
  87        }
  88    }
  89
  90    #[allow(dead_code)]
  91    fn name(&self) -> &str {
  92        match self {
  93            EntryKind::Variable(dap) => &dap.name,
  94            EntryKind::Scope(dap) => &dap.name,
  95        }
  96    }
  97}
  98
  99#[derive(Debug, Clone, PartialEq)]
 100struct ListEntry {
 101    dap_kind: EntryKind,
 102    path: EntryPath,
 103}
 104
 105impl ListEntry {
 106    fn as_variable(&self) -> Option<&dap::Variable> {
 107        self.dap_kind.as_variable()
 108    }
 109
 110    fn as_scope(&self) -> Option<&dap::Scope> {
 111        self.dap_kind.as_scope()
 112    }
 113
 114    fn item_id(&self) -> ElementId {
 115        use std::fmt::Write;
 116        let mut id = match &self.dap_kind {
 117            EntryKind::Variable(dap) => format!("variable-{}", dap.name),
 118            EntryKind::Scope(dap) => format!("scope-{}", dap.name),
 119        };
 120        for name in self.path.indices.iter() {
 121            _ = write!(id, "-{}", name);
 122        }
 123        SharedString::from(id).into()
 124    }
 125
 126    fn item_value_id(&self) -> ElementId {
 127        use std::fmt::Write;
 128        let mut id = match &self.dap_kind {
 129            EntryKind::Variable(dap) => format!("variable-{}", dap.name),
 130            EntryKind::Scope(dap) => format!("scope-{}", dap.name),
 131        };
 132        for name in self.path.indices.iter() {
 133            _ = write!(id, "-{}", name);
 134        }
 135        _ = write!(id, "-value");
 136        SharedString::from(id).into()
 137    }
 138}
 139
 140pub struct VariableList {
 141    entries: Vec<ListEntry>,
 142    entry_states: HashMap<EntryPath, EntryState>,
 143    selected_stack_frame_id: Option<StackFrameId>,
 144    list_handle: UniformListScrollHandle,
 145    scrollbar_state: ScrollbarState,
 146    session: Entity<Session>,
 147    selection: Option<EntryPath>,
 148    open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
 149    focus_handle: FocusHandle,
 150    edited_path: Option<(EntryPath, Entity<Editor>)>,
 151    disabled: bool,
 152    _subscriptions: Vec<Subscription>,
 153}
 154
 155impl VariableList {
 156    pub fn new(
 157        session: Entity<Session>,
 158        stack_frame_list: Entity<StackFrameList>,
 159        window: &mut Window,
 160        cx: &mut Context<Self>,
 161    ) -> Self {
 162        let focus_handle = cx.focus_handle();
 163
 164        let _subscriptions = vec![
 165            cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
 166            cx.subscribe(&session, |this, _, event, cx| match event {
 167                SessionEvent::Stopped(_) => {
 168                    this.selection.take();
 169                    this.edited_path.take();
 170                    this.selected_stack_frame_id.take();
 171                }
 172                SessionEvent::Variables => {
 173                    this.build_entries(cx);
 174                }
 175                _ => {}
 176            }),
 177            cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
 178                this.edited_path.take();
 179                cx.notify();
 180            }),
 181        ];
 182
 183        let list_state = UniformListScrollHandle::default();
 184
 185        Self {
 186            scrollbar_state: ScrollbarState::new(list_state.clone()),
 187            list_handle: list_state,
 188            session,
 189            focus_handle,
 190            _subscriptions,
 191            selected_stack_frame_id: None,
 192            selection: None,
 193            open_context_menu: None,
 194            disabled: false,
 195            edited_path: None,
 196            entries: Default::default(),
 197            entry_states: Default::default(),
 198        }
 199    }
 200
 201    pub(super) fn disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
 202        let old_disabled = std::mem::take(&mut self.disabled);
 203        self.disabled = disabled;
 204        if old_disabled != disabled {
 205            cx.notify();
 206        }
 207    }
 208
 209    pub(super) fn has_open_context_menu(&self) -> bool {
 210        self.open_context_menu.is_some()
 211    }
 212
 213    fn build_entries(&mut self, cx: &mut Context<Self>) {
 214        let Some(stack_frame_id) = self.selected_stack_frame_id else {
 215            return;
 216        };
 217
 218        let mut entries = vec![];
 219        let scopes: Vec<_> = self.session.update(cx, |session, cx| {
 220            session.scopes(stack_frame_id, cx).iter().cloned().collect()
 221        });
 222
 223        let mut contains_local_scope = false;
 224
 225        let mut stack = scopes
 226            .into_iter()
 227            .rev()
 228            .filter(|scope| {
 229                if scope
 230                    .presentation_hint
 231                    .as_ref()
 232                    .map(|hint| *hint == ScopePresentationHint::Locals)
 233                    .unwrap_or(scope.name.to_lowercase().starts_with("local"))
 234                {
 235                    contains_local_scope = true;
 236                }
 237
 238                self.session.update(cx, |session, cx| {
 239                    session.variables(scope.variables_reference, cx).len() > 0
 240                })
 241            })
 242            .map(|scope| {
 243                (
 244                    scope.variables_reference,
 245                    scope.variables_reference,
 246                    EntryPath::for_scope(&scope.name),
 247                    EntryKind::Scope(scope),
 248                )
 249            })
 250            .collect::<Vec<_>>();
 251
 252        let scopes_count = stack.len();
 253
 254        while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
 255        {
 256            match &dap_kind {
 257                EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
 258                EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
 259            }
 260
 261            let var_state = self
 262                .entry_states
 263                .entry(path.clone())
 264                .and_modify(|state| {
 265                    state.parent_reference = container_reference;
 266                    state.has_children = variables_reference != 0;
 267                })
 268                .or_insert(EntryState {
 269                    depth: path.indices.len(),
 270                    is_expanded: dap_kind.as_scope().is_some_and(|scope| {
 271                        (scopes_count == 1 && !contains_local_scope)
 272                            || scope
 273                                .presentation_hint
 274                                .as_ref()
 275                                .map(|hint| *hint == ScopePresentationHint::Locals)
 276                                .unwrap_or(scope.name.to_lowercase().starts_with("local"))
 277                    }),
 278                    parent_reference: container_reference,
 279                    has_children: variables_reference != 0,
 280                });
 281
 282            entries.push(ListEntry {
 283                dap_kind,
 284                path: path.clone(),
 285            });
 286
 287            if var_state.is_expanded {
 288                let children = self
 289                    .session
 290                    .update(cx, |session, cx| session.variables(variables_reference, cx));
 291                stack.extend(children.into_iter().rev().map(|child| {
 292                    (
 293                        variables_reference,
 294                        child.variables_reference,
 295                        path.with_child(child.name.clone().into()),
 296                        EntryKind::Variable(child),
 297                    )
 298                }));
 299            }
 300        }
 301
 302        self.entries = entries;
 303        cx.notify();
 304    }
 305
 306    fn handle_stack_frame_list_events(
 307        &mut self,
 308        _: Entity<StackFrameList>,
 309        event: &StackFrameListEvent,
 310        cx: &mut Context<Self>,
 311    ) {
 312        match event {
 313            StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
 314                self.selected_stack_frame_id = Some(*stack_frame_id);
 315                self.build_entries(cx);
 316            }
 317            StackFrameListEvent::BuiltEntries => {}
 318        }
 319    }
 320
 321    pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
 322        self.entries
 323            .iter()
 324            .filter_map(|entry| match &entry.dap_kind {
 325                EntryKind::Variable(dap) => Some(dap.clone()),
 326                EntryKind::Scope(_) => None,
 327            })
 328            .collect()
 329    }
 330
 331    fn render_entries(
 332        &mut self,
 333        ix: Range<usize>,
 334        window: &mut Window,
 335        cx: &mut Context<Self>,
 336    ) -> Vec<AnyElement> {
 337        ix.into_iter()
 338            .filter_map(|ix| {
 339                let (entry, state) = self
 340                    .entries
 341                    .get(ix)
 342                    .and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
 343
 344                match &entry.dap_kind {
 345                    EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
 346                    EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
 347                }
 348            })
 349            .collect()
 350    }
 351
 352    pub(crate) fn toggle_entry(&mut self, var_path: &EntryPath, cx: &mut Context<Self>) {
 353        let Some(entry) = self.entry_states.get_mut(var_path) else {
 354            log::error!("Could not find variable list entry state to toggle");
 355            return;
 356        };
 357
 358        entry.is_expanded = !entry.is_expanded;
 359        self.build_entries(cx);
 360    }
 361
 362    fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
 363        self.cancel(&Default::default(), window, cx);
 364        if let Some(variable) = self.entries.first() {
 365            self.selection = Some(variable.path.clone());
 366            self.build_entries(cx);
 367        }
 368    }
 369
 370    fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
 371        self.cancel(&Default::default(), window, cx);
 372        if let Some(variable) = self.entries.last() {
 373            self.selection = Some(variable.path.clone());
 374            self.build_entries(cx);
 375        }
 376    }
 377
 378    fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
 379        self.cancel(&Default::default(), window, cx);
 380        if let Some(selection) = &self.selection {
 381            let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
 382                if &var.path == selection && ix > 0 {
 383                    Some(ix.saturating_sub(1))
 384                } else {
 385                    None
 386                }
 387            });
 388
 389            if let Some(new_selection) =
 390                index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
 391            {
 392                self.selection = Some(new_selection);
 393                self.build_entries(cx);
 394            } else {
 395                self.select_last(&SelectLast, window, cx);
 396            }
 397        } else {
 398            self.select_last(&SelectLast, window, cx);
 399        }
 400    }
 401
 402    fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
 403        self.cancel(&Default::default(), window, cx);
 404        if let Some(selection) = &self.selection {
 405            let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
 406                if &var.path == selection {
 407                    Some(ix.saturating_add(1))
 408                } else {
 409                    None
 410                }
 411            });
 412
 413            if let Some(new_selection) =
 414                index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
 415            {
 416                self.selection = Some(new_selection);
 417                self.build_entries(cx);
 418            } else {
 419                self.select_first(&SelectFirst, window, cx);
 420            }
 421        } else {
 422            self.select_first(&SelectFirst, window, cx);
 423        }
 424    }
 425
 426    fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 427        self.edited_path.take();
 428        self.focus_handle.focus(window);
 429        cx.notify();
 430    }
 431
 432    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 433        if let Some((var_path, editor)) = self.edited_path.take() {
 434            let Some(state) = self.entry_states.get(&var_path) else {
 435                return;
 436            };
 437            let variables_reference = state.parent_reference;
 438            let Some(name) = var_path.leaf_name else {
 439                return;
 440            };
 441            let value = editor.read(cx).text(cx);
 442
 443            self.session.update(cx, |session, cx| {
 444                session.set_variable_value(variables_reference, name.into(), value, cx)
 445            });
 446        }
 447    }
 448
 449    fn collapse_selected_entry(
 450        &mut self,
 451        _: &CollapseSelectedEntry,
 452        window: &mut Window,
 453        cx: &mut Context<Self>,
 454    ) {
 455        if let Some(ref selected_entry) = self.selection {
 456            let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
 457                debug_panic!("Trying to toggle variable in variable list that has an no state");
 458                return;
 459            };
 460
 461            if !entry_state.is_expanded || !entry_state.has_children {
 462                self.select_prev(&SelectPrevious, window, cx);
 463            } else {
 464                entry_state.is_expanded = false;
 465                self.build_entries(cx);
 466            }
 467        }
 468    }
 469
 470    fn expand_selected_entry(
 471        &mut self,
 472        _: &ExpandSelectedEntry,
 473        window: &mut Window,
 474        cx: &mut Context<Self>,
 475    ) {
 476        if let Some(selected_entry) = &self.selection {
 477            let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
 478                debug_panic!("Trying to toggle variable in variable list that has an no state");
 479                return;
 480            };
 481
 482            if entry_state.is_expanded || !entry_state.has_children {
 483                self.select_next(&SelectNext, window, cx);
 484            } else {
 485                entry_state.is_expanded = true;
 486                self.build_entries(cx);
 487            }
 488        }
 489    }
 490
 491    fn deploy_variable_context_menu(
 492        &mut self,
 493        _variable: ListEntry,
 494        position: Point<Pixels>,
 495        window: &mut Window,
 496        cx: &mut Context<Self>,
 497    ) {
 498        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
 499            menu.action("Copy Name", CopyVariableName.boxed_clone())
 500                .action("Copy Value", CopyVariableValue.boxed_clone())
 501                .action("Edit Value", EditVariable.boxed_clone())
 502                .context(self.focus_handle.clone())
 503        });
 504
 505        cx.focus_view(&context_menu, window);
 506        let subscription = cx.subscribe_in(
 507            &context_menu,
 508            window,
 509            |this, _, _: &DismissEvent, window, cx| {
 510                if this.open_context_menu.as_ref().is_some_and(|context_menu| {
 511                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
 512                }) {
 513                    cx.focus_self(window);
 514                }
 515                this.open_context_menu.take();
 516                cx.notify();
 517            },
 518        );
 519
 520        self.open_context_menu = Some((context_menu, position, subscription));
 521    }
 522
 523    fn copy_variable_name(
 524        &mut self,
 525        _: &CopyVariableName,
 526        _window: &mut Window,
 527        cx: &mut Context<Self>,
 528    ) {
 529        let Some(selection) = self.selection.as_ref() else {
 530            return;
 531        };
 532        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 533            return;
 534        };
 535        let Some(variable) = entry.as_variable() else {
 536            return;
 537        };
 538        cx.write_to_clipboard(ClipboardItem::new_string(variable.name.clone()));
 539    }
 540
 541    fn copy_variable_value(
 542        &mut self,
 543        _: &CopyVariableValue,
 544        _window: &mut Window,
 545        cx: &mut Context<Self>,
 546    ) {
 547        let Some(selection) = self.selection.as_ref() else {
 548            return;
 549        };
 550        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 551            return;
 552        };
 553        let Some(variable) = entry.as_variable() else {
 554            return;
 555        };
 556        cx.write_to_clipboard(ClipboardItem::new_string(variable.value.clone()));
 557    }
 558
 559    fn edit_variable(&mut self, _: &EditVariable, window: &mut Window, cx: &mut Context<Self>) {
 560        let Some(selection) = self.selection.as_ref() else {
 561            return;
 562        };
 563        let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
 564            return;
 565        };
 566        let Some(variable) = entry.as_variable() else {
 567            return;
 568        };
 569
 570        let editor = Self::create_variable_editor(&variable.value, window, cx);
 571        self.edited_path = Some((entry.path.clone(), editor));
 572
 573        cx.notify();
 574    }
 575
 576    #[track_caller]
 577    #[cfg(test)]
 578    pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
 579        const INDENT: &'static str = "    ";
 580
 581        let entries = &self.entries;
 582        let mut visual_entries = Vec::with_capacity(entries.len());
 583        for entry in entries {
 584            let state = self
 585                .entry_states
 586                .get(&entry.path)
 587                .expect("If there's a variable entry there has to be a state that goes with it");
 588
 589            visual_entries.push(format!(
 590                "{}{} {}{}",
 591                INDENT.repeat(state.depth - 1),
 592                if state.is_expanded { "v" } else { ">" },
 593                entry.dap_kind.name(),
 594                if self.selection.as_ref() == Some(&entry.path) {
 595                    " <=== selected"
 596                } else {
 597                    ""
 598                }
 599            ));
 600        }
 601
 602        pretty_assertions::assert_eq!(expected, visual_entries);
 603    }
 604
 605    #[track_caller]
 606    #[cfg(test)]
 607    pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
 608        self.entries
 609            .iter()
 610            .filter_map(|entry| match &entry.dap_kind {
 611                EntryKind::Scope(scope) => Some(scope),
 612                _ => None,
 613            })
 614            .cloned()
 615            .collect()
 616    }
 617
 618    #[track_caller]
 619    #[cfg(test)]
 620    pub(crate) fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
 621        let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
 622        let mut idx = 0;
 623
 624        for entry in self.entries.iter() {
 625            match &entry.dap_kind {
 626                EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
 627                EntryKind::Scope(scope) => {
 628                    if scopes.len() > 0 {
 629                        idx += 1;
 630                    }
 631
 632                    scopes.push((scope.clone(), Vec::new()));
 633                }
 634            }
 635        }
 636
 637        scopes
 638    }
 639
 640    #[track_caller]
 641    #[cfg(test)]
 642    pub(crate) fn variables(&self) -> Vec<dap::Variable> {
 643        self.entries
 644            .iter()
 645            .filter_map(|entry| match &entry.dap_kind {
 646                EntryKind::Variable(variable) => Some(variable),
 647                _ => None,
 648            })
 649            .cloned()
 650            .collect()
 651    }
 652
 653    fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> {
 654        let editor = cx.new(|cx| {
 655            let mut editor = Editor::single_line(window, cx);
 656
 657            let refinement = TextStyleRefinement {
 658                font_size: Some(
 659                    TextSize::XSmall
 660                        .rems(cx)
 661                        .to_pixels(window.rem_size())
 662                        .into(),
 663                ),
 664                ..Default::default()
 665            };
 666            editor.set_text_style_refinement(refinement);
 667            editor.set_text(default, window, cx);
 668            editor.select_all(&editor::actions::SelectAll, window, cx);
 669            editor
 670        });
 671        editor.focus_handle(cx).focus(window);
 672        editor
 673    }
 674
 675    fn render_scope(
 676        &self,
 677        entry: &ListEntry,
 678        state: EntryState,
 679        cx: &mut Context<Self>,
 680    ) -> AnyElement {
 681        let Some(scope) = entry.as_scope() else {
 682            debug_panic!("Called render scope on non scope variable list entry variant");
 683            return div().into_any_element();
 684        };
 685
 686        let var_ref = scope.variables_reference;
 687        let is_selected = self
 688            .selection
 689            .as_ref()
 690            .is_some_and(|selection| selection == &entry.path);
 691
 692        let colors = get_entry_color(cx);
 693        let bg_hover_color = if !is_selected {
 694            colors.hover
 695        } else {
 696            colors.default
 697        };
 698        let border_color = if is_selected {
 699            colors.marked_active
 700        } else {
 701            colors.default
 702        };
 703        let path = entry.path.clone();
 704
 705        div()
 706            .id(var_ref as usize)
 707            .group("variable_list_entry")
 708            .pl_2()
 709            .border_1()
 710            .border_r_2()
 711            .border_color(border_color)
 712            .flex()
 713            .w_full()
 714            .h_full()
 715            .hover(|style| style.bg(bg_hover_color))
 716            .on_click(cx.listener({
 717                move |this, _, _window, cx| {
 718                    this.selection = Some(path.clone());
 719                    cx.notify();
 720                }
 721            }))
 722            .child(
 723                ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
 724                    .selectable(false)
 725                    .disabled(self.disabled)
 726                    .indent_level(state.depth)
 727                    .indent_step_size(px(10.))
 728                    .always_show_disclosure_icon(true)
 729                    .toggle(state.is_expanded)
 730                    .on_toggle({
 731                        let var_path = entry.path.clone();
 732                        cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
 733                    })
 734                    .child(
 735                        div()
 736                            .text_ui(cx)
 737                            .w_full()
 738                            .when(self.disabled, |this| {
 739                                this.text_color(Color::Disabled.color(cx))
 740                            })
 741                            .child(scope.name.clone()),
 742                    ),
 743            )
 744            .into_any()
 745    }
 746
 747    fn render_variable(
 748        &self,
 749        variable: &ListEntry,
 750        state: EntryState,
 751        window: &mut Window,
 752        cx: &mut Context<Self>,
 753    ) -> AnyElement {
 754        let dap = match &variable.dap_kind {
 755            EntryKind::Variable(dap) => dap,
 756            EntryKind::Scope(_) => {
 757                debug_panic!("Called render variable on variable list entry kind scope");
 758                return div().into_any_element();
 759            }
 760        };
 761
 762        let syntax_color_for = |name| cx.theme().syntax().get(name).color;
 763        let variable_name_color = if self.disabled {
 764            Some(Color::Disabled.color(cx))
 765        } else {
 766            match &dap
 767                .presentation_hint
 768                .as_ref()
 769                .and_then(|hint| hint.kind.as_ref())
 770                .unwrap_or(&VariablePresentationHintKind::Unknown)
 771            {
 772                VariablePresentationHintKind::Class
 773                | VariablePresentationHintKind::BaseClass
 774                | VariablePresentationHintKind::InnerClass
 775                | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
 776                VariablePresentationHintKind::Data => syntax_color_for("variable"),
 777                VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
 778            }
 779        };
 780        let variable_color = self
 781            .disabled
 782            .then(|| Color::Disabled.color(cx))
 783            .or_else(|| syntax_color_for("variable.special"));
 784
 785        let var_ref = dap.variables_reference;
 786        let colors = get_entry_color(cx);
 787        let is_selected = self
 788            .selection
 789            .as_ref()
 790            .is_some_and(|selected_path| *selected_path == variable.path);
 791
 792        let bg_hover_color = if !is_selected {
 793            colors.hover
 794        } else {
 795            colors.default
 796        };
 797        let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
 798            colors.marked_active
 799        } else {
 800            colors.default
 801        };
 802        let path = variable.path.clone();
 803        div()
 804            .id(variable.item_id())
 805            .group("variable_list_entry")
 806            .pl_2()
 807            .border_1()
 808            .border_r_2()
 809            .border_color(border_color)
 810            .h_4()
 811            .size_full()
 812            .hover(|style| style.bg(bg_hover_color))
 813            .on_click(cx.listener({
 814                move |this, _, _window, cx| {
 815                    this.selection = Some(path.clone());
 816                    cx.notify();
 817                }
 818            }))
 819            .child(
 820                ListItem::new(SharedString::from(format!(
 821                    "variable-item-{}-{}",
 822                    dap.name, state.depth
 823                )))
 824                .disabled(self.disabled)
 825                .selectable(false)
 826                .indent_level(state.depth)
 827                .indent_step_size(px(10.))
 828                .always_show_disclosure_icon(true)
 829                .when(var_ref > 0, |list_item| {
 830                    list_item.toggle(state.is_expanded).on_toggle(cx.listener({
 831                        let var_path = variable.path.clone();
 832                        move |this, _, _, cx| {
 833                            this.session.update(cx, |session, cx| {
 834                                session.variables(var_ref, cx);
 835                            });
 836
 837                            this.toggle_entry(&var_path, cx);
 838                        }
 839                    }))
 840                })
 841                .on_secondary_mouse_down(cx.listener({
 842                    let variable = variable.clone();
 843                    move |this, event: &MouseDownEvent, window, cx| {
 844                        this.selection = Some(variable.path.clone());
 845                        this.deploy_variable_context_menu(
 846                            variable.clone(),
 847                            event.position,
 848                            window,
 849                            cx,
 850                        );
 851                        cx.stop_propagation();
 852                    }
 853                }))
 854                .child(
 855                    h_flex()
 856                        .gap_1()
 857                        .text_ui_sm(cx)
 858                        .w_full()
 859                        .child(
 860                            Label::new(&dap.name).when_some(variable_name_color, |this, color| {
 861                                this.color(Color::from(color))
 862                            }),
 863                        )
 864                        .when(!dap.value.is_empty(), |this| {
 865                            this.child(div().w_full().id(variable.item_value_id()).map(|this| {
 866                                if let Some((_, editor)) = self
 867                                    .edited_path
 868                                    .as_ref()
 869                                    .filter(|(path, _)| path == &variable.path)
 870                                {
 871                                    this.child(div().size_full().px_2().child(editor.clone()))
 872                                } else {
 873                                    this.text_color(cx.theme().colors().text_muted)
 874                                        .when(
 875                                            !self.disabled
 876                                                && self
 877                                                    .session
 878                                                    .read(cx)
 879                                                    .capabilities()
 880                                                    .supports_set_variable
 881                                                    .unwrap_or_default(),
 882                                            |this| {
 883                                                let path = variable.path.clone();
 884                                                let variable_value = dap.value.clone();
 885                                                this.on_click(cx.listener(
 886                                                    move |this, click: &ClickEvent, window, cx| {
 887                                                        if click.down.click_count < 2 {
 888                                                            return;
 889                                                        }
 890                                                        let editor = Self::create_variable_editor(
 891                                                            &variable_value,
 892                                                            window,
 893                                                            cx,
 894                                                        );
 895                                                        this.edited_path =
 896                                                            Some((path.clone(), editor));
 897
 898                                                        cx.notify();
 899                                                    },
 900                                                ))
 901                                            },
 902                                        )
 903                                        .child(
 904                                            Label::new(format!("=  {}", &dap.value))
 905                                                .single_line()
 906                                                .truncate()
 907                                                .size(LabelSize::Small)
 908                                                .color(Color::Muted)
 909                                                .when_some(variable_color, |this, color| {
 910                                                    this.color(Color::from(color))
 911                                                }),
 912                                        )
 913                                }
 914                            }))
 915                        }),
 916                ),
 917            )
 918            .into_any()
 919    }
 920
 921    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
 922        div()
 923            .occlude()
 924            .id("variable-list-vertical-scrollbar")
 925            .on_mouse_move(cx.listener(|_, _, _, cx| {
 926                cx.notify();
 927                cx.stop_propagation()
 928            }))
 929            .on_hover(|_, _, cx| {
 930                cx.stop_propagation();
 931            })
 932            .on_any_mouse_down(|_, _, cx| {
 933                cx.stop_propagation();
 934            })
 935            .on_mouse_up(
 936                MouseButton::Left,
 937                cx.listener(|_, _, _, cx| {
 938                    cx.stop_propagation();
 939                }),
 940            )
 941            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
 942                cx.notify();
 943            }))
 944            .h_full()
 945            .absolute()
 946            .right_1()
 947            .top_1()
 948            .bottom_0()
 949            .w(px(12.))
 950            .cursor_default()
 951            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
 952    }
 953}
 954
 955impl Focusable for VariableList {
 956    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
 957        self.focus_handle.clone()
 958    }
 959}
 960
 961impl Render for VariableList {
 962    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 963        v_flex()
 964            .track_focus(&self.focus_handle)
 965            .key_context("VariableList")
 966            .id("variable-list")
 967            .group("variable-list")
 968            .overflow_y_scroll()
 969            .size_full()
 970            .on_action(cx.listener(Self::select_first))
 971            .on_action(cx.listener(Self::select_last))
 972            .on_action(cx.listener(Self::select_prev))
 973            .on_action(cx.listener(Self::select_next))
 974            .on_action(cx.listener(Self::cancel))
 975            .on_action(cx.listener(Self::confirm))
 976            .on_action(cx.listener(Self::expand_selected_entry))
 977            .on_action(cx.listener(Self::collapse_selected_entry))
 978            .on_action(cx.listener(Self::copy_variable_name))
 979            .on_action(cx.listener(Self::copy_variable_value))
 980            .on_action(cx.listener(Self::edit_variable))
 981            .child(
 982                uniform_list(
 983                    cx.entity().clone(),
 984                    "variable-list",
 985                    self.entries.len(),
 986                    move |this, range, window, cx| this.render_entries(range, window, cx),
 987                )
 988                .track_scroll(self.list_handle.clone())
 989                .gap_1_5()
 990                .size_full()
 991                .flex_grow(),
 992            )
 993            .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
 994                deferred(
 995                    anchored()
 996                        .position(*position)
 997                        .anchor(gpui::Corner::TopLeft)
 998                        .child(menu.clone()),
 999                )
1000                .with_priority(1)
1001            }))
1002            .child(self.render_vertical_scrollbar(cx))
1003    }
1004}
1005
1006struct EntryColors {
1007    default: Hsla,
1008    hover: Hsla,
1009    marked_active: Hsla,
1010}
1011
1012fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
1013    let colors = cx.theme().colors();
1014
1015    EntryColors {
1016        default: colors.panel_background,
1017        hover: colors.ghost_element_hover,
1018        marked_active: colors.ghost_element_selected,
1019    }
1020}