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
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue