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