variable_list.rs

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