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