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