debugger: Make exception breakpoints persistent (#34014)

Closes #33053
Release Notes:

- Exception breakpoint state is now persisted across debugging sessions.
This commit is contained in:
Piotr Osiewicz 2025-07-07 17:40:14 +02:00 committed by GitHub
parent 966e75b610
commit 6cb382c49f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 178 additions and 116 deletions

View file

@ -5,7 +5,8 @@ use std::{
time::Duration, time::Duration,
}; };
use dap::{Capabilities, ExceptionBreakpointsFilter}; use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName};
use db::kvp::KEY_VALUE_STORE;
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
@ -16,6 +17,7 @@ use project::{
Project, Project,
debugger::{ debugger::{
breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint}, breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
dap_store::{DapStore, PersistedAdapterOptions},
session::Session, session::Session,
}, },
worktree_store::WorktreeStore, worktree_store::WorktreeStore,
@ -48,6 +50,7 @@ pub(crate) enum SelectedBreakpointKind {
pub(crate) struct BreakpointList { pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
breakpoint_store: Entity<BreakpointStore>, breakpoint_store: Entity<BreakpointStore>,
dap_store: Entity<DapStore>,
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>, breakpoints: Vec<BreakpointEntry>,
@ -59,6 +62,7 @@ pub(crate) struct BreakpointList {
selected_ix: Option<usize>, selected_ix: Option<usize>,
input: Entity<Editor>, input: Entity<Editor>,
strip_mode: Option<ActiveBreakpointStripMode>, strip_mode: Option<ActiveBreakpointStripMode>,
serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
} }
impl Focusable for BreakpointList { impl Focusable for BreakpointList {
@ -85,12 +89,16 @@ impl BreakpointList {
let project = project.read(cx); let project = project.read(cx);
let breakpoint_store = project.breakpoint_store(); let breakpoint_store = project.breakpoint_store();
let worktree_store = project.worktree_store(); let worktree_store = project.worktree_store();
let dap_store = project.dap_store();
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new(); let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
cx.new(|cx| Self { let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
cx.new(|cx| {
let this = Self {
breakpoint_store, breakpoint_store,
dap_store,
worktree_store, worktree_store,
scrollbar_state, scrollbar_state,
breakpoints: Default::default(), breakpoints: Default::default(),
@ -103,6 +111,12 @@ impl BreakpointList {
selected_ix: None, selected_ix: None,
input: cx.new(|cx| Editor::single_line(window, cx)), input: cx.new(|cx| Editor::single_line(window, cx)),
strip_mode: None, strip_mode: None,
serialize_exception_breakpoints_task: None,
};
if let Some(name) = adapter_name {
_ = this.deserialize_exception_breakpoints(name, cx);
}
this
}) })
} }
@ -404,12 +418,8 @@ impl BreakpointList {
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx); self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
} }
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => { BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
if let Some(session) = &self.session {
let id = exception_breakpoint.id.clone(); let id = exception_breakpoint.id.clone();
session.update(cx, |session, cx| { self.toggle_exception_breakpoint(&id, cx);
session.toggle_exception_breakpoint(&id, cx);
});
}
} }
} }
cx.notify(); cx.notify();
@ -480,6 +490,64 @@ impl BreakpointList {
cx.notify(); cx.notify();
} }
fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
if let Some(session) = &self.session {
session.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx);
});
cx.notify();
const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(EXCEPTION_SERIALIZATION_INTERVAL)
.await;
this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
.await?;
Ok(())
}));
}
}
fn kvp_key(adapter_name: &str) -> String {
format!("debug_adapter_`{adapter_name}`_persistence")
}
fn serialize_exception_breakpoints(
&mut self,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
if let Some(session) = self.session.as_ref() {
let key = {
let session = session.read(cx);
let name = session.adapter().0;
Self::kvp_key(&name)
};
let settings = self.dap_store.update(cx, |this, cx| {
this.sync_adapter_options(session, cx);
});
let value = serde_json::to_string(&settings);
cx.background_executor()
.spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
} else {
return Task::ready(Result::Ok(()));
}
}
fn deserialize_exception_breakpoints(
&self,
adapter_name: DebugAdapterName,
cx: &mut Context<Self>,
) -> anyhow::Result<()> {
let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
return Ok(());
};
let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
self.dap_store
.update(cx, |this, _| this.set_adapter_options(adapter_name, value));
Ok(())
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@ -988,12 +1056,7 @@ impl ExceptionBreakpoint {
let list = list.clone(); let list = list.clone();
move |_, _, cx| { move |_, _, cx| {
list.update(cx, |this, cx| { list.update(cx, |this, cx| {
if let Some(session) = &this.session {
session.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx); this.toggle_exception_breakpoint(&id, cx);
});
cx.notify();
}
}) })
.ok(); .ok();
} }

