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",
|
"right": "variable_list::ExpandSelectedEntry",
|
||||||
"enter": "variable_list::EditVariable",
|
"enter": "variable_list::EditVariable",
|
||||||
"ctrl-c": "variable_list::CopyVariableValue",
|
"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",
|
"context": "DebugConsole > Editor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "menu::Confirm"
|
"enter": "menu::Confirm",
|
||||||
|
"alt-enter": "console::WatchExpression"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -864,7 +864,10 @@
|
||||||
"right": "variable_list::ExpandSelectedEntry",
|
"right": "variable_list::ExpandSelectedEntry",
|
||||||
"enter": "variable_list::EditVariable",
|
"enter": "variable_list::EditVariable",
|
||||||
"cmd-c": "variable_list::CopyVariableValue",
|
"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",
|
"context": "DebugConsole > Editor",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"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 editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Context, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task,
|
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
|
||||||
TextStyle, WeakEntity,
|
Render, Subscription, Task, TextStyle, WeakEntity, actions,
|
||||||
};
|
};
|
||||||
use language::{Buffer, CodeLabel, ToOffset};
|
use language::{Buffer, CodeLabel, ToOffset};
|
||||||
use menu::Confirm;
|
use menu::Confirm;
|
||||||
|
@ -21,7 +21,9 @@ use project::{
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
|
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
|
||||||
use theme::{Theme, ThemeSettings};
|
use theme::{Theme, ThemeSettings};
|
||||||
use ui::{Divider, prelude::*};
|
use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
|
||||||
|
|
||||||
|
actions!(console, [WatchExpression]);
|
||||||
|
|
||||||
pub struct Console {
|
pub struct Console {
|
||||||
console: Entity<Editor>,
|
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>) {
|
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let expression = self.query_bar.update(cx, |editor, cx| {
|
let expression = self.query_bar.update(cx, |editor, cx| {
|
||||||
let expression = editor.text(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 {
|
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||||
EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
|
EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
|
||||||
}
|
}
|
||||||
|
@ -408,15 +481,52 @@ impl Console {
|
||||||
|
|
||||||
impl Render for Console {
|
impl Render for Console {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
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()
|
v_flex()
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.key_context("DebugConsole")
|
.key_context("DebugConsole")
|
||||||
.on_action(cx.listener(Self::evaluate))
|
.on_action(cx.listener(Self::evaluate))
|
||||||
|
.on_action(cx.listener(Self::watch_expression))
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(self.render_console(cx))
|
.child(self.render_console(cx))
|
||||||
.when(self.is_running(cx), |this| {
|
.when(self.is_running(cx), |this| {
|
||||||
this.child(Divider::horizontal())
|
this.child(Divider::horizontal()).child(
|
||||||
.child(self.render_query_bar(cx))
|
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()
|
.border_2()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
|
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
|
||||||
use dap::{ScopePresentationHint, StackFrameId, VariablePresentationHintKind, VariableReference};
|
use dap::{
|
||||||
|
ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind,
|
||||||
|
VariableReference,
|
||||||
|
};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, FocusHandle,
|
Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity,
|
||||||
Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
|
FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription,
|
||||||
TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
|
TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list,
|
||||||
};
|
};
|
||||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
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 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;
|
use util::debug_panic;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
|
@ -19,7 +22,9 @@ actions!(
|
||||||
CollapseSelectedEntry,
|
CollapseSelectedEntry,
|
||||||
CopyVariableName,
|
CopyVariableName,
|
||||||
CopyVariableValue,
|
CopyVariableValue,
|
||||||
EditVariable
|
EditVariable,
|
||||||
|
AddWatch,
|
||||||
|
RemoveWatch,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -38,6 +43,13 @@ pub(crate) struct EntryPath {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
fn for_scope(scope_name: impl Into<SharedString>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
leaf_name: Some(scope_name.into()),
|
leaf_name: Some(scope_name.into()),
|
||||||
|
@ -68,11 +80,19 @@ impl EntryPath {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
enum EntryKind {
|
enum EntryKind {
|
||||||
|
Watcher(Watcher),
|
||||||
Variable(dap::Variable),
|
Variable(dap::Variable),
|
||||||
Scope(dap::Scope),
|
Scope(dap::Scope),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EntryKind {
|
impl EntryKind {
|
||||||
|
fn as_watcher(&self) -> Option<&Watcher> {
|
||||||
|
match self {
|
||||||
|
EntryKind::Watcher(watcher) => Some(watcher),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||||
match self {
|
match self {
|
||||||
EntryKind::Variable(dap) => Some(dap),
|
EntryKind::Variable(dap) => Some(dap),
|
||||||
|
@ -87,9 +107,10 @@ impl EntryKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[cfg(test)]
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
|
EntryKind::Watcher(watcher) => &watcher.expression,
|
||||||
EntryKind::Variable(dap) => &dap.name,
|
EntryKind::Variable(dap) => &dap.name,
|
||||||
EntryKind::Scope(dap) => &dap.name,
|
EntryKind::Scope(dap) => &dap.name,
|
||||||
}
|
}
|
||||||
|
@ -103,6 +124,10 @@ struct ListEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListEntry {
|
impl ListEntry {
|
||||||
|
fn as_watcher(&self) -> Option<&Watcher> {
|
||||||
|
self.dap_kind.as_watcher()
|
||||||
|
}
|
||||||
|
|
||||||
fn as_variable(&self) -> Option<&dap::Variable> {
|
fn as_variable(&self) -> Option<&dap::Variable> {
|
||||||
self.dap_kind.as_variable()
|
self.dap_kind.as_variable()
|
||||||
}
|
}
|
||||||
|
@ -114,6 +139,7 @@ impl ListEntry {
|
||||||
fn item_id(&self) -> ElementId {
|
fn item_id(&self) -> ElementId {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let mut id = match &self.dap_kind {
|
let mut id = match &self.dap_kind {
|
||||||
|
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
||||||
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
EntryKind::Scope(dap) => format!("scope-{}", dap.name),
|
||||||
};
|
};
|
||||||
|
@ -126,6 +152,7 @@ impl ListEntry {
|
||||||
fn item_value_id(&self) -> ElementId {
|
fn item_value_id(&self) -> ElementId {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let mut id = match &self.dap_kind {
|
let mut id = match &self.dap_kind {
|
||||||
|
EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression),
|
||||||
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
EntryKind::Variable(dap) => format!("variable-{}", dap.name),
|
||||||
EntryKind::Scope(dap) => format!("scope-{}", 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 {
|
pub struct VariableList {
|
||||||
entries: Vec<ListEntry>,
|
entries: Vec<ListEntry>,
|
||||||
entry_states: HashMap<EntryPath, EntryState>,
|
entry_states: HashMap<EntryPath, EntryState>,
|
||||||
|
@ -169,7 +201,7 @@ impl VariableList {
|
||||||
this.edited_path.take();
|
this.edited_path.take();
|
||||||
this.selected_stack_frame_id.take();
|
this.selected_stack_frame_id.take();
|
||||||
}
|
}
|
||||||
SessionEvent::Variables => {
|
SessionEvent::Variables | SessionEvent::Watchers => {
|
||||||
this.build_entries(cx);
|
this.build_entries(cx);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -216,6 +248,7 @@ impl VariableList {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut entries = vec![];
|
let mut entries = vec![];
|
||||||
|
|
||||||
let scopes: Vec<_> = self.session.update(cx, |session, cx| {
|
let scopes: Vec<_> = self.session.update(cx, |session, cx| {
|
||||||
session.scopes(stack_frame_id, cx).iter().cloned().collect()
|
session.scopes(stack_frame_id, cx).iter().cloned().collect()
|
||||||
});
|
});
|
||||||
|
@ -249,11 +282,27 @@ impl VariableList {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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();
|
let scopes_count = stack.len();
|
||||||
|
|
||||||
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
|
while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop()
|
||||||
{
|
{
|
||||||
match &dap_kind {
|
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::Variable(dap) => path = path.with_name(dap.name.clone().into()),
|
||||||
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
|
EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()),
|
||||||
}
|
}
|
||||||
|
@ -312,6 +361,9 @@ impl VariableList {
|
||||||
match event {
|
match event {
|
||||||
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
|
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
|
||||||
self.selected_stack_frame_id = Some(*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);
|
self.build_entries(cx);
|
||||||
}
|
}
|
||||||
StackFrameListEvent::BuiltEntries => {}
|
StackFrameListEvent::BuiltEntries => {}
|
||||||
|
@ -323,7 +375,7 @@ impl VariableList {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| match &entry.dap_kind {
|
.filter_map(|entry| match &entry.dap_kind {
|
||||||
EntryKind::Variable(dap) => Some(dap.clone()),
|
EntryKind::Variable(dap) => Some(dap.clone()),
|
||||||
EntryKind::Scope(_) => None,
|
EntryKind::Scope(_) | EntryKind::Watcher { .. } => None,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -342,6 +394,9 @@ impl VariableList {
|
||||||
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
|
.and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?;
|
||||||
|
|
||||||
match &entry.dap_kind {
|
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::Variable(_) => Some(self.render_variable(entry, *state, window, cx)),
|
||||||
EntryKind::Scope(_) => Some(self.render_scope(entry, *state, 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 {
|
let Some(state) = self.entry_states.get(&var_path) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let variables_reference = state.parent_reference;
|
let variables_reference = state.parent_reference;
|
||||||
let Some(name) = var_path.leaf_name else {
|
let Some(name) = var_path.leaf_name else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(stack_frame_id) = self.selected_stack_frame_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let value = editor.read(cx).text(cx);
|
let value = editor.read(cx).text(cx);
|
||||||
|
|
||||||
self.session.update(cx, |session, 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,
|
&mut self,
|
||||||
_variable: ListEntry,
|
entry: ListEntry,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
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, _, _| {
|
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
||||||
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
menu.when(entry.as_variable().is_some(), |menu| {
|
||||||
.action("Copy Value", CopyVariableValue.boxed_clone())
|
menu.action("Copy Name", CopyVariableName.boxed_clone())
|
||||||
.action("Edit Value", EditVariable.boxed_clone())
|
.action("Copy Value", CopyVariableValue.boxed_clone())
|
||||||
.context(self.focus_handle.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);
|
cx.focus_view(&context_menu, window);
|
||||||
|
@ -529,13 +616,18 @@ impl VariableList {
|
||||||
let Some(selection) = self.selection.as_ref() else {
|
let Some(selection) = self.selection.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
|
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
|
||||||
return;
|
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(
|
fn copy_variable_value(
|
||||||
|
@ -547,30 +639,94 @@ impl VariableList {
|
||||||
let Some(selection) = self.selection.as_ref() else {
|
let Some(selection) = self.selection.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
|
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
|
||||||
return;
|
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>) {
|
fn edit_variable(&mut self, _: &EditVariable, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(selection) = self.selection.as_ref() else {
|
let Some(selection) = self.selection.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
|
let Some(entry) = self.entries.iter().find(|entry| &entry.path == selection) else {
|
||||||
return;
|
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 {
|
let Some(variable) = entry.as_variable() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let editor = Self::create_variable_editor(&variable.value, window, cx);
|
let Some(stack_frame_id) = self.selected_stack_frame_id else {
|
||||||
self.edited_path = Some((entry.path.clone(), editor));
|
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]
|
#[track_caller]
|
||||||
|
@ -623,6 +779,7 @@ impl VariableList {
|
||||||
|
|
||||||
for entry in self.entries.iter() {
|
for entry in self.entries.iter() {
|
||||||
match &entry.dap_kind {
|
match &entry.dap_kind {
|
||||||
|
EntryKind::Watcher { .. } => continue,
|
||||||
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
|
EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()),
|
||||||
EntryKind::Scope(scope) => {
|
EntryKind::Scope(scope) => {
|
||||||
if scopes.len() > 0 {
|
if scopes.len() > 0 {
|
||||||
|
@ -672,6 +829,288 @@ impl VariableList {
|
||||||
editor
|
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(
|
fn render_scope(
|
||||||
&self,
|
&self,
|
||||||
entry: &ListEntry,
|
entry: &ListEntry,
|
||||||
|
@ -751,36 +1190,12 @@ impl VariableList {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let dap = match &variable.dap_kind {
|
let Some(dap) = &variable.as_variable() else {
|
||||||
EntryKind::Variable(dap) => dap,
|
debug_panic!("Called render variable on non variable variable list entry variant");
|
||||||
EntryKind::Scope(_) => {
|
return div().into_any_element();
|
||||||
debug_panic!("Called render variable on variable list entry kind scope");
|
|
||||||
return div().into_any_element();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let syntax_color_for = |name| cx.theme().syntax().get(name).color;
|
let variable_color = self.variable_color(dap.presentation_hint.as_ref(), cx);
|
||||||
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 var_ref = dap.variables_reference;
|
let var_ref = dap.variables_reference;
|
||||||
let colors = get_entry_color(cx);
|
let colors = get_entry_color(cx);
|
||||||
|
@ -811,6 +1226,7 @@ impl VariableList {
|
||||||
.size_full()
|
.size_full()
|
||||||
.hover(|style| style.bg(bg_hover_color))
|
.hover(|style| style.bg(bg_hover_color))
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
|
let path = path.clone();
|
||||||
move |this, _, _window, cx| {
|
move |this, _, _window, cx| {
|
||||||
this.selection = Some(path.clone());
|
this.selection = Some(path.clone());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -839,11 +1255,12 @@ impl VariableList {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.on_secondary_mouse_down(cx.listener({
|
.on_secondary_mouse_down(cx.listener({
|
||||||
let variable = variable.clone();
|
let path = path.clone();
|
||||||
|
let entry = variable.clone();
|
||||||
move |this, event: &MouseDownEvent, window, cx| {
|
move |this, event: &MouseDownEvent, window, cx| {
|
||||||
this.selection = Some(variable.path.clone());
|
this.selection = Some(path.clone());
|
||||||
this.deploy_variable_context_menu(
|
this.deploy_list_entry_context_menu(
|
||||||
variable.clone(),
|
entry.clone(),
|
||||||
event.position,
|
event.position,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -857,62 +1274,16 @@ impl VariableList {
|
||||||
.text_ui_sm(cx)
|
.text_ui_sm(cx)
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.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))
|
this.color(Color::from(color))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when(!dap.value.is_empty(), |this| {
|
.child(self.render_variable_value(
|
||||||
this.child(div().w_full().id(variable.item_value_id()).map(|this| {
|
&variable,
|
||||||
if let Some((_, editor)) = self
|
&variable_color,
|
||||||
.edited_path
|
dap.value.clone(),
|
||||||
.as_ref()
|
cx,
|
||||||
.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))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into_any()
|
.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_name))
|
||||||
.on_action(cx.listener(Self::copy_variable_value))
|
.on_action(cx.listener(Self::copy_variable_value))
|
||||||
.on_action(cx.listener(Self::edit_variable))
|
.on_action(cx.listener(Self::edit_variable))
|
||||||
|
.on_action(cx.listener(Self::add_watcher))
|
||||||
|
.on_action(cx.listener(Self::remove_watcher))
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
"variable-list",
|
"variable-list",
|
||||||
|
@ -1019,3 +1392,53 @@ fn get_entry_color(cx: &Context<VariableList>) -> EntryColors {
|
||||||
marked_active: colors.ghost_element_selected,
|
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::{
|
use crate::{
|
||||||
DebugPanel,
|
DebugPanel,
|
||||||
persistence::DebuggerPaneItem,
|
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},
|
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use dap::{
|
use dap::{
|
||||||
Scope, StackFrame, Variable,
|
Scope, StackFrame, Variable,
|
||||||
requests::{Initialize, Launch, Scopes, StackTrace, Variables},
|
requests::{Evaluate, Initialize, Launch, Scopes, StackTrace, Variables},
|
||||||
};
|
};
|
||||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||||
use menu::{SelectFirst, SelectNext, SelectPrevious};
|
use menu::{SelectFirst, SelectNext, SelectPrevious};
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use ui::SharedString;
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::path;
|
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,);
|
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::{
|
use dap::{
|
||||||
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
|
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
|
||||||
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
|
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
|
||||||
StartDebuggingRequestArgumentsRequest,
|
StartDebuggingRequestArgumentsRequest, VariablePresentationHint,
|
||||||
};
|
};
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use futures::channel::mpsc::UnboundedSender;
|
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 {
|
pub enum Mode {
|
||||||
Building,
|
Building,
|
||||||
Running(RunningMode),
|
Running(RunningMode),
|
||||||
|
@ -630,6 +638,7 @@ pub struct Session {
|
||||||
output: Box<circular_buffer::CircularBuffer<MAX_TRACKED_OUTPUT_EVENTS, dap::OutputEvent>>,
|
output: Box<circular_buffer::CircularBuffer<MAX_TRACKED_OUTPUT_EVENTS, dap::OutputEvent>>,
|
||||||
threads: IndexMap<ThreadId, Thread>,
|
threads: IndexMap<ThreadId, Thread>,
|
||||||
thread_states: ThreadStates,
|
thread_states: ThreadStates,
|
||||||
|
watchers: HashMap<SharedString, Watcher>,
|
||||||
variables: HashMap<VariableReference, Vec<dap::Variable>>,
|
variables: HashMap<VariableReference, Vec<dap::Variable>>,
|
||||||
stack_frames: IndexMap<StackFrameId, StackFrame>,
|
stack_frames: IndexMap<StackFrameId, StackFrame>,
|
||||||
locations: HashMap<u64, dap::LocationsResponse>,
|
locations: HashMap<u64, dap::LocationsResponse>,
|
||||||
|
@ -721,6 +730,7 @@ pub enum SessionEvent {
|
||||||
Stopped(Option<ThreadId>),
|
Stopped(Option<ThreadId>),
|
||||||
StackTrace,
|
StackTrace,
|
||||||
Variables,
|
Variables,
|
||||||
|
Watchers,
|
||||||
Threads,
|
Threads,
|
||||||
InvalidateInlineValue,
|
InvalidateInlineValue,
|
||||||
CapabilitiesLoaded,
|
CapabilitiesLoaded,
|
||||||
|
@ -788,6 +798,7 @@ impl Session {
|
||||||
child_session_ids: HashSet::default(),
|
child_session_ids: HashSet::default(),
|
||||||
parent_session,
|
parent_session,
|
||||||
capabilities: Capabilities::default(),
|
capabilities: Capabilities::default(),
|
||||||
|
watchers: HashMap::default(),
|
||||||
variables: Default::default(),
|
variables: Default::default(),
|
||||||
stack_frames: Default::default(),
|
stack_frames: Default::default(),
|
||||||
thread_states: ThreadStates::default(),
|
thread_states: ThreadStates::default(),
|
||||||
|
@ -2155,6 +2166,53 @@ impl Session {
|
||||||
.collect()
|
.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(
|
pub fn variables(
|
||||||
&mut self,
|
&mut self,
|
||||||
variables_reference: VariableReference,
|
variables_reference: VariableReference,
|
||||||
|
@ -2191,6 +2249,7 @@ impl Session {
|
||||||
|
|
||||||
pub fn set_variable_value(
|
pub fn set_variable_value(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
stack_frame_id: u64,
|
||||||
variables_reference: u64,
|
variables_reference: u64,
|
||||||
name: String,
|
name: String,
|
||||||
value: String,
|
value: String,
|
||||||
|
@ -2206,12 +2265,13 @@ impl Session {
|
||||||
move |this, response, cx| {
|
move |this, response, cx| {
|
||||||
let response = response.log_err()?;
|
let response = response.log_err()?;
|
||||||
this.invalidate_command_type::<VariablesCommand>();
|
this.invalidate_command_type::<VariablesCommand>();
|
||||||
|
this.refresh_watchers(stack_frame_id, cx);
|
||||||
cx.emit(SessionEvent::Variables);
|
cx.emit(SessionEvent::Variables);
|
||||||
Some(response)
|
Some(response)
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.detach()
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -396,6 +396,10 @@ impl ButtonLike {
|
||||||
Self::new(id).rounding(ButtonLikeRounding::Right)
|
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 {
|
pub fn opacity(mut self, opacity: f32) -> Self {
|
||||||
self.base = self.base.opacity(opacity);
|
self.base = self.base.opacity(opacity);
|
||||||
self
|
self
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue