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