debugger: Add variable watchers (#32743)
### This PR introduces support for adding watchers to specific expressions (such as variable names or evaluated expressions). This feature is useful in scenarios where many variables are in scope, but only a few are of interest—especially when tracking variables that change frequently. By allowing users to add watchers, it becomes easier to monitor the values of selected expressions across stack frames without having to sift through a large list of variables. https://github.com/user-attachments/assets/c49b470a-d912-4182-8419-7406ba4c8f1e ------ **TODO**: - [x] make render variable code reusable for render watch method - [x] use SharedString for watches because of a lot of cloning - [x] add tests - [x] basic test - [x] test step debugging Release Notes: - Debugger Beta: Add support for variable watchers --------- Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
parent
9f2c541ab0
commit
ad76db7244
7 changed files with 1243 additions and 123 deletions
|
@ -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,30 +639,94 @@ 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 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);
|
||||
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 editor = Self::create_variable_editor(&variable.value, window, cx);
|
||||
self.edited_path = Some((entry.path.clone(), editor));
|
||||
let Some(stack_frame_id) = self.selected_stack_frame_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
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]
|
||||
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue