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