Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:37:46.142Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

variable_list.rs

1667 lines · 58.7 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.