///|
/// A detached desired-configuration value. Accessors return copies: a backend
/// cannot mutate the session's authoritative arrays.
pub struct BreakpointConfiguration {
priv sources : Array[SourceBreakpoint]
priv functions : Array[FunctionBreakpoint]
}
///|
fn BreakpointConfiguration::new(
sources : Array[SourceBreakpoint],
functions : Array[FunctionBreakpoint],
) -> BreakpointConfiguration {
{ sources: sources.copy(), functions: functions.copy(), }
}
///|
pub fn BreakpointConfiguration::sources(
self : BreakpointConfiguration,
) -> Array[SourceBreakpoint] {
self.sources.copy()
}
///|
pub fn BreakpointConfiguration::functions(
self : BreakpointConfiguration,
) -> Array[FunctionBreakpoint] {
self.functions.copy()
}
///|
pub struct BreakpointSyncResult {
priv sources : Array[BreakpointSnapshot]
priv functions : Array[FunctionBreakpointSnapshot]
}
///|
pub fn BreakpointSyncResult::new(
sources : Array[BreakpointSnapshot],
functions : Array[FunctionBreakpointSnapshot],
) -> BreakpointSyncResult {
{ sources: sources.copy(), functions: functions.copy(), }
}
///|
pub fn BreakpointSyncResult::sources(
self : BreakpointSyncResult,
) -> Array[BreakpointSnapshot] {
self.sources.copy()
}
///|
pub fn BreakpointSyncResult::functions(
self : BreakpointSyncResult,
) -> Array[FunctionBreakpointSnapshot] {
self.functions.copy()
}
///|
pub(all) suberror BreakpointSyncError {
BreakpointSyncRejected(String)
BreakpointSyncUncertain(String)
} derive(Debug)
///|
pub extend BreakpointSyncError with @debug.Debug::{to_repr}
///|
pub enum BreakpointTarget {
SourceTarget(display_path~ : String, source_path~ : String, line~ : Int)
FunctionTarget(display_name~ : String, selector~ : FunctionBreakpointSelector)
} derive(Debug, Eq)
///|
pub extend BreakpointTarget with @debug.Debug::{to_repr}
///|
pub extend BreakpointTarget with Eq::{equal, not_equal}
///|
/// Installation freshness is independent from the desired enabled flag.
pub enum BreakpointInstallation {
NotInstalled
CurrentInstallation(BreakpointStatus)
PreviousInstallation(BreakpointStatus)
} derive(Debug, Eq)
///|
pub extend BreakpointInstallation with @debug.Debug::{to_repr}
///|
pub extend BreakpointInstallation with Eq::{equal, not_equal}
///|
pub struct BreakpointListEntry {
id : Int
target : BreakpointTarget
enabled : Bool
installation : BreakpointInstallation
actual_line : Int?
location_count : Int?
locations : Array[BreakpointLocation]
} derive(Debug)
///|
pub extend BreakpointListEntry with @debug.Debug::{to_repr}
///|
pub(all) enum BreakpointOperation {
DeleteBreakpoint
DisableBreakpoint
EnableBreakpoint
} derive(Debug, Eq)
///|
pub extend BreakpointOperation with @debug.Debug::{to_repr}
///|
pub extend BreakpointOperation with Eq::{equal, not_equal}
///|
pub struct BreakpointManagementResult {
operation : BreakpointOperation
breakpoint : BreakpointListEntry
changed : Bool
} derive(Debug)
///|
pub extend BreakpointManagementResult with @debug.Debug::{to_repr}
///|
pub fn DebugSession::breakpoint_configuration(
self : DebugSession,
) -> BreakpointConfiguration {
BreakpointConfiguration::new(
self.source_breakpoints,
self.function_breakpoints,
)
}
///|
fn installation_freshness(
status : BreakpointStatus,
current : Bool,
) -> BreakpointInstallation {
if status is Pending {
NotInstalled
} else if current {
CurrentInstallation(status)
} else {
PreviousInstallation(status)
}
}
///|
/// This query performs no async work and never waits for adapter preparation.
pub fn DebugSession::breakpoints(
self : DebugSession,
) -> Array[BreakpointListEntry] {
let sources : Map[Int, BreakpointSnapshot] = Map([])
let functions : Map[Int, FunctionBreakpointSnapshot] = Map([])
for snapshot in self.source_breakpoints() {
sources.set(snapshot.id(), snapshot)
}
for snapshot in self.function_breakpoints() {
functions.set(snapshot.id(), snapshot)
}
let entries : Array[BreakpointListEntry] = []
for breakpoint in self.source_breakpoints {
let snapshot = sources
.get(breakpoint.id())
.unwrap_or(breakpoint.pending_snapshot())
entries.push({
id: breakpoint.id(),
target: SourceTarget(
display_path=breakpoint.display_path(),
source_path=breakpoint.source_path(),
line=breakpoint.requested_line(),
),
enabled: breakpoint.enabled(),
installation: installation_freshness(
snapshot.status(),
self.state() is Stopped && breakpoint.enabled(),
),
actual_line: snapshot.actual_line(),
location_count: None,
locations: snapshot.locations(),
})
}
for breakpoint in self.function_breakpoints {
let snapshot = functions
.get(breakpoint.id())
.unwrap_or(breakpoint.pending_snapshot())
entries.push({
id: breakpoint.id(),
target: FunctionTarget(
display_name=breakpoint.display_name(),
selector=breakpoint.selector(),
),
enabled: breakpoint.enabled(),
installation: installation_freshness(
snapshot.status(),
self.state() is Stopped && breakpoint.enabled(),
),
actual_line: match snapshot.locations().get(0) {
Some(location) => location.line
None => None
},
location_count: snapshot.location_count(),
locations: snapshot.locations(),
})
}
entries.sort_by((left, right) => left.id.compare(right.id))
entries
}
///|
async fn DebugSession::synchronize_breakpoints(
self : DebugSession,
configuration : BreakpointConfiguration,
) -> BreakpointSyncResult {
try {
let result = self.backend.synchronize_breakpoints(configuration)
let expected_sources = configuration
.sources()
.filter(item => item.enabled())
let expected_functions = configuration
.functions()
.filter(item => item.enabled())
let sources_by_id : Map[Int, SourceBreakpoint] = Map([])
let functions_by_id : Map[Int, FunctionBreakpoint] = Map([])
for item in expected_sources {
sources_by_id.set(item.id(), item)
}
for item in expected_functions {
functions_by_id.set(item.id(), item)
}
let seen : Map[Int, Bool] = Map([])
guard result.sources.length() == expected_sources.length() &&
result.functions.length() == expected_functions.length() else {
raise BreakpointSyncError::BreakpointSyncUncertain(
"incomplete breakpoint synchronization result",
)
}
for snapshot in result.sources {
guard seen.get(snapshot.id()) is None &&
sources_by_id.get(snapshot.id()) is Some(expected) &&
expected.source_path() == snapshot.source_path() &&
expected.requested_line() == snapshot.requested_line() else {
raise BreakpointSyncError::BreakpointSyncUncertain(
"invalid source breakpoint synchronization result",
)
}
seen.set(snapshot.id(), true)
}
for snapshot in result.functions {
guard seen.get(snapshot.id()) is None &&
functions_by_id.get(snapshot.id()) is Some(expected) &&
expected.display_name() == snapshot.display_name() else {
raise BreakpointSyncError::BreakpointSyncUncertain(
"invalid function breakpoint synchronization result",
)
}
seen.set(snapshot.id(), true)
}
for snapshot in result.sources {
self.source_sync_results.set(snapshot.id(), snapshot)
}
for snapshot in result.functions {
self.function_sync_results.set(snapshot.id(), snapshot)
}
result
} catch {
BreakpointSyncError::BreakpointSyncRejected(message) =>
raise BreakpointManagementRejected(message)
error => {
let message = match error {
BreakpointSyncError::BreakpointSyncUncertain(message) => message
DebugBackendError::BackendFailure(message) => message
_ => @debug.to_string(error)
}
self.backend.abandon_execution() catch {
_ => ()
}
self.backend_failed(
"\{message}; current execution was discarded",
next_state=Ready,
)
}
}
}
///|
pub async fn DebugSession::manage_breakpoint(
self : DebugSession,
id : Int,
operation : BreakpointOperation,
) -> BreakpointManagementResult {
guard self.state() is Ready || self.state() is Stopped else {
raise OperationNotAllowed(state=self.state(), action="manage breakpoints")
}
guard id > 0 &&
self.breakpoints().iter().find_first(item => item.id == id)
is Some(previous) else {
raise BreakpointNotFound(id)
}
let desired_enabled = operation is EnableBreakpoint
if !(operation is DeleteBreakpoint) && previous.enabled == desired_enabled {
return { operation, breakpoint: previous, changed: false, }
}
let sources = self.source_breakpoints.filter_map(item => {
if item.id() != id {
Some(item)
} else if operation is DeleteBreakpoint {
None
} else {
Some(item.with_enabled(desired_enabled))
}
})
let functions = self.function_breakpoints.filter_map(item => {
if item.id() != id {
Some(item)
} else if operation is DeleteBreakpoint {
None
} else {
Some(item.with_enabled(desired_enabled))
}
})
if self.state() is Stopped {
let old_sources = self.source_breakpoints
let old_functions = self.function_breakpoints
if operation is EnableBreakpoint {
// Like adding an enabled breakpoint, resolution failure and uncertain
// execution loss retain the user's enabled intent for the next run.
self.source_breakpoints = sources
self.function_breakpoints = functions
}
try
self.synchronize_breakpoints(
BreakpointConfiguration::new(sources, functions),
)
|> ignore
catch {
BreakpointManagementRejected(message) => {
self.source_breakpoints = old_sources
self.function_breakpoints = old_functions
raise BreakpointManagementRejected(message)
}
error => raise error
}
}
// Commit only after the backend has confirmed the desired configuration.
self.source_breakpoints = sources
self.function_breakpoints = functions
let entry = self
.breakpoints()
.iter()
.find_first(item => item.id == id)
.unwrap_or(previous)
{ operation, breakpoint: entry, changed: true, }
}