From fe7ae6dc9e4928370ea4dee2940154eea04a7b30 Mon Sep 17 00:00:00 2001 From: Haydar Metin Date: Thu, 6 Jun 2024 10:33:37 +0200 Subject: [PATCH] Allow to create data breakpoints --- media/memory-table.css | 65 ++++++- package.json | 55 ++++++ src/common/breakpoint.ts | 45 +++++ src/common/debug-requests.ts | 24 ++- src/common/memory-range.ts | 1 + src/common/messaging.ts | 9 +- src/common/webview-context.ts | 17 ++ src/entry-points/browser/extension.ts | 6 +- src/entry-points/desktop/extension.ts | 6 +- .../adapter-registry/adapter-capabilities.ts | 7 +- src/plugin/adapter-registry/c-tracker.ts | 8 +- src/plugin/breakpoints/breakpoint-provider.ts | 52 ++++++ src/plugin/breakpoints/breakpoint-tracker.ts | 157 ++++++++++++++++ src/plugin/memory-webview-main.ts | 114 +++++++++++- src/plugin/session-tracker.ts | 47 ++++- src/webview/breakpoints/breakpoint-service.ts | 174 ++++++++++++++++++ src/webview/columns/address-column.tsx | 10 + src/webview/columns/data-column.tsx | 7 +- src/webview/components/memory-widget.tsx | 5 +- src/webview/memory-webview-view.tsx | 8 +- src/webview/utils/vscode-contexts.ts | 11 +- src/webview/variables/variable-decorations.ts | 7 +- 22 files changed, 795 insertions(+), 40 deletions(-) create mode 100644 src/common/breakpoint.ts create mode 100644 src/plugin/breakpoints/breakpoint-provider.ts create mode 100644 src/plugin/breakpoints/breakpoint-tracker.ts create mode 100644 src/webview/breakpoints/breakpoint-service.ts diff --git a/media/memory-table.css b/media/memory-table.css index 476dfb6..3d7858b 100644 --- a/media/memory-table.css +++ b/media/memory-table.css @@ -31,6 +31,53 @@ border-bottom: 1px dotted var(--vscode-editorHoverWidget-border); } +.memory-inspector-table .data-breakpoint { + outline: 1px solid var(--vscode-debugIcon-breakpointForeground); + outline-offset: 1px; +} + +.memory-inspector-table .data-breakpoint.data-breakpoint-external { + outline-style: dashed; +} + +.memory-inspector-table tbody .column-address { + position: relative; +} + +.memory-inspector-table tbody .address-status { + position: absolute; + left: -1px; + align-items: center; + display: flex; + justify-content: center; +} + +.memory-inspector-table tbody .address-status.codicon { + font-size: 12px; +} + +.memory-inspector-table tbody .address-status.codicon-debug-breakpoint { + color: var(--vscode-debugIcon-breakpointForeground); +} + +.memory-inspector-table tbody .address-status.codicon-debug-stackframe { + color: var(--vscode-debugIcon-breakpointCurrentStackframeForeground); +} + +.memory-inspector-table + tbody + .address-status.codicon-debug-breakpoint.codicon-debug-stackframe:after { + content: "\ea71"; + position: absolute; + left: 3px; + font-size: 6px; + color: var(--vscode-debugIcon-breakpointForeground); +} + +.memory-inspector-table tbody .debug-hit { + outline-color: var(--vscode-debugIcon-breakpointCurrentStackframeForeground); +} + /* == MoreMemorySelect == */ .bytes-select { @@ -90,7 +137,7 @@ .memory-inspector-table span.p-column-resizer { border-right: 2px solid var(--vscode-editor-lineHighlightBorder); - transition: border-right .1s ease-out; + transition: border-right 0.1s ease-out; } .memory-inspector-table span.p-column-resizer:hover { @@ -111,7 +158,7 @@ /* Basic hover formatting (copied from Monaco hovers) */ .memory-hover { min-width: fit-content; - max-width: var(--vscode-hover-maxWidth,500px); + max-width: var(--vscode-hover-maxWidth, 500px); border: 1px solid var(--vscode-editorHoverWidget-border); border-radius: 3px; @@ -127,24 +174,28 @@ border-collapse: collapse; border-style: hidden; } + .memory-hover table caption { padding: 4px; border-bottom: 1px solid var(--vscode-editorHoverWidget-border); } + .memory-hover td { border: 1px solid var(--vscode-editorHoverWidget-border); padding: 2px 8px; } + .memory-hover td:first-child { text-align: right; } /* Colors for the hover fields */ -.memory-hover .label-value-pair>.label { - color: var(--vscode-debugTokenExpression-string); - white-space: nowrap; +.memory-hover .label-value-pair > .label { + color: var(--vscode-debugTokenExpression-string); + white-space: nowrap; } -.memory-hover .label-value-pair>.value { + +.memory-hover .label-value-pair > .value { color: var(--vscode-debugTokenExpression-number); } @@ -152,9 +203,11 @@ .memory-hover .address-hover .primary { background-color: var(--vscode-list-hoverBackground); } + .memory-hover table caption { color: var(--vscode-symbolIcon-variableForeground); } + .memory-hover .address-hover .value.utf8, .memory-hover .data-hover .value.utf8, .memory-hover .variable-hover .value.type { diff --git a/package.json b/package.json index 8e4c893..45fc2bb 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,36 @@ "title": "Go to value in Memory Inspector", "category": "Memory" }, + { + "command": "memory-inspector.data-breakpoint.set.read", + "title": "Break on Value Read", + "enablement": "memory-inspector.canWrite", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.set.readWrite", + "title": "Break on Value Access", + "enablement": "memory-inspector.canWrite", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.set.write", + "title": "Break on Value Change", + "enablement": "memory-inspector.canWrite", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.remove", + "title": "Remove Breakpoint", + "enablement": "memory-inspector.canWrite", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.remove-all", + "title": "Remove All Breakpoints", + "enablement": "memory-inspector.canWrite", + "category": "Memory" + }, { "command": "memory-inspector.toggle-variables-column", "title": "Toggle Variables Column", @@ -214,6 +244,31 @@ "command": "memory-inspector.go-to-value", "group": "display@7", "when": "webviewId === memory-inspector.memory && memory-inspector.variable.isPointer" + }, + { + "command": "memory-inspector.data-breakpoint.set.read", + "group": "breakpoints@1", + "when": "webviewId === memory-inspector.memory && memory-inspector.breakpoint.isBreakable" + }, + { + "command": "memory-inspector.data-breakpoint.set.write", + "group": "breakpoints@2", + "when": "webviewId === memory-inspector.memory && memory-inspector.breakpoint.isBreakable" + }, + { + "command": "memory-inspector.data-breakpoint.set.readWrite", + "group": "breakpoints@3", + "when": "webviewId === memory-inspector.memory && memory-inspector.breakpoint.isBreakable" + }, + { + "command": "memory-inspector.data-breakpoint.remove", + "group": "breakpoints@4", + "when": "webviewId === memory-inspector.memory && memory-inspector.breakpoint.type === 'internal'" + }, + { + "command": "memory-inspector.data-breakpoint.remove-all", + "group": "breakpoints@5", + "when": "webviewId === memory-inspector.memory && memory-inspector.breakpoint.type === 'internal'" } ] }, diff --git a/src/common/breakpoint.ts b/src/common/breakpoint.ts new file mode 100644 index 0000000..cc02f47 --- /dev/null +++ b/src/common/breakpoint.ts @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugProtocol } from '@vscode/debugprotocol'; +import { DebugRequestTypes } from './debug-requests'; + +export interface TrackedDataBreakpoint { + type: TrackedBreakpointType; + breakpoint: DebugProtocol.DataBreakpoint; + /** + * The respective response for the breakpoint. + */ + response: DebugProtocol.SetDataBreakpointsResponse['body']['breakpoints'][0] +} + +export interface TrackedDataBreakpoints { + /** + * Breakpoints set from external contributors. + */ + external: TrackedDataBreakpoint[], + /** + * Breakpoints set from us. + */ + internal: TrackedDataBreakpoint[] +} + +export type TrackedBreakpointType = 'internal' | 'external'; + +export type DataBreakpointInfoArguments = DebugRequestTypes['dataBreakpointInfo'][0]; +export type DataBreakpointInfoResult = DebugRequestTypes['dataBreakpointInfo'][1]; +export type SetDataBreakpointsArguments = DebugRequestTypes['setDataBreakpoints'][0]; +export type SetDataBreakpointsResult = DebugRequestTypes['setDataBreakpoints'][1]; diff --git a/src/common/debug-requests.ts b/src/common/debug-requests.ts index 319eba2..126c20e 100644 --- a/src/common/debug-requests.ts +++ b/src/common/debug-requests.ts @@ -25,6 +25,8 @@ export interface DebugRequestTypes { 'scopes': [DebugProtocol.ScopesArguments, DebugProtocol.ScopesResponse['body']] 'variables': [DebugProtocol.VariablesArguments, DebugProtocol.VariablesResponse['body']] 'writeMemory': [DebugProtocol.WriteMemoryArguments, DebugProtocol.WriteMemoryResponse['body']] + 'dataBreakpointInfo': [DebugProtocol.DataBreakpointInfoArguments, DebugProtocol.DataBreakpointInfoResponse['body']] + 'setDataBreakpoints': [DebugProtocol.SetDataBreakpointsArguments, DebugProtocol.SetDataBreakpointsResponse['body']] } export interface DebugEvents { @@ -59,16 +61,28 @@ export function isDebugEvaluateArguments(args: DebugProtocol.EvaluateArguments | } export function isDebugRequest(command: K, message: unknown): message is DebugRequest { - const assumed = message ? message as DebugProtocol.Request : undefined; - return !!assumed && assumed.type === 'request' && assumed.command === command; + return isDebugRequestType(message) && message.command === command; } export function isDebugResponse(command: K, message: unknown): message is DebugResponse { - const assumed = message ? message as DebugProtocol.Response : undefined; - return !!assumed && assumed.type === 'response' && assumed.command === command; + return isDebugResponseType(message) && message.command === command; } export function isDebugEvent(event: K, message: unknown): message is DebugEvents[K] { + return isDebugEventType(message) && message.event === event; +} + +export function isDebugRequestType(message: unknown): message is DebugProtocol.Request { + const assumed = message ? message as DebugProtocol.Request : undefined; + return !!assumed && assumed.type === 'request'; +} + +export function isDebugResponseType(message: unknown): message is DebugProtocol.Response { + const assumed = message ? message as DebugProtocol.Response : undefined; + return !!assumed && assumed.type === 'response'; +} + +export function isDebugEventType(message: unknown): message is DebugProtocol.Event { const assumed = message ? message as DebugProtocol.Event : undefined; - return !!assumed && assumed.type === 'event' && assumed.event === event; + return !!assumed && assumed.type === 'event'; } diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 2f0d6c6..14e8cba 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -108,6 +108,7 @@ export interface VariableMetadata { type?: string; /** If applicable, a string representation of the variable's value */ value?: string; + parentVariablesReference?: number; isPointer?: boolean; } diff --git a/src/common/messaging.ts b/src/common/messaging.ts index 9b23854..9093f08 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -18,7 +18,8 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import type { NotificationType, RequestType } from 'vscode-messenger-common'; import { URI } from 'vscode-uri'; import { VariablesView } from '../plugin/external-views'; -import { DebugRequestTypes } from './debug-requests'; +import type { TrackedDataBreakpoints } from './breakpoint'; +import { DebugEvents, DebugRequestTypes } from './debug-requests'; import type { VariableRange, WrittenMemory } from './memory-range'; import { MemoryViewSettings } from './webview-configuration'; import { WebviewContext } from './webview-context'; @@ -32,6 +33,9 @@ export type ReadMemoryResult = DebugRequestTypes['readMemory'][1]; export type WriteMemoryArguments = DebugRequestTypes['writeMemory'][0]; export type WriteMemoryResult = DebugRequestTypes['writeMemory'][1]; +export type StoppedEvent = DebugEvents['stopped']; +export type ContinuedEvent = DebugEvents['continued']; + export type StoreMemoryArguments = MemoryOptions & { proposedOutputName?: string } | VariablesView.IVariablesContext | WebviewContext; export type StoreMemoryResult = void; @@ -52,6 +56,9 @@ export const resetMemoryViewSettingsType: NotificationType = { method: 're export const setTitleType: NotificationType = { method: 'setTitle' }; export const memoryWrittenType: NotificationType = { method: 'memoryWritten' }; export const sessionContextChangedType: NotificationType = { method: 'sessionContextChanged' }; +export const setTrackedBreakpointType: NotificationType = { method: 'setTrackedBreakpoints' }; +export const notifyStoppedType: NotificationType = { method: 'notifyStoppedType' }; +export const notifyContinuedType: NotificationType = { method: 'notifyContinuedType' }; // Requests export const setOptionsType: RequestType = { method: 'setOptions' }; diff --git a/src/common/webview-context.ts b/src/common/webview-context.ts index c652dc5..64e4aa8 100644 --- a/src/common/webview-context.ts +++ b/src/common/webview-context.ts @@ -39,6 +39,15 @@ export interface WebviewVariableContext extends WebviewCellContext { variable?: VariableMetadata } +export interface WebviewGroupContext extends WebviewCellContext { + memoryData?: { + group: { + startAddress: string; + length: number; + } + } +} + /** * Retrieves the currently visible (configurable) columns from the given {@link WebviewContext}. * @returns A string array containing the visible columns ids. @@ -62,6 +71,14 @@ export function isWebviewContext(args: WebviewContext | unknown): args is Webvie && typeof assumed.activeReadArguments?.memoryReference === 'string'; } +export function isWebviewGroupContext(args: WebviewVariableContext | unknown): args is Required { + const assumed = args ? args as WebviewGroupContext : undefined; + return !!assumed && isWebviewContext(args) + && !!assumed.memoryData + && (typeof assumed.memoryData.group.startAddress === 'string') + && (typeof assumed.memoryData.group.length === 'number'); +} + export function isWebviewVariableContext(args: WebviewVariableContext | unknown): args is Required { const assumed = args ? args as WebviewVariableContext : undefined; return !!assumed && isWebviewContext(args) diff --git a/src/entry-points/browser/extension.ts b/src/entry-points/browser/extension.ts index d8c6905..842432a 100644 --- a/src/entry-points/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -17,6 +17,8 @@ import * as vscode from 'vscode'; import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; +import { BreakpointProvider } from '../../plugin/breakpoints/breakpoint-provider'; +import { BreakpointTracker } from '../../plugin/breakpoints/breakpoint-tracker'; import { ContextTracker } from '../../plugin/context-tracker'; import { MemoryProvider } from '../../plugin/memory-provider'; import { MemoryStorage } from '../../plugin/memory-storage'; @@ -27,8 +29,10 @@ export const activate = async (context: vscode.ExtensionContext): Promise { - previous.push(this.variableToVariableRange(child, session)); + previous.push(this.variableToVariableRange(child, session, parent)); }); } else { this.logger.debug('Ignoring', parent.name); @@ -122,7 +122,10 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { return candidate.presentationHint !== 'registers' && candidate.name !== 'Registers'; } - protected variableToVariableRange(_variable: DebugProtocol.Variable, _session: vscode.DebugSession): Promise { + protected variableToVariableRange( + _variable: DebugProtocol.Variable, + _session: vscode.DebugSession, + _parent: WithChildren): Promise { throw new Error('To be implemented by derived classes!'); } diff --git a/src/plugin/adapter-registry/c-tracker.ts b/src/plugin/adapter-registry/c-tracker.ts index af48c45..08c5c72 100644 --- a/src/plugin/adapter-registry/c-tracker.ts +++ b/src/plugin/adapter-registry/c-tracker.ts @@ -18,7 +18,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import * as vscode from 'vscode'; import { sendRequest } from '../../common/debug-requests'; import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; -import { AdapterVariableTracker, decimalAddress, extractAddress, hexAddress, notADigit } from './adapter-capabilities'; +import { AdapterVariableTracker, decimalAddress, extractAddress, hexAddress, notADigit, WithChildren } from './adapter-capabilities'; export namespace CEvaluateExpression { export function sizeOf(expression: string): string { @@ -35,7 +35,10 @@ export class CTracker extends AdapterVariableTracker { * Resolves memory location and size using evaluate requests for `$(variable.name)` and `sizeof(variable.name)` * Ignores the presence or absence of variable.memoryReference. */ - protected override async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { + protected override async variableToVariableRange( + variable: DebugProtocol.Variable, + session: vscode.DebugSession, + parent: WithChildren): Promise { if (this.currentFrame === undefined || !variable.name) { this.logger.debug('Unable to resolve', variable.name, { noName: !variable.name, noFrame: this.currentFrame === undefined }); @@ -66,6 +69,7 @@ export class CTracker extends AdapterVariableTracker { endAddress: variableSize === undefined ? undefined : toHexStringWithRadixMarker(address + variableSize), value: variable.value, type: variable.type, + parentVariablesReference: parent.variablesReference, isPointer, }; return variableRange; diff --git a/src/plugin/breakpoints/breakpoint-provider.ts b/src/plugin/breakpoints/breakpoint-provider.ts new file mode 100644 index 0000000..b4cdc75 --- /dev/null +++ b/src/plugin/breakpoints/breakpoint-provider.ts @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DataBreakpointInfoArguments, DataBreakpointInfoResult, SetDataBreakpointsArguments, SetDataBreakpointsResult } from '../../common/breakpoint'; +import { sendRequest } from '../../common/debug-requests'; +import { SessionTracker } from '../session-tracker'; +import { BreakpointTracker } from './breakpoint-tracker'; + +export class BreakpointProvider { + + constructor(protected readonly sessionTracker: SessionTracker, protected readonly breakpointTracker: BreakpointTracker) { + this.breakpointTracker.onSetDataBreakpointResponse(() => { + this.setMemoryInspectorDataBreakpoint({ + breakpoints: this.breakpointTracker.internalDataBreakpoints.map(bp => bp.breakpoint) + }); + }); + } + + async setMemoryInspectorDataBreakpoint(args: SetDataBreakpointsArguments): Promise { + const session = this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsDataBreakpoints', 'set data breakpoint'); + this.breakpointTracker.notifySetDataBreakpointEnabled = false; + const breakpoints = [ + ...this.breakpointTracker.externalDataBreakpoints.map(bp => bp.breakpoint), + ...args.breakpoints]; + return sendRequest(session, 'setDataBreakpoints', { breakpoints }) + .then(response => { + const indexOfInternal = response.breakpoints.length - args.breakpoints.length; + this.breakpointTracker.setInternal(response.breakpoints.slice(indexOfInternal)); + return response; + }).finally(() => { + this.breakpointTracker.notifySetDataBreakpointEnabled = true; + }); + } + + async dataBreakpointInfo(args: DataBreakpointInfoArguments): Promise { + const session = this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsDataBreakpoints', 'data breakpoint info'); + return sendRequest(session, 'dataBreakpointInfo', args); + } +} diff --git a/src/plugin/breakpoints/breakpoint-tracker.ts b/src/plugin/breakpoints/breakpoint-tracker.ts new file mode 100644 index 0000000..e7a4032 --- /dev/null +++ b/src/plugin/breakpoints/breakpoint-tracker.ts @@ -0,0 +1,157 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugProtocol } from '@vscode/debugprotocol'; +import * as vscode from 'vscode'; +import { SetDataBreakpointsResult, TrackedDataBreakpoint, TrackedDataBreakpoints } from '../../common/breakpoint'; +import { isDebugRequest, isDebugResponse } from '../../common/debug-requests'; +import { isSessionEvent, SessionContinuedEvent, SessionEvent, SessionRequest, SessionResponse, SessionStoppedEvent, SessionTracker } from '../session-tracker'; + +export class BreakpointTracker { + protected _dataBreakpoints: TrackedDataBreakpoints = { external: [], internal: [] }; + protected _stoppedEvent?: SessionStoppedEvent; + protected dataBreakpointsRequest: Record = {}; + + protected _onBreakpointsChanged = new vscode.EventEmitter(); + readonly onBreakpointChanged = this._onBreakpointsChanged.event; + + protected _onSetDataBreakpointResponse = new vscode.EventEmitter(); + readonly onSetDataBreakpointResponse = this._onSetDataBreakpointResponse.event; + + protected _onStopped = new vscode.EventEmitter(); + readonly onStopped = this._onStopped.event; + + protected _onContinued = new vscode.EventEmitter(); + readonly onContinued = this._onContinued.event; + + notifySetDataBreakpointEnabled = true; + + get dataBreakpoints(): TrackedDataBreakpoints { + return this._dataBreakpoints; + } + + get internalDataBreakpoints(): TrackedDataBreakpoint[] { + return this._dataBreakpoints.internal; + } + + get externalDataBreakpoints(): TrackedDataBreakpoint[] { + return this._dataBreakpoints.external; + } + + get stoppedEvent(): SessionStoppedEvent | undefined { + return this._stoppedEvent; + } + + constructor(protected sessionTracker: SessionTracker) { + this.sessionTracker.onSessionEvent(event => this.onSessionEvent(event)); + this.sessionTracker.onSessionRequest(event => this.onSessionRequest(event)); + this.sessionTracker.onSessionResponse(event => this.onSessionResponse(event)); + } + + setInternal(internalBreakpoints: SetDataBreakpointsResult['breakpoints']): void { + this._dataBreakpoints.internal = []; + + const { external, internal } = this._dataBreakpoints; + const ids = internalBreakpoints.map(bp => bp.id); + for (let i = 0; i < external.length; i++) { + const tbp = external[i]; + if (ids.includes(tbp.response.id)) { + tbp.type = 'internal'; + internal.push(tbp); + } + } + + this._dataBreakpoints.external = external.filter(tbp => !ids.includes(tbp.response.id)); + this.fireDataBreakpoints(); + } + + protected onSessionEvent(event: SessionEvent): void { + if (!this.sessionTracker.isActive) { + return; + } + + if (isSessionEvent('stopped', event)) { + // TODO: Only for demo purposes + // Reason: The debugger does not set the hitBreakpointIds property + const demoEvent: SessionStoppedEvent = { + ...event, + data: { + ...event.data, + body: { + ...event.data.body, + hitBreakpointIds: this.externalDataBreakpoints.map(bp => bp.response.id ?? -1) + } + } + }; + this._stoppedEvent = demoEvent; + this._onStopped.fire(demoEvent); + } else if (isSessionEvent('continued', event)) { + this._stoppedEvent = undefined; + this._onContinued.fire(event); + } + } + + protected onSessionRequest(event: SessionRequest): void { + if (!this.sessionTracker.isActive) { + return; + } + + const { request } = event; + if (isDebugRequest('setDataBreakpoints', request)) { + this.dataBreakpointsRequest[request.seq] = request; + } + } + + protected onSessionResponse(event: SessionResponse): void { + if (!this.sessionTracker.isActive) { + return; + } + + const { response } = event; + if (isDebugResponse('setDataBreakpoints', response)) { + this._dataBreakpoints.external = []; + + const { external } = this._dataBreakpoints; + + const request = this.dataBreakpointsRequest[response.request_seq]; + if (request) { + if (response.success) { + for (let i = 0; i < response.body.breakpoints.length; i++) { + const bpResponse = response.body.breakpoints[i]; + if (bpResponse.verified) { + external.push({ + type: 'external', + breakpoint: request.arguments.breakpoints[i], + response: bpResponse + }); + } + } + } + + delete this.dataBreakpointsRequest[request.seq]; + } + + if (this.notifySetDataBreakpointEnabled) { + this._onSetDataBreakpointResponse.fire(response); + this.fireDataBreakpoints(); + } + } + } + + protected fireDataBreakpoints(): void { + this._onBreakpointsChanged.fire(this.dataBreakpoints); + } +} diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index e94771a..7f54592 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -14,9 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { DebugProtocol } from '@vscode/debugprotocol'; import * as vscode from 'vscode'; import { Messenger } from 'vscode-messenger'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import { SetDataBreakpointsArguments, SetDataBreakpointsResult } from '../common/breakpoint'; import * as manifest from '../common/manifest'; import { VariableRange } from '../common/memory-range'; import { @@ -26,6 +28,8 @@ import { logMessageType, MemoryOptions, memoryWrittenType, + notifyContinuedType, + notifyStoppedType, ReadMemoryArguments, ReadMemoryResult, readMemoryType, @@ -36,16 +40,19 @@ import { setMemoryViewSettingsType, setOptionsType, setTitleType, + setTrackedBreakpointType, showAdvancedOptionsType, StoreMemoryArguments, storeMemoryType, WebviewSelection, WriteMemoryArguments, WriteMemoryResult, - writeMemoryType, + writeMemoryType } from '../common/messaging'; import { MemoryViewSettings, ScrollingBehavior } from '../common/webview-configuration'; -import { getVisibleColumns, isWebviewVariableContext, WebviewContext } from '../common/webview-context'; +import { getVisibleColumns, isWebviewGroupContext, isWebviewVariableContext, WebviewContext } from '../common/webview-context'; +import { BreakpointProvider } from './breakpoints/breakpoint-provider'; +import { BreakpointTracker } from './breakpoints/breakpoint-tracker'; import { isVariablesContext } from './external-views'; import { outputChannelLogger } from './logger'; import { MemoryProvider } from './memory-provider'; @@ -67,12 +74,22 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { public static ToggleRadixPrefixCommandType = `${manifest.PACKAGE_NAME}.toggle-radix-prefix`; public static ShowAdvancedDisplayConfigurationCommandType = `${manifest.PACKAGE_NAME}.show-advanced-display-options`; public static GetWebviewSelectionCommandType = `${manifest.PACKAGE_NAME}.get-webview-selection`; + public static SetDataBreakpointReadCommandType = `${manifest.PACKAGE_NAME}.data-breakpoint.set.read`; + public static SetDataBreakpointReadWriteCommandType = `${manifest.PACKAGE_NAME}.data-breakpoint.set.readWrite`; + public static SetDataBreakpointWriteCommandType = `${manifest.PACKAGE_NAME}.data-breakpoint.set.write`; + public static RemoveDataBreakpointCommandType = `${manifest.PACKAGE_NAME}.data-breakpoint.remove`; + public static RemoveAllDataBreakpointCommandType = `${manifest.PACKAGE_NAME}.data-breakpoint.remove-all`; protected messenger: Messenger; protected panelIndices: number = 1; - public constructor(protected extensionUri: vscode.Uri, protected memoryProvider: MemoryProvider, protected sessionTracker: SessionTracker) { + public constructor( + protected extensionUri: vscode.Uri, + protected memoryProvider: MemoryProvider, + protected sessionTracker: SessionTracker, + protected breakpointTracker: BreakpointTracker, + protected breakpointProvider: BreakpointProvider) { this.messenger = new Messenger(); } @@ -104,6 +121,14 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.sendNotification(showAdvancedOptionsType, ctx.messageParticipant, undefined); }), vscode.commands.registerCommand(MemoryWebview.GetWebviewSelectionCommandType, (ctx: WebviewContext) => this.getWebviewSelection(ctx.messageParticipant)), + vscode.commands.registerCommand(MemoryWebview.SetDataBreakpointReadCommandType, (ctx: WebviewContext) => + this.onSetDataBreakpointCommand(ctx, 'read')), + vscode.commands.registerCommand(MemoryWebview.SetDataBreakpointWriteCommandType, (ctx: WebviewContext) => + this.onSetDataBreakpointCommand(ctx, 'write')), + vscode.commands.registerCommand(MemoryWebview.SetDataBreakpointReadWriteCommandType, (ctx: WebviewContext) => + this.onSetDataBreakpointCommand(ctx, 'readWrite')), + vscode.commands.registerCommand(MemoryWebview.RemoveDataBreakpointCommandType, (ctx: WebviewContext) => this.onRemoveDataBreakpointCommand(ctx)), + vscode.commands.registerCommand(MemoryWebview.RemoveAllDataBreakpointCommandType, (ctx: WebviewContext) => this.onRemoveDataBreakpointCommand(ctx, true)), ); }; @@ -202,7 +227,15 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.onNotification(setTitleType, title => { panel.title = title; }, { sender: participant }), this.messenger.onRequest(storeMemoryType, args => this.storeMemory(args), { sender: participant }), this.messenger.onRequest(applyMemoryType, () => this.applyMemory(), { sender: participant }), - this.sessionTracker.onSessionEvent(event => this.handleSessionEvent(participant, event)) + this.breakpointTracker.onBreakpointChanged(breakpoints => this.messenger.sendNotification(setTrackedBreakpointType, participant, breakpoints)), + this.breakpointTracker.onStopped(event => this.messenger.sendNotification(notifyStoppedType, participant, event.data)), + this.breakpointTracker.onContinued(event => this.messenger.sendNotification(notifyContinuedType, participant, event.data)), + this.sessionTracker.onSessionEvent(event => this.handleSessionEvent(participant, event)), + panel.onDidChangeViewState(view => { + if (view.webviewPanel.visible) { + this.setBreakpoints(participant); + } + }), ]; panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); } @@ -210,6 +243,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected async initialize(participant: WebviewIdMessageParticipant, panel: vscode.WebviewPanel, options?: MemoryOptions): Promise { this.setSessionContext(participant, this.createContext()); this.setInitialSettings(participant, panel.title); + this.setBreakpoints(participant); this.refresh(participant, options); } @@ -229,6 +263,13 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.sendNotification(sessionContextChangedType, webviewParticipant, context); } + protected setBreakpoints(webviewParticipant: WebviewIdMessageParticipant): void { + this.messenger.sendNotification(setTrackedBreakpointType, webviewParticipant, this.breakpointTracker.dataBreakpoints); + if (this.breakpointTracker.stoppedEvent) { + this.messenger.sendNotification(notifyStoppedType, webviewParticipant, this.breakpointTracker.stoppedEvent.data); + } + } + protected getMemoryViewSettings(messageParticipant: WebviewIdMessageParticipant, title: string): MemoryViewSettings { const memoryInspectorConfiguration = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); const bytesPerMau = memoryInspectorConfiguration.get(manifest.CONFIG_BYTES_PER_MAU, manifest.DEFAULT_BYTES_PER_MAU); @@ -303,6 +344,71 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { } } + protected async setDataBreakpoint(request: SetDataBreakpointsArguments): Promise { + try { + const result = await this.breakpointProvider.setMemoryInspectorDataBreakpoint(request); + return result; + } catch (err) { + return { + breakpoints: [] + }; + } + } + + protected async onSetDataBreakpointCommand(ctx: WebviewContext, accessType: DebugProtocol.DataBreakpointAccessType): Promise { + let dataId: string | undefined = undefined; + if (isWebviewGroupContext(ctx)) { + dataId = ctx.memoryData.group.startAddress; + } else if (isWebviewVariableContext(ctx)) { + const info = await this.breakpointProvider.dataBreakpointInfo({ + name: ctx.variable.name, + variablesReference: ctx.variable.parentVariablesReference + }); + if (!info.dataId) { + throw new Error(`DataBreakpointInfo returned for variable ${ctx.variable} an invalid info.`); + } + dataId = info.dataId; + } else { + throw new Error(`WebviewContext needs to be a Group or Variable context. It was: ${JSON.stringify(ctx, undefined, 2)}`); + } + + // Don't remove already existing breakpoints + const breakpoints = this.breakpointTracker.internalDataBreakpoints.map(bp => bp.breakpoint); + + return this.setDataBreakpoint({ + breakpoints: [ + ...breakpoints, + { + dataId, + accessType, + } + ] + }); + } + + protected async onRemoveDataBreakpointCommand(ctx: WebviewContext, removeAll: boolean = false): Promise { + if (removeAll) { + return this.setDataBreakpoint({ breakpoints: [] }); + } + + let dataId: string | undefined = undefined; + if (isWebviewGroupContext(ctx)) { + dataId = ctx.memoryData.group.startAddress; + } else if (isWebviewVariableContext(ctx)) { + dataId = ctx.variable.name; + } else { + throw new Error(`WebviewContext needs to be a Group or Variable context. It was: ${JSON.stringify(ctx, undefined, 2)}`); + } + + const breakpoints = this.breakpointTracker.internalDataBreakpoints + .filter(bp => bp.breakpoint.dataId !== dataId) + .map(bp => bp.breakpoint); + + return this.setDataBreakpoint({ + breakpoints + }); + } + protected getWebviewSelection(webviewParticipant: WebviewIdMessageParticipant): Promise { return this.messenger.sendRequest(getWebviewSelectionType, webviewParticipant, undefined); } diff --git a/src/plugin/session-tracker.ts b/src/plugin/session-tracker.ts index d10529f..780bb7f 100644 --- a/src/plugin/session-tracker.ts +++ b/src/plugin/session-tracker.ts @@ -13,10 +13,11 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { DebugProtocol } from '@vscode/debugprotocol'; +import type { DebugProtocol } from '@vscode/debugprotocol'; import * as vscode from 'vscode'; -import { isDebugEvent, isDebugRequest, isDebugResponse } from '../common/debug-requests'; -import { WrittenMemory } from '../common/memory-range'; +import { isDebugEvent, isDebugRequest, isDebugRequestType, isDebugResponse, isDebugResponseType } from '../common/debug-requests'; +import type { WrittenMemory } from '../common/memory-range'; +import type { ContinuedEvent, StoppedEvent } from '../common/messaging'; export interface SessionInfo { raw: vscode.DebugSession; @@ -26,6 +27,16 @@ export interface SessionInfo { stopped?: boolean; } +export interface SessionRequest { + session: SessionInfo; + request: DebugProtocol.Request +} + +export interface SessionResponse { + session: SessionInfo; + response: DebugProtocol.Response +} + export interface SessionEvent { event: string; session?: SessionInfo; @@ -45,11 +56,13 @@ export interface SessionMemoryWrittenEvent extends SessionEvent { export interface SessionStoppedEvent extends SessionEvent { event: 'stopped'; session: SessionInfo; + data: StoppedEvent; } export interface SessionContinuedEvent extends SessionEvent { event: 'continued'; session: SessionInfo; + data: ContinuedEvent } export interface SessionEvents { @@ -74,6 +87,10 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { private _onSessionEvent = new vscode.EventEmitter(); public readonly onSessionEvent = this._onSessionEvent.event; + private _onSessionRequest = new vscode.EventEmitter(); + public readonly onSessionRequest = this._onSessionRequest.event; + private _onSessionResponse = new vscode.EventEmitter(); + public readonly onSessionResponse = this._onSessionResponse.event; activate(context: vscode.ExtensionContext): void { context.subscriptions.push( @@ -111,6 +128,14 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { this._onSessionEvent.fire({ event, session: this.sessionInfo(session), data }); } + fireSessionRequest(session: vscode.DebugSession, data: DebugProtocol.Request): void { + this._onSessionRequest.fire({ session: this.sessionInfo(session), request: data }); + } + + fireSessionResponse(session: vscode.DebugSession, data: DebugProtocol.Response): void { + this._onSessionResponse.fire({ session: this.sessionInfo(session), response: data }); + } + protected async sessionWillStart(session: vscode.DebugSession): Promise { this._sessionInfo.set(session.id, { raw: session }); } @@ -120,23 +145,35 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { } protected willSendClientMessage(session: vscode.DebugSession, message: unknown): void { + // TODO: ONLY FOR DEMO PURPOSES + console.log('[SEND] ==>', message); if (isDebugRequest('initialize', message)) { this.sessionInfo(session).clientCapabilities = message.arguments; } + + if (isDebugRequestType(message)) { + this.fireSessionRequest(session, message); + } } protected adapterMessageReceived(session: vscode.DebugSession, message: unknown): void { + // TODO: ONLY FOR DEMO PURPOSES + console.log('[RECV] <==', message); if (isDebugResponse('initialize', message)) { this.sessionInfo(session).debugCapabilities = message.body; } else if (isDebugEvent('stopped', message)) { this.sessionInfo(session).stopped = true; - this.fireSessionEvent(session, 'stopped', undefined); + this.fireSessionEvent(session, 'stopped', message); } else if (isDebugEvent('continued', message)) { this.sessionInfo(session).stopped = false; - this.fireSessionEvent(session, 'continued', undefined); + this.fireSessionEvent(session, 'continued', message); } else if (isDebugEvent('memory', message)) { this.fireSessionEvent(session, 'memory-written', message.body); } + + if (isDebugResponseType(message)) { + this.fireSessionResponse(session, message); + } } get activeSession(): vscode.DebugSession | undefined { diff --git a/src/webview/breakpoints/breakpoint-service.ts b/src/webview/breakpoints/breakpoint-service.ts new file mode 100644 index 0000000..54ec97b --- /dev/null +++ b/src/webview/breakpoints/breakpoint-service.ts @@ -0,0 +1,174 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { DebugProtocol } from '@vscode/debugprotocol'; +import { HOST_EXTENSION } from 'vscode-messenger-common'; +import { TrackedBreakpointType, TrackedDataBreakpoint, TrackedDataBreakpoints } from '../../common/breakpoint'; +import { BigIntMemoryRange, BigIntVariableRange, doOverlap, isWithin } from '../../common/memory-range'; +import { getVariablesType, notifyContinuedType, notifyStoppedType, setTrackedBreakpointType, StoppedEvent } from '../../common/messaging'; +import { EventEmitter } from '../utils/events'; +import { UpdateExecutor } from '../utils/view-types'; +import { messenger } from '../view-messenger'; + +export interface BreakpointMetadata { + id?: number; + type: TrackedBreakpointType, + isHit: boolean; +} + +export class BreakpointService implements UpdateExecutor { + protected _breakpoints: TrackedDataBreakpoints = { external: [], internal: [] }; + protected _stoppedEvent?: StoppedEvent; + + protected variables: BigIntVariableRange[] = []; + + protected _onDidChange = new EventEmitter(); + readonly onDidChange = this._onDidChange.event; + + get breakpoints(): TrackedDataBreakpoints { + return this._breakpoints; + } + + get allBreakpoints(): TrackedDataBreakpoint[] { + return [...this.breakpoints.external, ...this.breakpoints.internal]; + } + + get stoppedEvent(): StoppedEvent | undefined { + return this._stoppedEvent; + } + + activate(): void { + messenger.onNotification(setTrackedBreakpointType, breakpoints => { + this._breakpoints = breakpoints; + this._onDidChange.fire(); + }); + messenger.onNotification(notifyStoppedType, event => { + this._stoppedEvent = event; + this._onDidChange.fire(); + }); + messenger.onNotification(notifyContinuedType, () => { + this._stoppedEvent = undefined; + this._onDidChange.fire(); + }); + } + + async fetchData(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise { + this.variables = (await messenger.sendRequest(getVariablesType, HOST_EXTENSION, currentViewParameters)) + .map(transmissible => { + const startAddress = BigInt(transmissible.startAddress); + return { + ...transmissible, + startAddress, + endAddress: transmissible.endAddress ? BigInt(transmissible.endAddress) : startAddress + BigInt(1) + }; + }); + } + + findByDataId(dataId: string): TrackedDataBreakpoint | undefined { + return [...this.breakpoints.external, ...this.breakpoints.internal].find(bp => bp.breakpoint.dataId === dataId); + } + + inRange(range: BigIntMemoryRange): TrackedDataBreakpoint[] { + const variables = this.findVariablesInRange(range); + return this.allBreakpoints.filter(bp => { + let isInRange = false; + try { + const bigint = BigInt(bp.breakpoint.dataId); + isInRange = isWithin(bigint, range); + } catch (ex) { + // Nothing to do + } + + return isInRange || variables.some(v => v.name === bp.breakpoint.dataId); + }); + } + + protected findVariablesInRange(range: BigIntMemoryRange): BigIntVariableRange[] { + return this.variables.filter(v => doOverlap(v, range)); + } + + isHit(breakpointOrDataId: TrackedDataBreakpoint | string): boolean { + if (this.stoppedEvent === undefined || + this.stoppedEvent.body.hitBreakpointIds === undefined || + this.stoppedEvent.body.hitBreakpointIds.length === 0) { + return false; + } + + const bp = typeof breakpointOrDataId === 'string' ? this.findByDataId(breakpointOrDataId) : breakpointOrDataId; + return !!bp?.response.id && this.stoppedEvent.body.hitBreakpointIds.includes(bp.response.id); + } + + metadata(breakpointOrDataId: TrackedDataBreakpoint | string): BreakpointMetadata | undefined { + const bp = typeof breakpointOrDataId === 'string' ? this.findByDataId(breakpointOrDataId) : breakpointOrDataId; + + if (bp?.type === 'external') { + return { + id: bp.response.id, + type: 'external', + isHit: this.isHit(breakpointOrDataId) + }; + } else if (bp?.type === 'internal') { + return { + id: bp.response.id, + type: 'internal', + isHit: this.isHit(breakpointOrDataId) + }; + } + + return undefined; + } +} + +export namespace BreakpointService { + export namespace style { + export const dataBreakpoint = 'data-breakpoint'; + export const dataBreakpointExternal = 'data-breakpoint-external'; + export const debugHit = 'debug-hit'; + } + + export function inlineClasses(metadata?: BreakpointMetadata): string[] { + const classes: string[] = []; + + if (metadata) { + if (metadata.type === 'external') { + classes.push(BreakpointService.style.dataBreakpoint, BreakpointService.style.dataBreakpointExternal); + } else if (metadata.type === 'internal') { + classes.push(BreakpointService.style.dataBreakpoint); + } + + if (metadata.isHit) { + classes.push(BreakpointService.style.debugHit); + } + } + + return classes; + } + + export function statusClasses(metadata: BreakpointMetadata[]): string[] { + const classes: string[] = []; + + if (metadata.length > 0) { + classes.push('codicon', 'codicon-debug-breakpoint'); + if (metadata.some(m => m.isHit)) { + classes.push('codicon-debug-stackframe'); + } + } + + return classes; + } +} + +export const breakpointService = new BreakpointService(); diff --git a/src/webview/columns/address-column.tsx b/src/webview/columns/address-column.tsx index ad6388a..c241519 100644 --- a/src/webview/columns/address-column.tsx +++ b/src/webview/columns/address-column.tsx @@ -14,22 +14,32 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { classNames } from 'primereact/utils'; import React, { ReactNode } from 'react'; import { Memory } from '../../common/memory'; import { BigIntMemoryRange, getAddressString, getRadixMarker } from '../../common/memory-range'; +import { BreakpointMetadata, BreakpointService, breakpointService } from '../breakpoints/breakpoint-service'; import { ColumnContribution, ColumnFittingType, TableRenderOptions } from './column-contribution-service'; export class AddressColumn implements ColumnContribution { static ID = 'address'; + static CLASS_NAME = 'column-address'; readonly id = AddressColumn.ID; + readonly className = AddressColumn.CLASS_NAME; readonly label = 'Address'; readonly priority = 0; fittingType: ColumnFittingType = 'content-width'; render(range: BigIntMemoryRange, _: Memory, options: TableRenderOptions): ReactNode { + const breakpointMetadata = breakpointService.inRange(range) + .map(bp => breakpointService.metadata(bp)) + .filter((bp): bp is BreakpointMetadata => bp !== undefined); + const statusClasses = BreakpointService.statusClasses(breakpointMetadata); + return + {statusClasses.length > 0 && } {options.showRadixPrefix && {getRadixMarker(options.addressRadix)}} {getAddressString(range.startAddress, options.addressRadix, options.effectiveAddressLength)} ; diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx index 4a929c2..98d68e8 100644 --- a/src/webview/columns/data-column.tsx +++ b/src/webview/columns/data-column.tsx @@ -15,11 +15,13 @@ ********************************************************************************/ import { InputText } from 'primereact/inputtext'; +import { classNames } from 'primereact/utils'; import * as React from 'react'; import { HOST_EXTENSION } from 'vscode-messenger-common'; import { Memory } from '../../common/memory'; import { BigIntMemoryRange, isWithin, toHexStringWithRadixMarker, toOffset } from '../../common/memory-range'; import { writeMemoryType } from '../../common/messaging'; +import { BreakpointService, breakpointService } from '../breakpoints/breakpoint-service'; import type { MemorySizeOptions } from '../components/memory-table'; import { decorationService } from '../decorations/decoration-service'; import { Disposable, FullNodeAttributes } from '../utils/view-types'; @@ -82,13 +84,14 @@ export class EditableDataColumnRow extends React.Component {maus} ; diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index e0829df..b1fb5e3 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -17,8 +17,7 @@ import React from 'react'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; import { Memory } from '../../common/memory'; -import { WebviewSelection } from '../../common/messaging'; -import { MemoryOptions, ReadMemoryArguments, SessionContext } from '../../common/messaging'; +import { MemoryOptions, ReadMemoryArguments, SessionContext, WebviewSelection } from '../../common/messaging'; import { MemoryDisplayConfiguration } from '../../common/webview-configuration'; import { ColumnStatus } from '../columns/column-contribution-service'; import { HoverService } from '../hovers/hover-service'; @@ -39,9 +38,9 @@ interface MemoryWidgetProps extends MemoryDisplayConfiguration { columns: ColumnStatus[]; effectiveAddressLength: number; isMemoryFetching: boolean; + isFrozen: boolean; updateMemoryState: (state: Partial) => void; toggleColumn(id: string, active: boolean): void; - isFrozen: boolean; toggleFrozen: () => void; updateMemoryDisplayConfiguration: (memoryArguments: Partial) => void; resetMemoryDisplayConfiguration: () => void; diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index 34c04bb..37c079f 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -44,6 +44,7 @@ import { } from '../common/messaging'; import { Change, hasChanged, hasChangedTo } from '../common/typescript'; import { MemoryDisplayConfiguration } from '../common/webview-configuration'; +import { breakpointService } from './breakpoints/breakpoint-service'; import { AddressColumn } from './columns/address-column'; import { AsciiColumn } from './columns/ascii-column'; import { columnContributionService, ColumnStatus } from './columns/column-contribution-service'; @@ -136,6 +137,8 @@ class App extends React.Component<{}, MemoryAppState> { messenger.onRequest(getWebviewSelectionType, () => this.getWebviewSelection()); messenger.onNotification(showAdvancedOptionsType, () => this.showAdvancedOptions()); messenger.sendNotification(readyType, HOST_EXTENSION, undefined); + breakpointService.activate(); + breakpointService.onDidChange(() => this.forceUpdate()); this.updatePeriodicRefresh(); } @@ -293,7 +296,10 @@ class App extends React.Component<{}, MemoryAppState> { try { const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, memoryOptions); await Promise.all(Array.from( - new Set(columnContributionService.getUpdateExecutors().concat(decorationService.getUpdateExecutors())), + new Set(columnContributionService + .getUpdateExecutors() + .concat(decorationService.getUpdateExecutors()) + .concat(breakpointService)), executor => executor.fetchData(memoryOptions) )); diff --git a/src/webview/utils/vscode-contexts.ts b/src/webview/utils/vscode-contexts.ts index aa56e3c..5e3aae1 100644 --- a/src/webview/utils/vscode-contexts.ts +++ b/src/webview/utils/vscode-contexts.ts @@ -16,6 +16,7 @@ import { BigIntVariableRange } from '../../common/memory-range'; import { WebviewContext } from '../../common/webview-context'; +import { BreakpointMetadata } from '../breakpoints/breakpoint-service'; /** * Custom data property used by VSCode to provide additional context info when opening a webview context menu * The data property needs to represent a valid JSON object. Data context properties up the parent chain are merged into the child context. @@ -60,13 +61,13 @@ export function createAppVscodeContext(context: Omit((result, current, index) => { if (index > 0) { result.push(', '); } + const breakpointMetadata = breakpointService.metadata(current.variable.name); result.push(React.createElement('span', { style: { color: current.color }, key: current.variable.name, - className: 'hoverable', + className: classNames('hoverable', ...BreakpointService.inlineClasses(breakpointMetadata)), 'data-column': 'variables', 'data-variables': stringifyWithBigInts(current.variable), - ...createVariableVscodeContext(current.variable) + ...createVariableVscodeContext(current.variable, breakpointMetadata) }, current.variable.name)); return result; }, []);