From ede5a2a6c8041197832c8089fb9d2e3f48bee103 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Mon, 24 Jun 2024 15:20:21 +0200 Subject: [PATCH 1/3] Extend support for data breakpoints in VS Code API - Allow the user to manage data breakpoints through vscode.debug API - Add methods to gain information about a potential data breakpoint - Allow data breakpoints to have different sources -- Keep use case where data id is already known (current) -- Add infrastructure for already existing address resolution -- Extend for dynamic variables resolved for a session --- Ensure dynamic variables are resolved in the debug model Communication: - Adapt DataBreakpoint with source between extension host and main - Expose DataBreakpoint in VS Code API, previously not exposed Minor: - Make bytes optional in data bytes info, as it is the same in the DAP Fixes https://github.com/microsoft/vscode/issues/195151 --- .../src/singlefolder-tests/debug.test.ts | 61 ++++++++++- .../api/browser/mainThreadDebugService.ts | 33 +++++- .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 12 ++- .../api/common/extHostDebugService.ts | 22 +++- src/vs/workbench/api/common/extHostTypes.ts | 20 ++-- .../contrib/debug/browser/debugSession.ts | 6 +- .../workbench/contrib/debug/common/debug.ts | 27 ++++- .../contrib/debug/common/debugModel.ts | 10 ++ src/vscode-dts/vscode.d.ts | 102 ++++++++++++++++++ 10 files changed, 267 insertions(+), 27 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index cc2f2675297c4..3ac4cbba82245 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { basename } from 'path'; -import { commands, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode'; +import { commands, DataBreakpoint, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode'; import { assertNoRpc, createRandomFile, disposeAll } from '../utils'; suite('vscode API - debug', function () { @@ -60,6 +60,65 @@ suite('vscode API - debug', function () { assert.strictEqual(functionBreakpoint.functionName, 'func'); }); + + test('data breakpoint - dataId', async function () { + debug.addBreakpoints([new DataBreakpoint({ type: 'variable', dataId: 'dataId' }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + const variableDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; + assert.strictEqual(variableDbp.condition, 'condition'); + assert.strictEqual(variableDbp.hitCondition, 'hitCondition'); + assert.strictEqual(variableDbp.logMessage, 'logMessage'); + assert.strictEqual(variableDbp.enabled, false); + assert.strictEqual(variableDbp.label, 'data'); + assert.strictEqual(variableDbp.source.type, 'variable'); + assert.strictEqual(variableDbp.source.dataId, 'dataId'); + assert.strictEqual(variableDbp.canPersist, false); + assert.strictEqual(variableDbp.accessType, 'readWrite'); + }); + + test('data breakpoint - variable', async function () { + debug.addBreakpoints([new DataBreakpoint('dataId', 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + const dataIdDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; + assert.strictEqual(dataIdDbp.condition, 'condition'); + assert.strictEqual(dataIdDbp.hitCondition, 'hitCondition'); + assert.strictEqual(dataIdDbp.logMessage, 'logMessage'); + assert.strictEqual(dataIdDbp.enabled, false); + assert.strictEqual(dataIdDbp.label, 'data'); + assert.strictEqual(dataIdDbp.source.type, 'variable'); + assert.strictEqual(dataIdDbp.source.dataId, 'dataId'); + assert.strictEqual(dataIdDbp.canPersist, false); + assert.strictEqual(dataIdDbp.accessType, 'readWrite'); + }); + + test('data breakpoint - address', async function () { + debug.addBreakpoints([new DataBreakpoint({ type: 'address', address: '0x00000', bytes: 4 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + const addressDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; + assert.strictEqual(addressDbp.condition, 'condition'); + assert.strictEqual(addressDbp.hitCondition, 'hitCondition'); + assert.strictEqual(addressDbp.logMessage, 'logMessage'); + assert.strictEqual(addressDbp.enabled, false); + assert.strictEqual(addressDbp.label, 'data'); + assert.strictEqual(addressDbp.source.type, 'address'); + assert.strictEqual(addressDbp.source.address, '0x00000'); + assert.strictEqual(addressDbp.source.bytes, 4); + assert.strictEqual(addressDbp.canPersist, false); + assert.strictEqual(addressDbp.accessType, 'readWrite'); + }); + + test('data breakpoint - dynamic variable', async function () { + debug.addBreakpoints([new DataBreakpoint({ type: 'dynamicVariable', name: 'i', variablesReference: 1000 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + const dynamicVariableDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; + assert.strictEqual(dynamicVariableDbp.condition, 'condition'); + assert.strictEqual(dynamicVariableDbp.hitCondition, 'hitCondition'); + assert.strictEqual(dynamicVariableDbp.logMessage, 'logMessage'); + assert.strictEqual(dynamicVariableDbp.enabled, false); + assert.strictEqual(dynamicVariableDbp.label, 'data'); + assert.strictEqual(dynamicVariableDbp.source.type, 'dynamicVariable'); + assert.strictEqual(dynamicVariableDbp.source.name, 'i'); + assert.strictEqual(dynamicVariableDbp.source.variablesReference, 1000); + assert.strictEqual(dynamicVariableDbp.canPersist, false); + assert.strictEqual(dynamicVariableDbp.accessType, 'readWrite'); + }); + test('start debugging', async function () { let stoppedEvents = 0; let variablesReceived: () => void; diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index cccdce7eb4ca3..e827ccb8a6fc9 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -8,7 +8,8 @@ import { URI as uri, UriComponents } from '../../../base/common/uri.js'; import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from '../../contrib/debug/common/debug.js'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, - IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto + IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto, + IDataBreakpointInfo } from '../common/extHost.protocol.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import severity from '../../../base/common/severity.js'; @@ -173,7 +174,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb const bps = this.debugService.getModel().getBreakpoints(); const fbps = this.debugService.getModel().getFunctionBreakpoints(); const dbps = this.debugService.getModel().getDataBreakpoints(); - if (bps.length > 0 || fbps.length > 0) { + if (bps.length > 0 || fbps.length > 0 || dbps.length > 0) { this._proxy.$acceptBreakpointsDelta({ added: this.convertToDto(bps).concat(this.convertToDto(fbps)).concat(this.convertToDto(dbps)) }); @@ -234,10 +235,16 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } else if (dto.type === 'data') { this.debugService.addDataBreakpoint({ description: dto.label, - src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId }, + src: dto.source.type === 'variable' ? { type: DataBreakpointSetType.Variable, dataId: dto.source.dataId } + : dto.source.type === 'dynamicVariable' ? { type: DataBreakpointSetType.DynamicVariable, name: dto.source.name, variablesReference: dto.source.variablesReference } + : { type: DataBreakpointSetType.Address, address: dto.source.address, bytes: dto.source.bytes }, + condition: dto.condition, + enabled: dto.enabled, + hitCondition: dto.hitCondition, canPersist: dto.canPersist, accessTypes: dto.accessTypes, accessType: dto.accessType, + logMessage: dto.logMessage, mode: dto.mode }); } @@ -369,6 +376,22 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.reject(new ErrorNoTelemetry('debug session not found')); } + public $getDataBreakpointInfo(sessionId: DebugSessionUUID, name: string, variablesReference?: number): Promise { + const session = this.debugService.getModel().getSession(sessionId, true); + if (session) { + return Promise.resolve(session.dataBreakpointInfo(name, variablesReference)); + } + return Promise.reject(new ErrorNoTelemetry('debug session not found')); + } + + public $getDataBytesBreakpointInfo(sessionId: DebugSessionUUID, address: string, bytes?: number): Promise { + const session = this.debugService.getModel().getSession(sessionId, true); + if (session) { + return Promise.resolve(session.dataBytesBreakpointInfo(address, bytes)); + } + return Promise.reject(new ErrorNoTelemetry('debug session not found')); + } + public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise { if (sessionId) { const session = this.debugService.getModel().getSession(sessionId, true); @@ -457,7 +480,9 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return { type: 'data', id: dbp.getId(), - dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address, + source: dbp.src.type === DataBreakpointSetType.Variable ? { type: 'variable', dataId: dbp.src.dataId } + : dbp.src.type === DataBreakpointSetType.DynamicVariable ? { type: 'dynamicVariable', name: dbp.src.name, variablesReference: dbp.src.variablesReference } + : { type: 'address', address: dbp.src.address, bytes: dbp.src.bytes }, enabled: dbp.enabled, condition: dbp.condition, hitCondition: dbp.hitCondition, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 561da239ea885..9a0a8e5c5a328 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1562,6 +1562,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CompletionTriggerKind: extHostTypes.CompletionTriggerKind, ConfigurationTarget: extHostTypes.ConfigurationTarget, CustomExecution: extHostTypes.CustomExecution, + DataBreakpoint: extHostTypes.DataBreakpoint, DebugAdapterExecutable: extHostTypes.DebugAdapterExecutable, DebugAdapterInlineImplementation: extHostTypes.DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer: extHostTypes.DebugAdapterNamedPipeServer, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7b0ba03044c0d..52a406e2edb8f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -83,7 +83,7 @@ import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../servic import * as search from '../../services/search/common/search.js'; import { TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; -import type { TerminalShellExecutionCommandLineConfidence } from 'vscode'; +import type * as vscode from 'vscode'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -1632,6 +1632,8 @@ export interface MainThreadDebugServiceShape extends IDisposable { $setDebugSessionName(id: DebugSessionUUID, name: string): void; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; $getDebugProtocolBreakpoint(id: DebugSessionUUID, breakpoinId: string): Promise; + $getDataBreakpointInfo(id: DebugSessionUUID, name: string, variablesReference?: number): Promise; + $getDataBytesBreakpointInfo(id: DebugSessionUUID, address: string, bytes?: number): Promise; $appendDebugConsole(value: string): void; $registerBreakpoints(breakpoints: Array): Promise; $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise; @@ -2338,8 +2340,8 @@ export interface ExtHostTerminalServiceShape { export interface ExtHostTerminalShellIntegrationShape { $shellIntegrationChange(instanceId: number): void; - $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void; - $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void; + $shellExecutionStart(instanceId: number, commandLineValue: string, commandLineConfidence: vscode.TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, cwd: UriComponents | undefined): void; + $shellExecutionEnd(instanceId: number, commandLineValue: string, commandLineConfidence: vscode.TerminalShellExecutionCommandLineConfidence, isTrusted: boolean, exitCode: number | undefined): void; $shellExecutionData(instanceId: number, data: string): void; $cwdChange(instanceId: number, cwd: UriComponents | undefined): void; $closeTerminal(instanceId: number): void; @@ -2392,9 +2394,11 @@ export interface IFunctionBreakpointDto extends IBreakpointDto { mode?: string; } +export type IDataBreakpointInfo = DebugProtocol.DataBreakpointInfoResponse['body']; + export interface IDataBreakpointDto extends IBreakpointDto { type: 'data'; - dataId: string; + source: vscode.DataBreakpointSource; canPersist: boolean; label: string; accessTypes?: DebugProtocol.DataBreakpointAccessType[]; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index d072857559cb4..c03908f9de31f 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -12,7 +12,7 @@ import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/ex import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { ISignService } from '../../../platform/sign/common/sign.js'; import { IWorkspaceFolder } from '../../../platform/workspace/common/workspace.js'; -import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThreadFocusDto, IStackFrameFocusDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from './extHost.protocol.js'; +import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThreadFocusDto, IStackFrameFocusDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape, IDataBreakpointDto } from './extHost.protocol.js'; import { IExtHostEditorTabs } from './extHostEditorTabs.js'; import { IExtHostExtensionService } from './extHostExtensionService.js'; import { IExtHostRpcService } from './extHostRpcService.js'; @@ -411,7 +411,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I this.fireBreakpointChanges(breakpoints, [], []); // convert added breakpoints to DTOs - const dtos: Array = []; + const dtos: Array = []; const map = new Map(); for (const bp of breakpoints) { if (bp instanceof SourceBreakpoint) { @@ -728,7 +728,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I if (bpd.type === 'function') { bp = new FunctionBreakpoint(bpd.functionName, bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage, bpd.mode); } else if (bpd.type === 'data') { - bp = new DataBreakpoint(bpd.label, bpd.dataId, bpd.canPersist, bpd.enabled, bpd.hitCondition, bpd.condition, bpd.logMessage, bpd.mode); + bp = new DataBreakpoint(bpd.source, bpd.accessType, bpd.canPersist, bpd.label, bpd.enabled, bpd.hitCondition, bpd.condition, bpd.logMessage, bpd.mode); } else { const uri = URI.revive(bpd.uri); bp = new SourceBreakpoint(new Location(uri, new Position(bpd.line, bpd.character)), bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage, bpd.mode); @@ -769,6 +769,16 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I sbp.hitCondition = bpd.hitCondition; sbp.logMessage = bpd.logMessage; sbp.location = new Location(URI.revive(bpd.uri), new Position(bpd.line, bpd.character)); + } else if (bp instanceof DataBreakpoint && bpd.type === 'data') { + const dbp = bp; + dbp.enabled = bpd.enabled; + dbp.condition = bpd.condition; + dbp.hitCondition = bpd.hitCondition; + dbp.logMessage = bpd.logMessage; + dbp.label = bpd.label; + dbp.source = bpd.source; + dbp.canPersist = bpd.canPersist; + dbp.accessType = bpd.accessType; } c.push(bp); } @@ -1133,6 +1143,12 @@ export class ExtHostDebugSession { }, getDebugProtocolBreakpoint(breakpoint: vscode.Breakpoint): Promise { return that._debugServiceProxy.$getDebugProtocolBreakpoint(that._id, breakpoint.id); + }, + getDataBreakpointInfo(name: string, variablesReference?: number): Promise { + return that._debugServiceProxy.$getDataBreakpointInfo(that._id, name, variablesReference); + }, + getDataBytesBreakpointInfo(address: string, bytes?: number): Promise { + return that._debugServiceProxy.$getDataBytesBreakpointInfo(that._id, address, bytes); } }); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 842dbbf56790b..1037ced99a54f 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3076,17 +3076,19 @@ export class FunctionBreakpoint extends Breakpoint { @es5ClassCompat export class DataBreakpoint extends Breakpoint { readonly label: string; - readonly dataId: string; + readonly source: vscode.DataBreakpointSource; readonly canPersist: boolean; + readonly accessType: vscode.DataBreakpointAccessType; - constructor(label: string, dataId: string, canPersist: boolean, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, mode?: string) { + constructor(source: vscode.DataBreakpointSource | string, accessType: vscode.DataBreakpointAccessType, canPersist?: boolean, label?: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, mode?: string) { super(enabled, condition, hitCondition, logMessage, mode); - if (!dataId) { - throw illegalArgument('dataId'); - } - this.label = label; - this.dataId = dataId; - this.canPersist = canPersist; + this.source = typeof source === 'string' ? { type: 'variable', dataId: source } : source; + this.accessType = accessType; + this.canPersist = canPersist ?? false; + this.label = label ? label + : this.source.type === 'variable' ? `DataId '${this.source.dataId}'` + : this.source.type === 'address' ? `Address '${this.source.address}${this.source.bytes ? `,${this.source.bytes}'` : ''}` + : `Variable '${this.source.name}${this.source.variablesReference ? `,${this.source.variablesReference}` : ''}'`; } } @@ -4127,7 +4129,7 @@ export function validateTestCoverageCount(cc?: vscode.TestCoverageCount) { } if (cc.covered > cc.total) { - throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total (${cc.total})`); + throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total(${cc.total})`); } if (cc.total < 0) { diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index b275e635fa8f6..6aab855e6d3d8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -545,7 +545,7 @@ export class DebugSession implements IDebugSession, IDisposable { } } - dataBytesBreakpointInfo(address: string, bytes: number): Promise { + dataBytesBreakpointInfo(address: string, bytes?: number): Promise { if (this.raw?.capabilities.supportsDataBreakpointBytes === false) { throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes")); } @@ -553,11 +553,11 @@ export class DebugSession implements IDebugSession, IDisposable { return this._dataBreakpointInfo({ name: address, bytes, asAddress: true }); } - dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + dataBreakpointInfo(name: string, variablesReference?: number): Promise { return this._dataBreakpointInfo({ name, variablesReference }); } - private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8910cce2bb2d0..da23fefcefecb 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -433,7 +433,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; dataBreakpointInfo(name: string, variablesReference?: number): Promise; - dataBytesBreakpointInfo(address: string, bytes: number): Promise; + dataBytesBreakpointInfo(address: string, bytes?: number): Promise; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendInstructionBreakpoints(dbps: IInstructionBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; @@ -646,6 +646,7 @@ export interface IExceptionBreakpoint extends IBaseBreakpoint { export const enum DataBreakpointSetType { Variable, Address, + DynamicVariable } /** @@ -654,8 +655,28 @@ export const enum DataBreakpointSetType { * can request info repeated and use session-specific data. */ export type DataBreakpointSource = - | { type: DataBreakpointSetType.Variable; dataId: string } - | { type: DataBreakpointSetType.Address; address: string; bytes: number }; + | { + /** The source type for variable-based data breakpoints. */ + type: DataBreakpointSetType.Variable; + /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ + dataId: string; + } + | { + /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.DynamicVariable; + /** The name of the variable's child to obtain data breakpoint information for. If `variablesReference` isn't specified, this can be an expression. */ + name: string; + /** Reference to the variable container if the data breakpoint is requested for a child of the container. */ + variablesReference?: number; + } + | { + /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.Address; + /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ + address: string; + /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ + bytes?: number; + }; export interface IDataBreakpoint extends IBaseBreakpoint { readonly description: string; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index e6dba4a911e8c..ed207fb7913da 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -1205,6 +1205,16 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { let dataId: string; if (this.src.type === DataBreakpointSetType.Variable) { dataId = this.src.dataId; + } else if (this.src.type === DataBreakpointSetType.DynamicVariable) { + let sessionDataId = this.sessionDataIdForAddr.get(session); + if (!sessionDataId) { + sessionDataId = (await session.dataBreakpointInfo(this.src.name, this.src.variablesReference))?.dataId; + if (!sessionDataId) { + return undefined; + } + this.sessionDataIdForAddr.set(session, sessionDataId); + } + dataId = sessionDataId; } else { let sessionDataId = this.sessionDataIdForAddr.get(session); if (!sessionDataId) { diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 1b2d9a0c924c1..6ec9f85c83ee7 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -16083,6 +16083,24 @@ declare module 'vscode' { * @returns A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. */ getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; + + /** + * Obtains information on a possible data breakpoint that could be set on an expression or variable. + * This will fail if the corresponding capability `supportsDataBreakpoints` is not supported by this session. + * + * @param name The name of the variable's child to obtain data breakpoint information for. If `variablesReference` isn't specified, this can be an expression. + * @param variablesReference Reference to the variable container if the data breakpoint is requested for a child of the container. + */ + getDataBreakpointInfo(name: string, variablesReference?: number): Thenable; + + /** + * Obtains information on a possible data breakpoint that could be set on an address for a given specified length. + * This will fail if the corresponding capability `supportsDataBreakpoints` and `supportsDataBreakpointBytes` is not supported by this session. + * + * @param address A memory address as a decimal value, or hex value if it is prefixed with `0x`. + * @param bytes If specified, returns information for the range of memory extending `bytes` number of bytes from the address. + */ + getDataBytesBreakpointInfo(address: string, bytes?: number): Thenable; } /** @@ -16448,6 +16466,90 @@ declare module 'vscode' { constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); } + /** + * Information on a possible data breakpoint. + */ + export interface DataBreakpointInfo { + /** An identifier for the data on which a data breakpoint can be created or null if no data breakpoint is available. Breakpoints added using the `dataId` may outlive the lifetime of the associated `dataId`. */ + dataId: string | null; + /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ + description: string; + /** Attribute lists the available access types for a potential data breakpoint. */ + accessTypes?: DataBreakpointAccessType[]; + /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ + canPersist?: boolean; + } + + /** + * The source for a data breakpoint. + */ + export type DataBreakpointSource = + | { + /** The source type for variable-based data breakpoints. */ + type: 'variable'; + /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ + dataId: string; + } + | { + /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: 'address'; + /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ + address: string; + /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ + bytes?: number; + } + | { + /** The source type for variables that are dynamically resolved when the breakpoint is activated. */ + type: 'dynamicVariable'; + /** The name of the variable's child to obtain data breakpoint information for. If `variablesReference` isn't specified, this can be an expression. */ + name: string; + /** Reference to the variable container if the data breakpoint is requested for a child of the container. */ + variablesReference?: number; + }; + + + /** + * A breakpoint specified by a variable or memory change. + */ + export class DataBreakpoint extends Breakpoint { + /** + * The human-readable label for the data breakpoint. + */ + label: string; + + /** + * The source for the data breakpoint. If the `dataId` is known already, it can be specified directly using the `variable` type. Alternatively, VSCode may resolve the `dataId` based on the address or dynamic variable that is specified. + */ + source: DataBreakpointSource; + + /** + * Flag to indicate if the data breakpoint could be persisted across sessions. + */ + canPersist: boolean; + + /** + * The access type of the data. + */ + accessType: DataBreakpointAccessType; + + /** + * Create a new data breakpoint. + * + * @param source The source for the data breakpoint. If the `dataId` is known already, it can be specified directly. If the dataId is not known, it can be retrieved via the `getDataBreakpointInfo` or `getDataBytesBreakpointInfo` request from the session. Alternatively, an address or variable can be specified as source which will be resolved in the context of the session in a similar manner. + * @param accessType The access type of the data breakpoint. + * @param canPersist Flag to indicate if the data breakpoint could be persisted across sessions. + * @param label The human-readable label for the data breakpoint. + * @param enabled Is breakpoint enabled. + * @param condition Expression for conditional breakpoints. + * @param hitCondition Expression that controls how many hits of the breakpoint are ignored. + * @param logMessage Log message to display when breakpoint is hit. + */ + constructor(source: DataBreakpointSource | string, accessType: DataBreakpointAccessType, canPersist?: boolean, label?: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); + } + + /** Access type for data breakpoints. */ + export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; + /** * Debug console mode used by debug session, see {@link DebugSessionOptions options}. */ From 8f778fca9510e8a5419d5fc1e0a5ee2df6bcbf5e Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Tue, 3 Sep 2024 17:32:04 +0200 Subject: [PATCH 2/3] Incorporate PR feedback - Properly extract API proposal into it's own file - Remove data breakpoint info convenience method - Provide proposal for different data breakpoint sources (discussion) - Ensure that breakpoint mode is properly updated and passed through --- .../src/singlefolder-tests/debug.test.ts | 37 ++++++- .../common/extensionsApiProposals.ts | 3 + .../api/browser/mainThreadDebugService.ts | 40 +++---- .../workbench/api/common/extHost.protocol.ts | 4 - .../api/common/extHostDebugService.ts | 30 ++++-- src/vs/workbench/api/common/extHostTypes.ts | 7 +- .../contrib/debug/browser/debugSession.ts | 4 +- .../workbench/contrib/debug/common/debug.ts | 40 +++++-- .../contrib/debug/common/debugModel.ts | 56 ++++++---- src/vscode-dts/vscode.d.ts | 102 ------------------ .../vscode.proposed.debugDataBreakpoints.d.ts | 96 +++++++++++++++++ 11 files changed, 238 insertions(+), 181 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index 3ac4cbba82245..45e8b381fea21 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -104,21 +104,48 @@ suite('vscode API - debug', function () { assert.strictEqual(addressDbp.accessType, 'readWrite'); }); - test('data breakpoint - dynamic variable', async function () { - debug.addBreakpoints([new DataBreakpoint({ type: 'dynamicVariable', name: 'i', variablesReference: 1000 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + test('data breakpoint - expression', async function () { + debug.addBreakpoints([new DataBreakpoint({ type: 'expression', expression: 'i' }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); const dynamicVariableDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; assert.strictEqual(dynamicVariableDbp.condition, 'condition'); assert.strictEqual(dynamicVariableDbp.hitCondition, 'hitCondition'); assert.strictEqual(dynamicVariableDbp.logMessage, 'logMessage'); assert.strictEqual(dynamicVariableDbp.enabled, false); assert.strictEqual(dynamicVariableDbp.label, 'data'); - assert.strictEqual(dynamicVariableDbp.source.type, 'dynamicVariable'); - assert.strictEqual(dynamicVariableDbp.source.name, 'i'); - assert.strictEqual(dynamicVariableDbp.source.variablesReference, 1000); + assert.strictEqual(dynamicVariableDbp.source.type, 'expression'); + assert.strictEqual(dynamicVariableDbp.source.expression, 'i'); assert.strictEqual(dynamicVariableDbp.canPersist, false); assert.strictEqual(dynamicVariableDbp.accessType, 'readWrite'); }); + test('data breakpoint - scoped', async function () { + debug.addBreakpoints([new DataBreakpoint({ type: 'scoped', expression: 'exp()', frameId: 1 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + const scopedExpression = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; + assert.strictEqual(scopedExpression.condition, 'condition'); + assert.strictEqual(scopedExpression.hitCondition, 'hitCondition'); + assert.strictEqual(scopedExpression.logMessage, 'logMessage'); + assert.strictEqual(scopedExpression.enabled, false); + assert.strictEqual(scopedExpression.label, 'data'); + assert.strictEqual(scopedExpression.source.type, 'scoped'); + assert.strictEqual(scopedExpression.source.frameId, 1); + assert.strictEqual(scopedExpression.source.expression, 'exp()'); + assert.strictEqual(scopedExpression.canPersist, false); + assert.strictEqual(scopedExpression.accessType, 'readWrite'); + + debug.addBreakpoints([new DataBreakpoint({ type: 'scoped', variable: 'var', variablesReference: 1 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + const scopedVariable = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; + assert.strictEqual(scopedVariable.condition, 'condition'); + assert.strictEqual(scopedVariable.hitCondition, 'hitCondition'); + assert.strictEqual(scopedVariable.logMessage, 'logMessage'); + assert.strictEqual(scopedVariable.enabled, false); + assert.strictEqual(scopedVariable.label, 'data'); + assert.strictEqual(scopedVariable.source.type, 'scoped'); + assert.strictEqual(scopedVariable.source.variablesReference, 1); + assert.strictEqual(scopedVariable.source.variable, 'var'); + assert.strictEqual(scopedVariable.canPersist, false); + assert.strictEqual(scopedVariable.accessType, 'readWrite'); + }); + test('start debugging', async function () { let stoppedEvents = 0; let variablesReceived: () => void; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 362b4298cdda1..f51de44e2c21a 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -154,6 +154,9 @@ const _allApiProposals = { customEditorMove: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts', }, + debugDataBreakpoints: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts', + }, debugVisualization: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.debugVisualization.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index e827ccb8a6fc9..fd763dc9f999e 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -8,8 +8,7 @@ import { URI as uri, UriComponents } from '../../../base/common/uri.js'; import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from '../../contrib/debug/common/debug.js'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, - IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto, - IDataBreakpointInfo + IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto } from '../common/extHost.protocol.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import severity from '../../../base/common/severity.js'; @@ -236,8 +235,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb this.debugService.addDataBreakpoint({ description: dto.label, src: dto.source.type === 'variable' ? { type: DataBreakpointSetType.Variable, dataId: dto.source.dataId } - : dto.source.type === 'dynamicVariable' ? { type: DataBreakpointSetType.DynamicVariable, name: dto.source.name, variablesReference: dto.source.variablesReference } - : { type: DataBreakpointSetType.Address, address: dto.source.address, bytes: dto.source.bytes }, + : dto.source.type === 'address' ? { type: DataBreakpointSetType.Address, address: dto.source.address, bytes: dto.source.bytes } + : dto.source.type === 'expression' ? { type: DataBreakpointSetType.Expression, expression: dto.source.expression } + : dto.source.frameId ? { type: DataBreakpointSetType.Scoped, expression: dto.source.expression, frameId: dto.source.frameId } + : dto.source.variablesReference ? { type: DataBreakpointSetType.Scoped, variable: dto.source.variable, variablesReference: dto.source.variablesReference } + : { type: DataBreakpointSetType.Variable, dataId: '' }, // should not happen condition: dto.condition, enabled: dto.enabled, hitCondition: dto.hitCondition, @@ -376,22 +378,6 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.reject(new ErrorNoTelemetry('debug session not found')); } - public $getDataBreakpointInfo(sessionId: DebugSessionUUID, name: string, variablesReference?: number): Promise { - const session = this.debugService.getModel().getSession(sessionId, true); - if (session) { - return Promise.resolve(session.dataBreakpointInfo(name, variablesReference)); - } - return Promise.reject(new ErrorNoTelemetry('debug session not found')); - } - - public $getDataBytesBreakpointInfo(sessionId: DebugSessionUUID, address: string, bytes?: number): Promise { - const session = this.debugService.getModel().getSession(sessionId, true); - if (session) { - return Promise.resolve(session.dataBytesBreakpointInfo(address, bytes)); - } - return Promise.reject(new ErrorNoTelemetry('debug session not found')); - } - public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise { if (sessionId) { const session = this.debugService.getModel().getSession(sessionId, true); @@ -473,7 +459,8 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb condition: fbp.condition, hitCondition: fbp.hitCondition, logMessage: fbp.logMessage, - functionName: fbp.name + functionName: fbp.name, + mode: fbp.mode } satisfies IFunctionBreakpointDto; } else if ('src' in bp) { const dbp: IDataBreakpoint = bp; @@ -481,15 +468,19 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb type: 'data', id: dbp.getId(), source: dbp.src.type === DataBreakpointSetType.Variable ? { type: 'variable', dataId: dbp.src.dataId } - : dbp.src.type === DataBreakpointSetType.DynamicVariable ? { type: 'dynamicVariable', name: dbp.src.name, variablesReference: dbp.src.variablesReference } - : { type: 'address', address: dbp.src.address, bytes: dbp.src.bytes }, + : dbp.src.type === DataBreakpointSetType.Address ? { type: 'address', address: dbp.src.address, bytes: dbp.src.bytes } + : dbp.src.type === DataBreakpointSetType.Expression ? { type: 'expression', expression: dbp.src.expression } + : dbp.src.frameId ? { type: 'scoped', expression: dbp.src.expression, frameId: dbp.src.frameId } + : dbp.src.variablesReference ? { type: 'scoped', variable: dbp.src.variable, variablesReference: dbp.src.variablesReference } + : { type: 'variable', dataId: '' }, // should not happen enabled: dbp.enabled, condition: dbp.condition, hitCondition: dbp.hitCondition, logMessage: dbp.logMessage, accessType: dbp.accessType, label: dbp.description, - canPersist: dbp.canPersist + canPersist: dbp.canPersist, + mode: dbp.mode } satisfies IDataBreakpointDto; } else if ('uri' in bp) { const sbp: IBreakpoint = bp; @@ -503,6 +494,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb uri: sbp.uri, line: sbp.lineNumber > 0 ? sbp.lineNumber - 1 : 0, character: (typeof sbp.column === 'number' && sbp.column > 0) ? sbp.column - 1 : 0, + mode: sbp.mode } satisfies ISourceBreakpointDto; } else { return undefined; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 52a406e2edb8f..ffd7dbf262717 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1632,8 +1632,6 @@ export interface MainThreadDebugServiceShape extends IDisposable { $setDebugSessionName(id: DebugSessionUUID, name: string): void; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; $getDebugProtocolBreakpoint(id: DebugSessionUUID, breakpoinId: string): Promise; - $getDataBreakpointInfo(id: DebugSessionUUID, name: string, variablesReference?: number): Promise; - $getDataBytesBreakpointInfo(id: DebugSessionUUID, address: string, bytes?: number): Promise; $appendDebugConsole(value: string): void; $registerBreakpoints(breakpoints: Array): Promise; $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise; @@ -2394,8 +2392,6 @@ export interface IFunctionBreakpointDto extends IBreakpointDto { mode?: string; } -export type IDataBreakpointInfo = DebugProtocol.DataBreakpointInfoResponse['body']; - export interface IDataBreakpointDto extends IBreakpointDto { type: 'data'; source: vscode.DataBreakpointSource; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index c03908f9de31f..9c2364d573714 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -31,6 +31,7 @@ import { IExtHostCommands } from './extHostCommands.js'; import * as Convert from './extHostTypeConverters.js'; import { coalesce } from '../../../base/common/arrays.js'; import { IExtHostTesting } from './extHostTesting.js'; +import { Mutable } from '../../../base/common/types.js'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -446,6 +447,20 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I functionName: bp.functionName, mode: bp.mode, }); + } else if (bp instanceof DataBreakpoint) { + dtos.push({ + type: 'data', + id: bp.id, + enabled: bp.enabled, + hitCondition: bp.hitCondition, + logMessage: bp.logMessage, + condition: bp.condition, + source: bp.source, + mode: bp.mode, + canPersist: bp.canPersist, + accessType: bp.accessType, + label: bp.label + }); } } @@ -756,21 +771,23 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I const bp = this._breakpoints.get(bpd.id); if (bp) { if (bp instanceof FunctionBreakpoint && bpd.type === 'function') { - const fbp = bp; + const fbp = >bp; fbp.enabled = bpd.enabled; fbp.condition = bpd.condition; fbp.hitCondition = bpd.hitCondition; fbp.logMessage = bpd.logMessage; fbp.functionName = bpd.functionName; + fbp.mode = bpd.mode; } else if (bp instanceof SourceBreakpoint && bpd.type === 'source') { - const sbp = bp; + const sbp = >bp; sbp.enabled = bpd.enabled; sbp.condition = bpd.condition; sbp.hitCondition = bpd.hitCondition; sbp.logMessage = bpd.logMessage; sbp.location = new Location(URI.revive(bpd.uri), new Position(bpd.line, bpd.character)); + sbp.mode = bpd.mode; } else if (bp instanceof DataBreakpoint && bpd.type === 'data') { - const dbp = bp; + const dbp = >bp; dbp.enabled = bpd.enabled; dbp.condition = bpd.condition; dbp.hitCondition = bpd.hitCondition; @@ -778,6 +795,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I dbp.label = bpd.label; dbp.source = bpd.source; dbp.canPersist = bpd.canPersist; + dbp.mode = bpd.mode; dbp.accessType = bpd.accessType; } c.push(bp); @@ -1143,12 +1161,6 @@ export class ExtHostDebugSession { }, getDebugProtocolBreakpoint(breakpoint: vscode.Breakpoint): Promise { return that._debugServiceProxy.$getDebugProtocolBreakpoint(that._id, breakpoint.id); - }, - getDataBreakpointInfo(name: string, variablesReference?: number): Promise { - return that._debugServiceProxy.$getDataBreakpointInfo(that._id, name, variablesReference); - }, - getDataBytesBreakpointInfo(address: string, bytes?: number): Promise { - return that._debugServiceProxy.$getDataBytesBreakpointInfo(that._id, address, bytes); } }); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1037ced99a54f..7f72af8eda985 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3088,7 +3088,10 @@ export class DataBreakpoint extends Breakpoint { this.label = label ? label : this.source.type === 'variable' ? `DataId '${this.source.dataId}'` : this.source.type === 'address' ? `Address '${this.source.address}${this.source.bytes ? `,${this.source.bytes}'` : ''}` - : `Variable '${this.source.name}${this.source.variablesReference ? `,${this.source.variablesReference}` : ''}'`; + : this.source.type === 'expression' ? `Expression '${this.source.expression}'` + : this.source.frameId ? `Scoped '${this.source.expression}@${this.source.frameId}'` + : this.source.variablesReference ? `Scoped '${this.source.variable}@${this.source.variablesReference}'` + : `Unknown data breakpoint`; } } @@ -4129,7 +4132,7 @@ export function validateTestCoverageCount(cc?: vscode.TestCoverageCount) { } if (cc.covered > cc.total) { - throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total(${cc.total})`); + throw new Error(`The total number of covered items (${cc.covered}) cannot be greater than the total (${cc.total})`); } if (cc.total < 0) { diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 6aab855e6d3d8..aa410c01a229e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -553,8 +553,8 @@ export class DebugSession implements IDebugSession, IDisposable { return this._dataBreakpointInfo({ name: address, bytes, asAddress: true }); } - dataBreakpointInfo(name: string, variablesReference?: number): Promise { - return this._dataBreakpointInfo({ name, variablesReference }); + dataBreakpointInfo(name: string, variablesReference?: number, frameId?: number): Promise { + return this._dataBreakpointInfo({ name, variablesReference, frameId }); } private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index da23fefcefecb..64a820f174087 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -432,7 +432,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; - dataBreakpointInfo(name: string, variablesReference?: number): Promise; + dataBreakpointInfo(name: string, variablesReference?: number, frameId?: number): Promise; dataBytesBreakpointInfo(address: string, bytes?: number): Promise; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendInstructionBreakpoints(dbps: IInstructionBreakpoint[]): Promise; @@ -646,7 +646,8 @@ export interface IExceptionBreakpoint extends IBaseBreakpoint { export const enum DataBreakpointSetType { Variable, Address, - DynamicVariable + Expression, + Scoped } /** @@ -661,14 +662,6 @@ export type DataBreakpointSource = /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ dataId: string; } - | { - /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ - type: DataBreakpointSetType.DynamicVariable; - /** The name of the variable's child to obtain data breakpoint information for. If `variablesReference` isn't specified, this can be an expression. */ - name: string; - /** Reference to the variable container if the data breakpoint is requested for a child of the container. */ - variablesReference?: number; - } | { /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ type: DataBreakpointSetType.Address; @@ -676,7 +669,32 @@ export type DataBreakpointSource = address: string; /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ bytes?: number; - }; + } + | { + /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.Expression; + /** A global expression that is first evaluated when the breakpoint is activated. */ + expression: string; + } + | { + /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.Scoped; + } & ( + | { + /** The name of the variable that is used for resolution. */ + variable: string; + /** Reference to the variable container that has the variable named `variable`. */ + variablesReference: number; + frameId?: never; + } + | { + /** The name of the expression that is used for resolution. */ + expression: string; + /** Reference to the stack frame to which the expression is scoped. */ + frameId: number; + variablesReference?: never; + } + ); export interface IDataBreakpoint extends IBaseBreakpoint { readonly description: string; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index ed207fb7913da..ac9f994a47c31 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -1175,7 +1175,7 @@ export interface IDataBreakpointOptions extends IBaseBreakpointOptions { } export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { - private readonly sessionDataIdForAddr = new WeakMap(); + private readonly sessionDataId = new WeakMap(); public readonly description: string; public readonly src: DataBreakpointSource; @@ -1197,34 +1197,46 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { this.accessTypes = opts.accessTypes; this.accessType = opts.accessType; if (opts.initialSessionData) { - this.sessionDataIdForAddr.set(opts.initialSessionData.session, opts.initialSessionData.dataId); + this.sessionDataId.set(opts.initialSessionData.session, opts.initialSessionData.dataId); } } async toDAP(session: IDebugSession): Promise { - let dataId: string; - if (this.src.type === DataBreakpointSetType.Variable) { - dataId = this.src.dataId; - } else if (this.src.type === DataBreakpointSetType.DynamicVariable) { - let sessionDataId = this.sessionDataIdForAddr.get(session); - if (!sessionDataId) { - sessionDataId = (await session.dataBreakpointInfo(this.src.name, this.src.variablesReference))?.dataId; - if (!sessionDataId) { - return undefined; + let dataId = this.sessionDataId.get(session); + if (!dataId) { + if (this.src.type === DataBreakpointSetType.Variable) { + dataId = this.src.dataId; + } else if (this.src.type === DataBreakpointSetType.Address) { + const sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; + if (sessionDataId) { + this.sessionDataId.set(session, sessionDataId); + dataId = sessionDataId; } - this.sessionDataIdForAddr.set(session, sessionDataId); - } - dataId = sessionDataId; - } else { - let sessionDataId = this.sessionDataIdForAddr.get(session); - if (!sessionDataId) { - sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; - if (!sessionDataId) { - return undefined; + } else if (this.src.type === DataBreakpointSetType.Expression) { + const sessionDataId = (await session.dataBreakpointInfo(this.src.expression))?.dataId; + if (sessionDataId) { + this.sessionDataId.set(session, sessionDataId); + dataId = sessionDataId; + } + } else { + // type === DataBreakpointSetType.Scoped + if (this.src.frameId) { + const sessionDataId = (await session.dataBreakpointInfo(this.src.expression, undefined, this.src.frameId))?.dataId; + if (sessionDataId) { + this.sessionDataId.set(session, sessionDataId); + dataId = sessionDataId; + } + } else if (this.src.variablesReference) { + const sessionDataId = (await session.dataBreakpointInfo(this.src.variable, this.src.variablesReference))?.dataId; + if (sessionDataId) { + this.sessionDataId.set(session, sessionDataId); + dataId = sessionDataId; + } } - this.sessionDataIdForAddr.set(session, sessionDataId); } - dataId = sessionDataId; + } + if (!dataId) { + return; } return { diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 6ec9f85c83ee7..1b2d9a0c924c1 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -16083,24 +16083,6 @@ declare module 'vscode' { * @returns A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. */ getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; - - /** - * Obtains information on a possible data breakpoint that could be set on an expression or variable. - * This will fail if the corresponding capability `supportsDataBreakpoints` is not supported by this session. - * - * @param name The name of the variable's child to obtain data breakpoint information for. If `variablesReference` isn't specified, this can be an expression. - * @param variablesReference Reference to the variable container if the data breakpoint is requested for a child of the container. - */ - getDataBreakpointInfo(name: string, variablesReference?: number): Thenable; - - /** - * Obtains information on a possible data breakpoint that could be set on an address for a given specified length. - * This will fail if the corresponding capability `supportsDataBreakpoints` and `supportsDataBreakpointBytes` is not supported by this session. - * - * @param address A memory address as a decimal value, or hex value if it is prefixed with `0x`. - * @param bytes If specified, returns information for the range of memory extending `bytes` number of bytes from the address. - */ - getDataBytesBreakpointInfo(address: string, bytes?: number): Thenable; } /** @@ -16466,90 +16448,6 @@ declare module 'vscode' { constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); } - /** - * Information on a possible data breakpoint. - */ - export interface DataBreakpointInfo { - /** An identifier for the data on which a data breakpoint can be created or null if no data breakpoint is available. Breakpoints added using the `dataId` may outlive the lifetime of the associated `dataId`. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Attribute lists the available access types for a potential data breakpoint. */ - accessTypes?: DataBreakpointAccessType[]; - /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - } - - /** - * The source for a data breakpoint. - */ - export type DataBreakpointSource = - | { - /** The source type for variable-based data breakpoints. */ - type: 'variable'; - /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ - dataId: string; - } - | { - /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ - type: 'address'; - /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ - address: string; - /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ - bytes?: number; - } - | { - /** The source type for variables that are dynamically resolved when the breakpoint is activated. */ - type: 'dynamicVariable'; - /** The name of the variable's child to obtain data breakpoint information for. If `variablesReference` isn't specified, this can be an expression. */ - name: string; - /** Reference to the variable container if the data breakpoint is requested for a child of the container. */ - variablesReference?: number; - }; - - - /** - * A breakpoint specified by a variable or memory change. - */ - export class DataBreakpoint extends Breakpoint { - /** - * The human-readable label for the data breakpoint. - */ - label: string; - - /** - * The source for the data breakpoint. If the `dataId` is known already, it can be specified directly using the `variable` type. Alternatively, VSCode may resolve the `dataId` based on the address or dynamic variable that is specified. - */ - source: DataBreakpointSource; - - /** - * Flag to indicate if the data breakpoint could be persisted across sessions. - */ - canPersist: boolean; - - /** - * The access type of the data. - */ - accessType: DataBreakpointAccessType; - - /** - * Create a new data breakpoint. - * - * @param source The source for the data breakpoint. If the `dataId` is known already, it can be specified directly. If the dataId is not known, it can be retrieved via the `getDataBreakpointInfo` or `getDataBytesBreakpointInfo` request from the session. Alternatively, an address or variable can be specified as source which will be resolved in the context of the session in a similar manner. - * @param accessType The access type of the data breakpoint. - * @param canPersist Flag to indicate if the data breakpoint could be persisted across sessions. - * @param label The human-readable label for the data breakpoint. - * @param enabled Is breakpoint enabled. - * @param condition Expression for conditional breakpoints. - * @param hitCondition Expression that controls how many hits of the breakpoint are ignored. - * @param logMessage Log message to display when breakpoint is hit. - */ - constructor(source: DataBreakpointSource | string, accessType: DataBreakpointAccessType, canPersist?: boolean, label?: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); - } - - /** Access type for data breakpoints. */ - export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; - /** * Debug console mode used by debug session, see {@link DebugSessionOptions options}. */ diff --git a/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts b/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts new file mode 100644 index 0000000000000..5f1f6a32bd597 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/195151 + + /** + * The source for a data breakpoint. + */ + export type DataBreakpointSource = + | { + /** The source type for fixed data identifiers that do not need to be re-resolved when the breakpoint is activated. */ + type: 'variable'; + /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ + dataId: string; + } + | { + /** The source type for address-based data breakpoints. Address-based data breakpoints are re-resolved when the breakpoint is activated. This type only applies to sessions that have the `supportsDataBreakpointBytes` capability. */ + type: 'address'; + /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ + address: string; + /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ + bytes?: number; + } + | { + /** The source type for expressions that are dynamically resolved when the breakpoint is activated. */ + type: 'expression'; + /** A global expression that is first evaluated when the breakpoint is activated. */ + expression: string; + } + | { + /** The source type for scoped variables and expressions that are dynamically resolved when the breakpoint is activated. */ + type: 'scoped'; + } & ( + | { + /** The name of the variable that is used for resolution. */ + variable: string; + /** Reference to the variable container that has the variable named `variable`. */ + variablesReference: number; + frameId?: never; + } + | { + /** The name of the expression that is used for resolution. */ + expression: string; + /** Reference to the stack frame to which the expression is scoped. */ + frameId: number; + variablesReference?: never; + } + ); + + + /** Access type for data breakpoints. */ + export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; + + /** + * A breakpoint specified by a variable or memory change. + */ + export class DataBreakpoint extends Breakpoint { + /** + * The human-readable label for the data breakpoint. + */ + label: string; + + /** + * The source for the data breakpoint. See the different source types on how they are resolved during breakpoint activation. + */ + source: DataBreakpointSource; + + /** + * Flag to indicate if the data breakpoint could be persisted across sessions. + */ + canPersist: boolean; + + /** + * The access type of the data. + */ + accessType: DataBreakpointAccessType; + + /** + * Create a new data breakpoint. + * + * @param source The source for the data breakpoint. If the `dataId` is known already, it can be specified directly. If the dataId is not known, it can be retrieved via a data breakpoint info request from the session. Alternatively, some sources offer dynamic resolution during breakpoint activation. + * @param accessType The access type of the data breakpoint. + * @param canPersist Flag to indicate if the data breakpoint could be persisted across sessions. + * @param label The human-readable label for the data breakpoint. + * @param enabled Is breakpoint enabled. + * @param condition Expression for conditional breakpoints. + * @param hitCondition Expression that controls how many hits of the breakpoint are ignored. + * @param logMessage Log message to display when breakpoint is hit. + */ + constructor(source: DataBreakpointSource | string, accessType: DataBreakpointAccessType, canPersist?: boolean, label?: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); + } +} From 10a283e222c8b65117b35c469aa08999a4490d53 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Fri, 1 Nov 2024 14:57:48 +0100 Subject: [PATCH 3/3] More PR feedback - Translate complex types into separate classes - Properly resolve data breakpoints as soon as the user adds them -- Use resolution result to store additional data used later --- .../src/singlefolder-tests/debug.test.ts | 63 ++---- .../api/browser/mainThreadDebugService.ts | 34 ++-- .../workbench/api/common/extHost.api.impl.ts | 5 + .../workbench/api/common/extHost.protocol.ts | 4 +- .../api/common/extHostDebugService.ts | 21 +- src/vs/workbench/api/common/extHostTypes.ts | 81 ++++++-- .../contrib/debug/browser/breakpointsView.ts | 12 +- .../contrib/debug/browser/debugService.ts | 2 +- .../contrib/debug/browser/variablesView.ts | 18 +- .../workbench/contrib/debug/common/debug.ts | 115 +++++++---- .../contrib/debug/common/debugModel.ts | 75 +------ .../contrib/debug/common/debugStorage.ts | 2 +- .../debug/test/browser/breakpoints.test.ts | 12 +- .../vscode.proposed.debugDataBreakpoints.d.ts | 184 ++++++++++++------ 14 files changed, 355 insertions(+), 273 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index 45e8b381fea21..0b991a5f47656 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { basename } from 'path'; -import { commands, DataBreakpoint, debug, Disposable, FunctionBreakpoint, window, workspace } from 'vscode'; +import { AddressDataBreakpointSource, commands, DataBreakpoint, debug, Disposable, ExpressionDataBreakpointSource, FrameScopedDataBreakpointSource, FunctionBreakpoint, ResolvedDataBreakpointSource, VariableScopedDataBreakpointSource, window, workspace } from 'vscode'; import { assertNoRpc, createRandomFile, disposeAll } from '../utils'; suite('vscode API - debug', function () { @@ -62,87 +62,64 @@ suite('vscode API - debug', function () { test('data breakpoint - dataId', async function () { - debug.addBreakpoints([new DataBreakpoint({ type: 'variable', dataId: 'dataId' }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + debug.addBreakpoints([new DataBreakpoint(new ResolvedDataBreakpointSource('dataId'), 'readWrite', false, 'condition', 'hitCondition', 'logMessage')]); const variableDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; assert.strictEqual(variableDbp.condition, 'condition'); assert.strictEqual(variableDbp.hitCondition, 'hitCondition'); assert.strictEqual(variableDbp.logMessage, 'logMessage'); assert.strictEqual(variableDbp.enabled, false); - assert.strictEqual(variableDbp.label, 'data'); - assert.strictEqual(variableDbp.source.type, 'variable'); - assert.strictEqual(variableDbp.source.dataId, 'dataId'); - assert.strictEqual(variableDbp.canPersist, false); + assert.strictEqual(variableDbp.resolution?.dataId, 'dataId'); + assert.strictEqual(variableDbp.resolution.canPersist, false); + assert.strictEqual(variableDbp.resolution.accessTypes, undefined); assert.strictEqual(variableDbp.accessType, 'readWrite'); }); - test('data breakpoint - variable', async function () { - debug.addBreakpoints([new DataBreakpoint('dataId', 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); - const dataIdDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; - assert.strictEqual(dataIdDbp.condition, 'condition'); - assert.strictEqual(dataIdDbp.hitCondition, 'hitCondition'); - assert.strictEqual(dataIdDbp.logMessage, 'logMessage'); - assert.strictEqual(dataIdDbp.enabled, false); - assert.strictEqual(dataIdDbp.label, 'data'); - assert.strictEqual(dataIdDbp.source.type, 'variable'); - assert.strictEqual(dataIdDbp.source.dataId, 'dataId'); - assert.strictEqual(dataIdDbp.canPersist, false); - assert.strictEqual(dataIdDbp.accessType, 'readWrite'); - }); - test('data breakpoint - address', async function () { - debug.addBreakpoints([new DataBreakpoint({ type: 'address', address: '0x00000', bytes: 4 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + debug.addBreakpoints([new DataBreakpoint(new AddressDataBreakpointSource('0x00000', 4), 'readWrite', false, 'condition', 'hitCondition', 'logMessage')]); const addressDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; assert.strictEqual(addressDbp.condition, 'condition'); assert.strictEqual(addressDbp.hitCondition, 'hitCondition'); assert.strictEqual(addressDbp.logMessage, 'logMessage'); assert.strictEqual(addressDbp.enabled, false); - assert.strictEqual(addressDbp.label, 'data'); - assert.strictEqual(addressDbp.source.type, 'address'); - assert.strictEqual(addressDbp.source.address, '0x00000'); - assert.strictEqual(addressDbp.source.bytes, 4); - assert.strictEqual(addressDbp.canPersist, false); + assert.strictEqual((addressDbp.source as AddressDataBreakpointSource).address, '0x00000'); + assert.strictEqual((addressDbp.source as AddressDataBreakpointSource).bytes, 4); + assert.strictEqual(addressDbp.resolution, undefined); assert.strictEqual(addressDbp.accessType, 'readWrite'); }); test('data breakpoint - expression', async function () { - debug.addBreakpoints([new DataBreakpoint({ type: 'expression', expression: 'i' }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + debug.addBreakpoints([new DataBreakpoint(new ExpressionDataBreakpointSource('i'), 'readWrite', false, 'condition', 'hitCondition', 'logMessage')]); const dynamicVariableDbp = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; assert.strictEqual(dynamicVariableDbp.condition, 'condition'); assert.strictEqual(dynamicVariableDbp.hitCondition, 'hitCondition'); assert.strictEqual(dynamicVariableDbp.logMessage, 'logMessage'); assert.strictEqual(dynamicVariableDbp.enabled, false); - assert.strictEqual(dynamicVariableDbp.label, 'data'); - assert.strictEqual(dynamicVariableDbp.source.type, 'expression'); - assert.strictEqual(dynamicVariableDbp.source.expression, 'i'); - assert.strictEqual(dynamicVariableDbp.canPersist, false); + assert.strictEqual((dynamicVariableDbp.source as ExpressionDataBreakpointSource).expression, 'i'); + assert.strictEqual(dynamicVariableDbp.resolution, undefined); assert.strictEqual(dynamicVariableDbp.accessType, 'readWrite'); }); test('data breakpoint - scoped', async function () { - debug.addBreakpoints([new DataBreakpoint({ type: 'scoped', expression: 'exp()', frameId: 1 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + debug.addBreakpoints([new DataBreakpoint(new FrameScopedDataBreakpointSource(1, 'exp()'), 'readWrite', false, 'condition', 'hitCondition', 'logMessage')]); const scopedExpression = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; assert.strictEqual(scopedExpression.condition, 'condition'); assert.strictEqual(scopedExpression.hitCondition, 'hitCondition'); assert.strictEqual(scopedExpression.logMessage, 'logMessage'); assert.strictEqual(scopedExpression.enabled, false); - assert.strictEqual(scopedExpression.label, 'data'); - assert.strictEqual(scopedExpression.source.type, 'scoped'); - assert.strictEqual(scopedExpression.source.frameId, 1); - assert.strictEqual(scopedExpression.source.expression, 'exp()'); - assert.strictEqual(scopedExpression.canPersist, false); + assert.strictEqual((scopedExpression.source as FrameScopedDataBreakpointSource).frameId, 1); + assert.strictEqual((scopedExpression.source as FrameScopedDataBreakpointSource).expression, 'exp()'); + assert.strictEqual(scopedExpression.resolution, undefined); assert.strictEqual(scopedExpression.accessType, 'readWrite'); - debug.addBreakpoints([new DataBreakpoint({ type: 'scoped', variable: 'var', variablesReference: 1 }, 'readWrite', false, 'data', false, 'condition', 'hitCondition', 'logMessage')]); + debug.addBreakpoints([new DataBreakpoint(new VariableScopedDataBreakpointSource(1, 'var'), 'readWrite', false, 'condition', 'hitCondition', 'logMessage')]); const scopedVariable = debug.breakpoints[debug.breakpoints.length - 1] as DataBreakpoint; assert.strictEqual(scopedVariable.condition, 'condition'); assert.strictEqual(scopedVariable.hitCondition, 'hitCondition'); assert.strictEqual(scopedVariable.logMessage, 'logMessage'); assert.strictEqual(scopedVariable.enabled, false); - assert.strictEqual(scopedVariable.label, 'data'); - assert.strictEqual(scopedVariable.source.type, 'scoped'); - assert.strictEqual(scopedVariable.source.variablesReference, 1); - assert.strictEqual(scopedVariable.source.variable, 'var'); - assert.strictEqual(scopedVariable.canPersist, false); + assert.strictEqual((scopedExpression.source as VariableScopedDataBreakpointSource).variablesReference, 1); + assert.strictEqual((scopedExpression.source as VariableScopedDataBreakpointSource).variable, 'exp()'); + assert.strictEqual(scopedExpression.resolution, undefined); assert.strictEqual(scopedVariable.accessType, 'readWrite'); }); diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index fd763dc9f999e..2941a9032a05c 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -20,6 +20,7 @@ import { IDebugVisualizerService } from '../../contrib/debug/common/debugVisuali import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { Event } from '../../../base/common/event.js'; import { isDefined } from '../../../base/common/types.js'; +import { AddressDataBreakpointSource, ExpressionDataBreakpointSource, FrameScopedDataBreakpointSource, ResolvedDataBreakpointSource, VariableScopedDataBreakpointSource } from '../common/extHostTypes.js'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -233,20 +234,18 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb }, dto.id); } else if (dto.type === 'data') { this.debugService.addDataBreakpoint({ - description: dto.label, - src: dto.source.type === 'variable' ? { type: DataBreakpointSetType.Variable, dataId: dto.source.dataId } - : dto.source.type === 'address' ? { type: DataBreakpointSetType.Address, address: dto.source.address, bytes: dto.source.bytes } - : dto.source.type === 'expression' ? { type: DataBreakpointSetType.Expression, expression: dto.source.expression } - : dto.source.frameId ? { type: DataBreakpointSetType.Scoped, expression: dto.source.expression, frameId: dto.source.frameId } - : dto.source.variablesReference ? { type: DataBreakpointSetType.Scoped, variable: dto.source.variable, variablesReference: dto.source.variablesReference } - : { type: DataBreakpointSetType.Variable, dataId: '' }, // should not happen + src: dto.source instanceof ResolvedDataBreakpointSource ? { type: DataBreakpointSetType.Variable, ...dto.source } + : dto.source instanceof AddressDataBreakpointSource ? { type: DataBreakpointSetType.Address, ...dto.source } + : dto.source instanceof ExpressionDataBreakpointSource ? { type: DataBreakpointSetType.Expression, ...dto.source } + : dto.source instanceof FrameScopedDataBreakpointSource ? { type: DataBreakpointSetType.FrameScoped, ...dto.source } + : dto.source instanceof VariableScopedDataBreakpointSource ? { type: DataBreakpointSetType.VariableScoped, ...dto.source } + : { type: DataBreakpointSetType.Variable, dataId: '-1' }, // should not happen condition: dto.condition, enabled: dto.enabled, hitCondition: dto.hitCondition, - canPersist: dto.canPersist, - accessTypes: dto.accessTypes, - accessType: dto.accessType, logMessage: dto.logMessage, + resolution: dto.resolution, + accessType: dto.accessType, mode: dto.mode }); } @@ -467,19 +466,18 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return { type: 'data', id: dbp.getId(), - source: dbp.src.type === DataBreakpointSetType.Variable ? { type: 'variable', dataId: dbp.src.dataId } - : dbp.src.type === DataBreakpointSetType.Address ? { type: 'address', address: dbp.src.address, bytes: dbp.src.bytes } - : dbp.src.type === DataBreakpointSetType.Expression ? { type: 'expression', expression: dbp.src.expression } - : dbp.src.frameId ? { type: 'scoped', expression: dbp.src.expression, frameId: dbp.src.frameId } - : dbp.src.variablesReference ? { type: 'scoped', variable: dbp.src.variable, variablesReference: dbp.src.variablesReference } - : { type: 'variable', dataId: '' }, // should not happen + source: dbp.src.type === DataBreakpointSetType.Variable ? new ResolvedDataBreakpointSource(dbp.src.dataId, dbp.src.canPersist) + : dbp.src.type === DataBreakpointSetType.Address ? new AddressDataBreakpointSource(dbp.src.address, dbp.src.bytes) + : dbp.src.type === DataBreakpointSetType.Expression ? new ExpressionDataBreakpointSource(dbp.src.expression) + : dbp.src.type === DataBreakpointSetType.FrameScoped ? new FrameScopedDataBreakpointSource(dbp.src.frameId, dbp.src.expression) + : dbp.src.type === DataBreakpointSetType.VariableScoped ? new VariableScopedDataBreakpointSource(dbp.src.variablesReference, dbp.src.variable) + : new ResolvedDataBreakpointSource('-1'), // should not happen enabled: dbp.enabled, condition: dbp.condition, hitCondition: dbp.hitCondition, logMessage: dbp.logMessage, accessType: dbp.accessType, - label: dbp.description, - canPersist: dbp.canPersist, + resolution: dbp.resolution, mode: dbp.mode } satisfies IDataBreakpointDto; } else if ('uri' in bp) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9a0a8e5c5a328..8126e13cc9c53 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1780,6 +1780,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TextSearchContextNew: TextSearchContextNew, TextSearchMatchNew: TextSearchMatchNew, TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageType, + ResolvedDataBreakpointSource: extHostTypes.ResolvedDataBreakpointSource, + AddressDataBreakpointSource: extHostTypes.AddressDataBreakpointSource, + ExpressionDataBreakpointSource: extHostTypes.ExpressionDataBreakpointSource, + VariableScopedDataBreakpointSource: extHostTypes.VariableScopedDataBreakpointSource, + FrameScopedDataBreakpointSource: extHostTypes.FrameScopedDataBreakpointSource, }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ffd7dbf262717..80a7586551952 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2395,9 +2395,7 @@ export interface IFunctionBreakpointDto extends IBreakpointDto { export interface IDataBreakpointDto extends IBreakpointDto { type: 'data'; source: vscode.DataBreakpointSource; - canPersist: boolean; - label: string; - accessTypes?: DebugProtocol.DataBreakpointAccessType[]; + resolution: vscode.DataBreakpointResolution; accessType: DebugProtocol.DataBreakpointAccessType; mode?: string; } diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 9c2364d573714..03bd231767b7c 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -397,7 +397,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I }); } - public addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise { + public async addBreakpoints(breakpoints0: vscode.Breakpoint[]): Promise { // filter only new breakpoints const breakpoints = breakpoints0.filter(bp => { const id = bp.id; @@ -408,6 +408,11 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I return false; }); + // resolve any data breakpoints + if (this.activeDebugSession) { + await Promise.allSettled(breakpoints.filter(bp => bp instanceof DataBreakpoint).map(bp => (bp as DataBreakpoint).resolve(this.activeDebugSession!))); + } + // send notification for added breakpoints this.fireBreakpointChanges(breakpoints, [], []); @@ -447,7 +452,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I functionName: bp.functionName, mode: bp.mode, }); - } else if (bp instanceof DataBreakpoint) { + } else if (bp instanceof DataBreakpoint && bp.resolution) { dtos.push({ type: 'data', id: bp.id, @@ -456,10 +461,9 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I logMessage: bp.logMessage, condition: bp.condition, source: bp.source, - mode: bp.mode, - canPersist: bp.canPersist, + resolution: bp.resolution, accessType: bp.accessType, - label: bp.label + mode: bp.mode }); } } @@ -743,7 +747,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I if (bpd.type === 'function') { bp = new FunctionBreakpoint(bpd.functionName, bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage, bpd.mode); } else if (bpd.type === 'data') { - bp = new DataBreakpoint(bpd.source, bpd.accessType, bpd.canPersist, bpd.label, bpd.enabled, bpd.hitCondition, bpd.condition, bpd.logMessage, bpd.mode); + bp = new DataBreakpoint(bpd.source, bpd.accessType, bpd.enabled, bpd.hitCondition, bpd.condition, bpd.logMessage, bpd.mode).set(bpd.resolution); } else { const uri = URI.revive(bpd.uri); bp = new SourceBreakpoint(new Location(uri, new Position(bpd.line, bpd.character)), bpd.enabled, bpd.condition, bpd.hitCondition, bpd.logMessage, bpd.mode); @@ -792,11 +796,10 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I dbp.condition = bpd.condition; dbp.hitCondition = bpd.hitCondition; dbp.logMessage = bpd.logMessage; - dbp.label = bpd.label; - dbp.source = bpd.source; - dbp.canPersist = bpd.canPersist; dbp.mode = bpd.mode; + dbp.source = bpd.source; dbp.accessType = bpd.accessType; + dbp.set(bpd.resolution); } c.push(bp); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 7f72af8eda985..5ff4ac3ba30a2 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3075,23 +3075,80 @@ export class FunctionBreakpoint extends Breakpoint { @es5ClassCompat export class DataBreakpoint extends Breakpoint { - readonly label: string; + private _resolution?: vscode.DataBreakpointResolution; + readonly source: vscode.DataBreakpointSource; - readonly canPersist: boolean; readonly accessType: vscode.DataBreakpointAccessType; - constructor(source: vscode.DataBreakpointSource | string, accessType: vscode.DataBreakpointAccessType, canPersist?: boolean, label?: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, mode?: string) { + constructor(source: vscode.DataBreakpointSource, accessType: vscode.DataBreakpointAccessType, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, mode?: string) { super(enabled, condition, hitCondition, logMessage, mode); - this.source = typeof source === 'string' ? { type: 'variable', dataId: source } : source; + this.source = source; this.accessType = accessType; - this.canPersist = canPersist ?? false; - this.label = label ? label - : this.source.type === 'variable' ? `DataId '${this.source.dataId}'` - : this.source.type === 'address' ? `Address '${this.source.address}${this.source.bytes ? `,${this.source.bytes}'` : ''}` - : this.source.type === 'expression' ? `Expression '${this.source.expression}'` - : this.source.frameId ? `Scoped '${this.source.expression}@${this.source.frameId}'` - : this.source.variablesReference ? `Scoped '${this.source.variable}@${this.source.variablesReference}'` - : `Unknown data breakpoint`; + if (source instanceof ResolvedDataBreakpointSource) { + this.set({ ...this.source }); + } + } + + protected async info(session: vscode.DebugSession): Promise { + // ResolvedDataBreakpointSource is already handled in the constructor + if (this.source instanceof AddressDataBreakpointSource) { + return session.customRequest('dataBreakpointInfo', { name: this.source.address, bytes: this.source.bytes, asAddress: true }) as Promise; + } + if (this.source instanceof ExpressionDataBreakpointSource) { + return session.customRequest('dataBreakpointInfo', { name: this.source.expression }) as Promise; + } + if (this.source instanceof VariableScopedDataBreakpointSource) { + return session.customRequest('dataBreakpointInfo', { name: this.source.variable, variablesReference: this.source.variablesReference }) as Promise; + } + if (this.source instanceof FrameScopedDataBreakpointSource) { + return session.customRequest('dataBreakpointInfo', { name: this.source.expression, frameId: this.source.frameId }) as Promise; + } + throw Error('Unknown data breakpoint source'); + } + + set(resolution: vscode.DataBreakpointResolution): this { + if (!this.resolution) { + this._resolution = resolution; + } + return this; + } + + async resolve(session: vscode.DebugSession): Promise { + return this.set(await this.info(session)); + } + + get resolution(): vscode.DataBreakpointResolution | undefined { + return this._resolution; + } +} + +@es5ClassCompat +export class ResolvedDataBreakpointSource { + constructor(public readonly dataId: string, public readonly canPersist: boolean = false, public readonly accessTypes?: vscode.DataBreakpointAccessType[], public readonly description: string = `DataId '${dataId}'`) { + } +} + +@es5ClassCompat +export class AddressDataBreakpointSource { + constructor(public readonly address: string, public readonly bytes?: number, public readonly description: string = `Address '${address}${bytes ? `,${bytes}'` : ''}`) { + } +} + +@es5ClassCompat +export class ExpressionDataBreakpointSource { + constructor(public readonly expression: string, public readonly description: string = `Expression '${expression}'`) { + } +} + +@es5ClassCompat +export class VariableScopedDataBreakpointSource { + constructor(public readonly variablesReference: number, public readonly variable: string, public readonly description: string = `Scoped '${variable}@${variablesReference}'`) { + } +} + +@es5ClassCompat +export class FrameScopedDataBreakpointSource { + constructor(public readonly frameId: number, public readonly expression: string, public readonly description: string = `Scoped '${expression}@${frameId}'`) { } } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index a5e9075c98379..cea80cfa17333 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -787,7 +787,7 @@ class DataBreakpointsRenderer implements IListRenderer !dbp.canPersist); + const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => !dbp.resolution.canPersist); dataBreakpoints.forEach(dbp => this.model.removeDataBreakpoints(dbp.getId())); if (this.configurationService.getValue('debug').console.closeOnEnd) { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index e5a6ce762411e..4a80aa33fba15 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -774,7 +774,11 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'write' }); + await debugService.addDataBreakpoint({ + src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, + resolution: { description: dataBreakpointInfoResponse.description, dataId: dataBreakpointInfoResponse.dataId!, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes }, + accessType: 'write' + }); } } }); @@ -785,7 +789,11 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'readWrite' }); + await debugService.addDataBreakpoint({ + src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, + resolution: { description: dataBreakpointInfoResponse.description, dataId: dataBreakpointInfoResponse.dataId!, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes }, + accessType: 'readWrite' + }); } } }); @@ -796,7 +804,11 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'read' }); + await debugService.addDataBreakpoint({ + src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, + resolution: { description: dataBreakpointInfoResponse.description, dataId: dataBreakpointInfoResponse.dataId!, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes }, + accessType: 'read' + }); } } }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 64a820f174087..72b01996d1382 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -647,59 +647,88 @@ export const enum DataBreakpointSetType { Variable, Address, Expression, - Scoped + FrameScoped, + VariableScoped } +export interface DataBreakpointResolution { + /** An identifier for the data on which a data breakpoint can be registered. */ + dataId?: string; + + /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ + description: string; + + /** Attribute lists the available access types for a potential data breakpoint. */ + accessTypes?: DebugProtocol.DataBreakpointAccessType[]; + + /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ + canPersist?: boolean; +} + +type ResolvedDataBreakpointSource = { + type: DataBreakpointSetType.Variable; + /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ + dataId: string; + /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ + canPersist?: boolean; + /** A human-readable label for the data breakpoint source. */ + description?: string; + /** Attribute lists the available access types for a potential data breakpoint. */ + accessTypes?: DebugProtocol.DataBreakpointAccessType[]; +}; + +type AddressDataBreakpointSource = { + /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.Address; + /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ + address: string; + /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ + bytes?: number; + /** A human-readable label for the data breakpoint source. */ + description?: string; +}; + +type ExpressionDataBreakpointSource = { + /** The source type for expression-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.Expression; + /** A global expression that is first evaluated when the breakpoint is activated. */ + expression: string; + /** A human-readable label for the data breakpoint source. */ + description?: string; +}; + +type FrameScopedDataBreakpointSource = { + /** The source type for frame scoped-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.FrameScoped; + /** Reference to the stack frame to which the expression is scoped. */ + frameId: number; + /** The name of the expression that is used for resolution. */ + expression: string; + /** A human-readable label for the data breakpoint source. */ + description: string; +}; + +type VariableScopedDataBreakpointSource = { + /** The source type for frame scoped-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ + type: DataBreakpointSetType.VariableScoped; + /** Reference to the variable container that has the variable named `variable`. */ + variablesReference: number; + /** The name of the variable that is used for resolution. */ + variable: string; + /** A human-readable label for the data breakpoint source. */ + description?: string; +}; + /** * Source for a data breakpoint. A data breakpoint on a variable always has a * `dataId` because it cannot reference that variable globally, but addresses * can request info repeated and use session-specific data. */ -export type DataBreakpointSource = - | { - /** The source type for variable-based data breakpoints. */ - type: DataBreakpointSetType.Variable; - /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ - dataId: string; - } - | { - /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ - type: DataBreakpointSetType.Address; - /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ - address: string; - /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ - bytes?: number; - } - | { - /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ - type: DataBreakpointSetType.Expression; - /** A global expression that is first evaluated when the breakpoint is activated. */ - expression: string; - } - | { - /** The source type for address-based data breakpoints. This only works on sessions that have the `supportsDataBreakpointBytes` capability. */ - type: DataBreakpointSetType.Scoped; - } & ( - | { - /** The name of the variable that is used for resolution. */ - variable: string; - /** Reference to the variable container that has the variable named `variable`. */ - variablesReference: number; - frameId?: never; - } - | { - /** The name of the expression that is used for resolution. */ - expression: string; - /** Reference to the stack frame to which the expression is scoped. */ - frameId: number; - variablesReference?: never; - } - ); +export type DataBreakpointSource = ResolvedDataBreakpointSource | AddressDataBreakpointSource | ExpressionDataBreakpointSource | FrameScopedDataBreakpointSource | VariableScopedDataBreakpointSource; export interface IDataBreakpoint extends IBaseBreakpoint { - readonly description: string; - readonly canPersist: boolean; readonly src: DataBreakpointSource; + readonly resolution: DataBreakpointResolution; readonly accessType: DebugProtocol.DataBreakpointAccessType; toDAP(session: IDebugSession): Promise; } diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index ac9f994a47c31..1b8c89e42220c 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IEditorPane } from '../../../common/editor.js'; -import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugEvaluatePosition, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State, isFrameDeemphasized } from './debug.js'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointResolution, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugEvaluatePosition, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State, isFrameDeemphasized } from './debug.js'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from './debugSource.js'; import { DebugStorage } from './debugStorage.js'; import { IDebugVisualizerService } from './debugVisualizers.js'; @@ -1166,21 +1166,14 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak } export interface IDataBreakpointOptions extends IBaseBreakpointOptions { - description: string; src: DataBreakpointSource; - canPersist: boolean; - initialSessionData?: { session: IDebugSession; dataId: string }; - accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; + resolution: DataBreakpointResolution; accessType: DebugProtocol.DataBreakpointAccessType; } export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { - private readonly sessionDataId = new WeakMap(); - - public readonly description: string; public readonly src: DataBreakpointSource; - public readonly canPersist: boolean; - public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; + public readonly resolution: DataBreakpointResolution; public readonly accessType: DebugProtocol.DataBreakpointAccessType; constructor( @@ -1188,59 +1181,17 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { id = generateUuid() ) { super(id, opts); - this.description = opts.description; if ('dataId' in opts) { // back compat with old saved variables in 1.87 opts.src = { type: DataBreakpointSetType.Variable, dataId: opts.dataId as string }; } this.src = opts.src; - this.canPersist = opts.canPersist; - this.accessTypes = opts.accessTypes; this.accessType = opts.accessType; - if (opts.initialSessionData) { - this.sessionDataId.set(opts.initialSessionData.session, opts.initialSessionData.dataId); - } + this.resolution = opts.resolution; } async toDAP(session: IDebugSession): Promise { - let dataId = this.sessionDataId.get(session); - if (!dataId) { - if (this.src.type === DataBreakpointSetType.Variable) { - dataId = this.src.dataId; - } else if (this.src.type === DataBreakpointSetType.Address) { - const sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; - if (sessionDataId) { - this.sessionDataId.set(session, sessionDataId); - dataId = sessionDataId; - } - } else if (this.src.type === DataBreakpointSetType.Expression) { - const sessionDataId = (await session.dataBreakpointInfo(this.src.expression))?.dataId; - if (sessionDataId) { - this.sessionDataId.set(session, sessionDataId); - dataId = sessionDataId; - } - } else { - // type === DataBreakpointSetType.Scoped - if (this.src.frameId) { - const sessionDataId = (await session.dataBreakpointInfo(this.src.expression, undefined, this.src.frameId))?.dataId; - if (sessionDataId) { - this.sessionDataId.set(session, sessionDataId); - dataId = sessionDataId; - } - } else if (this.src.variablesReference) { - const sessionDataId = (await session.dataBreakpointInfo(this.src.variable, this.src.variablesReference))?.dataId; - if (sessionDataId) { - this.sessionDataId.set(session, sessionDataId); - dataId = sessionDataId; - } - } - } - } - if (!dataId) { - return; - } - - return { - dataId, + return !this.resolution.dataId ? undefined : { + dataId: this.resolution.dataId, accessType: this.accessType, condition: this.condition, hitCondition: this.hitCondition, @@ -1250,24 +1201,18 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { override toJSON(): IDataBreakpointOptions & { id: string } { return { ...super.toJSON(), - description: this.description, + resolution: this.resolution, src: this.src, - accessTypes: this.accessTypes, - accessType: this.accessType, - canPersist: this.canPersist, + accessType: this.accessType }; } get supported(): boolean { - if (!this.data) { - return true; - } - - return this.data.supportsDataBreakpoints; + return !this.data || this.data.supportsDataBreakpoints; } override toString(): string { - return this.description; + return this.resolution.description; } } diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index f4b4dd6a7a6a7..54f173306926e 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -149,7 +149,7 @@ export class DebugStorage extends Disposable { this.storageService.remove(DEBUG_FUNCTION_BREAKPOINTS_KEY, StorageScope.WORKSPACE); } - const dataBreakpoints = debugModel.getDataBreakpoints().filter(dbp => dbp.canPersist); + const dataBreakpoints = debugModel.getDataBreakpoints().filter(dbp => dbp.resolution.canPersist); if (dataBreakpoints.length) { this.storageService.store(DEBUG_DATA_BREAKPOINTS_KEY, JSON.stringify(dataBreakpoints), StorageScope.WORKSPACE, StorageTarget.MACHINE); } else { diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 61e2ff90693c2..5e32f92fc851c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -313,17 +313,17 @@ suite('Debug - Breakpoints', () => { let eventCount = 0; disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); - model.addDataBreakpoint({ description: 'label', src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); - model.addDataBreakpoint({ description: 'second', src: { type: DataBreakpointSetType.Variable, dataId: 'secondId' }, canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); + model.addDataBreakpoint({ src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, resolution: { description: 'label', dataId: 'id', canPersist: true, accessTypes: ['read'] }, accessType: 'read' }, '1'); + model.addDataBreakpoint({ src: { type: DataBreakpointSetType.Variable, dataId: 'secondId' }, resolution: { description: 'second', dataId: 'secondId', canPersist: true, accessTypes: ['readWrite'] }, accessType: 'readWrite' }, '2'); model.updateDataBreakpoint('1', { condition: 'aCondition' }); model.updateDataBreakpoint('2', { hitCondition: '10' }); const dataBreakpoints = model.getDataBreakpoints(); - assert.strictEqual(dataBreakpoints[0].canPersist, true); + assert.strictEqual(dataBreakpoints[0].resolution.canPersist, true); assert.deepStrictEqual(dataBreakpoints[0].src, { type: DataBreakpointSetType.Variable, dataId: 'id' }); assert.strictEqual(dataBreakpoints[0].accessType, 'read'); assert.strictEqual(dataBreakpoints[0].condition, 'aCondition'); - assert.strictEqual(dataBreakpoints[1].canPersist, false); - assert.strictEqual(dataBreakpoints[1].description, 'second'); + assert.strictEqual(dataBreakpoints[1].resolution.canPersist, false); + assert.strictEqual(dataBreakpoints[1].resolution.description, 'second'); assert.strictEqual(dataBreakpoints[1].accessType, 'readWrite'); assert.strictEqual(dataBreakpoints[1].hitCondition, '10'); @@ -374,7 +374,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Disabled Logpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log-disabled'); - model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', src: { type: DataBreakpointSetType.Variable, dataId: 'id' } }); + model.addDataBreakpoint({ src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, resolution: { description: 'label', dataId: 'id', canPersist: true, accessTypes: ['read'] }, accessType: 'read' }); const dataBreakpoints = model.getDataBreakpoints(); result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0], ls, model); assert.strictEqual(result.message, 'Data Breakpoint'); diff --git a/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts b/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts index 5f1f6a32bd597..05ae015398e4d 100644 --- a/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts +++ b/src/vscode-dts/vscode.proposed.debugDataBreakpoints.d.ts @@ -7,76 +7,126 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/195151 - /** - * The source for a data breakpoint. - */ - export type DataBreakpointSource = - | { - /** The source type for fixed data identifiers that do not need to be re-resolved when the breakpoint is activated. */ - type: 'variable'; - /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ - dataId: string; - } - | { - /** The source type for address-based data breakpoints. Address-based data breakpoints are re-resolved when the breakpoint is activated. This type only applies to sessions that have the `supportsDataBreakpointBytes` capability. */ - type: 'address'; - /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ - address: string; - /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ - bytes?: number; - } - | { - /** The source type for expressions that are dynamically resolved when the breakpoint is activated. */ - type: 'expression'; - /** A global expression that is first evaluated when the breakpoint is activated. */ - expression: string; - } - | { - /** The source type for scoped variables and expressions that are dynamically resolved when the breakpoint is activated. */ - type: 'scoped'; - } & ( - | { - /** The name of the variable that is used for resolution. */ - variable: string; - /** Reference to the variable container that has the variable named `variable`. */ - variablesReference: number; - frameId?: never; - } - | { - /** The name of the expression that is used for resolution. */ - expression: string; - /** Reference to the stack frame to which the expression is scoped. */ - frameId: number; - variablesReference?: never; - } - ); + export interface DataBreakpointResolution { + /** An identifier for the data on which a data breakpoint can be registered. */ + dataId?: string; + /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ + description: string; - /** Access type for data breakpoints. */ - export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; + /** Attribute lists the available access types for a potential data breakpoint. */ + accessTypes?: DataBreakpointAccessType[]; + + /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ + canPersist?: boolean; + } + + export class ResolvedDataBreakpointSource implements DataBreakpointResolution { + /** An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. */ + dataId: string; + /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ + canPersist: boolean; + /** A human-readable label for the data breakpoint source. */ + description: string; + /** Attribute lists the available access types for a potential data breakpoint. */ + accessTypes?: DataBreakpointAccessType[]; - /** - * A breakpoint specified by a variable or memory change. - */ - export class DataBreakpoint extends Breakpoint { /** - * The human-readable label for the data breakpoint. + * Creates a new resolved data breakpoint source. + * + * @param dataId An identifier for the data. If it was retrieved using a `variablesReference` it may only be valid in the current suspended state, otherwise it's valid indefinitely. + * @param canPersist Attribute indicates that a potential data breakpoint could be persisted across sessions. + * @param description A human-readable label for the data breakpoint source. */ - label: string; + constructor(dataId: string, canPersist?: boolean, accessTypes?: DataBreakpointAccessType[], description?: string); + } + + export class AddressDataBreakpointSource { + /** A memory address as a decimal value, or hex value if it is prefixed with `0x`. */ + address: string; + /** If specified, returns information for the range of memory extending `bytes` number of bytes from the address. */ + bytes?: number; + /** A human-readable label for the data breakpoint source. */ + description: string; /** - * The source for the data breakpoint. See the different source types on how they are resolved during breakpoint activation. + * Creates a new address data breakpoint source. + * + * @param address A memory address as a decimal value, or hex value if it is prefixed with `0x`. + * @param bytes If specified, returns information for the range of memory extending `bytes` number of bytes from the address. + * @param description A human-readable label for the data breakpoint source. */ - source: DataBreakpointSource; + constructor(address: string, bytes?: number, description?: string); + } + + export class ExpressionDataBreakpointSource { + /** A global expression that is first evaluated when the breakpoint is activated. */ + expression: string; + /** A human-readable label for the data breakpoint source. */ + description: string; /** - * Flag to indicate if the data breakpoint could be persisted across sessions. + * Creates a new expression data breakpoint source. + * + * @param expression A global expression that is first evaluated when the breakpoint is activated. + * @param description A human-readable label for the data breakpoint source. */ - canPersist: boolean; + constructor(expression: string, description?: string); + } + + export class VariableScopedDataBreakpointSource { + /** Reference to the variable container that has the variable named `variable`. */ + variablesReference: number; + /** The name of the variable that is used for resolution. */ + variable: string; + /** A human-readable label for the data breakpoint source. */ + description: string; + + /** + * Creates a new variable scoped data breakpoint source. + * + * @param variablesReference Reference to the variable container that has the variable named `variable`. + * @param variable The name of the variable that is used for resolution. + * @param description A human-readable label for the data breakpoint source. + */ + constructor(variablesReference: number, variable: string, description?: string); + } + + export class FrameScopedDataBreakpointSource { + /** Reference to the stack frame to which the expression is scoped. */ + frameId: number; + /** The name of the expression that is used for resolution. */ + expression: string; + /** A human-readable label for the data breakpoint source. */ + description: string; /** - * The access type of the data. + * Creates a new frame scoped data breakpoint source. + * + * @param frameId Reference to the stack frame to which the expression is scoped. + * @param expression The name of the expression that is used for resolution. + * @param description A human-readable label for the data breakpoint source. */ + constructor(frameId: number, expression: string, description?: string); + } + + /** The source for a data breakpoint. */ + export type DataBreakpointSource = ResolvedDataBreakpointSource | AddressDataBreakpointSource | ExpressionDataBreakpointSource | VariableScopedDataBreakpointSource | FrameScopedDataBreakpointSource; + + /** Access type for data breakpoints. */ + export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; + + /** + * A breakpoint specified by a variable or memory change. + */ + export class DataBreakpoint extends Breakpoint { + /** The source for the data breakpoint. See the different source types on how they are resolved during breakpoint activation. */ + source: DataBreakpointSource; + + /** The resolution of the data breakpoint based on the given source. This may be undefined if the breakpoint was not yet resolved. */ + resolution?: DataBreakpointResolution; + + /** The access type of the data. */ accessType: DataBreakpointAccessType; /** @@ -84,13 +134,29 @@ declare module 'vscode' { * * @param source The source for the data breakpoint. If the `dataId` is known already, it can be specified directly. If the dataId is not known, it can be retrieved via a data breakpoint info request from the session. Alternatively, some sources offer dynamic resolution during breakpoint activation. * @param accessType The access type of the data breakpoint. - * @param canPersist Flag to indicate if the data breakpoint could be persisted across sessions. - * @param label The human-readable label for the data breakpoint. * @param enabled Is breakpoint enabled. * @param condition Expression for conditional breakpoints. * @param hitCondition Expression that controls how many hits of the breakpoint are ignored. * @param logMessage Log message to display when breakpoint is hit. */ - constructor(source: DataBreakpointSource | string, accessType: DataBreakpointAccessType, canPersist?: boolean, label?: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); + constructor(source: DataBreakpointSource | string, accessType: DataBreakpointAccessType, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); + + /** + * Resolves the data breakpoint and retrieves the dataId, canPersist, and description properties. + * By default, the data breakpoint is resolved automatically after the breakpoint is set. + * A data breakpoint is only resolved once at a certain point in time. + * Calling this method again will not update the resolution properties properties. + * + * @param session the session against which this breakpoint should be resolved. + */ + resolve(session: DebugSession): Thenable; + + /** + * Resolves the data breakpoint with the given resolution if no other resolution was yet reached. + * + * @param resolution a known resolution for this breakpoint + */ + set(resolution: DataBreakpointResolution): this; } + }