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