debugger: Improve variable list keyboard navigation (#27308)

This PR improves the keyboard navigation for the variable list. 
Before this PR, if you want to open/close nested variables, you had to
use the right/left & up/down arrow keys.
Now you can step through with just only using your left/right arrow
keys, this feels a bit more natural and more similar to how other
editors allow you to navigate through variables.

This PR also fixes the following issues:
- Allow selecting a scope to be the start of your selection
- Allow selecting previous item if the first item is selected

-----


https://github.com/user-attachments/assets/aff0b133-97be-4c09-8ee6-b11495ad5568

Release Notes:

- N/A
This commit is contained in:
Remco Smits 2025-03-25 00:36:09 +01:00 committed by GitHub
parent a52095f558
commit bcfc9e4437
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 256 additions and 26 deletions

View file

@ -18,6 +18,7 @@ actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
pub(crate) struct EntryState {
depth: usize,
is_expanded: bool,
has_children: bool,
parent_reference: VariableReference,
}
@ -246,6 +247,7 @@ impl VariableList {
.entry(path.clone())
.and_modify(|state| {
state.parent_reference = container_reference;
state.has_children = variables_reference != 0;
})
.or_insert(EntryState {
depth: path.indices.len(),
@ -258,6 +260,7 @@ impl VariableList {
.unwrap_or(scope.name.to_lowercase().starts_with("local"))
}),
parent_reference: container_reference,
has_children: variables_reference != 0,
});
entries.push(ListEntry {
@ -358,42 +361,46 @@ impl VariableList {
fn select_prev(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(selection) = &self.selection {
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
if &var.path == selection {
let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
if &var.path == selection && ix > 0 {
Some(ix.saturating_sub(1))
} else {
None
}
}) {
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
});
if let Some(new_selection) =
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
} else {
self.select_first(&SelectFirst, window, cx);
}
self.select_last(&SelectLast, window, cx);
}
} else {
self.select_first(&SelectFirst, window, cx);
self.select_last(&SelectLast, window, cx);
}
}
fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
self.cancel_variable_edit(&Default::default(), window, cx);
if let Some(selection) = &self.selection {
if let Some(var_ix) = self.entries.iter().enumerate().find_map(|(ix, var)| {
let index = self.entries.iter().enumerate().find_map(|(ix, var)| {
if &var.path == selection {
Some(ix.saturating_add(1))
} else {
None
}
}) {
if let Some(new_selection) = self.entries.get(var_ix).map(|var| var.path.clone()) {
});
if let Some(new_selection) =
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
{
self.selection = Some(new_selection);
cx.notify();
} else {
self.select_first(&SelectFirst, window, cx);
}
}
} else {
self.select_first(&SelectFirst, window, cx);
}
@ -437,7 +444,7 @@ impl VariableList {
fn collapse_selected_entry(
&mut self,
_: &CollapseSelectedEntry,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(ref selected_entry) = self.selection {
@ -446,27 +453,35 @@ impl VariableList {
return;
};
if !entry_state.is_expanded || !entry_state.has_children {
self.select_prev(&SelectPrevious, window, cx);
} else {
entry_state.is_expanded = false;
cx.notify();
}
}
}
fn expand_selected_entry(
&mut self,
_: &ExpandSelectedEntry,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(ref selected_entry) = self.selection {
if let Some(selected_entry) = &self.selection {
let Some(entry_state) = self.entry_states.get_mut(selected_entry) else {
debug_panic!("Trying to toggle variable in variable list that has an no state");
return;
};
if entry_state.is_expanded || !entry_state.has_children {
self.select_next(&SelectNext, window, cx);
} else {
entry_state.is_expanded = true;
cx.notify();
}
}
}
fn deploy_variable_context_menu(
&mut self,
@ -649,6 +664,7 @@ impl VariableList {
} else {
colors.default
};
let path = entry.path.clone();
div()
.id(var_ref as usize)
@ -661,7 +677,8 @@ impl VariableList {
.h_full()
.hover(|style| style.bg(bg_hover_color))
.on_click(cx.listener({
move |_this, _, _window, cx| {
move |this, _, _window, cx| {
this.selection = Some(path.clone());
cx.notify();
}
}))

View file

@ -1103,6 +1103,219 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
});
});
// select scope 2 backwards
cx.dispatch_action(SelectPrevious);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec!["> Scope 1", "> Scope 2 <=== selected"]);
});
});
// select scope 1 backwards
cx.dispatch_action(SelectNext);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]);
});
});
// test stepping through nested with ExpandSelectedEntry/CollapseSelectedEntry actions
cx.dispatch_action(ExpandSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1 <=== selected",
" > variable1",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(ExpandSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" > variable1 <=== selected",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(ExpandSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" v variable1 <=== selected",
" > nested1",
" > nested2",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(ExpandSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" v variable1",
" > nested1 <=== selected",
" > nested2",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(ExpandSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" v variable1",
" > nested1",
" > nested2 <=== selected",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(ExpandSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" v variable1",
" > nested1",
" > nested2",
" > variable2 <=== selected",
"> Scope 2",
]);
});
});
cx.dispatch_action(CollapseSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" v variable1",
" > nested1",
" > nested2 <=== selected",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(CollapseSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" v variable1",
" > nested1 <=== selected",
" > nested2",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(CollapseSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" v variable1 <=== selected",
" > nested1",
" > nested2",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(CollapseSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1",
" > variable1 <=== selected",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(CollapseSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec![
"v Scope 1 <=== selected",
" > variable1",
" > variable2",
"> Scope 2",
]);
});
});
cx.dispatch_action(CollapseSelectedEntry);
cx.run_until_parked();
running_state.update(cx, |debug_panel_item, cx| {
debug_panel_item
.variable_list()
.update(cx, |variable_list, _| {
variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]);
});
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)