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