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