Detailed changes
@@ -894,7 +894,10 @@
"right": "variable_list::ExpandSelectedEntry",
"enter": "variable_list::EditVariable",
"ctrl-c": "variable_list::CopyVariableValue",
- "ctrl-alt-c": "variable_list::CopyVariableName"
+ "ctrl-alt-c": "variable_list::CopyVariableName",
+ "delete": "variable_list::RemoveWatch",
+ "backspace": "variable_list::RemoveWatch",
+ "alt-enter": "variable_list::AddWatch"
}
},
{
@@ -1037,7 +1040,8 @@
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
- "enter": "menu::Confirm"
+ "enter": "menu::Confirm",
+ "alt-enter": "console::WatchExpression"
}
},
{
@@ -864,7 +864,10 @@
"right": "variable_list::ExpandSelectedEntry",
"enter": "variable_list::EditVariable",
"cmd-c": "variable_list::CopyVariableValue",
- "cmd-alt-c": "variable_list::CopyVariableName"
+ "cmd-alt-c": "variable_list::CopyVariableName",
+ "delete": "variable_list::RemoveWatch",
+ "backspace": "variable_list::RemoveWatch",
+ "alt-enter": "variable_list::AddWatch"
}
},
{
@@ -1135,7 +1138,8 @@
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
- "enter": "menu::Confirm"
+ "enter": "menu::Confirm",
+ "alt-enter": "console::WatchExpression"
}
},
{
@@ -9,8 +9,8 @@ use dap::OutputEvent;
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
use fuzzy::StringMatchCandidate;
use gpui::{
- Context, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task,
- TextStyle, WeakEntity,
+ Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
+ Render, Subscription, Task, TextStyle, WeakEntity, actions,
};
use language::{Buffer, CodeLabel, ToOffset};
use menu::Confirm;
@@ -21,7 +21,9 @@ use project::{
use settings::Settings;
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
use theme::{Theme, ThemeSettings};
-use ui::{Divider, prelude::*};
+use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
+
+actions!(console, [WatchExpression]);
pub struct Console {
console: Entity<Editor>,
@@ -329,6 +331,40 @@ impl Console {
});
}
+ pub fn watch_expression(
+ &mut self,
+ _: &WatchExpression,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let expression = self.query_bar.update(cx, |editor, cx| {
+ let expression = editor.text(cx);
+ cx.defer_in(window, |editor, window, cx| {
+ editor.clear(window, cx);
+ });
+
+ expression
+ });
+
+ self.session.update(cx, |session, cx| {
+ session
+ .evaluate(
+ expression.clone(),
+ Some(dap::EvaluateArgumentsContext::Repl),
+ self.stack_frame_list.read(cx).opened_stack_frame_id(),
+ None,
+ cx,
+ )
+ .detach();
+
+ if let Some(stack_frame_id) = self.stack_frame_list.read(cx).opened_stack_frame_id() {
+ session
+ .add_watcher(expression.into(), stack_frame_id, cx)
+ .detach();
+ }
+ });
+ }
+
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
let expression = self.query_bar.update(cx, |editor, cx| {
let expression = editor.text(cx);
@@ -352,6 +388,43 @@ impl Console {
});
}
+ fn render_submit_menu(
+ &self,
+ id: impl Into<ElementId>,
+ keybinding_target: Option<FocusHandle>,
+ cx: &App,
+ ) -> impl IntoElement {
+ PopoverMenu::new(id.into())
+ .trigger(
+ ui::ButtonLike::new_rounded_right("console-confirm-split-button-right")
+ .layer(ui::ElevationIndex::ModalSurface)
+ .size(ui::ButtonSize::None)
+ .child(
+ div()
+ .px_1()
+ .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
+ ),
+ )
+ .when(
+ self.stack_frame_list
+ .read(cx)
+ .opened_stack_frame_id()
+ .is_some(),
+ |this| {
+ this.menu(move |window, cx| {
+ Some(ContextMenu::build(window, cx, |context_menu, _, _| {
+ context_menu
+ .when_some(keybinding_target.clone(), |el, keybinding_target| {
+ el.context(keybinding_target.clone())
+ })
+ .action("Watch expression", WatchExpression.boxed_clone())
+ }))
+ })
+ },
+ )
+ .anchor(Corner::TopRight)
+ }
+
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
}
@@ -408,15 +481,52 @@ impl Console {
impl Render for Console {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let query_focus_handle = self.query_bar.focus_handle(cx);
+
v_flex()
.track_focus(&self.focus_handle)
.key_context("DebugConsole")
.on_action(cx.listener(Self::evaluate))
+ .on_action(cx.listener(Self::watch_expression))
.size_full()
.child(self.render_console(cx))
.when(self.is_running(cx), |this| {
- this.child(Divider::horizontal())
- .child(self.render_query_bar(cx))
+ this.child(Divider::horizontal()).child(
+ h_flex()
+ .gap_1()
+ .bg(cx.theme().colors().editor_background)
+ .child(self.render_query_bar(cx))
+ .child(SplitButton::new(
+ ui::ButtonLike::new_rounded_all(ElementId::Name(
+ "split-button-left-confirm-button".into(),
+ ))
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(Box::new(Confirm), cx)
+ })
+ .tooltip({
+ let query_focus_handle = query_focus_handle.clone();
+
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Evaluate",
+ &Confirm,
+ &query_focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .layer(ui::ElevationIndex::ModalSurface)
+ .size(ui::ButtonSize::Compact)
+ .child(Label::new("Evaluate")),
+ self.render_submit_menu(
+ ElementId::Name("split-button-right-confirm-button".into()),
+ Some(query_focus_handle.clone()),
+ cx,
+ )
+ .into_any_element(),
+ )),
+ )
})
.border_2()
}
@@ -1,15 +1,18 @@
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
-use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
+use dap::{
+ ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
+ VariableReference,
+};
use editor::Editor;
use gpui::{
- Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, FocusHandle,
- Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
+ Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
+ FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
-use project::debugger::session::{Session, SessionEvent};
+use project::debugger::session::{Session, SessionEvent, Watcher};
use std::{collections::HashMap, ops::Range, sync::Arc};
-use ui::{ContextMenu, ListItem, Scrollbar, ScrollbarState, prelude::*};
+use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::debug_panic;
actions!(
@@ -19,7 +22,9 @@ actions!(
CollapseSelectedEntry,
CopyVariableName,
CopyVariableValue,
- EditVariable
+ EditVariable,
+ AddWatch,
+ RemoveWatch,
]
);
@@ -38,6 +43,13 @@ pub(crate) struct EntryPath {
}
impl EntryPath {
+ fn for_watcher(expression: impl Into<SharedString>) -> Self {
+ Self {
+ leaf_name: Some(expression.into()),
+ indices: Arc::new([]),
+ }
+ }
+
fn for_scope(scope_name: impl Into<SharedString>) -> Self {
Self {
leaf_name: Some(scope_name.into()),
@@ -68,11 +80,19 @@ impl EntryPath {
#[derive(Debug, Clone, PartialEq)]
enum EntryKind {
+ Watcher(Watcher),
Variable(dap::Variable),
Scope(dap::Scope),
}
impl EntryKind {
+ fn as_watcher(&self) -> Option<&Watcher> {
+ match self {
+ EntryKind::Watcher(watcher) => Some(watcher),
+ _ => None,
+ }
+ }
+
fn as_variable(&self) -> Option<&dap::Variable> {
match self {
EntryKind::Variable(dap) => Some(dap),
@@ -87,9 +107,10 @@ impl EntryKind {
}
}
- #[allow(dead_code)]
+ #[cfg(test)]
fn name(&self) -> &str {
match self {
+ EntryKind::Watcher(watcher) => &watcher.expression,
EntryKind::Variable(dap) => &dap.name,
EntryKind::Scope(dap) => &dap.name,
}
@@ -103,6 +124,10 @@ struct ListEntry {
}
impl ListEntry {
+ fn as_watcher(&self) -> Option<&Watcher> {
+ self.dap_kind.as_watcher()
+ }
+
fn as_variable(&self) -> Option<&dap::Variable> {
self.dap_kind.as_variable()
}
@@ -114,6 +139,7 @@ impl ListEntry {
fn item_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
+ EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
};
@@ -126,6 +152,7 @@ impl ListEntry {
fn item_value_id(&self) -> ElementId {
use std::fmt::Write;
let mut id = match &self.dap_kind {
+ EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
};
@@ -137,6 +164,11 @@ impl ListEntry {
}
}
+struct VariableColor {
+ name: Option<Hsla>,
+ value: Option<Hsla>,
+}
+
pub struct VariableList {
entries: Vec<ListEntry>,
entry_states: HashMap<EntryPath, EntryState>,
@@ -169,7 +201,7 @@ impl VariableList {
this.edited_path.take();
this.selected_stack_frame_id.take();
}
- SessionEvent::Variables => {
+ SessionEvent::Variables | SessionEvent::Watchers => {
this.build_entries(cx);
}
_ => {}
@@ -216,6 +248,7 @@ impl VariableList {
};
let mut entries = vec![];
+
let scopes: Vec<_> = self.session.update(cx, |session, cx| {
session.scopes(stack_frame_id, cx).iter().cloned().collect()
});
@@ -249,11 +282,27 @@ impl VariableList {
})
.collect::<Vec<_>>();
+ let watches = self.session.read(cx).watchers().clone();
+ stack.extend(
+ watches
+ .into_values()
+ .map(|watcher| {
+ (
+ watcher.variables_reference,
+ watcher.variables_reference,
+ EntryPath::for_watcher(watcher.expression.clone()),
+ EntryKind::Watcher(watcher.clone()),
+ )
+ })
+ .collect::<Vec<_>>(),
+ );
+
let scopes_count = stack.len();
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
{
match &dap_kind {
+ EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()),
EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()),
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
}
@@ -312,6 +361,9 @@ impl VariableList {
match event {
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
self.selected_stack_frame_id = Some(*stack_frame_id);
+ self.session.update(cx, |session, cx| {
+ session.refresh_watchers(*stack_frame_id, cx);
+ });
self.build_entries(cx);
}
StackFrameListEvent::BuiltEntries => {}
@@ -323,7 +375,7 @@ impl VariableList {
.iter()
.filter_map(|entry| match &entry.dap_kind {
EntryKind::Variable(dap) => Some(dap.clone()),
- EntryKind::Scope(_) => None,
+ EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
})
.collect()
}
@@ -342,6 +394,9 @@ impl VariableList {
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
match &entry.dap_kind {
+ EntryKind::Watcher { .. } => {
+ Some(self.render_watcher(entry, *state, window, cx))
+ }
EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)),
}
@@ -434,14 +489,26 @@ impl VariableList {
let Some(state) = self.entry_states.get(&var_path) else {
return;
};
+
let variables_reference = state.parent_reference;
let Some(name) = var_path.leaf_name else {
return;
};
+
+ let Some(stack_frame_id) = self.selected_stack_frame_id else {
+ return;
+ };
+
let value = editor.read(cx).text(cx);
self.session.update(cx, |session, cx| {
- session.set_variable_value(variables_reference, name.into(), value, cx)
+ session.set_variable_value(
+ stack_frame_id,
+ variables_reference,
+ name.into(),
+ value,
+ cx,
+ )
});
}
}
@@ -488,18 +555,38 @@ impl VariableList {
}
}
- fn deploy_variable_context_menu(
+ fn deploy_list_entry_context_menu(
&mut self,
- _variable: ListEntry,
+ entry: ListEntry,
position: Point<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let supports_set_variable = self
+ .session
+ .read(cx)
+ .capabilities()
+ .supports_set_variable
+ .unwrap_or_default();
+
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
- menu.action("Copy Name", CopyVariableName.boxed_clone())
- .action("Copy Value", CopyVariableValue.boxed_clone())
- .action("Edit Value", EditVariable.boxed_clone())
- .context(self.focus_handle.clone())
+ menu.when(entry.as_variable().is_some(), |menu| {
+ menu.action("Copy Name", CopyVariableName.boxed_clone())
+ .action("Copy Value", CopyVariableValue.boxed_clone())
+ .when(supports_set_variable, |menu| {
+ menu.action("Edit Value", EditVariable.boxed_clone())
+ })
+ .action("Watch Variable", AddWatch.boxed_clone())
+ })
+ .when(entry.as_watcher().is_some(), |menu| {
+ menu.action("Copy Name", CopyVariableName.boxed_clone())
+ .action("Copy Value", CopyVariableValue.boxed_clone())
+ .when(supports_set_variable, |menu| {
+ menu.action("Edit Value", EditVariable.boxed_clone())
+ })
+ .action("Remove Watch", RemoveWatch.boxed_clone())
+ })
+ .context(self.focus_handle.clone())
});
cx.focus_view(&context_menu, window);
@@ -529,13 +616,18 @@ impl VariableList {
let Some(selection) = self.selection.as_ref() else {
return;
};
+
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
return;
};
- let Some(variable) = entry.as_variable() else {
- return;
+
+ let variable_name = match &entry.dap_kind {
+ EntryKind::Variable(dap) => dap.name.clone(),
+ EntryKind::Watcher(watcher) => watcher.expression.to_string(),
+ EntryKind::Scope(_) => return,
};
- cx.write_to_clipboard(ClipboardItem::new_string(variable.name.clone()));
+
+ cx.write_to_clipboard(ClipboardItem::new_string(variable_name));
}
fn copy_variable_value(
@@ -547,32 +639,96 @@ impl VariableList {
let Some(selection) = self.selection.as_ref() else {
return;
};
+
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
return;
};
- let Some(variable) = entry.as_variable() else {
- return;
+
+ let variable_value = match &entry.dap_kind {
+ EntryKind::Variable(dap) => dap.value.clone(),
+ EntryKind::Watcher(watcher) => watcher.value.to_string(),
+ EntryKind::Scope(_) => return,
};
- cx.write_to_clipboard(ClipboardItem::new_string(variable.value.clone()));
+
+ cx.write_to_clipboard(ClipboardItem::new_string(variable_value));
}
fn edit_variable(&mut self, _: &EditVariable, window: &mut Window, cx: &mut Context<Self>) {
let Some(selection) = self.selection.as_ref() else {
return;
};
+
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
return;
};
- let Some(variable) = entry.as_variable() else {
- return;
+
+ let variable_value = match &entry.dap_kind {
+ EntryKind::Watcher(watcher) => watcher.value.to_string(),
+ EntryKind::Variable(variable) => variable.value.clone(),
+ EntryKind::Scope(_) => return,
};
- let editor = Self::create_variable_editor(&variable.value, window, cx);
+ let editor = Self::create_variable_editor(&variable_value, window, cx);
self.edited_path = Some((entry.path.clone(), editor));
cx.notify();
}
+ fn add_watcher(&mut self, _: &AddWatch, _: &mut Window, cx: &mut Context<Self>) {
+ let Some(selection) = self.selection.as_ref() else {
+ return;
+ };
+
+ let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
+ return;
+ };
+
+ let Some(variable) = entry.as_variable() else {
+ return;
+ };
+
+ let Some(stack_frame_id) = self.selected_stack_frame_id else {
+ return;
+ };
+
+ let add_watcher_task = self.session.update(cx, |session, cx| {
+ let expression = variable
+ .evaluate_name
+ .clone()
+ .unwrap_or_else(|| variable.name.clone());
+
+ session.add_watcher(expression.into(), stack_frame_id, cx)
+ });
+
+ cx.spawn(async move |this, cx| {
+ add_watcher_task.await?;
+
+ this.update(cx, |this, cx| {
+ this.build_entries(cx);
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn remove_watcher(&mut self, _: &RemoveWatch, _: &mut Window, cx: &mut Context<Self>) {
+ let Some(selection) = self.selection.as_ref() else {
+ return;
+ };
+
+ let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
+ return;
+ };
+
+ let Some(watcher) = entry.as_watcher() else {
+ return;
+ };
+
+ self.session.update(cx, |session, _| {
+ session.remove_watcher(watcher.expression.clone());
+ });
+ self.build_entries(cx);
+ }
+
#[track_caller]
#[cfg(test)]
pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
@@ -623,6 +779,7 @@ impl VariableList {
for entry in self.entries.iter() {
match &entry.dap_kind {
+ EntryKind::Watcher { .. } => continue,
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
EntryKind::Scope(scope) => {
if scopes.len() > 0 {
@@ -672,6 +829,288 @@ impl VariableList {
editor
}
+ fn variable_color(
+ &self,
+ presentation_hint: Option<&VariablePresentationHint>,
+ cx: &Context<Self>,
+ ) -> VariableColor {
+ let syntax_color_for = |name| cx.theme().syntax().get(name).color;
+ let name = if self.disabled {
+ Some(Color::Disabled.color(cx))
+ } else {
+ match presentation_hint
+ .as_ref()
+ .and_then(|hint| hint.kind.as_ref())
+ .unwrap_or(&VariablePresentationHintKind::Unknown)
+ {
+ VariablePresentationHintKind::Class
+ | VariablePresentationHintKind::BaseClass
+ | VariablePresentationHintKind::InnerClass
+ | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
+ VariablePresentationHintKind::Data => syntax_color_for("variable"),
+ VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
+ }
+ };
+ let value = self
+ .disabled
+ .then(|| Color::Disabled.color(cx))
+ .or_else(|| syntax_color_for("variable.special"));
+
+ VariableColor { name, value }
+ }
+
+ fn render_variable_value(
+ &self,
+ entry: &ListEntry,
+ variable_color: &VariableColor,
+ value: String,
+ cx: &mut Context<Self>,
+ ) -> AnyElement {
+ if !value.is_empty() {
+ div()
+ .w_full()
+ .id(entry.item_value_id())
+ .map(|this| {
+ if let Some((_, editor)) = self
+ .edited_path
+ .as_ref()
+ .filter(|(path, _)| path == &entry.path)
+ {
+ this.child(div().size_full().px_2().child(editor.clone()))
+ } else {
+ this.text_color(cx.theme().colors().text_muted)
+ .when(
+ !self.disabled
+ && self
+ .session
+ .read(cx)
+ .capabilities()
+ .supports_set_variable
+ .unwrap_or_default(),
+ |this| {
+ let path = entry.path.clone();
+ let variable_value = value.clone();
+ this.on_click(cx.listener(
+ move |this, click: &ClickEvent, window, cx| {
+ if click.down.click_count < 2 {
+ return;
+ }
+ let editor = Self::create_variable_editor(
+ &variable_value,
+ window,
+ cx,
+ );
+ this.edited_path = Some((path.clone(), editor));
+
+ cx.notify();
+ },
+ ))
+ },
+ )
+ .child(
+ Label::new(format!("= {}", &value))
+ .single_line()
+ .truncate()
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .when_some(variable_color.value, |this, color| {
+ this.color(Color::from(color))
+ }),
+ )
+ }
+ })
+ .into_any_element()
+ } else {
+ Empty.into_any_element()
+ }
+ }
+
+ fn center_truncate_string(s: &str, mut max_chars: usize) -> String {
+ const ELLIPSIS: &str = "...";
+ const MIN_LENGTH: usize = 3;
+
+ max_chars = max_chars.max(MIN_LENGTH);
+
+ let char_count = s.chars().count();
+ if char_count <= max_chars {
+ return s.to_string();
+ }
+
+ if ELLIPSIS.len() + MIN_LENGTH > max_chars {
+ return s.chars().take(MIN_LENGTH).collect();
+ }
+
+ let available_chars = max_chars - ELLIPSIS.len();
+
+ let start_chars = available_chars / 2;
+ let end_chars = available_chars - start_chars;
+ let skip_chars = char_count - end_chars;
+
+ let mut start_boundary = 0;
+ let mut end_boundary = s.len();
+
+ for (i, (byte_idx, _)) in s.char_indices().enumerate() {
+ if i == start_chars {
+ start_boundary = byte_idx.max(MIN_LENGTH);
+ }
+
+ if i == skip_chars {
+ end_boundary = byte_idx;
+ }
+ }
+
+ if start_boundary >= end_boundary {
+ return s.chars().take(MIN_LENGTH).collect();
+ }
+
+ format!("{}{}{}", &s[..start_boundary], ELLIPSIS, &s[end_boundary..])
+ }
+
+ fn render_watcher(
+ &self,
+ entry: &ListEntry,
+ state: EntryState,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> AnyElement {
+ let Some(watcher) = &entry.as_watcher() else {
+ debug_panic!("Called render watcher on non watcher variable list entry variant");
+ return div().into_any_element();
+ };
+
+ let variable_color = self.variable_color(watcher.presentation_hint.as_ref(), cx);
+
+ let is_selected = self
+ .selection
+ .as_ref()
+ .is_some_and(|selection| selection == &entry.path);
+ let var_ref = watcher.variables_reference;
+
+ let colors = get_entry_color(cx);
+ let bg_hover_color = if !is_selected {
+ colors.hover
+ } else {
+ colors.default
+ };
+ let border_color = if is_selected {
+ colors.marked_active
+ } else {
+ colors.default
+ };
+ let path = entry.path.clone();
+
+ let weak = cx.weak_entity();
+ let focus_handle = self.focus_handle.clone();
+ let watcher_len = (self.list_handle.content_size().width.0 / 12.0).floor() - 3.0;
+ let watcher_len = watcher_len as usize;
+
+ div()
+ .id(entry.item_id())
+ .group("variable_list_entry")
+ .pl_2()
+ .border_1()
+ .border_r_2()
+ .border_color(border_color)
+ .flex()
+ .w_full()
+ .h_full()
+ .hover(|style| style.bg(bg_hover_color))
+ .on_click(cx.listener({
+ let path = path.clone();
+ move |this, _, _window, cx| {
+ this.selection = Some(path.clone());
+ cx.notify();
+ }
+ }))
+ .child(
+ ListItem::new(SharedString::from(format!(
+ "watcher-{}",
+ watcher.expression
+ )))
+ .selectable(false)
+ .disabled(self.disabled)
+ .selectable(false)
+ .indent_level(state.depth)
+ .indent_step_size(px(10.))
+ .always_show_disclosure_icon(true)
+ .when(var_ref > 0, |list_item| {
+ list_item.toggle(state.is_expanded).on_toggle(cx.listener({
+ let var_path = entry.path.clone();
+ move |this, _, _, cx| {
+ this.session.update(cx, |session, cx| {
+ session.variables(var_ref, cx);
+ });
+
+ this.toggle_entry(&var_path, cx);
+ }
+ }))
+ })
+ .on_secondary_mouse_down(cx.listener({
+ let path = path.clone();
+ let entry = entry.clone();
+ move |this, event: &MouseDownEvent, window, cx| {
+ this.selection = Some(path.clone());
+ this.deploy_list_entry_context_menu(
+ entry.clone(),
+ event.position,
+ window,
+ cx,
+ );
+ cx.stop_propagation();
+ }
+ }))
+ .child(
+ h_flex()
+ .gap_1()
+ .text_ui_sm(cx)
+ .w_full()
+ .child(
+ Label::new(&Self::center_truncate_string(
+ watcher.expression.as_ref(),
+ watcher_len,
+ ))
+ .when_some(variable_color.name, |this, color| {
+ this.color(Color::from(color))
+ }),
+ )
+ .child(self.render_variable_value(
+ &entry,
+ &variable_color,
+ watcher.value.to_string(),
+ cx,
+ )),
+ )
+ .end_slot(
+ IconButton::new(
+ SharedString::from(format!("watcher-{}-remove-button", watcher.expression)),
+ IconName::Close,
+ )
+ .on_click({
+ let weak = weak.clone();
+ let path = path.clone();
+ move |_, window, cx| {
+ weak.update(cx, |variable_list, cx| {
+ variable_list.selection = Some(path.clone());
+ variable_list.remove_watcher(&RemoveWatch, window, cx);
+ })
+ .ok();
+ }
+ })
+ .tooltip(move |window, cx| {
+ Tooltip::for_action_in(
+ "Remove Watch",
+ &RemoveWatch,
+ &focus_handle,
+ window,
+ cx,
+ )
+ })
+ .icon_size(ui::IconSize::Indicator),
+ ),
+ )
+ .into_any()
+ }
+
fn render_scope(
&self,
entry: &ListEntry,
@@ -751,36 +1190,12 @@ impl VariableList {
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
- let dap = match &variable.dap_kind {
- EntryKind::Variable(dap) => dap,
- EntryKind::Scope(_) => {
- debug_panic!("Called render variable on variable list entry kind scope");
- return div().into_any_element();
- }
+ let Some(dap) = &variable.as_variable() else {
+ debug_panic!("Called render variable on non variable variable list entry variant");
+ return div().into_any_element();
};
- let syntax_color_for = |name| cx.theme().syntax().get(name).color;
- let variable_name_color = if self.disabled {
- Some(Color::Disabled.color(cx))
- } else {
- match &dap
- .presentation_hint
- .as_ref()
- .and_then(|hint| hint.kind.as_ref())
- .unwrap_or(&VariablePresentationHintKind::Unknown)
- {
- VariablePresentationHintKind::Class
- | VariablePresentationHintKind::BaseClass
- | VariablePresentationHintKind::InnerClass
- | VariablePresentationHintKind::MostDerivedClass => syntax_color_for("type"),
- VariablePresentationHintKind::Data => syntax_color_for("variable"),
- VariablePresentationHintKind::Unknown | _ => syntax_color_for("variable"),
- }
- };
- let variable_color = self
- .disabled
- .then(|| Color::Disabled.color(cx))
- .or_else(|| syntax_color_for("variable.special"));
+ let variable_color = self.variable_color(dap.presentation_hint.as_ref(), cx);
let var_ref = dap.variables_reference;
let colors = get_entry_color(cx);
@@ -811,6 +1226,7 @@ impl VariableList {
.size_full()
.hover(|style| style.bg(bg_hover_color))
.on_click(cx.listener({
+ let path = path.clone();
move |this, _, _window, cx| {
this.selection = Some(path.clone());
cx.notify();
@@ -839,11 +1255,12 @@ impl VariableList {
}))
})
.on_secondary_mouse_down(cx.listener({
- let variable = variable.clone();
+ let path = path.clone();
+ let entry = variable.clone();
move |this, event: &MouseDownEvent, window, cx| {
- this.selection = Some(variable.path.clone());
- this.deploy_variable_context_menu(
- variable.clone(),
+ this.selection = Some(path.clone());
+ this.deploy_list_entry_context_menu(
+ entry.clone(),
event.position,
window,
cx,
@@ -857,62 +1274,16 @@ impl VariableList {
.text_ui_sm(cx)
.w_full()
.child(
- Label::new(&dap.name).when_some(variable_name_color, |this, color| {
+ Label::new(&dap.name).when_some(variable_color.name, |this, color| {
this.color(Color::from(color))
}),
)
- .when(!dap.value.is_empty(), |this| {
- this.child(div().w_full().id(variable.item_value_id()).map(|this| {
- if let Some((_, editor)) = self
- .edited_path
- .as_ref()
- .filter(|(path, _)| path == &variable.path)
- {
- this.child(div().size_full().px_2().child(editor.clone()))
- } else {
- this.text_color(cx.theme().colors().text_muted)
- .when(
- !self.disabled
- && self
- .session
- .read(cx)
- .capabilities()
- .supports_set_variable
- .unwrap_or_default(),
- |this| {
- let path = variable.path.clone();
- let variable_value = dap.value.clone();
- this.on_click(cx.listener(
- move |this, click: &ClickEvent, window, cx| {
- if click.down.click_count < 2 {
- return;
- }
- let editor = Self::create_variable_editor(
- &variable_value,
- window,
- cx,
- );
- this.edited_path =
- Some((path.clone(), editor));
-
- cx.notify();
- },
- ))
- },
- )
- .child(
- Label::new(format!("= {}", &dap.value))
- .single_line()
- .truncate()
- .size(LabelSize::Small)
- .color(Color::Muted)
- .when_some(variable_color, |this, color| {
- this.color(Color::from(color))
- }),
- )
- }
- }))
- }),
+ .child(self.render_variable_value(
+ &variable,
+ &variable_color,
+ dap.value.clone(),
+ cx,
+ )),
),
)
.into_any()
@@ -978,6 +1349,8 @@ impl Render for VariableList {
.on_action(cx.listener(Self::copy_variable_name))
.on_action(cx.listener(Self::copy_variable_value))
.on_action(cx.listener(Self::edit_variable))
+ .on_action(cx.listener(Self::add_watcher))
+ .on_action(cx.listener(Self::remove_watcher))
.child(
uniform_list(
"variable-list",
@@ -1019,3 +1392,53 @@ fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
marked_active: colors.ghost_element_selected,
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_center_truncate_string() {
+ // Test string shorter than limit - should not be truncated
+ assert_eq!(VariableList::center_truncate_string("short", 10), "short");
+
+ // Test exact length - should not be truncated
+ assert_eq!(
+ VariableList::center_truncate_string("exactly_10", 10),
+ "exactly_10"
+ );
+
+ // Test simple truncation
+ assert_eq!(
+ VariableList::center_truncate_string("value->value2->value3->value4", 20),
+ "value->v...3->value4"
+ );
+
+ // Test with very long expression
+ assert_eq!(
+ VariableList::center_truncate_string(
+ "object->property1->property2->property3->property4->property5",
+ 30
+ ),
+ "object->prope...ty4->property5"
+ );
+
+ // Test edge case with limit equal to ellipsis length
+ assert_eq!(VariableList::center_truncate_string("anything", 3), "any");
+
+ // Test edge case with limit less than ellipsis length
+ assert_eq!(VariableList::center_truncate_string("anything", 2), "any");
+
+ // Test with UTF-8 characters
+ assert_eq!(
+ VariableList::center_truncate_string("café->résumé->naïve->voilà", 15),
+ "café->...>voilà"
+ );
+
+ // Test with emoji (multi-byte UTF-8)
+ assert_eq!(
+ VariableList::center_truncate_string("😀->happy->face->😎->cool", 15),
+ "😀->hap...->cool"
+ );
+ }
+}
@@ -6,18 +6,21 @@ use std::sync::{
use crate::{
DebugPanel,
persistence::DebuggerPaneItem,
- session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
+ session::running::variable_list::{
+ AddWatch, CollapseSelectedEntry, ExpandSelectedEntry, RemoveWatch,
+ },
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use collections::HashMap;
use dap::{
Scope, StackFrame, Variable,
- requests::{Initialize, Launch, Scopes, StackTrace, Variables},
+ requests::{Evaluate, Initialize, Launch, Scopes, StackTrace, Variables},
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::{SelectFirst, SelectNext, SelectPrevious};
use project::{FakeFs, Project};
use serde_json::json;
+use ui::SharedString;
use unindent::Unindent as _;
use util::path;
@@ -1828,3 +1831,515 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
assert_eq!(variables, frame_2_variables,);
});
}
+
+#[gpui::test]
+async fn test_add_and_remove_watcher(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+
+ let test_file_content = r#"
+ const variable1 = "Value 1";
+ const variable2 = "Value 2";
+ "#
+ .unindent();
+
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "src": {
+ "test.js": test_file_content,
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ workspace
+ .update(cx, |workspace, window, cx| {
+ workspace.focus_panel::<DebugPanel>(window, cx);
+ })
+ .unwrap();
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
+
+ client.on_request::<dap::requests::Threads, _>(move |_, _| {
+ Ok(dap::ThreadsResponse {
+ threads: vec![dap::Thread {
+ id: 1,
+ name: "Thread 1".into(),
+ }],
+ })
+ });
+
+ let stack_frames = vec![StackFrame {
+ id: 1,
+ name: "Stack Frame 1".into(),
+ source: Some(dap::Source {
+ name: Some("test.js".into()),
+ path: Some(path!("/project/src/test.js").into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 1,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: None,
+ }];
+
+ client.on_request::<StackTrace, _>({
+ let stack_frames = Arc::new(stack_frames.clone());
+ move |_, args| {
+ assert_eq!(1, args.thread_id);
+
+ Ok(dap::StackTraceResponse {
+ stack_frames: (*stack_frames).clone(),
+ total_frames: None,
+ })
+ }
+ });
+
+ let scopes = vec![Scope {
+ name: "Scope 1".into(),
+ presentation_hint: None,
+ variables_reference: 2,
+ named_variables: None,
+ indexed_variables: None,
+ expensive: false,
+ source: None,
+ line: None,
+ column: None,
+ end_line: None,
+ end_column: None,
+ }];
+
+ client.on_request::<Scopes, _>({
+ let scopes = Arc::new(scopes.clone());
+ move |_, args| {
+ assert_eq!(1, args.frame_id);
+
+ Ok(dap::ScopesResponse {
+ scopes: (*scopes).clone(),
+ })
+ }
+ });
+
+ let variables = vec![
+ Variable {
+ name: "variable1".into(),
+ value: "value 1".into(),
+ type_: None,
+ presentation_hint: None,
+ evaluate_name: None,
+ variables_reference: 0,
+ named_variables: None,
+ indexed_variables: None,
+ memory_reference: None,
+ declaration_location_reference: None,
+ value_location_reference: None,
+ },
+ Variable {
+ name: "variable2".into(),
+ value: "value 2".into(),
+ type_: None,
+ presentation_hint: None,
+ evaluate_name: None,
+ variables_reference: 0,
+ named_variables: None,
+ indexed_variables: None,
+ memory_reference: None,
+ declaration_location_reference: None,
+ value_location_reference: None,
+ },
+ ];
+
+ client.on_request::<Variables, _>({
+ let variables = Arc::new(variables.clone());
+ move |_, args| {
+ assert_eq!(2, args.variables_reference);
+
+ Ok(dap::VariablesResponse {
+ variables: (*variables).clone(),
+ })
+ }
+ });
+
+ client.on_request::<Evaluate, _>({
+ move |_, args| {
+ assert_eq!("variable1", args.expression);
+
+ Ok(dap::EvaluateResponse {
+ result: "value1".to_owned(),
+ type_: None,
+ presentation_hint: None,
+ variables_reference: 2,
+ named_variables: None,
+ indexed_variables: None,
+ memory_reference: None,
+ value_location_reference: None,
+ })
+ }
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ cx.run_until_parked();
+
+ let running_state =
+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
+ cx.focus_self(window);
+ let running = item.running_state().clone();
+
+ let variable_list = running.update(cx, |state, cx| {
+ // have to do this because the variable list pane should be shown/active
+ // for testing the variable list
+ state.activate_item(DebuggerPaneItem::Variables, window, cx);
+
+ state.variable_list().clone()
+ });
+ variable_list.update(cx, |_, cx| cx.focus_self(window));
+ running
+ });
+ cx.run_until_parked();
+
+ // select variable 1 from first scope
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |_, cx| {
+ cx.dispatch_action(&SelectFirst);
+ cx.dispatch_action(&SelectNext);
+ });
+ });
+ cx.run_until_parked();
+
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |_, cx| {
+ cx.dispatch_action(&AddWatch);
+ });
+ });
+ cx.run_until_parked();
+
+ // assert watcher for variable1 was added
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |list, _| {
+ list.assert_visual_entries(vec![
+ "> variable1",
+ "v Scope 1",
+ " > variable1 <=== selected",
+ " > variable2",
+ ]);
+ });
+ });
+
+ session.update(cx, |session, _| {
+ let watcher = session
+ .watchers()
+ .get(&SharedString::from("variable1"))
+ .unwrap();
+
+ assert_eq!("value1", watcher.value.to_string());
+ assert_eq!("variable1", watcher.expression.to_string());
+ assert_eq!(2, watcher.variables_reference);
+ });
+
+ // select added watcher for variable1
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |_, cx| {
+ cx.dispatch_action(&SelectFirst);
+ });
+ });
+ cx.run_until_parked();
+
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |_, cx| {
+ cx.dispatch_action(&RemoveWatch);
+ });
+ });
+ cx.run_until_parked();
+
+ // assert watcher for variable1 was removed
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |list, _| {
+ list.assert_visual_entries(vec!["v Scope 1", " > variable1", " > variable2"]);
+ });
+ });
+}
+
+#[gpui::test]
+async fn test_refresh_watchers(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+
+ let test_file_content = r#"
+ const variable1 = "Value 1";
+ const variable2 = "Value 2";
+ "#
+ .unindent();
+
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "src": {
+ "test.js": test_file_content,
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ workspace
+ .update(cx, |workspace, window, cx| {
+ workspace.focus_panel::<DebugPanel>(window, cx);
+ })
+ .unwrap();
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
+
+ client.on_request::<dap::requests::Threads, _>(move |_, _| {
+ Ok(dap::ThreadsResponse {
+ threads: vec![dap::Thread {
+ id: 1,
+ name: "Thread 1".into(),
+ }],
+ })
+ });
+
+ let stack_frames = vec![StackFrame {
+ id: 1,
+ name: "Stack Frame 1".into(),
+ source: Some(dap::Source {
+ name: Some("test.js".into()),
+ path: Some(path!("/project/src/test.js").into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 1,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: None,
+ }];
+
+ client.on_request::<StackTrace, _>({
+ let stack_frames = Arc::new(stack_frames.clone());
+ move |_, args| {
+ assert_eq!(1, args.thread_id);
+
+ Ok(dap::StackTraceResponse {
+ stack_frames: (*stack_frames).clone(),
+ total_frames: None,
+ })
+ }
+ });
+
+ let scopes = vec![Scope {
+ name: "Scope 1".into(),
+ presentation_hint: None,
+ variables_reference: 2,
+ named_variables: None,
+ indexed_variables: None,
+ expensive: false,
+ source: None,
+ line: None,
+ column: None,
+ end_line: None,
+ end_column: None,
+ }];
+
+ client.on_request::<Scopes, _>({
+ let scopes = Arc::new(scopes.clone());
+ move |_, args| {
+ assert_eq!(1, args.frame_id);
+
+ Ok(dap::ScopesResponse {
+ scopes: (*scopes).clone(),
+ })
+ }
+ });
+
+ let variables = vec![
+ Variable {
+ name: "variable1".into(),
+ value: "value 1".into(),
+ type_: None,
+ presentation_hint: None,
+ evaluate_name: None,
+ variables_reference: 0,
+ named_variables: None,
+ indexed_variables: None,
+ memory_reference: None,
+ declaration_location_reference: None,
+ value_location_reference: None,
+ },
+ Variable {
+ name: "variable2".into(),
+ value: "value 2".into(),
+ type_: None,
+ presentation_hint: None,
+ evaluate_name: None,
+ variables_reference: 0,
+ named_variables: None,
+ indexed_variables: None,
+ memory_reference: None,
+ declaration_location_reference: None,
+ value_location_reference: None,
+ },
+ ];
+
+ client.on_request::<Variables, _>({
+ let variables = Arc::new(variables.clone());
+ move |_, args| {
+ assert_eq!(2, args.variables_reference);
+
+ Ok(dap::VariablesResponse {
+ variables: (*variables).clone(),
+ })
+ }
+ });
+
+ client.on_request::<Evaluate, _>({
+ move |_, args| {
+ assert_eq!("variable1", args.expression);
+
+ Ok(dap::EvaluateResponse {
+ result: "value1".to_owned(),
+ type_: None,
+ presentation_hint: None,
+ variables_reference: 2,
+ named_variables: None,
+ indexed_variables: None,
+ memory_reference: None,
+ value_location_reference: None,
+ })
+ }
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ cx.run_until_parked();
+
+ let running_state =
+ active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| {
+ cx.focus_self(window);
+ let running = item.running_state().clone();
+
+ let variable_list = running.update(cx, |state, cx| {
+ // have to do this because the variable list pane should be shown/active
+ // for testing the variable list
+ state.activate_item(DebuggerPaneItem::Variables, window, cx);
+
+ state.variable_list().clone()
+ });
+ variable_list.update(cx, |_, cx| cx.focus_self(window));
+ running
+ });
+ cx.run_until_parked();
+
+ // select variable 1 from first scope
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |_, cx| {
+ cx.dispatch_action(&SelectFirst);
+ cx.dispatch_action(&SelectNext);
+ });
+ });
+ cx.run_until_parked();
+
+ running_state.update(cx, |running_state, cx| {
+ running_state.variable_list().update(cx, |_, cx| {
+ cx.dispatch_action(&AddWatch);
+ });
+ });
+ cx.run_until_parked();
+
+ session.update(cx, |session, _| {
+ let watcher = session
+ .watchers()
+ .get(&SharedString::from("variable1"))
+ .unwrap();
+
+ assert_eq!("value1", watcher.value.to_string());
+ assert_eq!("variable1", watcher.expression.to_string());
+ assert_eq!(2, watcher.variables_reference);
+ });
+
+ client.on_request::<Evaluate, _>({
+ move |_, args| {
+ assert_eq!("variable1", args.expression);
+
+ Ok(dap::EvaluateResponse {
+ result: "value updated".to_owned(),
+ type_: None,
+ presentation_hint: None,
+ variables_reference: 3,
+ named_variables: None,
+ indexed_variables: None,
+ memory_reference: None,
+ value_location_reference: None,
+ })
+ }
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ cx.run_until_parked();
+
+ session.update(cx, |session, _| {
+ let watcher = session
+ .watchers()
+ .get(&SharedString::from("variable1"))
+ .unwrap();
+
+ assert_eq!("value updated", watcher.value.to_string());
+ assert_eq!("variable1", watcher.expression.to_string());
+ assert_eq!(3, watcher.variables_reference);
+ });
+}
@@ -26,7 +26,7 @@ use dap::{
use dap::{
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
- StartDebuggingRequestArgumentsRequest,
+ StartDebuggingRequestArgumentsRequest, VariablePresentationHint,
};
use futures::SinkExt;
use futures::channel::mpsc::UnboundedSender;
@@ -126,6 +126,14 @@ impl From<dap::Thread> for Thread {
}
}
+#[derive(Debug, Clone, PartialEq)]
+pub struct Watcher {
+ pub expression: SharedString,
+ pub value: SharedString,
+ pub variables_reference: u64,
+ pub presentation_hint: Option<VariablePresentationHint>,
+}
+
pub enum Mode {
Building,
Running(RunningMode),
@@ -630,6 +638,7 @@ pub struct Session {
output: Box<circular_buffer::CircularBuffer<MAX_TRACKED_OUTPUT_EVENTS, dap::OutputEvent>>,
threads: IndexMap<ThreadId, Thread>,
thread_states: ThreadStates,
+ watchers: HashMap<SharedString, Watcher>,
variables: HashMap<VariableReference, Vec<dap::Variable>>,
stack_frames: IndexMap<StackFrameId, StackFrame>,
locations: HashMap<u64, dap::LocationsResponse>,
@@ -721,6 +730,7 @@ pub enum SessionEvent {
Stopped(Option<ThreadId>),
StackTrace,
Variables,
+ Watchers,
Threads,
InvalidateInlineValue,
CapabilitiesLoaded,
@@ -788,6 +798,7 @@ impl Session {
child_session_ids: HashSet::default(),
parent_session,
capabilities: Capabilities::default(),
+ watchers: HashMap::default(),
variables: Default::default(),
stack_frames: Default::default(),
thread_states: ThreadStates::default(),
@@ -2155,6 +2166,53 @@ impl Session {
.collect()
}
+ pub fn watchers(&self) -> &HashMap<SharedString, Watcher> {
+ &self.watchers
+ }
+
+ pub fn add_watcher(
+ &mut self,
+ expression: SharedString,
+ frame_id: u64,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let request = self.mode.request_dap(EvaluateCommand {
+ expression: expression.to_string(),
+ context: Some(EvaluateArgumentsContext::Watch),
+ frame_id: Some(frame_id),
+ source: None,
+ });
+
+ cx.spawn(async move |this, cx| {
+ let response = request.await?;
+
+ this.update(cx, |session, cx| {
+ session.watchers.insert(
+ expression.clone(),
+ Watcher {
+ expression,
+ value: response.result.into(),
+ variables_reference: response.variables_reference,
+ presentation_hint: response.presentation_hint,
+ },
+ );
+ cx.emit(SessionEvent::Watchers);
+ })
+ })
+ }
+
+ pub fn refresh_watchers(&mut self, frame_id: u64, cx: &mut Context<Self>) {
+ let watches = self.watchers.clone();
+ for (_, watch) in watches.into_iter() {
+ self.add_watcher(watch.expression.clone(), frame_id, cx)
+ .detach();
+ }
+ }
+
+ pub fn remove_watcher(&mut self, expression: SharedString) {
+ self.watchers.remove(&expression);
+ }
+
pub fn variables(
&mut self,
variables_reference: VariableReference,
@@ -2191,6 +2249,7 @@ impl Session {
pub fn set_variable_value(
&mut self,
+ stack_frame_id: u64,
variables_reference: u64,
name: String,
value: String,
@@ -2206,12 +2265,13 @@ impl Session {
move |this, response, cx| {
let response = response.log_err()?;
this.invalidate_command_type::<VariablesCommand>();
+ this.refresh_watchers(stack_frame_id, cx);
cx.emit(SessionEvent::Variables);
Some(response)
},
cx,
)
- .detach()
+ .detach();
}
}
@@ -396,6 +396,10 @@ impl ButtonLike {
Self::new(id).rounding(ButtonLikeRounding::Right)
}
+ pub fn new_rounded_all(id: impl Into<ElementId>) -> Self {
+ Self::new(id).rounding(ButtonLikeRounding::All)
+ }
+
pub fn opacity(mut self, opacity: f32) -> Self {
self.base = self.base.opacity(opacity);
self