debugger: Fix stack frame filter not persisting between sessions (#44352)

This bug was caused by using two separate keys for writing/reading from
the KVP database. The bug didn't show up in my debug build because there
was an old entry of a valid key.

I added an integration test for this feature to prevent future
regressions as well.

Release Notes:

- debugger: Fix a bug where the stack frame filter state wouldn't
persist between sessions
This commit is contained in:
Anthony Eid
2025-12-08 01:43:30 +00:00
committed by GitHub
parent d1e45e27de
commit 63cc90cd2c
3 changed files with 208 additions and 17 deletions
@@ -4,6 +4,7 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use dap::StackFrameId;
use dap::adapters::DebugAdapterName;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
@@ -20,7 +21,7 @@ use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
use project::{ProjectItem, ProjectPath};
use ui::{Tooltip, WithScrollbar, prelude::*};
use workspace::{ItemHandle, Workspace};
use workspace::{ItemHandle, Workspace, WorkspaceId};
use super::RunningState;
@@ -58,6 +59,14 @@ impl From<StackFrameFilter> for String {
}
}
pub(crate) fn stack_frame_filter_key(
adapter_name: &DebugAdapterName,
workspace_id: WorkspaceId,
) -> String {
let database_id: i64 = workspace_id.into();
format!("stack-frame-list-filter-{}-{}", adapter_name.0, database_id)
}
pub struct StackFrameList {
focus_handle: FocusHandle,
_subscription: Subscription,
@@ -107,14 +116,18 @@ impl StackFrameList {
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let list_filter = KEY_VALUE_STORE
.read_kvp(&format!(
"stack-frame-list-filter-{}",
session.read(cx).adapter().0
))
let list_filter = workspace
.read_with(cx, |workspace, _| workspace.database_id())
.ok()
.flatten()
.map(StackFrameFilter::from_str_or_default)
.and_then(|database_id| {
let key = stack_frame_filter_key(&session.read(cx).adapter(), database_id);
KEY_VALUE_STORE
.read_kvp(&key)
.ok()
.flatten()
.map(StackFrameFilter::from_str_or_default)
})
.unwrap_or(StackFrameFilter::All);
let mut this = Self {
@@ -807,15 +820,8 @@ impl StackFrameList {
.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(),
);
let key = stack_frame_filter_key(&self.session.read(cx).adapter(), database_id);
let save_task = KEY_VALUE_STORE.write_kvp(key, self.list_filter.into());
cx.background_spawn(save_task).detach();
}
@@ -1,12 +1,15 @@
use crate::{
debugger_panel::DebugPanel,
session::running::stack_frame_list::{StackFrameEntry, StackFrameFilter},
session::running::stack_frame_list::{
StackFrameEntry, StackFrameFilter, stack_frame_filter_key,
},
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
StackFrame,
requests::{Scopes, StackTrace, Threads},
};
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, ToPoint as _};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
@@ -1085,3 +1088,180 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC
);
});
}
#[gpui::test]
async fn test_stack_frame_filter_persistence(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"src": {
"test.js": "function main() { console.log('hello'); }",
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, _, _| {
workspace.set_random_database_id();
})
.unwrap();
let threads_response = dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
};
let stack_trace_response = dap::StackTraceResponse {
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: 1,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
}],
total_frames: None,
};
let stopped_event = 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,
};
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let adapter_name = session.update(cx, |session, _| session.adapter());
client.on_request::<Threads, _>({
let threads_response = threads_response.clone();
move |_, _| Ok(threads_response.clone())
});
client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
client.on_request::<StackTrace, _>({
let stack_trace_response = stack_trace_response.clone();
move |_, _| Ok(stack_trace_response.clone())
});
client
.fake_event(dap::messages::Events::Stopped(stopped_event.clone()))
.await;
cx.run_until_parked();
let stack_frame_list =
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone())
});
stack_frame_list.update(cx, |stack_frame_list, _cx| {
assert_eq!(
stack_frame_list.list_filter(),
StackFrameFilter::All,
"Initial filter should be All"
);
});
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
assert_eq!(
stack_frame_list.list_filter(),
StackFrameFilter::OnlyUserFrames,
"Filter should be OnlyUserFrames after toggle"
);
});
cx.run_until_parked();
let workspace_id = workspace
.update(cx, |workspace, _window, _cx| workspace.database_id())
.ok()
.flatten()
.expect("workspace id has to be some for this test to work properly");
let key = stack_frame_filter_key(&adapter_name, workspace_id);
let stored_value = KEY_VALUE_STORE.read_kvp(&key).unwrap();
assert_eq!(
stored_value,
Some(StackFrameFilter::OnlyUserFrames.into()),
"Filter should be persisted in KVP store with key: {}",
key
);
client
.fake_event(dap::messages::Events::Terminated(None))
.await;
cx.run_until_parked();
let session2 = start_debug_session(&workspace, cx, |_| {}).unwrap();
let client2 = session2.update(cx, |session, _| session.adapter_client().unwrap());
client2.on_request::<Threads, _>({
let threads_response = threads_response.clone();
move |_, _| Ok(threads_response.clone())
});
client2.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
client2.on_request::<StackTrace, _>({
let stack_trace_response = stack_trace_response.clone();
move |_, _| Ok(stack_trace_response.clone())
});
client2
.fake_event(dap::messages::Events::Stopped(stopped_event.clone()))
.await;
cx.run_until_parked();
let stack_frame_list2 =
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone())
});
stack_frame_list2.update(cx, |stack_frame_list, _cx| {
assert_eq!(
stack_frame_list.list_filter(),
StackFrameFilter::OnlyUserFrames,
"Filter should be restored from KVP store in new session"
);
});
}
+5
View File
@@ -6069,6 +6069,11 @@ impl Workspace {
.on_action(cx.listener(Workspace::cancel))
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_random_database_id(&mut self) {
self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
}
#[cfg(any(test, feature = "test-support"))]
pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
use node_runtime::NodeRuntime;