variable_list.rs

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