// ***************************************************************************** // Copyright (C) 2018 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { CommandService, Emitter, Event, MapUtils } from '@theia/core/lib/common'; import { LabelProvider, OpenerService, StorageService } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { SourceBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, BaseBreakpoint, InstructionBreakpoint, DataBreakpoint } from './breakpoint-marker'; import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint'; import { DebugFunctionBreakpoint } from '../model/debug-function-breakpoint'; import { DebugInstructionBreakpoint } from '../model/debug-instruction-breakpoint'; import { DebugExceptionBreakpoint } from '../view/debug-exception-breakpoint'; import { DebugDataBreakpoint } from '../model/debug-data-breakpoint'; import { BPCapabilities, DebugBreakpoint, DebugBreakpointOptions } from '../model/debug-breakpoint'; import { DebugProtocol } from '@vscode/debugprotocol'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileChangeType } from '@theia/filesystem/lib/common/files'; export interface BreakpointsChangeEvent { uri: URI added: T[] removed: T[] changed: T[] } export type SourceBreakpointsChangeEvent = BreakpointsChangeEvent; export type FunctionBreakpointsChangeEvent = BreakpointsChangeEvent; export type InstructionBreakpointsChangeEvent = BreakpointsChangeEvent; export type DataBreakpointsChangeEvent = BreakpointsChangeEvent; @injectable() export class BreakpointManager { static EXCEPTION_URI = new URI('debug:exception://'); static FUNCTION_URI = new URI('debug:function://'); static INSTRUCTION_URI = new URI('debug:instruction://'); static DATA_URI = new URI('debug:data://'); // ── Source breakpoints, keyed by URI string ── protected readonly sourceBreakpoints = new Map(); // ── Injected services ── @inject(StorageService) protected readonly storage: StorageService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; @inject(OpenerService) protected readonly openerService: OpenerService; @inject(CommandService) protected readonly commandService: CommandService; @inject(FileService) protected readonly fileService: FileService; // ── Events ── protected readonly onDidChangeMarkersEmitter = new Emitter(); /** * Fires when any breakpoint type changes (source, function, instruction, data, or exception). * The URI identifies the affected resource or the synthetic URI for non-source breakpoint types * (e.g. {@link BreakpointManager.FUNCTION_URI}). */ get onDidChangeMarkers(): Event { return this.onDidChangeMarkersEmitter.event; } protected fireOnDidChangeMarkers(uri: URI): void { this.onDidChangeMarkersEmitter.fire(uri); } protected readonly onDidChangeBreakpointsEmitter = new Emitter(); readonly onDidChangeBreakpoints = this.onDidChangeBreakpointsEmitter.event; protected readonly onDidChangeFunctionBreakpointsEmitter = new Emitter(); readonly onDidChangeFunctionBreakpoints = this.onDidChangeFunctionBreakpointsEmitter.event; protected readonly onDidChangeInstructionBreakpointsEmitter = new Emitter(); readonly onDidChangeInstructionBreakpoints = this.onDidChangeInstructionBreakpointsEmitter.event; protected readonly onDidChangeDataBreakpointsEmitter = new Emitter(); readonly onDidChangeDataBreakpoints = this.onDidChangeDataBreakpointsEmitter.event; // ── Cached options object ── protected _breakpointOptions: DebugBreakpointOptions | undefined; // ── Initialization ── @postConstruct() protected init(): void { this.fileService.onDidFilesChange(event => { if (event.gotDeleted()) { for (const uriString of this.sourceBreakpoints.keys()) { const uri = new URI(uriString); if (event.contains(uri, FileChangeType.DELETED)) { this.sourceBreakpoints.delete(uriString); this.fireOnDidChangeMarkers(uri); } } } }); } // ── Source breakpoint storage ── getBreakpoints(uri?: URI): readonly DebugSourceBreakpoint[] { if (uri) { return this.sourceBreakpoints.get(uri.toString()) ?? []; } const result: DebugSourceBreakpoint[] = []; for (const bps of this.sourceBreakpoints.values()) { result.push(...bps); } return result; } getUris(): IterableIterator { return this.sourceBreakpoints.keys(); } hasBreakpoints(): boolean { return this.sourceBreakpoints.size > 0 || this.functionBreakpoints.length > 0 || this.instructionBreakpoints.length > 0; } /** * Replace the source breakpoints for a URI. Incoming `breakpoints` are * plain `SourceBreakpoint` data; existing `DebugSourceBreakpoint` wrappers * are preserved by ID so that session data survives position changes. */ setBreakpoints(uri: URI, breakpoints: SourceBreakpoint[]): void { const current = this.getBreakpoints(uri); const currentById = new Map(current.map(bp => [bp.id, bp])); const sorted = breakpoints .slice() .sort((a, b) => (a.raw.line - b.raw.line) || ((a.raw.column || 0) - (b.raw.column || 0))); const seen = new Set(); const newBps: DebugSourceBreakpoint[] = []; for (const bp of sorted) { const posKey = `${bp.raw.line}:${bp.raw.column ?? 0}`; if (seen.has(posKey)) { continue; } seen.add(posKey); // Prefer matching by ID (preserves identity across position changes). const existing = currentById.get(bp.id); if (existing) { existing.origin.raw = bp.raw; existing.origin.enabled = bp.enabled; newBps.push(existing); } else { newBps.push(this.toDebugSourceBreakpoint(bp)); } } this.applySourceBreakpoints(uri, newBps, current); } addBreakpoint(breakpoint: SourceBreakpoint): DebugSourceBreakpoint { const uri = new URI(breakpoint.uri); const current = this.getBreakpoints(uri); // Check for positional duplicate. const duplicate = current.find( c => c.line === breakpoint.raw.line && c.column === breakpoint.raw.column ); if (duplicate) { return duplicate; } const bp = this.toDebugSourceBreakpoint(breakpoint); const newBps = [...current, bp]; this.applySourceBreakpoints(uri, newBps, current); return bp; } removeBreakpoint(breakpoint: DebugSourceBreakpoint): void { const current = this.getBreakpoints(breakpoint.uri); const index = current.indexOf(breakpoint); if (index === -1) { return; } const retained = [...current.slice(0, index), ...current.slice(index + 1)]; this.applySourceBreakpoints(breakpoint.uri, retained, current); } /** * Diff `oldBps` → `newBps`, store, fire markers and typed events. * Both arrays must be for the same URI. */ protected applySourceBreakpoints(uri: URI, newBps: readonly DebugSourceBreakpoint[], oldBps: readonly DebugSourceBreakpoint[]): void { const oldById = new Map(oldBps.map(bp => [bp.id, bp])); const added: DebugSourceBreakpoint[] = []; const changed: DebugSourceBreakpoint[] = []; let didChange = false; for (const bp of newBps) { const old = oldById.get(bp.id); if (!old) { added.push(bp); didChange = true; } else { changed.push(bp); // Identity match: the wrapper was mutated in place, or the same // object was passed back (editor model position update). // Either way we must re-fire to keep decorations in sync. didChange = true; oldById.delete(bp.id); } } const removed = Array.from(oldById.values()); didChange ||= removed.length > 0; if (!didChange) { return; } if (newBps.length > 0) { this.sourceBreakpoints.set(uri.toString(), [...newBps]); } else { this.sourceBreakpoints.delete(uri.toString()); } this.fireOnDidChangeMarkers(uri); this.onDidChangeBreakpointsEmitter.fire({ uri, added, removed, changed }); } removeBreakpoints(): void { for (const uriString of [...this.sourceBreakpoints.keys()]) { const uri = new URI(uriString); const old = this.sourceBreakpoints.get(uriString) ?? []; this.sourceBreakpoints.delete(uriString); this.fireOnDidChangeMarkers(uri); if (old.length) { this.onDidChangeBreakpointsEmitter.fire({ uri, added: [], removed: old, changed: [] }); } } this.setFunctionBreakpoints([]); this.setInstructionBreakpoints([]); this.setDataBreakpoints([]); } // ── Query helpers ── getLineBreakpoints(uri: URI, line: number): DebugSourceBreakpoint[] { return this.getBreakpoints(uri).filter(bp => bp.line === line); } getInlineBreakpoint(uri: URI, line: number, column: number): DebugSourceBreakpoint | undefined { return this.getBreakpoints(uri).find(bp => bp.line === line && bp.column === column); } getBreakpointById(id: string): DebugBreakpoint | undefined { for (const bp of this.allBreakpoints()) { if (bp.id === id) { return bp; } } } * allBreakpoints(): IterableIterator { for (const bps of this.sourceBreakpoints.values()) { yield* bps; } yield* this.functionBreakpoints; yield* this.instructionBreakpoints; yield* this.exceptionBreakpoints; yield* this.dataBreakpoints; } // ── Session data ── updateSessionData(sessionId: string, sessionCapabilities: DebugProtocol.Capabilities, bps?: Map): void { const bpCapabilities = this.toBpCapabilities(sessionCapabilities); const updatedUris = new Map(); for (const bp of this.allBreakpoints()) { if (!bps) { if (bp.update(sessionId, undefined)) { MapUtils.addOrInsertWith(updatedUris, bp.uri.toString(), bp); } } else { const dataForBp = bps.get(bp.id); if (!dataForBp) { continue; } bp.update(sessionId, { ...bpCapabilities, ...dataForBp }); MapUtils.addOrInsertWith(updatedUris, bp.uri.toString(), bp); } } for (const changed of updatedUris.values()) { this.fireTypedBreakpointEvent(changed[0].uri, [], changed, []); } } protected toBpCapabilities(capabilities: DebugProtocol.Capabilities): BPCapabilities { return { supportsConditionalBreakpoints: !!capabilities.supportsConditionalBreakpoints, supportsHitConditionalBreakpoints: !!capabilities.supportsHitConditionalBreakpoints, supportsLogPoints: !!capabilities.supportsLogPoints, supportsFunctionBreakpoints: !!capabilities.supportsFunctionBreakpoints, supportsDataBreakpoints: !!capabilities.supportsDataBreakpoints, supportsInstructionBreakpoints: !!capabilities.supportsInstructionBreakpoints, }; } // ── Breakpoint construction ── protected toDebugSourceBreakpoint(source: SourceBreakpoint): DebugSourceBreakpoint { return DebugSourceBreakpoint.create(source, this.getBreakpointOptions()); } getBreakpointOptions(): DebugBreakpointOptions { if (!this._breakpointOptions) { this._breakpointOptions = { labelProvider: this.labelProvider, openerService: this.openerService, commandService: this.commandService, breakpoints: this }; } return this._breakpointOptions; } // ── Enable / disable ── protected _breakpointsEnabled = true; get breakpointsEnabled(): boolean { return this._breakpointsEnabled; } set breakpointsEnabled(breakpointsEnabled: boolean) { if (this._breakpointsEnabled !== breakpointsEnabled) { this._breakpointsEnabled = breakpointsEnabled; for (const uri of this.getUris()) { this.fireOnDidChangeMarkers(new URI(uri)); } this.fireOnDidChangeMarkers(BreakpointManager.FUNCTION_URI); this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI); this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI); } } enableAllBreakpoints(enabled: boolean): void { for (const uriString of this.getUris()) { let didChange = false; const uri = new URI(uriString); const bps = this.getBreakpoints(uri); for (const bp of bps) { didChange ||= this.doEnableBreakpoint(bp, enabled); } if (didChange) { this.fireOnDidChangeMarkers(uri); this.onDidChangeBreakpointsEmitter.fire({ uri, added: [], removed: [], changed: [...bps] }); } } let didChangeFunction = false; for (const breakpoint of this.functionBreakpoints) { if (breakpoint.origin.enabled !== enabled) { breakpoint.origin.enabled = enabled; didChangeFunction = true; } } if (didChangeFunction) { this.fireOnDidChangeMarkers(BreakpointManager.FUNCTION_URI); this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: BreakpointManager.FUNCTION_URI, added: [], removed: [], changed: [...this.functionBreakpoints] }); } let didChangeInstruction = false; for (const breakpoint of this.instructionBreakpoints) { if (breakpoint.origin.enabled !== enabled) { breakpoint.origin.enabled = enabled; didChangeInstruction = true; } } if (didChangeInstruction) { this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, added: [], removed: [], changed: [...this.instructionBreakpoints] }); } let didChangeData = false; for (const breakpoint of this.dataBreakpoints) { if (breakpoint.origin.enabled !== enabled) { breakpoint.origin.enabled = enabled; didChangeData = true; } } if (didChangeData) { this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI); this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, added: [], removed: [], changed: [...this.dataBreakpoints] }); } } enableBreakpoint(breakpoint: T, enabled: boolean): void { const didChange = this.doEnableBreakpoint(breakpoint, enabled); if (didChange) { this.fireBreakpointChanged(breakpoint); } } protected doEnableBreakpoint(breakpoint: DebugBreakpoint, enabled: boolean): boolean { if (breakpoint.origin.enabled !== enabled) { breakpoint.origin.enabled = enabled; return true; } return false; } // ── Generic update / fire ── updateBreakpoint>(bp: T, update: Partial): void { bp.origin.raw = { ...bp.origin.raw, ...update }; this.fireBreakpointChanged(bp); } fireBreakpointChanged(breakpoint: DebugBreakpoint): void { this.fireOnDidChangeMarkers(breakpoint.uri); this.fireTypedBreakpointEvent(breakpoint.uri, [], [breakpoint], []); } protected fireTypedBreakpointEvent(uri: URI, added: DebugBreakpoint[], changed: DebugBreakpoint[], removed: DebugBreakpoint[]): void { // All breakpoints in a single call are the same type (grouped by URI). const sample = added[0] ?? changed[0] ?? removed[0]; if (!sample) { return; } if (sample instanceof DebugSourceBreakpoint) { this.onDidChangeBreakpointsEmitter.fire({ uri, added, changed, removed } as SourceBreakpointsChangeEvent); } else if (sample instanceof DebugFunctionBreakpoint) { this.onDidChangeFunctionBreakpointsEmitter.fire({ uri, added, changed, removed } as FunctionBreakpointsChangeEvent); } else if (sample instanceof DebugInstructionBreakpoint) { this.onDidChangeInstructionBreakpointsEmitter.fire({ uri, added, changed, removed } as InstructionBreakpointsChangeEvent); } else if (sample instanceof DebugDataBreakpoint) { this.onDidChangeDataBreakpointsEmitter.fire({ uri, added, changed, removed } as DataBreakpointsChangeEvent); } } // ── Bulk remove by ID (plugin API) ── removeBreakpointsById(ids: string[]): void { const toRemove = new Set(ids); // Source breakpoints for (const [uriString, bps] of this.sourceBreakpoints.entries()) { const retained = bps.filter(bp => !toRemove.has(bp.id)); if (retained.length !== bps.length) { const removed = bps.filter(bp => toRemove.has(bp.id)); const uri = new URI(uriString); if (retained.length > 0) { this.sourceBreakpoints.set(uriString, retained); } else { this.sourceBreakpoints.delete(uriString); } this.fireOnDidChangeMarkers(uri); this.onDidChangeBreakpointsEmitter.fire({ uri, removed, added: [], changed: [] }); } } // Function breakpoints const functionRemoved: DebugFunctionBreakpoint[] = []; this.functionBreakpoints = this.functionBreakpoints.filter(bp => { if (toRemove.has(bp.id)) { functionRemoved.push(bp); return false; } return true; }); if (functionRemoved.length) { this.fireOnDidChangeMarkers(BreakpointManager.FUNCTION_URI); this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: BreakpointManager.FUNCTION_URI, removed: functionRemoved, added: [], changed: [] }); } // Instruction breakpoints const instructionRemoved: DebugInstructionBreakpoint[] = []; this.instructionBreakpoints = this.instructionBreakpoints.filter(bp => { if (toRemove.has(bp.id)) { instructionRemoved.push(bp); return false; } return true; }); if (instructionRemoved.length) { this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, removed: instructionRemoved, added: [], changed: [] }); } // Data breakpoints const dataRemoved: DebugDataBreakpoint[] = []; this.dataBreakpoints = this.dataBreakpoints.filter(bp => { if (toRemove.has(bp.id)) { dataRemoved.push(bp); return false; } return true; }); if (dataRemoved.length) { this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI); this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, removed: dataRemoved, added: [], changed: [] }); } } // ── Exception breakpoints ── protected exceptionBreakpoints = new Array(); getExceptionBreakpoint(filter: DebugProtocol.ExceptionBreakpointsFilter): DebugExceptionBreakpoint | undefined { return this.exceptionBreakpoints.find(candidate => ExceptionBreakpoint.matches(candidate.origin.raw, filter)); } getExceptionBreakpoints(): readonly DebugExceptionBreakpoint[] { return this.exceptionBreakpoints; } addExceptionBreakpoints(filters: DebugProtocol.ExceptionBreakpointsFilter[], sessionId: string): void { for (const filter of filters) { let bp = this.exceptionBreakpoints.find(candidate => ExceptionBreakpoint.matches(candidate.origin.raw, filter)); if (!bp) { bp = DebugExceptionBreakpoint.create(ExceptionBreakpoint.create(filter), this.getBreakpointOptions()); this.exceptionBreakpoints.push(bp); } bp.setSessionEnablement(sessionId, true); this.doUpdateExceptionBreakpointVisibility(sessionId); } this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI); } updateExceptionBreakpointVisibility(sessionId: string): void { this.doUpdateExceptionBreakpointVisibility(sessionId); this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI); } clearExceptionSessionEnablement(sessionId: string): void { for (const bp of this.exceptionBreakpoints) { bp.setSessionEnablement(sessionId, false); } this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI); } protected doUpdateExceptionBreakpointVisibility(sessionId: string): void { for (const bp of this.exceptionBreakpoints) { bp.setPersistentVisibility(bp.isEnabledForSession(sessionId)); } } // ── Function breakpoints ── protected functionBreakpoints: DebugFunctionBreakpoint[] = []; getFunctionBreakpoints(): readonly DebugFunctionBreakpoint[] { return this.functionBreakpoints; } setFunctionBreakpoints(functionBreakpoints: FunctionBreakpoint[]): void { const oldBreakpoints = new Map(this.functionBreakpoints.map(b => [b.id, b])); this.functionBreakpoints = functionBreakpoints.map(bp => { const existing = oldBreakpoints.get(bp.id); if (existing) { return existing; } return DebugFunctionBreakpoint.create(bp, this.getBreakpointOptions()); }); this.fireOnDidChangeMarkers(BreakpointManager.FUNCTION_URI); const added: DebugFunctionBreakpoint[] = []; const removed: DebugFunctionBreakpoint[] = []; const changed: DebugFunctionBreakpoint[] = []; const ids = new Set(); for (const newBreakpoint of this.functionBreakpoints) { ids.add(newBreakpoint.id); if (oldBreakpoints.has(newBreakpoint.id)) { changed.push(newBreakpoint); } else { added.push(newBreakpoint); } } for (const [id, breakpoint] of oldBreakpoints.entries()) { if (!ids.has(id)) { removed.push(breakpoint); } } this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: BreakpointManager.FUNCTION_URI, added, removed, changed }); } addFunctionBreakpoint(bp: FunctionBreakpoint): void { const duplicate = this.functionBreakpoints.find(c => c.origin.raw.name === bp.raw.name); if (duplicate) { return; } const newBp = DebugFunctionBreakpoint.create(bp, this.getBreakpointOptions()); this.functionBreakpoints = [...this.functionBreakpoints, newBp]; this.fireOnDidChangeMarkers(newBp.uri); this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: newBp.uri, added: [newBp], changed: [], removed: [] }); } updateFunctionBreakpoint(bp: DebugFunctionBreakpoint, update: Partial): void { if (!this.functionBreakpoints.includes(bp)) { return; } const removed: DebugFunctionBreakpoint[] = []; if ('name' in update && !update.name) { throw new Error('Name field of function breakpoint must be populated.'); } else if ('name' in update) { this.functionBreakpoints = this.functionBreakpoints.filter(candidate => { if (candidate !== bp && candidate.origin.raw.name === update.name) { removed.push(candidate); return false; } return true; }); } bp.origin.raw = { ...bp.origin.raw, ...update }; this.fireOnDidChangeMarkers(bp.uri); this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: bp.uri, changed: [bp], removed, added: [] }); } removeFunctionBreakpoint(breakpoint: DebugFunctionBreakpoint): void { const index = this.functionBreakpoints.indexOf(breakpoint); if (index === -1) { return; } const removed = this.functionBreakpoints.splice(index, 1); this.fireOnDidChangeMarkers(breakpoint.uri); this.onDidChangeFunctionBreakpointsEmitter.fire({ uri: breakpoint.uri, removed, added: [], changed: [] }); } // ── Instruction breakpoints ── protected instructionBreakpoints: DebugInstructionBreakpoint[] = []; getInstructionBreakpoints(): ReadonlyArray { return this.instructionBreakpoints; } protected setInstructionBreakpoints(newBreakpoints: InstructionBreakpoint[]): void { const oldBreakpoints = new Map(this.instructionBreakpoints.map(bp => [bp.id, bp])); const currentBreakpoints = newBreakpoints.map(bp => { const existing = oldBreakpoints.get(bp.id); if (existing) { return existing; } return DebugInstructionBreakpoint.create(bp, this.getBreakpointOptions()); }); const added: DebugInstructionBreakpoint[] = []; const changed: DebugInstructionBreakpoint[] = []; for (const breakpoint of currentBreakpoints) { const old = oldBreakpoints.get(breakpoint.id); if (old) { changed.push(old); } else { added.push(breakpoint); } oldBreakpoints.delete(breakpoint.id); } const removed = Array.from(oldBreakpoints.values()); this.instructionBreakpoints = currentBreakpoints; this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, added, removed, changed }); } addInstructionBreakpoint(address: string, offset: number, condition?: string, hitCondition?: string): void { const duplicate = this.instructionBreakpoints.find( c => c.origin.raw.instructionReference === address && (c.origin.raw.offset ?? 0) === (offset ?? 0) ); if (duplicate) { return; } const newBp = DebugInstructionBreakpoint.create(InstructionBreakpoint.create({ instructionReference: address, offset, condition, hitCondition, }), this.getBreakpointOptions()); this.instructionBreakpoints = [...this.instructionBreakpoints, newBp]; this.fireOnDidChangeMarkers(BreakpointManager.INSTRUCTION_URI); this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: BreakpointManager.INSTRUCTION_URI, added: [newBp], removed: [], changed: [] }); } removeInstructionBreakpoint(breakpoint: DebugInstructionBreakpoint): void { const index = this.instructionBreakpoints.indexOf(breakpoint); if (index === -1) { return; } const removed = this.instructionBreakpoints.splice(index, 1); this.fireOnDidChangeMarkers(breakpoint.uri); this.onDidChangeInstructionBreakpointsEmitter.fire({ uri: breakpoint.uri, removed, added: [], changed: [] }); } removeInstructionBreakpointAt(address: string): void { const match = this.instructionBreakpoints.find(candidate => candidate.origin.raw.instructionReference === address); if (match) { this.removeInstructionBreakpoint(match); } } clearInstructionBreakpoints(): void { this.setInstructionBreakpoints([]); } // ── Data breakpoints ── protected dataBreakpoints: DebugDataBreakpoint[] = []; getDataBreakpoints(): readonly DebugDataBreakpoint[] { return this.dataBreakpoints; } setDataBreakpoints(breakpoints: DataBreakpoint[]): void { const oldBreakpoints = new Map(this.dataBreakpoints.map(bp => [bp.id, bp])); const newBreakpoints = breakpoints.map(bp => { const existing = oldBreakpoints.get(bp.id); if (existing) { return existing; } return DebugDataBreakpoint.create(bp, this.getBreakpointOptions()); }); const added: DebugDataBreakpoint[] = []; const changed: DebugDataBreakpoint[] = []; for (const bp of newBreakpoints) { if (oldBreakpoints.has(bp.id)) { changed.push(bp); } else { added.push(bp); } oldBreakpoints.delete(bp.id); } const removed = Array.from(oldBreakpoints.values()); this.dataBreakpoints = newBreakpoints; this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI); this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, added, removed, changed }); } addDataBreakpoint(breakpoint: DataBreakpoint): void { const duplicate = this.dataBreakpoints.find(c => c.origin.raw.dataId === breakpoint.raw.dataId); if (duplicate) { return; } const newBp = DebugDataBreakpoint.create(breakpoint, this.getBreakpointOptions()); this.dataBreakpoints = [...this.dataBreakpoints, newBp]; this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI); this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, added: [newBp], removed: [], changed: [] }); } updateDataBreakpoint(bp: DebugDataBreakpoint, options: { enabled?: boolean; raw?: Partial> }): void { if (!this.dataBreakpoints.includes(bp)) { return; } if (options.raw) { Object.assign(bp.origin.raw, options.raw); } if (options.enabled !== undefined) { bp.origin.enabled = options.enabled; } this.fireBreakpointChanged(bp); } removeDataBreakpoint(bp: DebugDataBreakpoint): void { const index = this.dataBreakpoints.indexOf(bp); if (index < 0) { return; } const removed = this.dataBreakpoints.splice(index, 1); this.fireOnDidChangeMarkers(BreakpointManager.DATA_URI); this.onDidChangeDataBreakpointsEmitter.fire({ uri: BreakpointManager.DATA_URI, added: [], removed, changed: [] }); } // ── Persistence ── async load(): Promise { const data = await this.storage.getData('breakpoints', { breakpointsEnabled: true, breakpoints: {} }); this._breakpointsEnabled = data.breakpointsEnabled; // eslint-disable-next-line guard-for-in for (const uri in data.breakpoints) { this.setBreakpoints(new URI(uri), data.breakpoints[uri]); } if (data.functionBreakpoints) { this.setFunctionBreakpoints(data.functionBreakpoints); } if (data.exceptionBreakpoints) { this.exceptionBreakpoints = data.exceptionBreakpoints.map(bp => DebugExceptionBreakpoint.create(bp, this.getBreakpointOptions())); this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI); } if (data.instructionBreakpoints) { this.setInstructionBreakpoints(data.instructionBreakpoints); } if (data.dataBreakpoints) { this.setDataBreakpoints(data.dataBreakpoints); } } save(): void { const data: BreakpointManager.Data = { breakpointsEnabled: this._breakpointsEnabled, breakpoints: {} }; for (const uri of this.getUris()) { data.breakpoints[uri] = (this.sourceBreakpoints.get(uri) ?? []).map(bp => bp.origin); } if (this.functionBreakpoints.length) { data.functionBreakpoints = this.functionBreakpoints.map(({ origin }) => origin); } if (this.exceptionBreakpoints.length) { data.exceptionBreakpoints = this.exceptionBreakpoints.filter(candidate => candidate.isPersistentlyVisible()).map(({ origin }) => origin); } if (this.instructionBreakpoints.length) { data.instructionBreakpoints = this.instructionBreakpoints.map(({ origin }) => origin); } const dataBreakpointsToStore = this.dataBreakpoints.filter(candidate => candidate.origin.info.canPersist); if (dataBreakpointsToStore.length) { data.dataBreakpoints = dataBreakpointsToStore.map(({ origin }) => origin); } this.storage.setData('breakpoints', data); } } export namespace BreakpointManager { export interface Data { breakpointsEnabled: boolean; breakpoints: { [uri: string]: SourceBreakpoint[]; } exceptionBreakpoints?: ExceptionBreakpoint[]; functionBreakpoints?: FunctionBreakpoint[]; instructionBreakpoints?: InstructionBreakpoint[]; dataBreakpoints?: DataBreakpoint[]; } }