debugger: Add ability to only show stack frame entries from visible work trees (#37061)
This PR adds a toggleable filter to the stack frame list that filters out entries that don't exist within a user's project (visible work trees). This works by keeping a vector of entry indices that exist within a user's project and updates the list state based on these entries when filtering the list. I went with this approach so the stack frame list wouldn't have to rebuild itself whenever the filter is toggled and it could persist its state across toggles (uncollapsing a collapse list). It was also easier to keep track of selected entries on toggle using the vector as well. ### Preview https://github.com/user-attachments/assets/d86c7485-c885-4bbb-bebb-2f6385674925 Release Notes: - debugger: Add option to only show stack frames from user's project in stack frame list
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter-icon lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>
|
||||
|
After Width: | Height: | Size: 305 B |
@@ -234,6 +234,7 @@ impl PythonDebugAdapter {
|
||||
.await
|
||||
.map_err(|e| format!("{e:#?}"))?
|
||||
.success();
|
||||
|
||||
if !did_succeed {
|
||||
return Err("Failed to create base virtual environment".into());
|
||||
}
|
||||
|
||||
@@ -85,6 +85,10 @@ actions!(
|
||||
Rerun,
|
||||
/// Toggles expansion of the selected item in the debugger UI.
|
||||
ToggleExpandItem,
|
||||
/// Toggle the user frame filter in the stack frame list
|
||||
/// When toggled on, only frames from the user's code are shown
|
||||
/// When toggled off, all frames are shown
|
||||
ToggleUserFrames,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -272,12 +276,25 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let active_item = active_item.clone();
|
||||
move |_: &ToggleIgnoreBreakpoints, _, cx| {
|
||||
active_item
|
||||
.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action(move |_: &ToggleUserFrames, _, cx| {
|
||||
if let Some((thread_status, stack_frame_list)) = active_item
|
||||
.read_with(cx, |item, cx| {
|
||||
(item.thread_status(cx), item.stack_frame_list().clone())
|
||||
})
|
||||
.ok()
|
||||
{
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.toggle_frame_filter(thread_status, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -270,12 +270,9 @@ pub(crate) fn deserialize_pane_layout(
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| match child {
|
||||
DebuggerPaneItem::Frames => Box::new(SubView::new(
|
||||
stack_frame_list.focus_handle(cx),
|
||||
stack_frame_list.clone().into(),
|
||||
DebuggerPaneItem::Frames,
|
||||
cx,
|
||||
)),
|
||||
DebuggerPaneItem::Frames => {
|
||||
Box::new(SubView::stack_frame_list(stack_frame_list.clone(), cx))
|
||||
}
|
||||
DebuggerPaneItem::Variables => Box::new(SubView::new(
|
||||
variable_list.focus_handle(cx),
|
||||
variable_list.clone().into(),
|
||||
|
||||
@@ -158,6 +158,29 @@ impl SubView {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn stack_frame_list(
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let weak_list = stack_frame_list.downgrade();
|
||||
let this = Self::new(
|
||||
stack_frame_list.focus_handle(cx),
|
||||
stack_frame_list.into(),
|
||||
DebuggerPaneItem::Frames,
|
||||
cx,
|
||||
);
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.with_actions(Box::new(move |_, cx| {
|
||||
weak_list
|
||||
.update(cx, |this, _| this.render_control_strip())
|
||||
.unwrap_or_else(|_| div().into_any_element())
|
||||
}));
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub(crate) fn console(console: Entity<Console>, cx: &mut App) -> Entity<Self> {
|
||||
let weak_console = console.downgrade();
|
||||
let this = Self::new(
|
||||
|
||||
@@ -4,16 +4,17 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use dap::StackFrameId;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
|
||||
Stateful, Subscription, Task, WeakEntity, list,
|
||||
Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
|
||||
MouseButton, Stateful, Subscription, Task, WeakEntity, list,
|
||||
};
|
||||
use util::debug_panic;
|
||||
|
||||
use crate::StackTraceView;
|
||||
use crate::{StackTraceView, ToggleUserFrames};
|
||||
use language::PointUtf16;
|
||||
use project::debugger::breakpoint_store::ActiveStackFrame;
|
||||
use project::debugger::session::{Session, SessionEvent, StackFrame};
|
||||
use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
|
||||
use project::{ProjectItem, ProjectPath};
|
||||
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
|
||||
use workspace::{ItemHandle, Workspace};
|
||||
@@ -26,6 +27,34 @@ pub enum StackFrameListEvent {
|
||||
BuiltEntries,
|
||||
}
|
||||
|
||||
/// Represents the filter applied to the stack frame list
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
enum StackFrameFilter {
|
||||
/// Show all frames
|
||||
All,
|
||||
/// Show only frames from the user's code
|
||||
OnlyUserFrames,
|
||||
}
|
||||
|
||||
impl StackFrameFilter {
|
||||
fn from_str_or_default(s: impl AsRef<str>) -> Self {
|
||||
match s.as_ref() {
|
||||
"user" => StackFrameFilter::OnlyUserFrames,
|
||||
"all" => StackFrameFilter::All,
|
||||
_ => StackFrameFilter::All,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StackFrameFilter> for String {
|
||||
fn from(filter: StackFrameFilter) -> Self {
|
||||
match filter {
|
||||
StackFrameFilter::All => "all".to_string(),
|
||||
StackFrameFilter::OnlyUserFrames => "user".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StackFrameList {
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
@@ -37,6 +66,8 @@ pub struct StackFrameList {
|
||||
opened_stack_frame_id: Option<StackFrameId>,
|
||||
scrollbar_state: ScrollbarState,
|
||||
list_state: ListState,
|
||||
list_filter: StackFrameFilter,
|
||||
filter_entries_indices: Vec<usize>,
|
||||
error: Option<SharedString>,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
@@ -73,6 +104,16 @@ impl StackFrameList {
|
||||
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
|
||||
let scrollbar_state = ScrollbarState::new(list_state.clone());
|
||||
|
||||
let list_filter = KEY_VALUE_STORE
|
||||
.read_kvp(&format!(
|
||||
"stack-frame-list-filter-{}",
|
||||
session.read(cx).adapter().0
|
||||
))
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(StackFrameFilter::from_str_or_default)
|
||||
.unwrap_or(StackFrameFilter::All);
|
||||
|
||||
let mut this = Self {
|
||||
session,
|
||||
workspace,
|
||||
@@ -80,9 +121,11 @@ impl StackFrameList {
|
||||
state,
|
||||
_subscription,
|
||||
entries: Default::default(),
|
||||
filter_entries_indices: Vec::default(),
|
||||
error: None,
|
||||
selected_ix: None,
|
||||
opened_stack_frame_id: None,
|
||||
list_filter,
|
||||
list_state,
|
||||
scrollbar_state,
|
||||
_refresh_task: Task::ready(()),
|
||||
@@ -103,7 +146,15 @@ impl StackFrameList {
|
||||
) -> Vec<dap::StackFrame> {
|
||||
self.entries
|
||||
.iter()
|
||||
.flat_map(|frame| match frame {
|
||||
.enumerate()
|
||||
.filter(|(ix, _)| {
|
||||
self.list_filter == StackFrameFilter::All
|
||||
|| self
|
||||
.filter_entries_indices
|
||||
.binary_search_by_key(&ix, |ix| ix)
|
||||
.is_ok()
|
||||
})
|
||||
.flat_map(|(_, frame)| match frame {
|
||||
StackFrameEntry::Normal(frame) => vec![frame.clone()],
|
||||
StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
|
||||
StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
|
||||
@@ -126,7 +177,15 @@ impl StackFrameList {
|
||||
self.stack_frames(cx)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|stack_frame| stack_frame.dap)
|
||||
.enumerate()
|
||||
.filter(|(ix, _)| {
|
||||
self.list_filter == StackFrameFilter::All
|
||||
|| self
|
||||
.filter_entries_indices
|
||||
.binary_search_by_key(&ix, |ix| ix)
|
||||
.is_ok()
|
||||
})
|
||||
.map(|(_, stack_frame)| stack_frame.dap)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -192,7 +251,32 @@ impl StackFrameList {
|
||||
return;
|
||||
}
|
||||
};
|
||||
for stack_frame in &stack_frames {
|
||||
|
||||
let worktree_prefixes: Vec<_> = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.visible_worktrees(cx)
|
||||
.map(|tree| tree.read(cx).abs_path())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut filter_entries_indices = Vec::default();
|
||||
for (ix, stack_frame) in stack_frames.iter().enumerate() {
|
||||
let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
|
||||
source.path.as_ref().is_some_and(|path| {
|
||||
worktree_prefixes
|
||||
.iter()
|
||||
.filter_map(|tree| tree.to_str())
|
||||
.any(|tree| path.starts_with(tree))
|
||||
})
|
||||
});
|
||||
|
||||
if frame_in_visible_worktree {
|
||||
filter_entries_indices.push(ix);
|
||||
}
|
||||
|
||||
match stack_frame.dap.presentation_hint {
|
||||
Some(dap::StackFramePresentationHint::Deemphasize)
|
||||
| Some(dap::StackFramePresentationHint::Subtle) => {
|
||||
@@ -225,8 +309,10 @@ impl StackFrameList {
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries));
|
||||
self.filter_entries_indices.push(entries.len() - 1);
|
||||
}
|
||||
self.entries = entries;
|
||||
self.filter_entries_indices = filter_entries_indices;
|
||||
|
||||
if let Some(ix) = first_stack_frame_with_path
|
||||
.or(first_stack_frame)
|
||||
@@ -242,7 +328,14 @@ impl StackFrameList {
|
||||
self.selected_ix = ix;
|
||||
}
|
||||
|
||||
self.list_state.reset(self.entries.len());
|
||||
match self.list_filter {
|
||||
StackFrameFilter::All => {
|
||||
self.list_state.reset(self.entries.len());
|
||||
}
|
||||
StackFrameFilter::OnlyUserFrames => {
|
||||
self.list_state.reset(self.filter_entries_indices.len());
|
||||
}
|
||||
}
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -572,6 +665,11 @@ impl StackFrameList {
|
||||
}
|
||||
|
||||
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let ix = match self.list_filter {
|
||||
StackFrameFilter::All => ix,
|
||||
StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
|
||||
};
|
||||
|
||||
match &self.entries[ix] {
|
||||
StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
|
||||
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
|
||||
@@ -702,6 +800,67 @@ impl StackFrameList {
|
||||
self.activate_selected_entry(window, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_frame_filter(
|
||||
&mut self,
|
||||
thread_status: Option<ThreadStatus>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.list_filter = match self.list_filter {
|
||||
StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
|
||||
StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
|
||||
};
|
||||
|
||||
if let Some(database_id) = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| workspace.database_id())
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let database_id: i64 = database_id.into();
|
||||
let save_task = KEY_VALUE_STORE.write_kvp(
|
||||
format!(
|
||||
"stack-frame-list-filter-{}-{}",
|
||||
self.session.read(cx).adapter().0,
|
||||
database_id,
|
||||
),
|
||||
self.list_filter.into(),
|
||||
);
|
||||
cx.background_spawn(save_task).detach();
|
||||
}
|
||||
|
||||
if let Some(ThreadStatus::Stopped) = thread_status {
|
||||
match self.list_filter {
|
||||
StackFrameFilter::All => {
|
||||
self.list_state.reset(self.entries.len());
|
||||
}
|
||||
StackFrameFilter::OnlyUserFrames => {
|
||||
self.list_state.reset(self.filter_entries_indices.len());
|
||||
if !self
|
||||
.selected_ix
|
||||
.map(|ix| self.filter_entries_indices.contains(&ix))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
self.selected_ix = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ix) = self.selected_ix {
|
||||
let scroll_to = match self.list_filter {
|
||||
StackFrameFilter::All => ix,
|
||||
StackFrameFilter::OnlyUserFrames => self
|
||||
.filter_entries_indices
|
||||
.binary_search_by_key(&ix, |ix| *ix)
|
||||
.expect("This index will always exist"),
|
||||
};
|
||||
self.list_state.scroll_to_reveal_item(scroll_to);
|
||||
}
|
||||
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div().p_1().size_full().child(
|
||||
list(
|
||||
@@ -711,6 +870,30 @@ impl StackFrameList {
|
||||
.size_full(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_control_strip(&self) -> AnyElement {
|
||||
let tooltip_title = match self.list_filter {
|
||||
StackFrameFilter::All => "Show stack frames from your project",
|
||||
StackFrameFilter::OnlyUserFrames => "Show all stack frames",
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"filter-by-visible-worktree-stack-frame-list",
|
||||
IconName::ListFilter,
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx)
|
||||
})
|
||||
.toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StackFrameList {
|
||||
|
||||
@@ -752,3 +752,288 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
let test_file_content = r#"
|
||||
function main() {
|
||||
doSomething();
|
||||
}
|
||||
|
||||
function doSomething() {
|
||||
console.log('doing something');
|
||||
}
|
||||
"#
|
||||
.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;
|
||||
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::<Threads, _>(move |_, _| {
|
||||
Ok(dap::ThreadsResponse {
|
||||
threads: vec![dap::Thread {
|
||||
id: 1,
|
||||
name: "Thread 1".into(),
|
||||
}],
|
||||
})
|
||||
});
|
||||
|
||||
client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
|
||||
|
||||
let stack_frames = vec![
|
||||
StackFrame {
|
||||
id: 1,
|
||||
name: "main".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: 2,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
StackFrame {
|
||||
id: 2,
|
||||
name: "node:internal/modules/cjs/loader".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("loader.js".into()),
|
||||
path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 100,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
|
||||
},
|
||||
StackFrame {
|
||||
id: 3,
|
||||
name: "node:internal/modules/run_main".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("run_main.js".into()),
|
||||
path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 50,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
|
||||
},
|
||||
StackFrame {
|
||||
id: 4,
|
||||
name: "doSomething".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: 3,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
];
|
||||
|
||||
// Store a copy for assertions
|
||||
let stack_frames_for_assertions = stack_frames.clone();
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
// trigger threads to load
|
||||
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx));
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// select first thread
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
|
||||
session.running_state().update(cx, |running_state, cx| {
|
||||
running_state.select_current_thread(
|
||||
&running_state
|
||||
.session()
|
||||
.update(cx, |session, cx| session.threads(cx)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// trigger stack frames to load
|
||||
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.dap_stack_frames(cx);
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.build_entries(true, window, cx);
|
||||
|
||||
// Verify we have the expected collapsed structure
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Collapsed(vec![
|
||||
stack_frames_for_assertions[1].clone(),
|
||||
stack_frames_for_assertions[2].clone()
|
||||
]),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
]
|
||||
);
|
||||
|
||||
// Test 1: Verify filtering works
|
||||
let all_frames = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially");
|
||||
|
||||
// Toggle to user frames only
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
let user_frames = stack_frame_list.dap_stack_frames(cx);
|
||||
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
|
||||
assert_eq!(user_frames[0].name, "main");
|
||||
assert_eq!(user_frames[1].name, "doSomething");
|
||||
|
||||
// Test 2: Verify filtering toggles correctly
|
||||
// Check we can toggle back and see all frames again
|
||||
|
||||
// Toggle back to all frames
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
let all_frames_again = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(
|
||||
all_frames_again.len(),
|
||||
4,
|
||||
"Should see all 4 frames after toggling back"
|
||||
);
|
||||
|
||||
// Test 3: Verify collapsed entries stay expanded
|
||||
stack_frame_list.expand_collapsed_entry(1, cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
]
|
||||
);
|
||||
|
||||
// Toggle filter twice
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
// Verify entries remain expanded
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
],
|
||||
"Expanded entries should remain expanded after toggling filter"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ pub enum IconName {
|
||||
Library,
|
||||
LineHeight,
|
||||
ListCollapse,
|
||||
ListFilter,
|
||||
ListTodo,
|
||||
ListTree,
|
||||
ListX,
|
||||
|
||||
Reference in New Issue
Block a user