1use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
2use dap::{
3 ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
4 VariableReference,
5};
6use editor::Editor;
7use gpui::{
8 Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
9 FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
10 TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
11};
12use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
13use project::debugger::session::{Session, SessionEvent, Watcher};
14use std::{collections::HashMap, ops::Range, sync::Arc};
15use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
16use util::debug_panic;
17
18actions!(
19 variable_list,
20 [
21 /// Expands the selected variable entry to show its children.
22 ExpandSelectedEntry,
23 /// Collapses the selected variable entry to hide its children.
24 CollapseSelectedEntry,
25 /// Copies the variable name to the clipboard.
26 CopyVariableName,
27 /// Copies the variable value to the clipboard.
28 CopyVariableValue,
29 /// Edits the value of the selected variable.
30 EditVariable,
31 /// Adds the selected variable to the watch list.
32 AddWatch,
33 /// Removes the selected variable from the watch list.
34 RemoveWatch,
35 ]
36);
37
38#[derive(Debug, Copy, Clone, PartialEq, Eq)]
39pub(crate) struct EntryState {
40 depth: usize,
41 is_expanded: bool,
42 has_children: bool,
43 parent_reference: VariableReference,
44}
45
46#[derive(Debug, PartialEq, Eq, Hash, Clone)]
47pub(crate) struct EntryPath {
48 pub leaf_name: Option<SharedString>,
49 pub indices: Arc<[SharedString]>,
50}
51
52impl EntryPath {
53 fn for_watcher(expression: impl Into<SharedString>) -> Self {
54 Self {
55 leaf_name: Some(expression.into()),
56 indices: Arc::new([]),
57 }
58 }
59
60 fn for_scope(scope_name: impl Into<SharedString>) -> Self {
61 Self {
62 leaf_name: Some(scope_name.into()),
63 indices: Arc::new([]),
64 }
65 }
66
67 fn with_name(&self, name: SharedString) -> Self {
68 Self {
69 leaf_name: Some(name),
70 indices: self.indices.clone(),
71 }
72 }
73
74 /// Create a new child of this variable path
75 fn with_child(&self, name: SharedString) -> Self {
76 Self {
77 leaf_name: None,
78 indices: self
79 .indices
80 .iter()
81 .cloned()
82 .chain(std::iter::once(name))
83 .collect(),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq)]
89enum EntryKind {
90 Watcher(Watcher),
91 Variable(dap::Variable),
92 Scope(dap::Scope),
93}
94
95impl EntryKind {
96 fn as_watcher(&self) -> Option<&Watcher> {
97 match self {
98 EntryKind::Watcher(watcher) => Some(watcher),
99 _ => None,
100 }
101 }
102
103 fn as_variable(&self) -> Option<&dap::Variable> {
104 match self {
105 EntryKind::Variable(dap) => Some(dap),
106 _ => None,
107 }
108 }
109
110 fn as_scope(&self) -> Option<&dap::Scope> {
111 match self {
112 EntryKind::Scope(dap) => Some(dap),
113 _ => None,
114 }
115 }
116
117 #[cfg(test)]
118 fn name(&self) -> &str {
119 match self {
120 EntryKind::Watcher(watcher) => &watcher.expression,
121 EntryKind::Variable(dap) => &dap.name,
122 EntryKind::Scope(dap) => &dap.name,
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq)]
128struct ListEntry {
129 dap_kind: EntryKind,
130 path: EntryPath,
131}
132
133impl ListEntry {
134 fn as_watcher(&self) -> Option<&Watcher> {
135 self.dap_kind.as_watcher()
136 }
137
138 fn as_variable(&self) -> Option<&dap::Variable> {
139 self.dap_kind.as_variable()
140 }
141
142 fn as_scope(&self) -> Option<&dap::Scope> {
143 self.dap_kind.as_scope()
144 }
145
146 fn item_id(&self) -> ElementId {
147 use std::fmt::Write;
148 let mut id = match &self.dap_kind {
149 EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
150 EntryKind::Variable(dap) => format!("variable-{}", dap.name),
151 EntryKind::Scope(dap) => format!("scope-{}", dap.name),
152 };
153 for name in self.path.indices.iter() {
154 _ = write!(id, "-{}", name);
155 }
156 SharedString::from(id).into()
157 }
158
159 fn item_value_id(&self) -> ElementId {
160 use std::fmt::Write;
161 let mut id = match &self.dap_kind {
162 EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
163 EntryKind::Variable(dap) => format!("variable-{}", dap.name),
164 EntryKind::Scope(dap) => format!("scope-{}", dap.name),
165 };
166 for name in self.path.indices.iter() {
167 _ = write!(id, "-{}", name);
168 }
169 _ = write!(id, "-value");
170 SharedString::from(id).into()
171 }
172}
173
174struct VariableColor {
175 name: Option<Hsla>,
176 value: Option<Hsla>,
177}
178
179pub struct VariableList {
180 entries: Vec<ListEntry>,
181 entry_states: HashMap<EntryPath, EntryState>,
182 selected_stack_frame_id: Option<StackFrameId>,
183 list_handle: UniformListScrollHandle,
184 scrollbar_state: ScrollbarState,
185 session: Entity<Session>,
186 selection: Option<EntryPath>,
187 open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
188 focus_handle: FocusHandle,
189 edited_path: Option<(EntryPath, Entity<Editor>)>,
190 disabled: bool,
191 _subscriptions: Vec<Subscription>,
192}
193
194impl VariableList {
195 pub fn new(
196 session: Entity<Session>,
197 stack_frame_list: Entity<StackFrameList>,
198 window: &mut Window,
199 cx: &mut Context<Self>,
200 ) -> Self {
201 let focus_handle = cx.focus_handle();
202
203 let _subscriptions = vec![
204 cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
205 cx.subscribe(&session, |this, _, event, cx| match event {
206 SessionEvent::Stopped(_) => {
207 this.selection.take();
208 this.edited_path.take();
209 this.selected_stack_frame_id.take();
210 }
211 SessionEvent::Variables | SessionEvent::Watchers => {
212 this.build_entries(cx);
213 }
214 _ => {}
215 }),
216 cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
217 this.edited_path.take();
218 cx.notify();
219 }),
220 ];
221
222 let list_state = UniformListScrollHandle::default();
223
224 Self {
225 scrollbar_state: ScrollbarState::new(list_state.clone()),
226 list_handle: list_state,
227 session,
228 focus_handle,
229 _subscriptions,
230 selected_stack_frame_id: None,
231 selection: None,
232 open_context_menu: None,
233 disabled: false,
234 edited_path: None,
235 entries: Default::default(),
236 entry_states: Default::default(),
237 }
238 }
239
240 pub(super) fn disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
241 let old_disabled = std::mem::take(&mut self.disabled);
242 self.disabled = disabled;
243 if old_disabled != disabled {
244 cx.notify();
245 }
246 }
247
248 pub(super) fn has_open_context_menu(&self) -> bool {
249 self.open_context_menu.is_some()
250 }
251
252 fn build_entries(&mut self, cx: &mut Context<Self>) {
253 let Some(stack_frame_id) = self.selected_stack_frame_id else {
254 return;
255 };
256
257 let mut entries = vec![];
258
259 let scopes: Vec<_> = self.session.update(cx, |session, cx| {
260 session.scopes(stack_frame_id, cx).iter().cloned().collect()
261 });
262
263 let mut contains_local_scope = false;
264
265 let mut stack = scopes
266 .into_iter()
267 .rev()
268 .filter(|scope| {
269 if scope
270 .presentation_hint
271 .as_ref()
272 .map(|hint| *hint == ScopePresentationHint::Locals)
273 .unwrap_or(scope.name.to_lowercase().starts_with("local"))
274 {
275 contains_local_scope = true;
276 }
277
278 self.session.update(cx, |session, cx| {
279 session.variables(scope.variables_reference, cx).len() > 0
280 })
281 })
282 .map(|scope| {
283 (
284 scope.variables_reference,
285 scope.variables_reference,
286 EntryPath::for_scope(&scope.name),
287 EntryKind::Scope(scope),
288 )
289 })
290 .collect::<Vec<_>>();
291
292 let watches = self.session.read(cx).watchers().clone();
293 stack.extend(
294 watches
295 .into_values()
296 .map(|watcher| {
297 (
298 watcher.variables_reference,
299 watcher.variables_reference,
300 EntryPath::for_watcher(watcher.expression.clone()),
301 EntryKind::Watcher(watcher.clone()),
302 )
303 })
304 .collect::<Vec<_>>(),
305 );
306
307 let scopes_count = stack.len();
308
309 while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
310 {
311 match &dap_kind {
312 EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
313 EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
314 EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
315 }
316
317 let var_state = self
318 .entry_states
319 .entry(path.clone())
320 .and_modify(|state| {
321 state.parent_reference = container_reference;
322 state.has_children = variables_reference != 0;
323 })
324 .or_insert(EntryState {
325 depth: path.indices.len(),
326 is_expanded: dap_kind.as_scope().is_some_and(|scope| {
327 (scopes_count == 1 && !contains_local_scope)
328 || scope
329 .presentation_hint
330 .as_ref()
331 .map(|hint| *hint == ScopePresentationHint::Locals)
332 .unwrap_or(scope.name.to_lowercase().starts_with("local"))
333 }),
334 parent_reference: container_reference,
335 has_children: variables_reference != 0,
336 });
337
338 entries.push(ListEntry {
339 dap_kind,
340 path: path.clone(),
341 });
342
343 if var_state.is_expanded {
344 let children = self
345 .session
346 .update(cx, |session, cx| session.variables(variables_reference, cx));
347 stack.extend(children.into_iter().rev().map(|child| {
348 (
349 variables_reference,
350 child.variables_reference,
351 path.with_child(child.name.clone().into()),
352 EntryKind::Variable(child),
353 )
354 }));
355 }
356 }
357
358 self.entries = entries;
359 cx.notify();
360 }
361
362 fn handle_stack_frame_list_events(
363 &mut self,
364 _: Entity<StackFrameList>,
365 event: &StackFrameListEvent,
366 cx: &mut Context<Self>,
367 ) {
368 match event {
369 StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
370 self.selected_stack_frame_id = Some(*stack_frame_id);
371 self.session.update(cx, |session, cx| {
372 session.refresh_watchers(*stack_frame_id, cx);
373 });
374 self.build_entries(cx);
375 }
376 StackFrameListEvent::BuiltEntries => {}
377 }
378 }
379
380 pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
381 self.entries
382 .iter()
383 .filter_map(|entry| match &entry.dap_kind {
384 EntryKind::Variable(dap) => Some(dap.clone()),
385 EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
386 })
387 .collect()
388 }
389
390 fn render_entries(
391 &mut self,
392 ix: Range<usize>,
393 window: &mut Window,
394 cx: &mut Context<Self>,
395 ) -> Vec<AnyElement> {
396 ix.into_iter()
397 .filter_map(|ix| {
398 let (entry, state) = self
399 .entries
400 .get(ix)
401 .and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
402
403 match &entry.dap_kind {
404 EntryKind::Watcher { .. } => {
405 Some(self.render_watcher(entry, *state, window, cx))
406 }
407 EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
408 EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
409 }
410 })
411 .collect()
412 }
413
414 pub(crate) fn toggle_entry(&mut self, var_path: &EntryPath, cx: &mut Context<Self>) {
415 let Some(entry) = self.entry_states.get_mut(var_path) else {
416 log::error!("Could not find variable list entry state to toggle");
417 return;
418 };
419
420 entry.is_expanded = !entry.is_expanded;
421 self.build_entries(cx);
422 }
423
424 fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
425 self.cancel(&Default::default(), window, cx);
426 if let Some(variable) = self.entries.first() {
427 self.selection = Some(variable.path.clone());
428 self.build_entries(cx);
429 }
430 }
431
432 fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context<Self>) {
433 self.cancel(&Default::default(), window, cx);
434 if let Some(variable) = self.entries.last() {
435 self.selection = Some(variable.path.clone());
436 self.build_entries(cx);
437 }
438 }
439
440 fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
441 self.cancel(&Default::default(), window, cx);
442 if let Some(selection) = &self.selection {
443 let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
444 if &var.path == selection && ix > 0 {
445 Some(ix.saturating_sub(1))
446 } else {
447 None
448 }
449 });
450
451 if let Some(new_selection) =
452 index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
453 {
454 self.selection = Some(new_selection);
455 self.build_entries(cx);
456 } else {
457 self.select_last(&SelectLast, window, cx);
458 }
459 } else {
460 self.select_last(&SelectLast, window, cx);
461 }
462 }
463
464 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
465 self.cancel(&Default::default(), window, cx);
466 if let Some(selection) = &self.selection {
467 let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
468 if &var.path == selection {
469 Some(ix.saturating_add(1))
470 } else {
471 None
472 }
473 });
474
475 if let Some(new_selection) =
476 index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
477 {
478 self.selection = Some(new_selection);
479 self.build_entries(cx);
480 } else {
481 self.select_first(&SelectFirst, window, cx);
482 }
483 } else {
484 self.select_first(&SelectFirst, window, cx);
485 }
486 }
487
488 fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
489 self.edited_path.take();
490 self.focus_handle.focus(window);
491 cx.notify();
492 }
493
494 fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
495 if let Some((var_path, editor)) = self.edited_path.take() {
496 let Some(state) = self.entry_states.get(&var_path) else {
497 return;
498 };
499
500 let variables_reference = state.parent_reference;
501 let Some(name) = var_path.leaf_name else {
502 return;
503 };
504
505 let Some(stack_frame_id) = self.selected_stack_frame_id else {
506 return;
507 };
508
509 let value = editor.read(cx).text(cx);
510
511 self.session.update(cx, |session, cx| {
512 session.set_variable_value(
513 stack_frame_id,
514 variables_reference,
515 name.into(),
516 value,
517 cx,
518 )
519 });
520 }
521 }
522
523 fn collapse_selected_entry(
524 &mut self,
525 _: &CollapseSelectedEntry,
526 window: &mut Window,
527 cx: &mut Context<Self>,
528 ) {
529 if let Some(ref selected_entry) = self.selection {
530 let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
531 debug_panic!("Trying to toggle variable in variable list that has an no state");
532 return;
533 };
534
535 if !entry_state.is_expanded || !entry_state.has_children {
536 self.select_prev(&SelectPrevious, window, cx);
537 } else {
538 entry_state.is_expanded = false;
539 self.build_entries(cx);
540 }
541 }
542 }
543
544 fn expand_selected_entry(
545 &mut self,
546 _: &ExpandSelectedEntry,
547 window: &mut Window,
548 cx: &mut Context<Self>,
549 ) {
550 if let Some(selected_entry) = &self.selection {
551 let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
552 debug_panic!("Trying to toggle variable in variable list that has an no state");
553 return;
554 };
555
556 if entry_state.is_expanded || !entry_state.has_children {
557 self.select_next(&SelectNext, window, cx);
558 } else {
559 entry_state.is_expanded = true;
560 self.build_entries(cx);
561 }
562 }
563 }
564
565 fn deploy_list_entry_context_menu(
566 &mut self,
567 entry: ListEntry,
568 position: Point<Pixels>,
569 window: &mut Window,
570 cx: &mut Context<Self>,
571 ) {
572 let supports_set_variable = self
573 .session
574 .read(cx)
575 .capabilities()
576 .supports_set_variable
577 .unwrap_or_default();
578
579 let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
580 menu.when(entry.as_variable().is_some(), |menu| {
581 menu.action("Copy Name", CopyVariableName.boxed_clone())
582 .action("Copy Value", CopyVariableValue.boxed_clone())
583 .when(supports_set_variable, |menu| {
584 menu.action("Edit Value", EditVariable.boxed_clone())
585 })
586 .action("Watch Variable", AddWatch.boxed_clone())
587 })
588 .when(entry.as_watcher().is_some(), |menu| {
589 menu.action("Copy Name", CopyVariableName.boxed_clone())
590 .action("Copy Value", CopyVariableValue.boxed_clone())
591 .when(supports_set_variable, |menu| {
592 menu.action("Edit Value", EditVariable.boxed_clone())
593 })
594 .action("Remove Watch", RemoveWatch.boxed_clone())
595 })
596 .context(self.focus_handle.clone())
597 });
598
599 cx.focus_view(&context_menu, window);
600 let subscription = cx.subscribe_in(
601 &context_menu,
602 window,
603 |this, _, _: &DismissEvent, window, cx| {
604 if this.open_context_menu.as_ref().is_some_and(|context_menu| {
605 context_menu.0.focus_handle(cx).contains_focused(window, cx)
606 }) {
607 cx.focus_self(window);
608 }
609 this.open_context_menu.take();
610 cx.notify();
611 },
612 );
613
614 self.open_context_menu = Some((context_menu, position, subscription));
615 }
616
617 fn copy_variable_name(
618 &mut self,
619 _: &CopyVariableName,
620 _window: &mut Window,
621 cx: &mut Context<Self>,
622 ) {
623 let Some(selection) = self.selection.as_ref() else {
624 return;
625 };
626
627 let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
628 return;
629 };
630
631 let variable_name = match &entry.dap_kind {
632 EntryKind::Variable(dap) => dap.name.clone(),
633 EntryKind::Watcher(watcher) => watcher.expression.to_string(),
634 EntryKind::Scope(_) => return,
635 };
636
637 cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
638 }
639
640 fn copy_variable_value(
641 &mut self,
642 _: &CopyVariableValue,
643 _window: &mut Window,
644 cx: &mut Context<Self>,
645 ) {
646 let Some(selection) = self.selection.as_ref() else {
647 return;
648 };
649
650 let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
651 return;
652 };
653
654 let variable_value = match &entry.dap_kind {
655 EntryKind::Variable(dap) => dap.value.clone(),
656 EntryKind::Watcher(watcher) => watcher.value.to_string(),
657 EntryKind::Scope(_) => return,
658 };
659
660 cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
661 }
662
663 fn edit_variable(&mut self, _: &EditVariable, window: &mut Window, cx: &mut Context<Self>) {
664 let Some(selection) = self.selection.as_ref() else {
665 return;
666 };
667
668 let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
669 return;
670 };
671
672 let variable_value = match &entry.dap_kind {
673 EntryKind::Watcher(watcher) => watcher.value.to_string(),
674 EntryKind::Variable(variable) => variable.value.clone(),
675 EntryKind::Scope(_) => return,
676 };
677
678 let editor = Self::create_variable_editor(&variable_value, window, cx);
679 self.edited_path = Some((entry.path.clone(), editor));
680
681 cx.notify();
682 }
683
684 fn add_watcher(&mut self, _: &AddWatch, _: &mut Window, cx: &mut Context<Self>) {
685 let Some(selection) = self.selection.as_ref() else {
686 return;
687 };
688
689 let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
690 return;
691 };
692
693 let Some(variable) = entry.as_variable() else {
694 return;
695 };
696
697 let Some(stack_frame_id) = self.selected_stack_frame_id else {
698 return;
699 };
700
701 let add_watcher_task = self.session.update(cx, |session, cx| {
702 let expression = variable
703 .evaluate_name
704 .clone()
705 .unwrap_or_else(|| variable.name.clone());
706
707 session.add_watcher(expression.into(), stack_frame_id, cx)
708 });
709
710 cx.spawn(async move |this, cx| {
711 add_watcher_task.await?;
712
713 this.update(cx, |this, cx| {
714 this.build_entries(cx);
715 })
716 })
717 .detach_and_log_err(cx);
718 }
719
720 fn remove_watcher(&mut self, _: &RemoveWatch, _: &mut Window, cx: &mut Context<Self>) {
721 let Some(selection) = self.selection.as_ref() else {
722 return;
723 };
724
725 let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
726 return;
727 };
728
729 let Some(watcher) = entry.as_watcher() else {
730 return;
731 };
732
733 self.session.update(cx, |session, _| {
734 session.remove_watcher(watcher.expression.clone());
735 });
736 self.build_entries(cx);
737 }
738
739 #[track_caller]
740 #[cfg(test)]
741 pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
742 const INDENT: &'static str = " ";
743
744 let entries = &self.entries;
745 let mut visual_entries = Vec::with_capacity(entries.len());
746 for entry in entries {
747 let state = self
748 .entry_states
749 .get(&entry.path)
750 .expect("If there's a variable entry there has to be a state that goes with it");
751
752 visual_entries.push(format!(
753 "{}{} {}{}",
754 INDENT.repeat(state.depth - 1),
755 if state.is_expanded { "v" } else { ">" },
756 entry.dap_kind.name(),
757 if self.selection.as_ref() == Some(&entry.path) {
758 " <=== selected"
759 } else {
760 ""
761 }
762 ));
763 }
764
765 pretty_assertions::assert_eq!(expected, visual_entries);
766 }
767
768 #[track_caller]
769 #[cfg(test)]
770 pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
771 self.entries
772 .iter()
773 .filter_map(|entry| match &entry.dap_kind {
774 EntryKind::Scope(scope) => Some(scope),
775 _ => None,
776 })
777 .cloned()
778 .collect()
779 }
780
781 #[track_caller]
782 #[cfg(test)]
783 pub(crate) fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
784 let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
785 let mut idx = 0;
786
787 for entry in self.entries.iter() {
788 match &entry.dap_kind {
789 EntryKind::Watcher { .. } => continue,
790 EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
791 EntryKind::Scope(scope) => {
792 if scopes.len() > 0 {
793 idx += 1;
794 }
795
796 scopes.push((scope.clone(), Vec::new()));
797 }
798 }
799 }
800
801 scopes
802 }
803
804 #[track_caller]
805 #[cfg(test)]
806 pub(crate) fn variables(&self) -> Vec<dap::Variable> {
807 self.entries
808 .iter()
809 .filter_map(|entry| match &entry.dap_kind {
810 EntryKind::Variable(variable) => Some(variable),
811 _ => None,
812 })
813 .cloned()
814 .collect()
815 }
816
817 fn create_variable_editor(default: &str, window: &mut Window, cx: &mut App) -> Entity<Editor> {
818 let editor = cx.new(|cx| {
819 let mut editor = Editor::single_line(window, cx);
820
821 let refinement = TextStyleRefinement {
822 font_size: Some(
823 TextSize::XSmall
824 .rems(cx)
825 .to_pixels(window.rem_size())
826 .into(),
827 ),
828 ..Default::default()
829 };
830 editor.set_text_style_refinement(refinement);
831 editor.set_text(default, window, cx);
832 editor.select_all(&editor::actions::SelectAll, window, cx);
833 editor
834 });
835 editor.focus_handle(cx).focus(window);
836 editor
837 }
838
839 fn variable_color(
840 &self,
841 presentation_hint: Option<&VariablePresentationHint>,
842 cx: &Context<Self>,
843 ) -> VariableColor {
844 let syntax_color_for = |name| cx.theme().syntax().get(name).color;
845 let name = if self.disabled {
846 Some(Color::Disabled.color(cx))
847 } else {
848 match presentation_hint
849 .as_ref()
850 .and_then(|hint| hint.kind.as_ref())
851 .unwrap_or(&VariablePresentationHintKind::Unknown)
852 {
853 VariablePresentationHintKind::Class
854 | VariablePresentationHintKind::BaseClass
855 | VariablePresentationHintKind::InnerClass
856 | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
857 VariablePresentationHintKind::Data => syntax_color_for("variable"),
858 VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
859 }
860 };
861 let value = self
862 .disabled
863 .then(|| Color::Disabled.color(cx))
864 .or_else(|| syntax_color_for("variable.special"));
865
866 VariableColor { name, value }
867 }
868
869 fn render_variable_value(
870 &self,
871 entry: &ListEntry,
872 variable_color: &VariableColor,
873 value: String,
874 cx: &mut Context<Self>,
875 ) -> AnyElement {
876 if !value.is_empty() {
877 div()
878 .w_full()
879 .id(entry.item_value_id())
880 .map(|this| {
881 if let Some((_, editor)) = self
882 .edited_path
883 .as_ref()
884 .filter(|(path, _)| path == &entry.path)
885 {
886 this.child(div().size_full().px_2().child(editor.clone()))
887 } else {
888 this.text_color(cx.theme().colors().text_muted)
889 .when(
890 !self.disabled
891 && self
892 .session
893 .read(cx)
894 .capabilities()
895 .supports_set_variable
896 .unwrap_or_default(),
897 |this| {
898 let path = entry.path.clone();
899 let variable_value = value.clone();
900 this.on_click(cx.listener(
901 move |this, click: &ClickEvent, window, cx| {
902 if click.down.click_count < 2 {
903 return;
904 }
905 let editor = Self::create_variable_editor(
906 &variable_value,
907 window,
908 cx,
909 );
910 this.edited_path = Some((path.clone(), editor));
911
912 cx.notify();
913 },
914 ))
915 },
916 )
917 .child(
918 Label::new(format!("= {}", &value))
919 .single_line()
920 .truncate()
921 .size(LabelSize::Small)
922 .color(Color::Muted)
923 .when_some(variable_color.value, |this, color| {
924 this.color(Color::from(color))
925 }),
926 )
927 }
928 })
929 .into_any_element()
930 } else {
931 Empty.into_any_element()
932 }
933 }
934
935 fn center_truncate_string(s: &str, mut max_chars: usize) -> String {
936 const ELLIPSIS: &str = "...";
937 const MIN_LENGTH: usize = 3;
938
939 max_chars = max_chars.max(MIN_LENGTH);
940
941 let char_count = s.chars().count();
942 if char_count <= max_chars {
943 return s.to_string();
944 }
945
946 if ELLIPSIS.len() + MIN_LENGTH > max_chars {
947 return s.chars().take(MIN_LENGTH).collect();
948 }
949
950 let available_chars = max_chars - ELLIPSIS.len();
951
952 let start_chars = available_chars / 2;
953 let end_chars = available_chars - start_chars;
954 let skip_chars = char_count - end_chars;
955
956 let mut start_boundary = 0;
957 let mut end_boundary = s.len();
958
959 for (i, (byte_idx, _)) in s.char_indices().enumerate() {
960 if i == start_chars {
961 start_boundary = byte_idx.max(MIN_LENGTH);
962 }
963
964 if i == skip_chars {
965 end_boundary = byte_idx;
966 }
967 }
968
969 if start_boundary >= end_boundary {
970 return s.chars().take(MIN_LENGTH).collect();
971 }
972
973 format!("{}{}{}", &s[..start_boundary], ELLIPSIS, &s[end_boundary..])
974 }
975
976 fn render_watcher(
977 &self,
978 entry: &ListEntry,
979 state: EntryState,
980 _window: &mut Window,
981 cx: &mut Context<Self>,
982 ) -> AnyElement {
983 let Some(watcher) = &entry.as_watcher() else {
984 debug_panic!("Called render watcher on non watcher variable list entry variant");
985 return div().into_any_element();
986 };
987
988 let variable_color = self.variable_color(watcher.presentation_hint.as_ref(), cx);
989
990 let is_selected = self
991 .selection
992 .as_ref()
993 .is_some_and(|selection| selection == &entry.path);
994 let var_ref = watcher.variables_reference;
995
996 let colors = get_entry_color(cx);
997 let bg_hover_color = if !is_selected {
998 colors.hover
999 } else {
1000 colors.default
1001 };
1002 let border_color = if is_selected {
1003 colors.marked_active
1004 } else {
1005 colors.default
1006 };
1007 let path = entry.path.clone();
1008
1009 let weak = cx.weak_entity();
1010 let focus_handle = self.focus_handle.clone();
1011 let watcher_len = (self.list_handle.content_size().width.0 / 12.0).floor() - 3.0;
1012 let watcher_len = watcher_len as usize;
1013
1014 div()
1015 .id(entry.item_id())
1016 .group("variable_list_entry")
1017 .pl_2()
1018 .border_1()
1019 .border_r_2()
1020 .border_color(border_color)
1021 .flex()
1022 .w_full()
1023 .h_full()
1024 .hover(|style| style.bg(bg_hover_color))
1025 .on_click(cx.listener({
1026 let path = path.clone();
1027 move |this, _, _window, cx| {
1028 this.selection = Some(path.clone());
1029 cx.notify();
1030 }
1031 }))
1032 .child(
1033 ListItem::new(SharedString::from(format!(
1034 "watcher-{}",
1035 watcher.expression
1036 )))
1037 .selectable(false)
1038 .disabled(self.disabled)
1039 .selectable(false)
1040 .indent_level(state.depth)
1041 .indent_step_size(px(10.))
1042 .always_show_disclosure_icon(true)
1043 .when(var_ref > 0, |list_item| {
1044 list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1045 let var_path = entry.path.clone();
1046 move |this, _, _, cx| {
1047 this.session.update(cx, |session, cx| {
1048 session.variables(var_ref, cx);
1049 });
1050
1051 this.toggle_entry(&var_path, cx);
1052 }
1053 }))
1054 })
1055 .on_secondary_mouse_down(cx.listener({
1056 let path = path.clone();
1057 let entry = entry.clone();
1058 move |this, event: &MouseDownEvent, window, cx| {
1059 this.selection = Some(path.clone());
1060 this.deploy_list_entry_context_menu(
1061 entry.clone(),
1062 event.position,
1063 window,
1064 cx,
1065 );
1066 cx.stop_propagation();
1067 }
1068 }))
1069 .child(
1070 h_flex()
1071 .gap_1()
1072 .text_ui_sm(cx)
1073 .w_full()
1074 .child(
1075 Label::new(&Self::center_truncate_string(
1076 watcher.expression.as_ref(),
1077 watcher_len,
1078 ))
1079 .when_some(variable_color.name, |this, color| {
1080 this.color(Color::from(color))
1081 }),
1082 )
1083 .child(self.render_variable_value(
1084 &entry,
1085 &variable_color,
1086 watcher.value.to_string(),
1087 cx,
1088 )),
1089 )
1090 .end_slot(
1091 IconButton::new(
1092 SharedString::from(format!("watcher-{}-remove-button", watcher.expression)),
1093 IconName::Close,
1094 )
1095 .on_click({
1096 let weak = weak.clone();
1097 let path = path.clone();
1098 move |_, window, cx| {
1099 weak.update(cx, |variable_list, cx| {
1100 variable_list.selection = Some(path.clone());
1101 variable_list.remove_watcher(&RemoveWatch, window, cx);
1102 })
1103 .ok();
1104 }
1105 })
1106 .tooltip(move |window, cx| {
1107 Tooltip::for_action_in(
1108 "Remove Watch",
1109 &RemoveWatch,
1110 &focus_handle,
1111 window,
1112 cx,
1113 )
1114 })
1115 .icon_size(ui::IconSize::Indicator),
1116 ),
1117 )
1118 .into_any()
1119 }
1120
1121 fn render_scope(
1122 &self,
1123 entry: &ListEntry,
1124 state: EntryState,
1125 cx: &mut Context<Self>,
1126 ) -> AnyElement {
1127 let Some(scope) = entry.as_scope() else {
1128 debug_panic!("Called render scope on non scope variable list entry variant");
1129 return div().into_any_element();
1130 };
1131
1132 let var_ref = scope.variables_reference;
1133 let is_selected = self
1134 .selection
1135 .as_ref()
1136 .is_some_and(|selection| selection == &entry.path);
1137
1138 let colors = get_entry_color(cx);
1139 let bg_hover_color = if !is_selected {
1140 colors.hover
1141 } else {
1142 colors.default
1143 };
1144 let border_color = if is_selected {
1145 colors.marked_active
1146 } else {
1147 colors.default
1148 };
1149 let path = entry.path.clone();
1150
1151 div()
1152 .id(var_ref as usize)
1153 .group("variable_list_entry")
1154 .pl_2()
1155 .border_1()
1156 .border_r_2()
1157 .border_color(border_color)
1158 .flex()
1159 .w_full()
1160 .h_full()
1161 .hover(|style| style.bg(bg_hover_color))
1162 .on_click(cx.listener({
1163 move |this, _, _window, cx| {
1164 this.selection = Some(path.clone());
1165 cx.notify();
1166 }
1167 }))
1168 .child(
1169 ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
1170 .selectable(false)
1171 .disabled(self.disabled)
1172 .indent_level(state.depth)
1173 .indent_step_size(px(10.))
1174 .always_show_disclosure_icon(true)
1175 .toggle(state.is_expanded)
1176 .on_toggle({
1177 let var_path = entry.path.clone();
1178 cx.listener(move |this, _, _, cx| this.toggle_entry(&var_path, cx))
1179 })
1180 .child(
1181 div()
1182 .text_ui(cx)
1183 .w_full()
1184 .when(self.disabled, |this| {
1185 this.text_color(Color::Disabled.color(cx))
1186 })
1187 .child(scope.name.clone()),
1188 ),
1189 )
1190 .into_any()
1191 }
1192
1193 fn render_variable(
1194 &self,
1195 variable: &ListEntry,
1196 state: EntryState,
1197 window: &mut Window,
1198 cx: &mut Context<Self>,
1199 ) -> AnyElement {
1200 let Some(dap) = &variable.as_variable() else {
1201 debug_panic!("Called render variable on non variable variable list entry variant");
1202 return div().into_any_element();
1203 };
1204
1205 let variable_color = self.variable_color(dap.presentation_hint.as_ref(), cx);
1206
1207 let var_ref = dap.variables_reference;
1208 let colors = get_entry_color(cx);
1209 let is_selected = self
1210 .selection
1211 .as_ref()
1212 .is_some_and(|selected_path| *selected_path == variable.path);
1213
1214 let bg_hover_color = if !is_selected {
1215 colors.hover
1216 } else {
1217 colors.default
1218 };
1219 let border_color = if is_selected && self.focus_handle.contains_focused(window, cx) {
1220 colors.marked_active
1221 } else {
1222 colors.default
1223 };
1224 let path = variable.path.clone();
1225 div()
1226 .id(variable.item_id())
1227 .group("variable_list_entry")
1228 .pl_2()
1229 .border_1()
1230 .border_r_2()
1231 .border_color(border_color)
1232 .h_4()
1233 .size_full()
1234 .hover(|style| style.bg(bg_hover_color))
1235 .on_click(cx.listener({
1236 let path = path.clone();
1237 move |this, _, _window, cx| {
1238 this.selection = Some(path.clone());
1239 cx.notify();
1240 }
1241 }))
1242 .child(
1243 ListItem::new(SharedString::from(format!(
1244 "variable-item-{}-{}",
1245 dap.name, state.depth
1246 )))
1247 .disabled(self.disabled)
1248 .selectable(false)
1249 .indent_level(state.depth)
1250 .indent_step_size(px(10.))
1251 .always_show_disclosure_icon(true)
1252 .when(var_ref > 0, |list_item| {
1253 list_item.toggle(state.is_expanded).on_toggle(cx.listener({
1254 let var_path = variable.path.clone();
1255 move |this, _, _, cx| {
1256 this.session.update(cx, |session, cx| {
1257 session.variables(var_ref, cx);
1258 });
1259
1260 this.toggle_entry(&var_path, cx);
1261 }
1262 }))
1263 })
1264 .on_secondary_mouse_down(cx.listener({
1265 let path = path.clone();
1266 let entry = variable.clone();
1267 move |this, event: &MouseDownEvent, window, cx| {
1268 this.selection = Some(path.clone());
1269 this.deploy_list_entry_context_menu(
1270 entry.clone(),
1271 event.position,
1272 window,
1273 cx,
1274 );
1275 cx.stop_propagation();
1276 }
1277 }))
1278 .child(
1279 h_flex()
1280 .gap_1()
1281 .text_ui_sm(cx)
1282 .w_full()
1283 .child(
1284 Label::new(&dap.name).when_some(variable_color.name, |this, color| {
1285 this.color(Color::from(color))
1286 }),
1287 )
1288 .child(self.render_variable_value(
1289 &variable,
1290 &variable_color,
1291 dap.value.clone(),
1292 cx,
1293 )),
1294 ),
1295 )
1296 .into_any()
1297 }
1298
1299 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
1300 div()
1301 .occlude()
1302 .id("variable-list-vertical-scrollbar")
1303 .on_mouse_move(cx.listener(|_, _, _, cx| {
1304 cx.notify();
1305 cx.stop_propagation()
1306 }))
1307 .on_hover(|_, _, cx| {
1308 cx.stop_propagation();
1309 })
1310 .on_any_mouse_down(|_, _, cx| {
1311 cx.stop_propagation();
1312 })
1313 .on_mouse_up(
1314 MouseButton::Left,
1315 cx.listener(|_, _, _, cx| {
1316 cx.stop_propagation();
1317 }),
1318 )
1319 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
1320 cx.notify();
1321 }))
1322 .h_full()
1323 .absolute()
1324 .right_1()
1325 .top_1()
1326 .bottom_0()
1327 .w(px(12.))
1328 .cursor_default()
1329 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
1330 }
1331}
1332
1333impl Focusable for VariableList {
1334 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
1335 self.focus_handle.clone()
1336 }
1337}
1338
1339impl Render for VariableList {
1340 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1341 v_flex()
1342 .track_focus(&self.focus_handle)
1343 .key_context("VariableList")
1344 .id("variable-list")
1345 .group("variable-list")
1346 .overflow_y_scroll()
1347 .size_full()
1348 .on_action(cx.listener(Self::select_first))
1349 .on_action(cx.listener(Self::select_last))
1350 .on_action(cx.listener(Self::select_prev))
1351 .on_action(cx.listener(Self::select_next))
1352 .on_action(cx.listener(Self::cancel))
1353 .on_action(cx.listener(Self::confirm))
1354 .on_action(cx.listener(Self::expand_selected_entry))
1355 .on_action(cx.listener(Self::collapse_selected_entry))
1356 .on_action(cx.listener(Self::copy_variable_name))
1357 .on_action(cx.listener(Self::copy_variable_value))
1358 .on_action(cx.listener(Self::edit_variable))
1359 .on_action(cx.listener(Self::add_watcher))
1360 .on_action(cx.listener(Self::remove_watcher))
1361 .child(
1362 uniform_list(
1363 "variable-list",
1364 self.entries.len(),
1365 cx.processor(move |this, range: Range<usize>, window, cx| {
1366 this.render_entries(range, window, cx)
1367 }),
1368 )
1369 .track_scroll(self.list_handle.clone())
1370 .gap_1_5()
1371 .size_full()
1372 .flex_grow(),
1373 )
1374 .children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
1375 deferred(
1376 anchored()
1377 .position(*position)
1378 .anchor(gpui::Corner::TopLeft)
1379 .child(menu.clone()),
1380 )
1381 .with_priority(1)
1382 }))
1383 .child(self.render_vertical_scrollbar(cx))
1384 }
1385}
1386
1387struct EntryColors {
1388 default: Hsla,
1389 hover: Hsla,
1390 marked_active: Hsla,
1391}
1392
1393fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
1394 let colors = cx.theme().colors();
1395
1396 EntryColors {
1397 default: colors.panel_background,
1398 hover: colors.ghost_element_hover,
1399 marked_active: colors.ghost_element_selected,
1400 }
1401}
1402
1403#[cfg(test)]
1404mod tests {
1405 use super::*;
1406
1407 #[test]
1408 fn test_center_truncate_string() {
1409 // Test string shorter than limit - should not be truncated
1410 assert_eq!(VariableList::center_truncate_string("short", 10), "short");
1411
1412 // Test exact length - should not be truncated
1413 assert_eq!(
1414 VariableList::center_truncate_string("exactly_10", 10),
1415 "exactly_10"
1416 );
1417
1418 // Test simple truncation
1419 assert_eq!(
1420 VariableList::center_truncate_string("value->value2->value3->value4", 20),
1421 "value->v...3->value4"
1422 );
1423
1424 // Test with very long expression
1425 assert_eq!(
1426 VariableList::center_truncate_string(
1427 "object->property1->property2->property3->property4->property5",
1428 30
1429 ),
1430 "object->prope...ty4->property5"
1431 );
1432
1433 // Test edge case with limit equal to ellipsis length
1434 assert_eq!(VariableList::center_truncate_string("anything", 3), "any");
1435
1436 // Test edge case with limit less than ellipsis length
1437 assert_eq!(VariableList::center_truncate_string("anything", 2), "any");
1438
1439 // Test with UTF-8 characters
1440 assert_eq!(
1441 VariableList::center_truncate_string("café->résumé->naïve->voilà", 15),
1442 "café->...>voilà"
1443 );
1444
1445 // Test with emoji (multi-byte UTF-8)
1446 assert_eq!(
1447 VariableList::center_truncate_string("😀->happy->face->😎->cool", 15),
1448 "😀->hap...->cool"
1449 );
1450 }
1451}