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