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