View file

@ -14,15 +14,13 @@ use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait; use async_trait::async_trait;
use collections::HashMap; use collections::HashMap;
use dap::{ use dap::{
Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest, Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId,
EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
adapters::{ adapters::{
DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments, DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
}, },
client::SessionId, client::SessionId,
inline_value::VariableLookupKind, inline_value::VariableLookupKind,
messages::Message, messages::Message,
requests::{Completions, Evaluate},
}; };
use fs::Fs; use fs::Fs;
use futures::{ use futures::{
@ -40,6 +38,7 @@ use rpc::{
AnyProtoClient, TypedEnvelope, AnyProtoClient, TypedEnvelope,
proto::{self}, proto::{self},
}; };
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsLocation, WorktreeId}; use settings::{Settings, SettingsLocation, WorktreeId};
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
@ -93,10 +92,23 @@ pub struct DapStore {
worktree_store: Entity<WorktreeStore>, worktree_store: Entity<WorktreeStore>,
sessions: BTreeMap<SessionId, Entity<Session>>, sessions: BTreeMap<SessionId, Entity<Session>>,
next_session_id: u32, next_session_id: u32,
adapter_options: BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>>,
} }
impl EventEmitter<DapStoreEvent> for DapStore {} impl EventEmitter<DapStoreEvent> for DapStore {}
#[derive(Clone, Serialize, Deserialize)]
pub struct PersistedExceptionBreakpoint {
pub enabled: bool,
}
/// Represents best-effort serialization of adapter state during last session (e.g. watches)
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct PersistedAdapterOptions {
/// Which exception breakpoints were enabled during the last session with this adapter?
pub exception_breakpoints: BTreeMap<String, PersistedExceptionBreakpoint>,
}
impl DapStore { impl DapStore {
pub fn init(client: &AnyProtoClient, cx: &mut App) { pub fn init(client: &AnyProtoClient, cx: &mut App) {
static ADD_LOCATORS: Once = Once::new(); static ADD_LOCATORS: Once = Once::new();
@ -173,6 +185,7 @@ impl DapStore {
breakpoint_store, breakpoint_store,
worktree_store, worktree_store,
sessions: Default::default(), sessions: Default::default(),
adapter_options: Default::default(),
} }
} }
@ -520,65 +533,6 @@ impl DapStore {
)) ))
} }
pub fn evaluate(
&self,
session_id: &SessionId,
stack_frame_id: u64,
expression: String,
context: EvaluateArgumentsContext,
source: Option<Source>,
cx: &mut Context<Self>,
) -> Task<Result<EvaluateResponse>> {
let Some(client) = self
.session_by_id(session_id)
.and_then(|client| client.read(cx).adapter_client())
else {
return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
};
cx.background_executor().spawn(async move {
client
.request::<Evaluate>(EvaluateArguments {
expression: expression.clone(),
frame_id: Some(stack_frame_id),
context: Some(context),
format: None,
line: None,
column: None,
source,
})
.await
})
}
pub fn completions(
&self,
session_id: &SessionId,
stack_frame_id: u64,
text: String,
completion_column: u64,
cx: &mut Context<Self>,
) -> Task<Result<Vec<CompletionItem>>> {
let Some(client) = self
.session_by_id(session_id)
.and_then(|client| client.read(cx).adapter_client())
else {
return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
};
cx.background_executor().spawn(async move {
Ok(client
.request::<Completions>(CompletionsArguments {
frame_id: Some(stack_frame_id),
line: None,
text,
column: completion_column,
})
.await?
.targets)
})
}
pub fn resolve_inline_value_locations( pub fn resolve_inline_value_locations(
&self, &self,
session: Entity<Session>, session: Entity<Session>,
@ -853,6 +807,45 @@ impl DapStore {
}) })
}) })
} }
pub fn sync_adapter_options(
&mut self,
session: &Entity<Session>,
cx: &App,
) -> Arc<PersistedAdapterOptions> {
let session = session.read(cx);
let adapter = session.adapter();
let exceptions = session.exception_breakpoints();
let exception_breakpoints = exceptions
.map(|(exception, enabled)| {
(
exception.filter.clone(),
PersistedExceptionBreakpoint { enabled: *enabled },
)
})
.collect();
let options = Arc::new(PersistedAdapterOptions {
exception_breakpoints,
});
self.adapter_options.insert(adapter, options.clone());
options
}
pub fn set_adapter_options(
&mut self,
adapter: DebugAdapterName,
options: PersistedAdapterOptions,
) {
self.adapter_options.insert(adapter, Arc::new(options));
}
pub fn adapter_options(&self, name: &str) -> Option<Arc<PersistedAdapterOptions>> {
self.adapter_options.get(name).cloned()
}
pub fn all_adapter_options(&self) -> &BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>> {
&self.adapter_options
}
} }
#[derive(Clone)] #[derive(Clone)]

