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