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