View file

@ -409,17 +409,6 @@ impl RunningMode {
}; };
let configuration_done_supported = ConfigurationDone::is_supported(capabilities); let configuration_done_supported = ConfigurationDone::is_supported(capabilities);
let exception_filters = capabilities
.exception_breakpoint_filters
.as_ref()
.map(|exception_filters| {
exception_filters
.iter()
.filter(|filter| filter.default == Some(true))
.cloned()
.collect::<Vec<_>>()
})
.unwrap_or_default();
// From spec (on initialization sequence): // From spec (on initialization sequence):
// client sends a setExceptionBreakpoints request if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not true) // client sends a setExceptionBreakpoints request if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not true)
// //
@ -434,10 +423,20 @@ impl RunningMode {
.unwrap_or_default(); .unwrap_or_default();
let this = self.clone(); let this = self.clone();
let worktree = self.worktree().clone(); let worktree = self.worktree().clone();
let mut filters = capabilities
.exception_breakpoint_filters
.clone()
.unwrap_or_default();
let configuration_sequence = cx.spawn({ let configuration_sequence = cx.spawn({
async move |_, cx| { async move |session, cx| {
let breakpoint_store = let adapter_name = session.read_with(cx, |this, _| this.adapter())?;
dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?; let (breakpoint_store, adapter_defaults) =
dap_store.read_with(cx, |dap_store, _| {
(
dap_store.breakpoint_store().clone(),
dap_store.adapter_options(&adapter_name),
)
})?;
initialized_rx.await?; initialized_rx.await?;
let errors_by_path = cx let errors_by_path = cx
.update(|cx| this.send_source_breakpoints(false, &breakpoint_store, cx))? .update(|cx| this.send_source_breakpoints(false, &breakpoint_store, cx))?
@ -471,7 +470,25 @@ impl RunningMode {
})?; })?;
if should_send_exception_breakpoints { if should_send_exception_breakpoints {
this.send_exception_breakpoints(exception_filters, supports_exception_filters) _ = session.update(cx, |this, _| {
filters.retain(|filter| {
let is_enabled = if let Some(defaults) = adapter_defaults.as_ref() {
defaults
.exception_breakpoints
.get(&filter.filter)
.map(|options| options.enabled)
.unwrap_or_else(|| filter.default.unwrap_or_default())
} else {
filter.default.unwrap_or_default()
};
this.exception_breakpoints
.entry(filter.filter.clone())
.or_insert_with(|| (filter.clone(), is_enabled));
is_enabled
});
});
this.send_exception_breakpoints(filters, supports_exception_filters)
.await .await
.ok(); .ok();
} }
@ -1233,18 +1250,7 @@ impl Session {
Ok(capabilities) => { Ok(capabilities) => {
this.update(cx, |session, cx| { this.update(cx, |session, cx| {
session.capabilities = capabilities; session.capabilities = capabilities;
let filters = session
.capabilities
.exception_breakpoint_filters
.clone()
.unwrap_or_default();
for filter in filters {
let default = filter.default.unwrap_or_default();
session
.exception_breakpoints
.entry(filter.filter.clone())
.or_insert_with(|| (filter, default));
}
cx.emit(SessionEvent::CapabilitiesLoaded); cx.emit(SessionEvent::CapabilitiesLoaded);
})?; })?;
return Ok(()); return Ok(());