diff --git a/media/memory-table.css b/media/memory-table.css index 8d33747..8eb3dea 100644 --- a/media/memory-table.css +++ b/media/memory-table.css @@ -32,6 +32,44 @@ outline-offset: -1px; } +.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 { @@ -92,7 +130,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 { @@ -113,7 +151,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; @@ -129,24 +167,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); } @@ -154,9 +196,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 { @@ -171,26 +215,29 @@ } .p-datatable .p-datatable-tbody > tr > td[data-column="data"][role="cell"], -.p-datatable .p-datatable-tbody > tr > td[data-column="variables"][role="cell"] { +.p-datatable + .p-datatable-tbody + > tr + > td[data-column="variables"][role="cell"] { padding: 0 12px; vertical-align: middle; } /* Group Styles */ -[role='group']:hover { +[role="group"]:hover { border-bottom: 0px; outline: 1px solid var(--vscode-list-focusOutline); } -[role='group'][data-group-selected='true'] { +[role="group"][data-group-selected="true"] { background: var(--vscode-list-activeSelectionBackground); color: var(--vscode-list-activeSelectionForeground); outline: 1px solid var(--vscode-list-activeSelectionBackground); } -[role='group']:focus-visible, -[role='group']:focus { +[role="group"]:focus-visible, +[role="group"]:focus { outline: 1px solid var(--vscode-list-focusOutline); } @@ -198,11 +245,12 @@ [data-column="ascii"][role="group"] { padding: 4px 12px; display: flex; + align-items: center; outline-offset: -1px; } [data-column="data"][role="group"], -[data-column="variables"][role="group"] { +[data-column="variables"][role="group"] { padding: 4px 1px; line-height: 23.5px; outline-offset: -1px; @@ -241,7 +289,17 @@ text-indent: 2px; } -.p-datatable .p-datatable-tbody > tr > td.p-highlight:has(>.selected) { +.p-datatable .p-datatable-tbody > tr > td.p-highlight:has(> .selected) { background: transparent; outline: none; -} \ No newline at end of file +} + +/* == Data Breakpoint == */ + +.memory-inspector-table .data-breakpoint { + outline: 1px solid var(--vscode-debugIcon-breakpointForeground); +} + +.memory-inspector-table .data-breakpoint.data-breakpoint-external { + outline-style: dashed; +} diff --git a/package.json b/package.json index 4406a8a..2830358 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,42 @@ "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 && memory-inspector.dataBreakpoints", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.set.readWrite", + "title": "Break on Value Access", + "enablement": "memory-inspector.canWrite && memory-inspector.dataBreakpoints", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.set.write", + "title": "Break on Value Change", + "enablement": "memory-inspector.canWrite && memory-inspector.dataBreakpoints", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.remove", + "title": "Remove Breakpoint", + "enablement": "memory-inspector.canWrite && memory-inspector.dataBreakpoints", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.remove-all", + "title": "Remove All Memory Inspector Breakpoints", + "enablement": "memory-inspector.canWrite && memory-inspector.dataBreakpoints", + "category": "Memory" + }, + { + "command": "memory-inspector.data-breakpoint.inspect-breakpoints", + "title": "Inspect Breakpoints", + "enablement": "memory-inspector.canWrite && memory-inspector.dataBreakpoints && (memory-inspector.loggingVerbosity == 'debug' || memory-inspector.loggingVerbosity == 'info')", + "category": "Memory" + }, { "command": "memory-inspector.toggle-variables-column", "title": "Toggle Variables Column", @@ -156,6 +192,22 @@ }, { "command": "memory-inspector.apply-file" + }, + { + "command": "memory-inspector.data-breakpoint.set.read", + "when": "false" + }, + { + "command": "memory-inspector.data-breakpoint.set.readWrite", + "when": "false" + }, + { + "command": "memory-inspector.data-breakpoint.set.write", + "when": "false" + }, + { + "command": "memory-inspector.data-breakpoint.remove", + "when": "false" } ], "debug/variables/context": [ @@ -237,6 +289,31 @@ "command": "memory-inspector.reset-display-options", "group": "a_reset@2", "when": "webviewId === memory-inspector.memory && optionsMenu" + }, + { + "command": "memory-inspector.data-breakpoint.set.read", + "group": "breakpoints@1", + "when": "memory-inspector.dataBreakpoints && webviewId === memory-inspector.memory && memory-inspector.breakpoint.isBreakable" + }, + { + "command": "memory-inspector.data-breakpoint.set.write", + "group": "breakpoints@2", + "when": "memory-inspector.dataBreakpoints && webviewId === memory-inspector.memory && memory-inspector.breakpoint.isBreakable" + }, + { + "command": "memory-inspector.data-breakpoint.set.readWrite", + "group": "breakpoints@3", + "when": "memory-inspector.dataBreakpoints && webviewId === memory-inspector.memory && memory-inspector.breakpoint.isBreakable" + }, + { + "command": "memory-inspector.data-breakpoint.remove", + "group": "breakpoints@4", + "when": "memory-inspector.dataBreakpoints && webviewId === memory-inspector.memory && memory-inspector.breakpoint.type === 'internal'" + }, + { + "command": "memory-inspector.data-breakpoint.remove-all", + "group": "breakpoints@5", + "when": "memory-inspector.dataBreakpoints && webviewId === memory-inspector.memory && memory-inspector.breakpoint.type === 'internal'" } ] }, @@ -279,6 +356,14 @@ ], "description": "C-based debuggers to activate (requires debug session restart)" }, + "memory-inspector.dataBreakpoints": { + "type": "boolean", + "default:": false, + "description": "Enable data breakpoint support.", + "tags": [ + "experimental" + ] + }, "memory-inspector.refreshOnStop": { "type": "string", "enum": [ @@ -447,4 +532,4 @@ "workspace", "ui" ] -} \ No newline at end of file +} diff --git a/src/common/breakpoint.ts b/src/common/breakpoint.ts new file mode 100644 index 0000000..6234560 --- /dev/null +++ b/src/common/breakpoint.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * 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'; + +/** + * Temp. workaround till we have a proper API for this within VSCode. + * + * @see {@link https://github.com/microsoft/vscode/pull/226735} + */ +export interface ExperimentalDataBreakpoint { + type: TrackedBreakpointType; + breakpoint: DebugProtocol.DataBreakpoint; + /** + * The respective response for the breakpoint. + */ + response: DebugProtocol.Breakpoint; +} + +/** + * Temp. workaround till we have a proper API for this within VSCode. + * + * @see {@link https://github.com/microsoft/vscode/pull/226735} + */ +export interface ExperimentalDataBreakpoints { + /** + * Breakpoints set from external contributors. + */ + external: ExperimentalDataBreakpoint[], + /** + * Breakpoints set from us. + */ + internal: ExperimentalDataBreakpoint[] +} + +/** + * Temp. workaround till we have a proper API for this within VSCode. + */ +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/manifest.ts b/src/common/manifest.ts index 571fdc3..a35643a 100644 --- a/src/common/manifest.ts +++ b/src/common/manifest.ts @@ -21,6 +21,7 @@ export const EDITOR_NAME = `${PACKAGE_NAME}.inspect`; // Misc export const CONFIG_LOGGING_VERBOSITY = 'loggingVerbosity'; +export const CONFIG_LOGGING_VERBOSITY_PREFERENCE = `${PACKAGE_NAME}.${CONFIG_LOGGING_VERBOSITY}`; export const DEFAULT_LOGGING_VERBOSITY = 'warn'; export const CONFIG_DEBUG_TYPES = 'debugTypes'; export const DEFAULT_DEBUG_TYPES = ['gdb', 'embedded-debug', 'arm-debugger']; @@ -80,3 +81,8 @@ export const DEFAULT_VISIBLE_COLUMNS = [CONFIG_SHOW_VARIABLES_COLUMN, CONFIG_SHO // Extension Settings export const CONFIG_ALLOW_DEBUGGER_OVERWRITE_SETTINGS = 'allowDebuggerOverwriteSettings'; + +// Breakpoints +export const CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS = 'dataBreakpoints'; +export const CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS_PREFERENCE = `${PACKAGE_NAME}.${CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS}`; +export const DEFAULT_EXPERIMENTAL_DATA_BREAKPOINTS = false; diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 2f0d6c6..4f5c401 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; } @@ -116,6 +117,15 @@ export interface VariableRange extends MemoryRange, VariableMetadata { } /** Suitable for arithemetic */ export interface BigIntVariableRange extends BigIntMemoryRange, VariableMetadata { } +export namespace BigIntVariableRange { + export function is(value: unknown): value is BigIntVariableRange { + return typeof value === 'object' + && typeof (value as BigIntVariableRange).startAddress === 'bigint' + && typeof (value as BigIntVariableRange).endAddress === 'bigint' + && typeof (value as BigIntVariableRange).name === 'string'; + } +} + export function areVariablesEqual(one: BigIntVariableRange, other: BigIntVariableRange): boolean { return areRangesEqual(one, other) && one.name === other.name diff --git a/src/common/messaging.ts b/src/common/messaging.ts index df57f02..80e19c0 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -17,7 +17,8 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import type { NotificationType, RequestType } from 'vscode-messenger-common'; import { URI } from 'vscode-uri'; -import { DebugRequestTypes } from './debug-requests'; +import type { ExperimentalDataBreakpoints } from './breakpoint'; +import { DebugEvents, DebugRequestTypes } from './debug-requests'; import { VariablesView } from './external-views'; import type { VariableRange, WrittenMemory } from './memory-range'; import { MemoryViewSettings } from './webview-configuration'; @@ -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; @@ -58,6 +62,9 @@ export const memoryWrittenType: NotificationType = { method: 'mem export const sessionsChangedType: NotificationType = { method: 'sessionsChanged' }; export const setSessionType: NotificationType = { method: 'setSession' }; export const sessionContextChangedType: NotificationType = { method: 'sessionContextChanged' }; +export const setExperimentalBreakpointType: NotificationType = { method: 'setExperimentalBreakpoints' }; +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 ac142f2..896ec44 100644 --- a/src/common/webview-context.ts +++ b/src/common/webview-context.ts @@ -41,6 +41,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. @@ -64,6 +73,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 5adf924..a669312 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 { MemoryProviderManager } from '../../plugin/memory-provider-manager'; import { MemoryStorage } from '../../plugin/memory-storage'; @@ -27,9 +29,13 @@ 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); @@ -126,7 +126,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..c4c8e93 --- /dev/null +++ b/src/plugin/breakpoints/breakpoint-provider.ts @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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) + }); + }); + this.breakpointTracker.onDataBreakpointsPreferenceChanged(isEnabled => { + if (!isEnabled) { + // Use only external breakpoints + this.setMemoryInspectorDataBreakpoint({ + breakpoints: this.breakpointTracker.externalDataBreakpoints.map(bp => bp.breakpoint) + }); + } + }); + } + + async setMemoryInspectorDataBreakpoint(args: SetDataBreakpointsArguments): Promise { + const activeSession = this.sessionTracker.getActiveSession(); + if (!activeSession) { + throw new Error('No active session'); + } + + const session = this.sessionTracker.assertDebugCapability(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 activeSession = this.sessionTracker.getActiveSession(); + if (!activeSession) { + throw new Error('No active session'); + } + const session = this.sessionTracker.assertDebugCapability(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..95479f9 --- /dev/null +++ b/src/plugin/breakpoints/breakpoint-tracker.ts @@ -0,0 +1,177 @@ +/******************************************************************************** + * 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 { ExperimentalDataBreakpoint, ExperimentalDataBreakpoints, SetDataBreakpointsResult } from '../../common/breakpoint'; +import { isDebugRequest, isDebugResponse } from '../../common/debug-requests'; +import * as manifest from '../../common/manifest'; +import { isSessionEvent, SessionContinuedEvent, SessionEvent, SessionRequest, SessionResponse, SessionStoppedEvent, SessionTracker } from '../session-tracker'; + +/** + * Tracks data breakpoints and provides events for changes. + * + * It will still track data breakpoints even if the feature is disabled, but will not notify about changes. + */ +export class BreakpointTracker { + protected _dataBreakpoints: ExperimentalDataBreakpoints = { external: [], internal: [] }; + protected _stoppedEvent?: SessionStoppedEvent; + protected _isEnabled = false; + protected dataBreakpointsRequest: Record = {}; + + protected _onBreakpointsChanged = new vscode.EventEmitter(); + readonly onBreakpointChanged = this._onBreakpointsChanged.event; + + protected _onSetDataBreakpointResponse = new vscode.EventEmitter(); + readonly onSetDataBreakpointResponse = this._onSetDataBreakpointResponse.event; + + protected _onDataBreakpointsPreferenceChanged = new vscode.EventEmitter(); + readonly onDataBreakpointsPreferenceChanged = this._onDataBreakpointsPreferenceChanged.event; + + protected _onStopped = new vscode.EventEmitter(); + readonly onStopped = this._onStopped.event; + + protected _onContinued = new vscode.EventEmitter(); + readonly onContinued = this._onContinued.event; + + get dataBreakpoints(): ExperimentalDataBreakpoints { + return this.isEnabled ? this._dataBreakpoints : { external: [], internal: [] }; + } + + get internalDataBreakpoints(): ExperimentalDataBreakpoint[] { + return this._dataBreakpoints.internal; + } + + get externalDataBreakpoints(): ExperimentalDataBreakpoint[] { + return this._dataBreakpoints.external; + } + + get stoppedEvent(): SessionStoppedEvent | undefined { + return this._stoppedEvent; + } + + get isEnabled(): boolean { + return this._isEnabled; + } + + notifySetDataBreakpointEnabled = true; + + 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)); + + this.onConfigurationChange(); + vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration(manifest.CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS_PREFERENCE)) { + this.onConfigurationChange(); + } + }); + + this.onDataBreakpointsPreferenceChanged(() => { + this.fireDataBreakpoints(); + }); + } + + protected onConfigurationChange(): void { + const configuration = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); + const value = configuration.get(manifest.CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS); + this._isEnabled = !!value; + this._onDataBreakpointsPreferenceChanged.fire(this._isEnabled); + } + + 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 bp = external[i]; + if (ids.includes(bp.response.id)) { + bp.type = 'internal'; + internal.push(bp); + } + } + + this._dataBreakpoints.external = external.filter(bp => !ids.includes(bp.response.id)); + this.fireDataBreakpoints(); + } + + protected onSessionEvent(event: SessionEvent): void { + if (!this.sessionTracker.getActiveSession()) { + return; + } + + if (isSessionEvent('stopped', event)) { + this._stoppedEvent = event; + this._onStopped.fire(event); + } else if (isSessionEvent('continued', event)) { + this._stoppedEvent = undefined; + this._onContinued.fire(event); + } + } + + protected onSessionRequest(event: SessionRequest): void { + if (!this.sessionTracker.getActiveSession()) { + return; + } + + const { request } = event; + if (isDebugRequest('setDataBreakpoints', request)) { + this.dataBreakpointsRequest[request.seq] = request; + } + } + + protected onSessionResponse(event: SessionResponse): void { + if (!this.sessionTracker.getActiveSession()) { + 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.isEnabled) { + this._onSetDataBreakpointResponse.fire(response); + this.fireDataBreakpoints(); + } + } + } + + protected fireDataBreakpoints(): void { + this._onBreakpointsChanged.fire(this.dataBreakpoints); + } +} diff --git a/src/plugin/context-tracker.ts b/src/plugin/context-tracker.ts index 30b27a7..4160881 100644 --- a/src/plugin/context-tracker.ts +++ b/src/plugin/context-tracker.ts @@ -24,6 +24,18 @@ export class ContextTracker { constructor(protected sessionTracker: SessionTracker) { this.sessionTracker.onSessionEvent(event => this.onSessionEvent(event)); + + this.onDataBreakpointPreferenceChange(); + this.onLoggingVerbosityPreferenceChange(); + + vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration(manifest.CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS_PREFERENCE)) { + this.onDataBreakpointPreferenceChange(); + } + if (event.affectsConfiguration(manifest.CONFIG_LOGGING_VERBOSITY_PREFERENCE)) { + this.onLoggingVerbosityPreferenceChange(); + } + }); } onSessionEvent(event: SessionEvent): void { @@ -32,4 +44,18 @@ export class ContextTracker { vscode.commands.executeCommand('setContext', ContextTracker.WriteKey, !!event.session?.debugCapabilities?.supportsWriteMemoryRequest); } } + + private onDataBreakpointPreferenceChange(): void { + this.onPreferenceChange(manifest.CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS, manifest.CONFIG_EXPERIMENTAL_DATA_BREAKPOINTS_PREFERENCE); + } + + private onLoggingVerbosityPreferenceChange(): void { + this.onPreferenceChange(manifest.CONFIG_LOGGING_VERBOSITY, manifest.CONFIG_LOGGING_VERBOSITY_PREFERENCE); + } + + private onPreferenceChange(preferenceKey: string, contextKey: string): void { + const configuration = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); + const value = configuration.get(preferenceKey); + vscode.commands.executeCommand('setContext', contextKey, value); + } } diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 2ba550d..aa6be29 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 { isVariablesContext } from '../common/external-views'; import * as manifest from '../common/manifest'; import { VariableRange } from '../common/memory-range'; @@ -28,6 +30,8 @@ import { logMessageType, MemoryOptions, memoryWrittenType, + notifyContinuedType, + notifyStoppedType, ReadMemoryArguments, ReadMemoryResult, readMemoryType, @@ -36,6 +40,7 @@ import { SessionContext, sessionContextChangedType, sessionsChangedType, + setExperimentalBreakpointType, setMemoryViewSettingsType, setOptionsType, setSessionType, @@ -46,11 +51,13 @@ import { WebviewSelection, WriteMemoryArguments, WriteMemoryResult, - writeMemoryType, + writeMemoryType } from '../common/messaging'; import { MemoryDisplaySettings, MemoryDisplaySettingsContribution, MemoryViewSettings, ScrollingBehavior } from '../common/webview-configuration'; -import { getVisibleColumns, isWebviewVariableContext, WebviewContext } from '../common/webview-context'; +import { getVisibleColumns, isWebviewGroupContext, isWebviewVariableContext, WebviewContext } from '../common/webview-context'; import { AddressPaddingOptions } from '../webview/utils/view-types'; +import { BreakpointProvider } from './breakpoints/breakpoint-provider'; +import { BreakpointTracker } from './breakpoints/breakpoint-tracker'; import { outputChannelLogger } from './logger'; import type { MemoryProvider } from './memory-provider'; import type { MemoryProviderManager } from './memory-provider-manager'; @@ -74,6 +81,12 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { public static ResetDisplayOptionsToDebuggerDefaultsType = `${manifest.PACKAGE_NAME}.reset-display-options-to-debugger-defaults`; 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 InspectDataBreakpointCommandType = `${manifest.PACKAGE_NAME}.data-breakpoint.inspect-breakpoints`; + public static RemoveAllDataBreakpointCommandType = `${manifest.PACKAGE_NAME}.data-breakpoint.remove-all`; protected messenger: Messenger; protected panelIndices: number = 1; @@ -83,7 +96,9 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected extensionUri: vscode.Uri, protected memoryProviderManager: MemoryProviderManager, protected sessionTracker: SessionTracker, - protected memoryStorage: MemoryStorage) { + protected memoryStorage: MemoryStorage, + protected breakpointTracker: BreakpointTracker, + protected breakpointProvider: BreakpointProvider) { this.messenger = new Messenger({ ignoreHiddenViews: false }); } @@ -126,6 +141,18 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { }), 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.InspectDataBreakpointCommandType, () => { + // Same approach is also used for "Inspect Context Keys" + outputChannelLogger.info('[Breakpoints]:', 'Logging all data breakpoints', this.breakpointTracker.dataBreakpoints); + }), + vscode.commands.registerCommand(MemoryWebview.RemoveAllDataBreakpointCommandType, (ctx: WebviewContext) => this.onRemoveDataBreakpointCommand(ctx, true)), ); }; @@ -224,7 +251,15 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.onNotification(setSessionType, sessionId => this.setSession(participant, sessionId), { sender: participant }), this.messenger.onRequest(storeMemoryType, args => this.storeMemory(participant, args), { sender: participant }), this.messenger.onRequest(applyMemoryType, () => this.applyMemory(participant), { sender: participant }), - this.sessionTracker.onSessionEvent(event => this.handleSessionEvent(participant, event)) + this.sessionTracker.onSessionEvent(event => this.handleSessionEvent(participant, event)), + this.breakpointTracker.onBreakpointChanged(breakpoints => this.messenger.sendNotification(setExperimentalBreakpointType, participant, breakpoints)), + this.breakpointTracker.onStopped(event => this.messenger.sendNotification(notifyStoppedType, participant, event.data)), + this.breakpointTracker.onContinued(event => this.messenger.sendNotification(notifyContinuedType, participant, event.data)), + panel.onDidChangeViewState(view => { + if (view.webviewPanel.visible) { + this.setBreakpoints(participant); + } + }), ]; panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); } @@ -258,6 +293,13 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.sendNotification(sessionContextChangedType, webviewParticipant, context); } + protected setBreakpoints(webviewParticipant: WebviewIdMessageParticipant): void { + this.messenger.sendNotification(setExperimentalBreakpointType, webviewParticipant, this.breakpointTracker.dataBreakpoints); + if (this.breakpointTracker.stoppedEvent) { + this.messenger.sendNotification(notifyStoppedType, webviewParticipant, this.breakpointTracker.stoppedEvent.data); + } + } + protected getDefaultMemoryDisplaySettings(): MemoryDisplaySettings { const memoryInspectorSettings = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); const bytesPerMau = memoryInspectorSettings.get(manifest.CONFIG_BYTES_PER_MAU, manifest.DEFAULT_BYTES_PER_MAU); @@ -333,6 +375,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { protected async ready(participant: WebviewIdMessageParticipant, panel: vscode.WebviewPanel): Promise { this.setSession(participant, vscode.debug.activeDebugSession?.id); this.setSessions(participant, this.sessionTracker.getSessions()); + this.setBreakpoints(participant); await this.setMemoryDisplaySettings(participant, panel.title); } @@ -380,6 +423,73 @@ 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) { + vscode.window.showErrorMessage(`DataBreakpointInfo returned for variable ${ctx.variable.name} an invalid info: ${info.description}`); + return; + } + dataId = info.dataId; + } else { + vscode.window.showErrorMessage(`WebviewContext needs to be a Group or Variable context. It was: ${JSON.stringify(ctx, undefined, 2)}`); + return; + } + + // 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 a0233ca..b697e9b 100644 --- a/src/plugin/session-tracker.ts +++ b/src/plugin/session-tracker.ts @@ -13,11 +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 { isDebugEvent, isDebugRequest, isDebugRequestType, isDebugResponse, isDebugResponseType } from '../common/debug-requests'; import { WrittenMemory } from '../common/memory-range'; -import type { Session } from '../common/messaging'; +import type { ContinuedEvent, Session, StoppedEvent } from '../common/messaging'; export interface SessionInfo { raw: vscode.DebugSession; @@ -27,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; @@ -46,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 SessionsChangedEvent extends SessionEvent { @@ -80,6 +92,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( @@ -120,6 +136,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 }); this.fireSessionEvent(session, 'sessions-changed', undefined); @@ -134,6 +158,10 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { 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 { @@ -141,13 +169,17 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { 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); + } } public getSessions(): Session[] { @@ -155,6 +187,11 @@ export class SessionTracker implements vscode.DebugAdapterTrackerFactory { .map(info => ({ id: info.raw.id, name: info.raw.name })); } + getActiveSession(): vscode.DebugSession | undefined { + return Array.from(this._sessionInfo.values()) + .find(info => info.active)?.raw; + } + validSession(sessionId: string | undefined): boolean { return !!sessionId && this._sessionInfo.has(sessionId); } diff --git a/src/webview/breakpoints/breakpoint-decorator.ts b/src/webview/breakpoints/breakpoint-decorator.ts new file mode 100644 index 0000000..ac8b3dc --- /dev/null +++ b/src/webview/breakpoints/breakpoint-decorator.ts @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (C) 2025 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 { BigIntVariableRange, toHexStringWithRadixMarker } from '../../common/memory-range'; +import { DataColumnRenderGroup } from '../columns/data-column'; +import { MemoryRowData } from '../components/memory-table'; +import { Decorator } from '../decorations/decoration-service'; +import { EventEmitter, IEvent } from '../utils/events'; +import { Decoration, LocatedDecoration } from '../utils/view-types'; +import { BreakpointMetadata, BreakpointService, breakpointService } from './breakpoint-service'; + +export class BreakpointDecorator implements Decorator { + readonly id = 'breakpoints'; + + protected onDidChangeEmitter = new EventEmitter(); + get onDidChange(): IEvent { return this.onDidChangeEmitter.event; } + + decorateFor?(_location: string, context: unknown): LocatedDecoration | undefined { + if (DataColumnRenderGroup.is(context)) { + const breakpoint = breakpointService.metadata(toHexStringWithRadixMarker(context.startAddress)); + return { classNames: BreakpointService.inlineClasses(breakpoint) }; + } else if (BigIntVariableRange.is(context)) { + const breakpoint = breakpointService.metadata(context.name); + return { classNames: BreakpointService.inlineClasses(breakpoint) }; + } else if (MemoryRowData.is(context)) { + const breakpointMetadata = breakpointService.inRange(context) + .map(bp => breakpointService.metadata(bp)) + .filter((bp): bp is BreakpointMetadata => bp !== undefined); + const statusClasses = BreakpointService.statusClasses(breakpointMetadata); + return { classNames: statusClasses }; + } + + return undefined; + } +} diff --git a/src/webview/breakpoints/breakpoint-service.ts b/src/webview/breakpoints/breakpoint-service.ts new file mode 100644 index 0000000..d853fc4 --- /dev/null +++ b/src/webview/breakpoints/breakpoint-service.ts @@ -0,0 +1,195 @@ +/******************************************************************************** + * 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 { ExperimentalDataBreakpoint, ExperimentalDataBreakpoints, TrackedBreakpointType } from '../../common/breakpoint'; +import { BigIntMemoryRange, BigIntVariableRange, doOverlap, isWithin } from '../../common/memory-range'; +import { getVariablesType, notifyContinuedType, notifyStoppedType, setExperimentalBreakpointType, 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: ExperimentalDataBreakpoints = { external: [], internal: [] }; + protected _stoppedEvent?: StoppedEvent; + + protected variables: BigIntVariableRange[] = []; + + protected _onDidChange = new EventEmitter(); + readonly onDidChange = this._onDidChange.event; + + get breakpoints(): ExperimentalDataBreakpoints { + return this._breakpoints; + } + + get allBreakpoints(): ExperimentalDataBreakpoint[] { + return [...this.breakpoints.external, ...this.breakpoints.internal]; + } + + get stoppedEvent(): StoppedEvent | undefined { + return this._stoppedEvent; + } + + activate(): void { + messenger.onNotification(setExperimentalBreakpointType, 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): ExperimentalDataBreakpoint | undefined { + return [...this.breakpoints.external, ...this.breakpoints.internal].find(bp => this.normalizeDataId(bp.breakpoint.dataId) === this.normalizeDataId(dataId)); + } + + /** + * Normalize the dataId to remove any non-essential information + */ + protected normalizeDataId(dataId: string): string { + let normalizeDataId = dataId; + + // Remove the module name if it exists + if (normalizeDataId.includes('::')) { + normalizeDataId = normalizeDataId.split('::')[1]; + } + + // Remove array index if it exists + normalizeDataId = normalizeDataId.replace(/\[\d+\]/g, ''); + + return normalizeDataId; + } + + inRange(range: BigIntMemoryRange): ExperimentalDataBreakpoint[] { + 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)); + } + + /** + * Check if the given breakpoint or dataId is hit in the current stopped event + * The event may not provide any `hitBreakpointIds`. + */ + isHit(breakpointOrDataId: ExperimentalDataBreakpoint | 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: ExperimentalDataBreakpoint | 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/breakpoints/breakpoint-vscode-context.ts b/src/webview/breakpoints/breakpoint-vscode-context.ts new file mode 100644 index 0000000..0c926bf --- /dev/null +++ b/src/webview/breakpoints/breakpoint-vscode-context.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (C) 2025 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 { BigIntVariableRange, toHexStringWithRadixMarker } from '../../common/memory-range'; +import { DataColumnRenderGroup } from '../columns/data-column'; +import { JsonMap, VsCodeContextContribution } from '../vscode-context/vscode-context-contribution-service'; +import { breakpointService } from './breakpoint-service'; + +export class BreakpointColumnVscodeContextContribution implements VsCodeContextContribution { + readonly id = 'breakpoints'; + + contribute(_location: string, context: unknown): JsonMap | undefined { + if (DataColumnRenderGroup.is(context)) { + const breakpoint = breakpointService.metadata(toHexStringWithRadixMarker(context.startAddress)); + return { breakpoint: { ...breakpoint, isBreakable: true } }; + } else if (BigIntVariableRange.is(context)) { + const breakpoint = breakpointService.metadata(context.name); + return { breakpoint: { ...breakpoint, isBreakable: true } }; + } + + return undefined; + + } +} diff --git a/src/webview/columns/address-column.tsx b/src/webview/columns/address-column.tsx index 54d8d34..8b9b55b 100644 --- a/src/webview/columns/address-column.tsx +++ b/src/webview/columns/address-column.tsx @@ -14,16 +14,20 @@ * 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 { getAddressString, getRadixMarker } from '../../common/memory-range'; import { MemoryRowData } from '../components/memory-table'; +import { decorationService } from '../decorations/decoration-service'; import { ColumnContribution, ColumnFittingType, ColumnRenderProps } from './column-contribution-service'; import { createDefaultSelection, groupAttributes, SelectionProps } from './table-group'; 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; @@ -35,8 +39,12 @@ export class AddressColumn implements ColumnContribution { getSelection: () => config.selection, setSelection: config.setSelection }; + + const decoration = decorationService.getDecorationFor(AddressColumn.ID, row); + const groupProps = groupAttributes({ columnIndex, rowIndex: row.rowIndex, groupIndex: 0, maxGroupIndex: 0 }, selectionProps); return + {decoration && decoration.classNames.length > 0 && } {config.tableConfig.showRadixPrefix && {getRadixMarker(config.tableConfig.addressRadix)}} {getAddressString(row.startAddress, config.tableConfig.addressRadix, config.tableConfig.effectiveAddressLength)} ; diff --git a/src/webview/columns/data-column-vscode-context.ts b/src/webview/columns/data-column-vscode-context.ts new file mode 100644 index 0000000..96b3baa --- /dev/null +++ b/src/webview/columns/data-column-vscode-context.ts @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (C) 2025 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 { JsonMap, VsCodeContextContribution } from '../vscode-context/vscode-context-contribution-service'; +import { DataColumnRenderGroup } from './data-column'; + +export class DataColumnVscodeContextContribution implements VsCodeContextContribution { + readonly id = 'data-column'; + + contribute(_location: string, context: unknown): JsonMap | undefined { + if (DataColumnRenderGroup.is(context)) { + return ({ memoryData: { group: { startAddress: context.startAddress, length: context.length } } }); + } + + return undefined; + + } +} diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx index 5ee3d15..7e426bc 100644 --- a/src/webview/columns/data-column.tsx +++ b/src/webview/columns/data-column.tsx @@ -25,9 +25,9 @@ import { writeMemoryType } from '../../common/messaging'; import type { MemoryRowData, MemorySizeOptions, MemoryTableSelection, MemoryTableState } from '../components/memory-table'; import { decorationService } from '../decorations/decoration-service'; import { Disposable, FullNodeAttributes } from '../utils/view-types'; -import { createGroupVscodeContext } from '../utils/vscode-contexts'; import { characterWidthInContainer, elementInnerWidth, hasCtrlCmdMask } from '../utils/window'; import { messenger } from '../view-messenger'; +import { vsCodeContextContributionService } from '../vscode-context/vscode-context-contribution-service'; import { AddressColumn } from './address-column'; import { ColumnContribution, ColumnRenderProps } from './column-contribution-service'; import { @@ -45,6 +45,19 @@ export interface DataColumnSelection extends MemoryTableSelection { editingRange?: BigIntMemoryRange; } +export interface DataColumnRenderGroup { + startAddress: bigint; + length: number; +} + +export namespace DataColumnRenderGroup { + export function is(value: unknown): value is DataColumnRenderGroup { + return !!value + && typeof (value as DataColumnRenderGroup).startAddress === 'bigint' + && typeof (value as DataColumnRenderGroup).length === 'number'; + } +} + export namespace DataColumnSelection { export function is(selection?: MemoryTableSelection): selection is DataColumnSelection { return !!selection && 'selectedRange' in selection; @@ -155,9 +168,14 @@ export class EditableDataColumnRow extends React.Component {maus} ; diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index 3466203..7c98ac5 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -33,7 +33,7 @@ import { ColumnRenderProps, TableRenderOptions } from '../columns/column-contrib import { DataColumn } from '../columns/data-column'; import type { HoverService } from '../hovers/hover-service'; import { Decoration, isTrigger } from '../utils/view-types'; -import { createColumnVscodeContext, createSectionVscodeContext } from '../utils/vscode-contexts'; +import { createColumnVscodeContext, createSectionVscodeContext } from '../vscode-context/vscode-contexts'; export interface MoreMemorySelectProps { activeReadArguments: Required; @@ -153,6 +153,14 @@ export interface MemoryRowData { endAddress: bigint; } +export namespace MemoryRowData { + export function is(value: unknown): value is MemoryRowData { + return typeof value === 'object' && typeof (value as MemoryRowData).rowIndex === 'number' + && typeof (value as MemoryRowData).startAddress === 'bigint' + && typeof (value as MemoryRowData).endAddress === 'bigint'; + } +} + export interface MemoryTableSelection { column: { columnIndex: number; diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index 952af30..06c0958 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -24,7 +24,7 @@ import { MemoryDataDisplaySettings } from '../../common/webview-configuration'; import { ColumnStatus } from '../columns/column-contribution-service'; import { HoverService } from '../hovers/hover-service'; import { Decoration, MemoryState } from '../utils/view-types'; -import { createAppVscodeContext, VscodeContext } from '../utils/vscode-contexts'; +import { createAppVscodeContext, VscodeContext } from '../vscode-context/vscode-contexts'; import { MemoryTable } from './memory-table'; import { OptionsWidget } from './options-widget'; diff --git a/src/webview/components/options-widget.tsx b/src/webview/components/options-widget.tsx index 994a827..0738e51 100644 --- a/src/webview/components/options-widget.tsx +++ b/src/webview/components/options-widget.tsx @@ -31,7 +31,7 @@ import { tryToNumber } from '../../common/typescript'; import { TableRenderOptions } from '../columns/column-contribution-service'; import { DEFAULT_MEMORY_DISPLAY_CONFIGURATION } from '../memory-webview-view'; import { AddressPaddingOptions, DEFAULT_READ_ARGUMENTS, MemoryState, SerializedTableRenderOptions } from '../utils/view-types'; -import { createOverlayMoreActionsVscodeContext, createSectionVscodeContext } from '../utils/vscode-contexts'; +import { createOverlayMoreActionsVscodeContext, createSectionVscodeContext } from '../vscode-context/vscode-contexts'; import { MultiSelectBar } from './multi-select'; export interface OptionsWidgetProps diff --git a/src/webview/decorations/decoration-service.ts b/src/webview/decorations/decoration-service.ts index 101464b..064f97e 100644 --- a/src/webview/decorations/decoration-service.ts +++ b/src/webview/decorations/decoration-service.ts @@ -16,11 +16,13 @@ import { compareBigInt, determineRelationship, isWithin, RangeRelationship } from '../../common/memory-range'; import { EventEmitter, IEvent } from '../utils/events'; -import { areDecorationsEqual, Decoration, Disposable, UpdateExecutor } from '../utils/view-types'; +import { areDecorationsEqual, Decoration, Disposable, LocatedDecoration, UpdateExecutor } from '../utils/view-types'; export interface Decorator extends Partial { readonly id: string; readonly onDidChange: IEvent; + + decorateFor?(location: string, context: unknown): LocatedDecoration | undefined; } class DecorationService { @@ -101,6 +103,7 @@ class DecorationService { protected currentDecorationIndex = 0; protected lastCall?: bigint; + getDecoration(address: bigint): Decoration | undefined { if (this.currentDecorations.length === 0) { return undefined; } if (this.lastCall === undefined || address < this.lastCall) { this.currentDecorationIndex = 0; } @@ -112,6 +115,20 @@ class DecorationService { return isWithin(address, this.currentDecorations[this.currentDecorationIndex].range) ? this.currentDecorations[this.currentDecorationIndex] : undefined; } + getDecorationFor(location: string, context: unknown): LocatedDecoration | undefined { + return Array.from(this.decorators.values()) + .map(contribution => contribution.decorateFor?.(location, context)) + .reduce((previous, current) => { + if (previous && current) { + previous.classNames.push(...current.classNames); + } + + return previous; + }, { + classNames: [] + } as LocatedDecoration); + } + getUpdateExecutors(): UpdateExecutor[] { return Array.from(this.decorators.values()).filter((candidate): candidate is Decorator & UpdateExecutor => candidate.fetchData !== undefined); } diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index 6eb6c1f..81f1517 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -46,10 +46,14 @@ import { } from '../common/messaging'; import { Change, hasChanged, hasChangedTo } from '../common/typescript'; import { MemoryDisplaySettings, MemoryViewSettings } from '../common/webview-configuration'; +import { BreakpointDecorator } from './breakpoints/breakpoint-decorator'; +import { breakpointService } from './breakpoints/breakpoint-service'; +import { BreakpointColumnVscodeContextContribution } from './breakpoints/breakpoint-vscode-context'; import { AddressColumn } from './columns/address-column'; import { AsciiColumn } from './columns/ascii-column'; import { columnContributionService, ColumnStatus } from './columns/column-contribution-service'; import { DataColumn } from './columns/data-column'; +import { DataColumnVscodeContextContribution } from './columns/data-column-vscode-context'; import { MemoryWidget } from './components/memory-widget'; import { decorationService } from './decorations/decoration-service'; import { AddressHover } from './hovers/address-hover'; @@ -58,7 +62,9 @@ import { HoverService, hoverService } from './hovers/hover-service'; import { VariableHover } from './hovers/variable-hover'; import { AddressPaddingOptions, Decoration, DEFAULT_READ_ARGUMENTS, MemoryState } from './utils/view-types'; import { variableDecorator } from './variables/variable-decorations'; +import { VariableVscodeContextContribution } from './variables/variable-vscode-context'; import { messenger } from './view-messenger'; +import { vsCodeContextContributionService } from './vscode-context/vscode-context-contribution-service'; export interface MemoryAppState extends MemoryState, MemoryViewSettings { sessions: Session[]; @@ -107,9 +113,13 @@ class App extends React.Component<{}, MemoryAppState> { columnContributionService.register(variableDecorator); columnContributionService.register(new AsciiColumn()); decorationService.register(variableDecorator); + decorationService.register(new BreakpointDecorator()); hoverService.register(new AddressHover()); hoverService.register(new DataHover()); hoverService.register(new VariableHover()); + vsCodeContextContributionService.register(new VariableVscodeContextContribution()); + vsCodeContextContributionService.register(new DataColumnVscodeContextContribution()); + vsCodeContextContributionService.register(new BreakpointColumnVscodeContextContribution()); this.state = { messageParticipant: { type: 'webview', webviewId: '' }, title: 'Memory', @@ -149,6 +159,9 @@ 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(); } @@ -320,7 +333,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/view-types.ts b/src/webview/utils/view-types.ts index 27aab28..46481d6 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -41,6 +41,14 @@ export interface Decoration { classNames: string[]; } +/** + * A decoration that is located in a specific location in the view + * instead of being associated with a specific range. + */ +export interface LocatedDecoration { + classNames: string[]; +} + export function areDecorationsEqual(one: Decoration, other: Decoration): boolean { return areRangesEqual(one.range, other.range) && deepequal(one.style, other.style); } diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index b15cfcf..d413c5e 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -14,8 +14,9 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { ReactNode } from 'react'; +import { classNames } from 'primereact/utils'; import * as React from 'react'; +import { ReactNode } from 'react'; import { HOST_EXTENSION } from 'vscode-messenger-common'; import * as manifest from '../../common/manifest'; import { areVariablesEqual, BigIntMemoryRange, BigIntVariableRange, compareBigInt, doOverlap } from '../../common/memory-range'; @@ -24,11 +25,11 @@ import { stringifyWithBigInts } from '../../common/typescript'; import { ColumnContribution, ColumnRenderProps } from '../columns/column-contribution-service'; import { createDefaultSelection, groupAttributes, SelectionProps } from '../columns/table-group'; import { MemoryRowData } from '../components/memory-table'; -import { Decorator } from '../decorations/decoration-service'; +import { decorationService, Decorator } from '../decorations/decoration-service'; import { EventEmitter, IEvent } from '../utils/events'; import { Decoration, MemoryState } from '../utils/view-types'; -import { createVariableVscodeContext } from '../utils/vscode-contexts'; import { messenger } from '../view-messenger'; +import { vsCodeContextContributionService } from '../vscode-context/vscode-context-contribution-service'; const NON_HC_COLORS = [ 'var(--vscode-terminal-ansiBlue)', @@ -90,13 +91,15 @@ export class VariableDecorator implements ColumnContribution, Decorator { const variables = this.getVariablesInRange(row); return variables?.reduce((result, current, index) => { if (index > 0) { result.push(', '); } + const decorations = decorationService.getDecorationFor(VariableDecorator.ID, current.variable); + result.push(React.createElement('span', { style: { color: current.color }, key: current.variable.name, - className: 'hoverable', + className: classNames('hoverable', ...decorations?.classNames ?? []), 'data-column': 'variables', 'data-variables': stringifyWithBigInts(current.variable), - ...createVariableVscodeContext(current.variable), + ...vsCodeContextContributionService.createContext(VariableDecorator.ID, current.variable), ...groupAttributes({ columnIndex, rowIndex: row.rowIndex, groupIndex: index, maxGroupIndex: variables.length - 1 }, selectionProps) }, current.variable.name)); return result; diff --git a/src/webview/variables/variable-vscode-context.ts b/src/webview/variables/variable-vscode-context.ts new file mode 100644 index 0000000..19f17b5 --- /dev/null +++ b/src/webview/variables/variable-vscode-context.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (C) 2025 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 { BigIntVariableRange } from '../../common/memory-range'; +import { JsonMap, VsCodeContextContribution } from '../vscode-context/vscode-context-contribution-service'; + +export class VariableVscodeContextContribution implements VsCodeContextContribution { + readonly id = 'variable'; + + contribute(_location: string, context: unknown): JsonMap | undefined { + if (BigIntVariableRange.is(context)) { + const { name, type, value, parentVariablesReference, isPointer } = context; + return { + variable: { name, type, value, parentVariablesReference, isPointer } + }; + + } + + return undefined; + + } +} diff --git a/src/webview/vscode-context/vscode-context-contribution-service.ts b/src/webview/vscode-context/vscode-context-contribution-service.ts new file mode 100644 index 0000000..dcef390 --- /dev/null +++ b/src/webview/vscode-context/vscode-context-contribution-service.ts @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (C) 2025 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 { createVscodeContext, VscodeContext } from './vscode-contexts'; + +export type JsonArray = JsonAny[]; +export type JsonAny = JsonPrimitive | JsonMap | JsonArray | bigint | null | undefined; +export type JsonPrimitive = string | number | boolean; +export interface JsonMap { + [key: string]: JsonAny +} + +export interface VsCodeContextContribution { + readonly id: string; + contribute(location: string, context: unknown): JsonMap | undefined +} + +export class VsCodeContextContributionService { + private readonly contributions = new Map(); + + register(contribution: VsCodeContextContribution): void { + this.contributions.set(contribution.id, contribution); + } + + unregister(id: string): void { + this.contributions.delete(id); + } + + createContext(location: string, context: unknown): VscodeContext { + const contributions = Array.from(this.contributions.values()).map(contribution => contribution.contribute(location, context)); + const vsCodeContext = Object.assign({}, ...contributions); + + return createVscodeContext(vsCodeContext); + } +} + +export const vsCodeContextContributionService = new VsCodeContextContributionService(); diff --git a/src/webview/utils/vscode-contexts.ts b/src/webview/vscode-context/vscode-contexts.ts similarity index 86% rename from src/webview/utils/vscode-contexts.ts rename to src/webview/vscode-context/vscode-contexts.ts index 9ea3206..b7aed86 100644 --- a/src/webview/utils/vscode-contexts.ts +++ b/src/webview/vscode-context/vscode-contexts.ts @@ -14,7 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { BigIntVariableRange } from '../../common/memory-range'; import { WebviewContext } from '../../common/webview-context'; /** * Custom data property used by VSCode to provide additional context info when opening a webview context menu @@ -64,15 +63,6 @@ export function createAppVscodeContext(context: Omit