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