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}