diff --git a/media/options-widget.css b/media/options-widget.css index 5275d33..73538a7 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -35,10 +35,11 @@ flex-grow: 1; } +.memory-options-widget .p-button { + align-self: end; +} + .memory-options-widget .edit-label-toggle { - position: absolute; - right: 24px; - top: 8px; opacity: 0; transition: opacity 0.2s; } diff --git a/package.json b/package.json index 98525a6..d9d0453 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,15 @@ "formik": "^2.4.5", "lodash": "^4.17.21", "memoize-one": "^6.0.0", + "nrf-intel-hex": "^1.4.0", "primeflex": "^3.3.1", "primereact": "^10.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "vscode-messenger": "^0.4.3", "vscode-messenger-common": "^0.4.3", - "vscode-messenger-webview": "^0.4.3" + "vscode-messenger-webview": "^0.4.3", + "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/lodash": "^4.14.202", @@ -107,6 +109,18 @@ "title": "Advanced Display Options", "category": "Memory", "enablement": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.store-file", + "title": "Store Memory to File", + "enablement": "memory-inspector.canRead", + "category": "Memory" + }, + { + "command": "memory-inspector.apply-file", + "title": "Apply Memory from File", + "enablement": "memory-inspector.canWrite", + "category": "Memory" } ], "menus": { @@ -118,12 +132,22 @@ { "command": "memory-inspector.show-variable", "when": "false" + }, + { + "command": "memory-inspector.store-file" + }, + { + "command": "memory-inspector.apply-file" } ], "debug/variables/context": [ { "command": "memory-inspector.show-variable", "when": "canViewMemory && memory-inspector.canRead" + }, + { + "command": "memory-inspector.store-file", + "when": "canViewMemory && memory-inspector.canRead" } ], "view/item/context": [ @@ -132,6 +156,20 @@ "when": "canViewMemory && memory-inspector.canRead" } ], + "explorer/context": [ + { + "command": "memory-inspector.apply-file", + "group": "debug", + "when": "memory-inspector.canWrite && resourceExtname === .hex" + } + ], + "editor/context": [ + { + "command": "memory-inspector.apply-file", + "group": "debug", + "when": "memory-inspector.canWrite && resourceExtname === .hex" + } + ], "webview/context": [ { "command": "memory-inspector.toggle-variables-column", @@ -152,6 +190,16 @@ "command": "memory-inspector.show-advanced-display-options", "group": "display@4", "when": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.store-file", + "group": "display@5", + "when": "webviewId === memory-inspector.memory" + }, + { + "command": "memory-inspector.apply-file", + "group": "display@6", + "when": "webviewId === memory-inspector.memory" } ] }, diff --git a/src/common/debug-requests.ts b/src/common/debug-requests.ts new file mode 100644 index 0000000..dd3d78a --- /dev/null +++ b/src/common/debug-requests.ts @@ -0,0 +1,72 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +// inspired by https://github.com/eclipse-theia/theia/blob/master/packages/debug/src/browser/debug-session-connection.ts + +import type { DebugProtocol } from '@vscode/debugprotocol'; +import type { DebugSession } from 'vscode'; + +export interface DebugRequestTypes { + 'evaluate': [DebugProtocol.EvaluateArguments, DebugProtocol.EvaluateResponse['body']] + 'initialize': [DebugProtocol.InitializeRequestArguments, DebugProtocol.InitializeResponse['body']] + 'readMemory': [DebugProtocol.ReadMemoryArguments, DebugProtocol.ReadMemoryResponse['body']] + 'scopes': [DebugProtocol.ScopesArguments, DebugProtocol.ScopesResponse['body']] + 'variables': [DebugProtocol.VariablesArguments, DebugProtocol.VariablesResponse['body']] + 'writeMemory': [DebugProtocol.WriteMemoryArguments, DebugProtocol.WriteMemoryResponse['body']] +} + +export interface DebugEvents { + 'memory': DebugProtocol.MemoryEvent, + 'stopped': DebugProtocol.StoppedEvent +} + +export type DebugRequest = Omit & { command: C, arguments: A }; +export type DebugResponse = Omit & { command: C, body: B }; +export type DebugEvent = DebugProtocol.Event & { body: T }; + +export async function sendRequest(session: DebugSession, + command: K, args: DebugRequestTypes[K][0]): Promise { + return session.customRequest(command, args); +} + +export function isDebugVariable(variable: DebugProtocol.Variable | unknown): variable is DebugProtocol.Variable { + const assumed = variable ? variable as DebugProtocol.Variable : undefined; + return typeof assumed?.name === 'string' && typeof assumed?.value === 'string'; +} + +export function isDebugScope(scope: DebugProtocol.Scope | unknown): scope is DebugProtocol.Scope { + const assumed = scope ? scope as DebugProtocol.Scope : undefined; + return typeof assumed?.name === 'string' && typeof assumed?.variablesReference === 'number'; +} + +export function isDebugEvaluateArguments(args: DebugProtocol.EvaluateArguments | unknown): args is DebugProtocol.EvaluateArguments { + const assumed = args ? args as DebugProtocol.EvaluateArguments : undefined; + return typeof assumed?.expression === 'string'; +} + +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; +} + +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; +} + +export function isDebugEvent(event: K, message: unknown): message is DebugEvents[K] { + const assumed = message ? message as DebugProtocol.Event : undefined; + return !!assumed && assumed.type === 'event' && assumed.event === event; +} diff --git a/src/common/intel-hex.ts b/src/common/intel-hex.ts new file mode 100644 index 0000000..3ea4a45 --- /dev/null +++ b/src/common/intel-hex.ts @@ -0,0 +1,43 @@ +/******************************************************************************** + * 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 { URI, Utils } from 'vscode-uri'; + +export namespace IntelHEX { + export namespace FileExtensions { + export const All = [ + // General + 'hex', 'mcs', 'int', 'ihex', 'ihe', 'ihx', + // Platform-specific + 'h80', 'h86', 'a43', 'a90', + // Binary or Intel hex + 'obj', 'obl', 'obh', 'rom', 'eep' + ]; + export const Default = 'hex'; + + export function applyIfMissing(file: URI): URI { + const extWithDot = Utils.extname(file); + if (extWithDot.length === 0 || !IntelHEX.FileExtensions.All.includes(extWithDot.slice(1))) { + return URI.file(file.fsPath + '.' + IntelHEX.FileExtensions.Default); + } + return file; + }; + }; + export const DialogFilters = { + 'Intel HEX Files': IntelHEX.FileExtensions.All, + 'All Files': ['*'] + }; +}; diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 8205ede..4d8d97f 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -27,6 +27,12 @@ export interface MemoryRange { endAddress?: string; } +export interface WrittenMemory { + memoryReference: string; + offset?: number; + count?: number +} + /** Suitable for arithemetic */ export interface BigIntMemoryRange { startAddress: bigint; @@ -85,7 +91,7 @@ export function getRadixMarker(radix: Radix): string { return radixPrefixMap[radix]; } -export function getAddressString(address: bigint, radix: Radix, paddedLength: number = 0): string { +export function getAddressString(address: bigint | number, radix: Radix, paddedLength: number = 0): string { return address.toString(radix).padStart(paddedLength, '0'); } @@ -93,8 +99,8 @@ export function getAddressLength(padding: number, radix: Radix): number { return Math.ceil(padding / Math.log2(radix)); } -export function toHexStringWithRadixMarker(target: bigint): string { - return `${getRadixMarker(Radix.Hexadecimal)}${getAddressString(target, Radix.Hexadecimal)}`; +export function toHexStringWithRadixMarker(target: bigint | number, paddedLength: number = 0): string { + return `${getRadixMarker(Radix.Hexadecimal)}${getAddressString(target, Radix.Hexadecimal, paddedLength)}`; } export interface VariableMetadata { diff --git a/src/common/memory.ts b/src/common/memory.ts new file mode 100644 index 0000000..8101555 --- /dev/null +++ b/src/common/memory.ts @@ -0,0 +1,61 @@ +/******************************************************************************** + * 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 { ReadMemoryArguments, ReadMemoryResult } from './messaging'; + +export interface Memory { + address: bigint; + bytes: Uint8Array; +} + +export function createMemoryFromRead(result: ReadMemoryResult, request?: ReadMemoryArguments): Memory { + if (!result?.data) { + const message = request ? `No memory provided for address ${request.memoryReference}` + + `, offset ${request.offset} and count ${request.count}!` : 'No memory provided.'; + throw new Error(message); + } + const address = BigInt(result.address); + const bytes = stringToBytesMemory(result.data); + return { bytes, address }; +} + +export function stringToBytesMemory(data: string): Uint8Array { + return Uint8Array.from(Buffer.from(data, 'base64')); +} + +export function bytesToStringMemory(data: Uint8Array): string { + return Buffer.from(data).toString('base64'); +} + +export function validateMemoryReference(reference: string): string | undefined { + const asNumber = Number(reference); + // we allow an address that is not a number, e.g., an expression, but if it is a number it must be >= 0 + return !isNaN(asNumber) && asNumber < 0 ? 'Value must be >= 0' : undefined; +} + +export function validateOffset(offset: string): string | undefined { + const asNumber = Number(offset); + return isNaN(asNumber) ? 'Must be number' : undefined; +} + +export function validateCount(count: string): string | undefined { + const asNumber = Number(count); + if (isNaN(asNumber)) { + return 'Must be number'; + } else if (asNumber <= 0) { + return 'Value must be > 0'; + } +} diff --git a/src/common/messaging.ts b/src/common/messaging.ts index 9ac79eb..0fe2e43 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -17,20 +17,49 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import type { NotificationType, RequestType } from 'vscode-messenger-common'; import { MemoryViewSettings } from '../webview/utils/view-types'; -import type { VariableRange } from './memory-range'; +import type { VariableRange, WrittenMemory } from './memory-range'; +import { DebugRequestTypes } from './debug-requests'; +import { URI } from 'vscode-uri'; +import { VariablesView } from '../plugin/external-views'; +import { WebviewContext } from './webview-context'; -export type MemoryReadResult = DebugProtocol.ReadMemoryResponse['body']; -export type MemoryWriteResult = DebugProtocol.WriteMemoryResponse['body']; +// convenience types for easier readability and better semantics +export type MemoryOptions = Partial; +export type ReadMemoryArguments = DebugRequestTypes['readMemory'][0]; +export type ReadMemoryResult = DebugRequestTypes['readMemory'][1]; + +export type WriteMemoryArguments = DebugRequestTypes['writeMemory'][0]; +export type WriteMemoryResult = DebugRequestTypes['writeMemory'][1]; + +export type StoreMemoryArguments = MemoryOptions & { proposedOutputName?: string } | VariablesView.IVariablesContext | WebviewContext; +export type StoreMemoryResult = void; + +export type ApplyMemoryArguments = URI | undefined; +export type ApplyMemoryResult = MemoryOptions; + +export interface SessionContext { + sessionId?: string; + canRead: boolean; + canWrite: boolean; +} + +// Notifications export const readyType: NotificationType = { method: 'ready' }; -export const logMessageType: RequestType = { method: 'logMessage' }; export const setMemoryViewSettingsType: NotificationType> = { method: 'setMemoryViewSettings' }; export const resetMemoryViewSettingsType: NotificationType = { method: 'resetMemoryViewSettings' }; export const setTitleType: NotificationType = { method: 'setTitle' }; -export const setOptionsType: RequestType, void> = { method: 'setOptions' }; -export const readMemoryType: RequestType = { method: 'readMemory' }; -export const writeMemoryType: RequestType = { method: 'writeMemory' }; -export const getVariables: RequestType = { method: 'getVariables' }; +export const memoryWrittenType: NotificationType = { method: 'memoryWritten' }; +export const sessionContextChangedType: NotificationType = { method: 'sessionContextChanged' }; + +// Requests +export const setOptionsType: RequestType = { method: 'setOptions' }; +export const logMessageType: RequestType = { method: 'logMessage' }; +export const readMemoryType: RequestType = { method: 'readMemory' }; +export const writeMemoryType: RequestType = { method: 'writeMemory' }; +export const getVariablesType: RequestType = { method: 'getVariables' }; +export const storeMemoryType: RequestType = { method: 'storeMemory' }; +export const applyMemoryType: RequestType = { method: 'applyMemory' }; export const showAdvancedOptionsType: NotificationType = { method: 'showAdvancedOptions' }; export const getWebviewSelectionType: RequestType = { method: 'getWebviewSelection' }; diff --git a/src/common/typescript.ts b/src/common/typescript.ts index e1bef53..d8ef137 100644 --- a/src/common/typescript.ts +++ b/src/common/typescript.ts @@ -19,3 +19,8 @@ export function tryToNumber(value?: string | number): number | undefined { if (value === '' || isNaN(asNumber)) { return undefined; } return asNumber; } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function stringifyWithBigInts(object: any, space?: string | number): any { + return JSON.stringify(object, (_key, value) => typeof value === 'bigint' ? value.toString() : value, space); +} diff --git a/src/common/webview-context.ts b/src/common/webview-context.ts index 044c442..264b11a 100644 --- a/src/common/webview-context.ts +++ b/src/common/webview-context.ts @@ -16,6 +16,7 @@ import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; import { VariableMetadata } from './memory-range'; +import { ReadMemoryArguments } from './messaging'; export interface WebviewContext { messageParticipant: WebviewIdMessageParticipant, @@ -23,6 +24,7 @@ export interface WebviewContext { showAsciiColumn: boolean showVariablesColumn: boolean, showRadixPrefix: boolean, + activeReadArguments: Required } export interface WebviewCellContext extends WebviewContext { @@ -48,3 +50,11 @@ export function getVisibleColumns(context: WebviewContext): string[] { } return columns; } + +export function isWebviewContext(args: WebviewContext | unknown): args is WebviewContext { + const assumed = args ? args as WebviewContext : undefined; + return typeof assumed?.messageParticipant?.type === 'string' && assumed.messageParticipant.type === 'webview' && typeof assumed.messageParticipant.webviewId === 'string' + && typeof assumed.webviewSection === 'string' && typeof assumed.showAsciiColumn === 'boolean' && typeof assumed.showVariablesColumn === 'boolean' + && typeof assumed.showRadixPrefix === 'boolean' && typeof assumed.activeReadArguments?.count === 'number' && typeof assumed.activeReadArguments?.offset === 'number' + && typeof assumed.activeReadArguments?.memoryReference === 'string'; +} diff --git a/src/entry-points/browser/extension.ts b/src/entry-points/browser/extension.ts index 64c62e7..da20df5 100644 --- a/src/entry-points/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -19,16 +19,19 @@ import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry' import { MemoryProvider } from '../../plugin/memory-provider'; import { MemoryWebview } from '../../plugin/memory-webview-main'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; +import { MemoryStorage } from '../../plugin/memory-storage'; export const activate = async (context: vscode.ExtensionContext): Promise => { const registry = new AdapterRegistry(); const memoryProvider = new MemoryProvider(registry); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); + const memoryStorage = new MemoryStorage(memoryProvider); const cAdapter = new CAdapter(registry); registry.activate(context); memoryProvider.activate(context); memoryView.activate(context); + memoryStorage.activate(context); cAdapter.activate(context); return registry; diff --git a/src/entry-points/desktop/extension.ts b/src/entry-points/desktop/extension.ts index d4cb368..52c6a12 100644 --- a/src/entry-points/desktop/extension.ts +++ b/src/entry-points/desktop/extension.ts @@ -19,16 +19,19 @@ import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry' import { MemoryProvider } from '../../plugin/memory-provider'; import { MemoryWebview } from '../../plugin/memory-webview-main'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; +import { MemoryStorage } from '../../plugin/memory-storage'; export const activate = async (context: vscode.ExtensionContext): Promise => { const registry = new AdapterRegistry(); const memoryProvider = new MemoryProvider(registry); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); + const memoryStorage = new MemoryStorage(memoryProvider); const cAdapter = new CAdapter(registry); memoryProvider.activate(context); registry.activate(context); memoryView.activate(context); + memoryStorage.activate(context); cAdapter.activate(context); return registry; diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index b07beac..63bc225 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -18,6 +18,7 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; import { VariableRange } from '../../common/memory-range'; import { Logger } from '../logger'; +import { isDebugRequest, isDebugResponse } from '../../common/debug-requests'; /** Represents capabilities that may be achieved with particular debug adapters but are not part of the DAP */ export interface AdapterCapabilities { @@ -25,14 +26,32 @@ export interface AdapterCapabilities { getVariables?(session: vscode.DebugSession): Promise; /** Resolve symbols resident in the memory at the specified range. Will be preferred to {@link getVariables} if present. */ getResidents?(session: vscode.DebugSession, params: DebugProtocol.ReadMemoryArguments): Promise; + /** Resolves the address of a given variable in bytes withthe current context. */ + getAddressOfVariable?(session: vscode.DebugSession, variableName: string): Promise; + /** Resolves the size of a given variable in bytes within the current context. */ + getSizeOfVariable?(session: vscode.DebugSession, variableName: string): Promise; initializeAdapterTracker?(session: vscode.DebugSession): vscode.DebugAdapterTracker | undefined; } export type WithChildren = Original & { children?: Array> }; export type VariablesTree = Record>; export const hexAddress = /0x[0-9a-f]+/i; +export const decimalAddress = /[0-9]+/i; export const notADigit = /[^0-9]/; +export function extractHexAddress(text?: string): string | undefined { + return text ? hexAddress.exec(text)?.[0] : undefined; +} + +export function extractDecimalAddress(text?: string): string | undefined { + return text ? decimalAddress.exec(text)?.[0] : undefined; +} + +export function extractAddress(text?: string): string | undefined { + // search for hex address first as a hex adress (0x12345678) also matches an integer address (12345678) + return text ? extractHexAddress(text) ?? extractDecimalAddress(text) : undefined; +} + /** This class implements some of the basic elements of tracking adapter sessions in order to maintain a list of variables. */ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { protected currentFrame?: number; @@ -42,9 +61,9 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { constructor(protected readonly onEnd: vscode.Disposable, protected logger: Logger) { } onWillReceiveMessage(message: unknown): void { - if (isScopesRequest(message)) { + if (isDebugRequest('scopes', message)) { this.currentFrame = message.arguments.frameId; - } else if (isVariableRequest(message)) { + } else if (isDebugRequest('variables', message)) { if (message.arguments.variablesReference in this.variablesTree) { this.pendingMessages.set(message.seq, message.arguments.variablesReference); } @@ -53,7 +72,7 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { /** Produces a two-level tree of scopes and their immediate children. Does not handle expansion of complex variables. */ onDidSendMessage(message: unknown): void { - if (isScopesResponse(message)) { + if (isDebugResponse('scopes', message)) { this.variablesTree = {}; // Scopes request implies that all scopes will be queried again. for (const scope of message.body.scopes) { if (this.isDesiredScope(scope)) { @@ -62,7 +81,7 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { } } } - } else if (isVariableResponse(message)) { + } else if (isDebugResponse('variables', message)) { if (this.pendingMessages.has(message.request_seq)) { const parentReference = this.pendingMessages.get(message.request_seq)!; this.pendingMessages.delete(message.request_seq); @@ -106,9 +125,15 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { protected variableToVariableRange(_variable: DebugProtocol.Variable, _session: vscode.DebugSession): Promise { throw new Error('To be implemented by derived classes!'); } + + /** Resolves the address of a given variable in bytes within the current context. */ + getAddressOfVariable?(variableName: string, session: vscode.DebugSession): Promise; + + /** Resolves the size of a given variable in bytes within the current context. */ + getSizeOfVariable?(variableName: string, session: vscode.DebugSession): Promise; } -export class VariableTracker { +export class VariableTracker implements AdapterCapabilities { protected sessions = new Map(); protected types: string[]; @@ -127,27 +152,15 @@ export class VariableTracker { } } - getVariables(session: vscode.DebugSession): Promise { - return Promise.resolve(this.sessions.get(session.id)?.getLocals(session) ?? []); + async getVariables(session: vscode.DebugSession): Promise { + return this.sessions.get(session.id)?.getLocals(session) ?? []; } -} - -export function isScopesRequest(message: unknown): message is DebugProtocol.ScopesRequest { - const candidate = message as DebugProtocol.ScopesRequest; - return !!candidate && candidate.command === 'scopes'; -} - -export function isVariableRequest(message: unknown): message is DebugProtocol.VariablesRequest { - const candidate = message as DebugProtocol.VariablesRequest; - return !!candidate && candidate.command === 'variables'; -} -export function isScopesResponse(message: unknown): message is DebugProtocol.ScopesResponse { - const candidate = message as DebugProtocol.ScopesResponse; - return !!candidate && candidate.command === 'scopes' && Array.isArray(candidate.body.scopes); -} + async getAddressOfVariable(session: vscode.DebugSession, variableName: string): Promise { + return this.sessions.get(session.id)?.getAddressOfVariable?.(variableName, session); + } -export function isVariableResponse(message: unknown): message is DebugProtocol.VariablesResponse { - const candidate = message as DebugProtocol.VariablesResponse; - return !!candidate && candidate.command === 'variables' && Array.isArray(candidate.body.variables); + async getSizeOfVariable(session: vscode.DebugSession, variableName: string): Promise { + return this.sessions.get(session.id)?.getSizeOfVariable?.(variableName, session); + } } diff --git a/src/plugin/adapter-registry/c-tracker.ts b/src/plugin/adapter-registry/c-tracker.ts index 2d709e0..a3377d9 100644 --- a/src/plugin/adapter-registry/c-tracker.ts +++ b/src/plugin/adapter-registry/c-tracker.ts @@ -16,10 +16,21 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { AdapterVariableTracker, hexAddress, notADigit } from './adapter-capabilities'; +import { AdapterVariableTracker, extractAddress, notADigit } from './adapter-capabilities'; import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; +import { sendRequest } from '../../common/debug-requests'; + +export namespace CEvaluateExpression { + export function sizeOf(expression: string): string { + return `sizeof(${expression})`; + } + export function addressOf(expression: string): string { + return `&(${expression})`; + } +}; 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. @@ -30,26 +41,39 @@ export class CTracker extends AdapterVariableTracker { { noName: !variable.name, noFrame: this.currentFrame === undefined }); return undefined; } + let variableAddress = extractAddress(variable.memoryReference); + let variableSize: bigint | undefined = undefined; try { - const [addressResponse, sizeResponse] = await Promise.all([ - session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch', frameId: this.currentFrame }), - session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'watch', frameId: this.currentFrame }), - ]) as DebugProtocol.EvaluateResponse['body'][]; - const addressPart = hexAddress.exec(addressResponse.result); - if (!addressPart) { return undefined; } - const startAddress = BigInt(addressPart[0]); - const endAddress = notADigit.test(sizeResponse.result) ? undefined : startAddress + BigInt(sizeResponse.result); - this.logger.debug('Resolved', variable.name, { start: addressPart[0], size: sizeResponse.result }); - return { - name: variable.name, - startAddress: toHexStringWithRadixMarker(startAddress), - endAddress: endAddress === undefined ? undefined : toHexStringWithRadixMarker(endAddress), - value: variable.value, - type: variable.type, - }; + [variableAddress, variableSize] = await Promise.all([ + variableAddress ?? this.getAddressOfVariable(variable.name, session), + this.getSizeOfVariable(variable.name, session) + ]); } catch (err) { this.logger.warn('Unable to resolve location and size of', variable.name + (err instanceof Error ? ':\n\t' + err.message : '')); + // fall through as we may still have a valid variable address that we can use + } + if (!variableAddress) { return undefined; } + this.logger.debug('Resolved', variable.name, { start: variableAddress, size: variableSize }); + const address = BigInt(variableAddress); + const variableRange: VariableRange = { + name: variable.name, + startAddress: toHexStringWithRadixMarker(address), + endAddress: variableSize === undefined ? undefined : toHexStringWithRadixMarker(address + variableSize), + value: variable.value, + type: variable.type, + }; + return variableRange; + } + + async getAddressOfVariable(variableName: string, session: vscode.DebugSession): Promise { + const response = await sendRequest(session, 'evaluate', { expression: CEvaluateExpression.addressOf(variableName), context: 'watch', frameId: this.currentFrame }); + return extractAddress(response.result); + } + + async getSizeOfVariable(variableName: string, session: vscode.DebugSession): Promise { + const response = await sendRequest(session, 'evaluate', { expression: CEvaluateExpression.sizeOf(variableName), context: 'watch', frameId: this.currentFrame }); + return notADigit.test(response.result) ? undefined : BigInt(response.result); } } diff --git a/src/plugin/external-views.ts b/src/plugin/external-views.ts new file mode 100644 index 0000000..6c350ba --- /dev/null +++ b/src/plugin/external-views.ts @@ -0,0 +1,32 @@ +/******************************************************************************** + * 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 { isDebugVariable, isDebugScope, isDebugEvaluateArguments } from '../common/debug-requests'; + +export namespace VariablesView { + // from https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/debug/browser/variablesView.ts + export interface IVariablesContext { + sessionId: string | undefined; + container: DebugProtocol.Variable | DebugProtocol.Scope | DebugProtocol.EvaluateArguments; + variable: DebugProtocol.Variable; + } +} + +export function isVariablesContext(context: VariablesView.IVariablesContext | unknown): context is VariablesView.IVariablesContext { + const assumed = context ? context as VariablesView.IVariablesContext : undefined; + return isDebugVariable(assumed?.variable) && (isDebugVariable(assumed?.container) || isDebugScope(assumed?.container) || isDebugEvaluateArguments(assumed?.container)); +} diff --git a/src/plugin/logger.ts b/src/plugin/logger.ts index bd0271e..aaf9542 100644 --- a/src/plugin/logger.ts +++ b/src/plugin/logger.ts @@ -16,6 +16,7 @@ import * as vscode from 'vscode'; import * as manifest from './manifest'; +import { stringifyWithBigInts } from '../common/typescript'; export enum Verbosity { off = 0, @@ -50,7 +51,7 @@ export abstract class Logger { return; } - const result = messages.map(message => typeof message === 'string' ? message : JSON.stringify(message, undefined, '\t')).join(' '); + const result = messages.map(message => typeof message === 'string' ? message : stringifyWithBigInts(message, '\t')).join(' '); this.logMessage(result); } diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index f6ba15f..14640e6 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -14,30 +14,34 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import * as vscode from 'vscode'; -import * as manifest from './manifest'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { MemoryReadResult, MemoryWriteResult } from '../common/messaging'; +import * as vscode from 'vscode'; +import { VariableRange, WrittenMemory } from '../common/memory-range'; +import { ReadMemoryResult, SessionContext, WriteMemoryResult } from '../common/messaging'; import { AdapterRegistry } from './adapter-registry/adapter-registry'; -import { VariableRange } from '../common/memory-range'; +import * as manifest from './manifest'; +import { isDebugEvent, isDebugRequest, isDebugResponse, sendRequest } from '../common/debug-requests'; +import { stringToBytesMemory } from '../common/memory'; export interface LabeledUint8Array extends Uint8Array { label?: string; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isInitializeMessage = (message: any): message is DebugProtocol.InitializeResponse => message.command === 'initialize' && message.type === 'response'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isStoppedEvent = (message: any): boolean => message.type === 'event' && message.event === 'stopped'; - export class MemoryProvider { public static ReadKey = `${manifest.PACKAGE_NAME}.canRead`; public static WriteKey = `${manifest.PACKAGE_NAME}.canWrite`; - private _onDidStopDebug: vscode.EventEmitter = new vscode.EventEmitter(); - public readonly onDidStopDebug: vscode.Event = this._onDidStopDebug.event; + private _onDidStopDebug = new vscode.EventEmitter(); + public readonly onDidStopDebug = this._onDidStopDebug.event; + + private _onDidWriteMemory = new vscode.EventEmitter(); + public readonly onDidWriteMemory = this._onDidWriteMemory.event; - protected readonly sessions = new Map(); + private _onDidChangeSessionContext = new vscode.EventEmitter(); + public readonly onDidChangeSessionContext = this._onDidChangeSessionContext.event; + + protected readonly sessionDebugCapabilities = new Map(); + protected readonly sessionClientCapabilities = new Map(); constructor(protected adapterRegistry: AdapterRegistry) { } @@ -57,30 +61,33 @@ export class MemoryProvider { contributedTracker?.onWillStopSession?.(); }, onDidSendMessage: message => { - if (isInitializeMessage(message)) { + if (isDebugResponse('initialize', message)) { // Check for right capabilities in the adapter - this.sessions.set(session.id, message.body); + this.sessionDebugCapabilities.set(session.id, message.body); if (vscode.debug.activeDebugSession?.id === session.id) { - this.setContext(message.body); + this.setContext(session); } - } - if (isStoppedEvent(message)) { + } else if (isDebugEvent('stopped', message)) { this._onDidStopDebug.fire(session); + } else if (isDebugEvent('memory', message)) { + this._onDidWriteMemory.fire(message.body); } contributedTracker?.onDidSendMessage?.(message); }, onError: error => { contributedTracker?.onError?.(error); }, onExit: (code, signal) => { contributedTracker?.onExit?.(code, signal); }, - onWillReceiveMessage: message => { contributedTracker?.onWillReceiveMessage?.(message); } + onWillReceiveMessage: message => { + if (isDebugRequest('initialize', message)) { + this.sessionClientCapabilities.set(session.id, message.arguments); + } + contributedTracker?.onWillReceiveMessage?.(message); + } }); }; context.subscriptions.push( vscode.debug.registerDebugAdapterTrackerFactory('*', { createDebugAdapterTracker }), - vscode.debug.onDidChangeActiveDebugSession(session => { - const capabilities = session && this.sessions.get(session.id); - this.setContext(capabilities); - }) + vscode.debug.onDidChangeActiveDebugSession(session => this.setContext(session)) ); } @@ -89,23 +96,44 @@ export class MemoryProvider { } protected debugSessionTerminated(session: vscode.DebugSession): void { - this.sessions.delete(session.id); + this.sessionDebugCapabilities.delete(session.id); + this.sessionClientCapabilities.delete(session.id); + } + + createContext(session = vscode.debug.activeDebugSession): SessionContext { + const sessionId = session?.id; + const capabilities = sessionId ? this.sessionDebugCapabilities.get(sessionId) : undefined; + return { + sessionId, + canRead: !!capabilities?.supportsReadMemoryRequest, + canWrite: !!capabilities?.supportsWriteMemoryRequest + }; } - protected setContext(capabilities?: DebugProtocol.Capabilities): void { - vscode.commands.executeCommand('setContext', MemoryProvider.ReadKey, !!capabilities?.supportsReadMemoryRequest); - vscode.commands.executeCommand('setContext', MemoryProvider.WriteKey, !!capabilities?.supportsWriteMemoryRequest); + protected setContext(session?: vscode.DebugSession): void { + const newContext = this.createContext(session); + vscode.commands.executeCommand('setContext', MemoryProvider.ReadKey, newContext.canRead); + vscode.commands.executeCommand('setContext', MemoryProvider.WriteKey, newContext.canWrite); + this._onDidChangeSessionContext.fire(newContext); } /** Returns the session if the capability is present, otherwise throws. */ protected assertCapability(capability: keyof DebugProtocol.Capabilities, action: string): vscode.DebugSession { const session = this.assertActiveSession(action); - if (!this.sessions.get(session.id)?.[capability]) { + if (!this.hasDebugCapabilitiy(session, capability)) { throw new Error(`Cannot ${action}. Session does not have capability ${capability}.`); } return session; } + protected hasDebugCapabilitiy(session: vscode.DebugSession, capability: keyof DebugProtocol.Capabilities): boolean { + return !!this.sessionDebugCapabilities.get(session.id)?.[capability]; + } + + protected hasClientCapabilitiy(session: vscode.DebugSession, capability: keyof DebugProtocol.InitializeRequestArguments): boolean { + return !!this.sessionClientCapabilities.get(session.id)?.[capability]; + } + protected assertActiveSession(action: string): vscode.DebugSession { if (!vscode.debug.activeDebugSession) { throw new Error(`Cannot ${action}. No active debug session.`); @@ -113,12 +141,22 @@ export class MemoryProvider { return vscode.debug.activeDebugSession; } - public async readMemory(readMemoryArguments: DebugProtocol.ReadMemoryArguments): Promise { - return this.assertCapability('supportsReadMemoryRequest', 'read memory').customRequest('readMemory', readMemoryArguments); + public async readMemory(args: DebugProtocol.ReadMemoryArguments): Promise { + return sendRequest(this.assertCapability('supportsReadMemoryRequest', 'read memory'), 'readMemory', args); } - public async writeMemory(writeMemoryArguments: DebugProtocol.WriteMemoryArguments): Promise { - return this.assertCapability('supportsWriteMemoryRequest', 'write memory').customRequest('writeMemory', writeMemoryArguments); + public async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { + const session = this.assertCapability('supportsWriteMemoryRequest', 'write memory'); + return sendRequest(session, 'writeMemory', args).then(response => { + if (!this.hasClientCapabilitiy(session, 'supportsMemoryEvent')) { + // we only send out a custom event if we don't expect the client to handle the memory event + // since our client is VS Code we can assume that they will always support this but better to be safe + const offset = response?.offset ? (args.offset ?? 0) + response.offset : args.offset; + const count = response?.bytesWritten ?? stringToBytesMemory(args.data).length; + this._onDidWriteMemory.fire({ memoryReference: args.memoryReference, offset, count }); + } + return response; + }); } public async getVariables(variableArguments: DebugProtocol.ReadMemoryArguments): Promise { @@ -127,4 +165,16 @@ export class MemoryProvider { if (handler?.getResidents) { return handler.getResidents(session, variableArguments); } return handler?.getVariables?.(session) ?? []; } + + public async getAddressOfVariable(variableName: string): Promise { + const session = this.assertActiveSession('get address of variable'); + const handler = this.adapterRegistry?.getHandlerForSession(session.type); + return handler?.getAddressOfVariable?.(session, variableName); + } + + public async getSizeOfVariable(variableName: string): Promise { + const session = this.assertActiveSession('get address of variable'); + const handler = this.adapterRegistry?.getHandlerForSession(session.type); + return handler?.getSizeOfVariable?.(session, variableName); + } } diff --git a/src/plugin/memory-storage.ts b/src/plugin/memory-storage.ts new file mode 100644 index 0000000..65915bb --- /dev/null +++ b/src/plugin/memory-storage.ts @@ -0,0 +1,205 @@ +/******************************************************************************** + * 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 MemoryMap from 'nrf-intel-hex'; +import * as vscode from 'vscode'; +import { URI, Utils } from 'vscode-uri'; +import { IntelHEX } from '../common/intel-hex'; +import { + bytesToStringMemory, createMemoryFromRead, + validateCount, validateMemoryReference, validateOffset +} from '../common/memory'; +import { toHexStringWithRadixMarker } from '../common/memory-range'; +import * as manifest from './manifest'; +import { MemoryProvider } from './memory-provider'; +import { ApplyMemoryArguments, ApplyMemoryResult, MemoryOptions, StoreMemoryArguments } from '../common/messaging'; +import { isVariablesContext } from './external-views'; +import { isWebviewContext } from '../common/webview-context'; + +export const StoreCommandType = `${manifest.PACKAGE_NAME}.store-file`; +export const ApplyCommandType = `${manifest.PACKAGE_NAME}.apply-file`; + +const VALID_FILE_NAME_CHARS = /[^a-zA-Z0-9 _-]/g; + +type StoreMemoryOptions = Required & { + proposedOutputName?: string, + outputFile: vscode.Uri; +}; + +const DEFAULT_STORE_OPTIONS: Omit = { + memoryReference: toHexStringWithRadixMarker(0n, 8), + offset: 0, + count: 256 +}; + +interface ApplyMemoryOptions { + uri: vscode.Uri; +} + +export class MemoryStorage { + constructor(protected memoryProvider: MemoryProvider) { + } + + public activate(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.commands.registerCommand(StoreCommandType, args => this.storeMemory(args)), + vscode.commands.registerCommand(ApplyCommandType, args => this.applyMemory(args)) + ); + } + + public async storeMemory(args?: StoreMemoryArguments): Promise { + const providedDefaultOptions = await this.storeArgsToOptions(args); + const options = await this.getStoreMemoryOptions(providedDefaultOptions); + if (!options) { + // user aborted process + return; + } + + const { outputFile, ...readArgs } = options; + try { + const memoryResponse = await this.memoryProvider.readMemory(readArgs); + const memory = createMemoryFromRead(memoryResponse); + const memoryMap = new MemoryMap({ [Number(memory.address)]: memory.bytes }); + await vscode.workspace.fs.writeFile(outputFile, new TextEncoder().encode(memoryMap.asHexString())); + } catch (error) { + if (error instanceof Error) { + vscode.window.showErrorMessage(`Could not write memory to '${vscode.workspace.asRelativePath(outputFile)}': ${error.message}`); + } else { + vscode.window.showErrorMessage(`Could not write memory to '${vscode.workspace.asRelativePath(outputFile)}': ${error}`); + } + return; + } + + const option = await vscode.window.showInformationMessage(`File '${vscode.workspace.asRelativePath(outputFile)}' saved.`, 'Open File'); + if (option === 'Open File') { + await vscode.window.showTextDocument(outputFile); + } + } + + protected async storeArgsToOptions(args?: StoreMemoryArguments): Promise> { + if (!args) { + return {}; + } + if (isWebviewContext(args)) { + return { ...args.activeReadArguments }; + } + if (isVariablesContext(args)) { + try { + const variableName = args.variable.evaluateName ?? args.variable.name; + const count = await this.memoryProvider.getSizeOfVariable(variableName); + const memoryReference = args.variable.memoryReference ?? await this.memoryProvider.getAddressOfVariable(variableName); + return { count: Number(count), memoryReference, offset: 0, proposedOutputName: variableName }; + } catch (error) { + // ignore, we are just using them as default values + return { memoryReference: args.variable.memoryReference, offset: 0 }; + } + } + return args; + } + + protected async getStoreMemoryOptions(providedDefault?: Partial): Promise { + const memoryReference = await vscode.window.showInputBox({ + title: 'Store Memory to File (1/3)', + prompt: 'Start Memory Address', + placeHolder: 'Hex address or expression', + value: providedDefault?.memoryReference ?? DEFAULT_STORE_OPTIONS.memoryReference, + validateInput: validateMemoryReference + }); + if (!memoryReference) { + return; + } + const offset = await vscode.window.showInputBox({ + title: 'Store Memory to File (2/3)', + prompt: 'Memory Address Offset', + placeHolder: 'Positive or negative offset in bytes', + value: providedDefault?.offset?.toString() ?? DEFAULT_STORE_OPTIONS.offset.toString(), + validateInput: validateOffset + }); + if (!offset) { + return; + } + const count = await vscode.window.showInputBox({ + title: 'Store Memory to File (3/3)', + prompt: 'Length', + placeHolder: 'Number of bytes to read', + value: providedDefault?.count?.toString() ?? DEFAULT_STORE_OPTIONS.count.toString(), + validateInput: validateCount + }); + if (!count) { + return; + } + const workspaceUri = vscode.workspace.workspaceFolders?.[0]?.uri; + const proposedName = providedDefault?.proposedOutputName ?? memoryReference + '_' + count; + const validName = proposedName.replace(VALID_FILE_NAME_CHARS, ''); + const defaultUri = workspaceUri ? Utils.joinPath(workspaceUri, validName) : workspaceUri; + const saveFile = await vscode.window.showSaveDialog({ title: 'Store Memory', defaultUri, filters: IntelHEX.DialogFilters }); + if (!saveFile) { + return; + } + const outputFile = IntelHEX.FileExtensions.applyIfMissing(saveFile); + return { memoryReference, offset: Number(offset), count: Number(count), outputFile }; + } + + public async applyMemory(args?: ApplyMemoryArguments): Promise { + const providedDefaultOptions = await this.applyArgsToOptions(args); + const options = await this.getApplyMemoryOptions(providedDefaultOptions); + if (!options) { + // user aborted process + return {}; + } + try { + const byteContent = await vscode.workspace.fs.readFile(options.uri); + const memoryMap = MemoryMap.fromHex(new TextDecoder().decode(byteContent)); + let memoryReference: string | undefined; + let count: number | undefined; + for (const [address, memory] of memoryMap) { + memoryReference = toHexStringWithRadixMarker(address); + count = memory.length; + const data = bytesToStringMemory(memory); + await this.memoryProvider.writeMemory({ memoryReference, data }); + } + await vscode.window.showInformationMessage(`Memory from '${vscode.workspace.asRelativePath(options.uri)}' applied.`); + return { memoryReference, count, offset: 0 }; + } catch (error) { + if (error instanceof Error) { + vscode.window.showErrorMessage(`Could not apply memory from '${vscode.workspace.asRelativePath(options.uri)}': ${error.message}`); + } else { + vscode.window.showErrorMessage(`Could not apply memory from '${vscode.workspace.asRelativePath(options.uri)}': ${error}`); + } + return {}; + } + } + + protected async applyArgsToOptions(args?: ApplyMemoryArguments): Promise> { + return URI.isUri(args) ? { uri: args } : {}; + } + + protected async getApplyMemoryOptions(providedDefault?: Partial): Promise { + if (providedDefault?.uri) { + // if we are already given a URI, let's not bother the user and simply use it + return { uri: providedDefault.uri }; + } + const selectedUris = await vscode.window.showOpenDialog({ + title: 'Apply Memory', + filters: IntelHEX.DialogFilters, + defaultUri: vscode.workspace.workspaceFolders?.[0]?.uri + }); + if (selectedUris && selectedUris?.length > 0) { + return { uri: selectedUris[0] }; + } + return undefined; + } +} diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 21c6e3b..6462148 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -15,45 +15,47 @@ ********************************************************************************/ import * as vscode from 'vscode'; -import type { DebugProtocol } from '@vscode/debugprotocol'; -import * as manifest from './manifest'; import { Messenger } from 'vscode-messenger'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import { Endianness, VariableRange } from '../common/memory-range'; import { - readyType, + MemoryOptions, + ReadMemoryArguments, + ReadMemoryResult, + SessionContext, + StoreMemoryArguments, + WebviewSelection, + WriteMemoryArguments, + WriteMemoryResult, + applyMemoryType, + getVariablesType, + getWebviewSelectionType, logMessageType, - setOptionsType, + memoryWrittenType, readMemoryType, - writeMemoryType, - MemoryReadResult, - MemoryWriteResult, - getVariables, - setMemoryViewSettingsType, + readyType, resetMemoryViewSettingsType, + sessionContextChangedType, + setMemoryViewSettingsType, + setOptionsType, setTitleType, showAdvancedOptionsType, - getWebviewSelectionType, - WebviewSelection, + storeMemoryType, + writeMemoryType, } from '../common/messaging'; -import { MemoryProvider } from './memory-provider'; -import { outputChannelLogger } from './logger'; -import { Endianness, VariableRange } from '../common/memory-range'; -import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types'; import { WebviewContext, getVisibleColumns } from '../common/webview-context'; - -interface Variable { - name: string; - value: string; - variablesReference: number; - memoryReference: number; -} +import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types'; +import { outputChannelLogger } from './logger'; +import * as manifest from './manifest'; +import { MemoryProvider } from './memory-provider'; +import { ApplyCommandType, StoreCommandType } from './memory-storage'; +import { isVariablesContext } from './external-views'; enum RefreshEnum { off = 0, on = 1 } -const isMemoryVariable = (variable: Variable): variable is Variable => variable && !!(variable as Variable).memoryReference; const CONFIGURABLE_COLUMNS = [ manifest.CONFIG_SHOW_ASCII_COLUMN, manifest.CONFIG_SHOW_VARIABLES_COLUMN, @@ -89,10 +91,10 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { context.subscriptions.push( vscode.window.registerCustomEditorProvider(manifest.EDITOR_NAME, this), vscode.commands.registerCommand(MemoryWebview.ShowCommandType, () => this.show()), - vscode.commands.registerCommand(MemoryWebview.VariableCommandType, node => { - const variable = node.variable; - if (isMemoryVariable(variable)) { - this.show({ memoryReference: variable.memoryReference.toString() }); + vscode.commands.registerCommand(MemoryWebview.VariableCommandType, async args => { + if (isVariablesContext(args)) { + const memoryReference = args.variable.memoryReference ?? await this.memoryProvider.getAddressOfVariable(args.variable.name); + this.show({ memoryReference }); } }), vscode.commands.registerCommand(MemoryWebview.ToggleVariablesColumnCommandType, (ctx: WebviewContext) => { @@ -138,7 +140,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { await this.show({ memoryReference }, webviewPanel); } - public async show(initialMemory?: Partial, panel?: vscode.WebviewPanel): Promise { + public async show(initialMemory?: MemoryOptions, panel?: vscode.WebviewPanel): Promise { const distPathUri = vscode.Uri.joinPath(this.extensionUri, 'dist', 'views'); const mediaPathUri = vscode.Uri.joinPath(this.extensionUri, 'media'); const codiconPathUri = vscode.Uri.joinPath(this.extensionUri, 'node_modules', '@vscode', 'codicons', 'dist'); @@ -198,12 +200,13 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { `; } - protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: Partial): void { + protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: MemoryOptions): void { const participant = this.messenger.registerWebviewPanel(panel); const disposables = [ this.messenger.onNotification(readyType, () => { this.setInitialSettings(participant, panel.title); + this.setSessionContext(participant, this.memoryProvider.createContext()); this.refresh(participant, options); }, { sender: participant }), this.messenger.onRequest(setOptionsType, o => { @@ -212,15 +215,19 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.onRequest(logMessageType, message => outputChannelLogger.info('[webview]:', message), { sender: participant }), this.messenger.onRequest(readMemoryType, request => this.readMemory(request), { sender: participant }), this.messenger.onRequest(writeMemoryType, request => this.writeMemory(request), { sender: participant }), - this.messenger.onRequest(getVariables, request => this.getVariables(request), { sender: participant }), + this.messenger.onRequest(getVariablesType, request => this.getVariables(request), { sender: participant }), this.messenger.onNotification(resetMemoryViewSettingsType, () => this.setInitialSettings(participant, panel.title), { sender: participant }), 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.memoryProvider.onDidStopDebug(() => { if (this.refreshOnStop === RefreshEnum.on) { this.refresh(participant); } }), + this.memoryProvider.onDidChangeSessionContext(context => this.setSessionContext(participant, context)), + this.memoryProvider.onDidWriteMemory(writtenMemory => this.messenger.sendNotification(memoryWrittenType, participant, writtenMemory)) ]; panel.onDidChangeViewState(newState => { if (newState.webviewPanel.visible) { @@ -230,7 +237,7 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); } - protected async refresh(participant: WebviewIdMessageParticipant, options?: Partial): Promise { + protected async refresh(participant: WebviewIdMessageParticipant, options: MemoryOptions = {}): Promise { this.messenger.sendRequest(setOptionsType, participant, options); } @@ -242,6 +249,10 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.sendNotification(setMemoryViewSettingsType, webviewParticipant, settings); } + protected setSessionContext(webviewParticipant: WebviewIdMessageParticipant, context: SessionContext): void { + this.messenger.sendNotification(sessionContextChangedType, webviewParticipant, context); + } + protected getMemoryViewSettings(messageParticipant: WebviewIdMessageParticipant, title: string): MemoryViewSettings { const memoryInspectorConfiguration = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); const bytesPerWord = memoryInspectorConfiguration.get(manifest.CONFIG_BYTES_PER_WORD, manifest.DEFAULT_BYTES_PER_WORD); @@ -261,27 +272,27 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { }; } - protected async readMemory(request: DebugProtocol.ReadMemoryArguments): Promise { + protected async readMemory(request: ReadMemoryArguments): Promise { try { return await this.memoryProvider.readMemory(request); } catch (err) { - outputChannelLogger.error('Error fetching memory', err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + this.logError('Error fetching memory', err); } } - protected async writeMemory(request: DebugProtocol.WriteMemoryArguments): Promise { + protected async writeMemory(request: WriteMemoryArguments): Promise { try { return await this.memoryProvider.writeMemory(request); } catch (err) { - outputChannelLogger.error('Error writing memory', err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + this.logError('Error writing memory', err); } } - protected async getVariables(request: DebugProtocol.ReadMemoryArguments): Promise { + protected async getVariables(request: ReadMemoryArguments): Promise { try { return await this.memoryProvider.getVariables(request); } catch (err) { - outputChannelLogger.error('Error fetching variables', err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + this.logError('Error fetching variables', err); return []; } } @@ -301,4 +312,26 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.setMemoryViewSettings(ctx.messageParticipant, { visibleColumns }); } + + protected async storeMemory(storeArguments: StoreMemoryArguments): Promise { + // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. + // However, we want to fail early in case the user tries to execute a disabled command + if (!this.memoryProvider.createContext().canRead) { + throw new Error('Cannot read memory, no valid debug session.'); + } + return vscode.commands.executeCommand(StoreCommandType, storeArguments); + } + + protected async applyMemory(): Promise { + // Even if we disable the command in VS Code through enablement or when condition, programmatic execution is still possible. + // However, we want to fail early in case the user tries to execute a disabled command + if (!this.memoryProvider.createContext().canWrite) { + throw new Error('Cannot write memory, no valid debug session.'); + } + return vscode.commands.executeCommand(ApplyCommandType); + } + + protected logError(msg: string, err: unknown): void { + outputChannelLogger.error(msg, err instanceof Error ? `: ${err.message}\n${err.stack}` : ''); + } } diff --git a/src/webview/columns/address-column.tsx b/src/webview/columns/address-column.tsx index ccbc3fd..e9d8b51 100644 --- a/src/webview/columns/address-column.tsx +++ b/src/webview/columns/address-column.tsx @@ -17,7 +17,7 @@ import React, { ReactNode } from 'react'; import { BigIntMemoryRange, getAddressString, getRadixMarker } from '../../common/memory-range'; import { ColumnContribution, ColumnFittingType, TableRenderOptions } from './column-contribution-service'; -import { Memory } from '../utils/view-types'; +import { Memory } from '../../common/memory'; export class AddressColumn implements ColumnContribution { static ID = 'address'; diff --git a/src/webview/columns/ascii-column.ts b/src/webview/columns/ascii-column.ts index 866b510..cb522bd 100644 --- a/src/webview/columns/ascii-column.ts +++ b/src/webview/columns/ascii-column.ts @@ -17,7 +17,7 @@ import { ReactNode } from 'react'; import { BigIntMemoryRange, toOffset } from '../../common/memory-range'; import { ColumnContribution, TableRenderOptions } from './column-contribution-service'; -import { Memory } from '../utils/view-types'; +import { Memory } from '../../common/memory'; function isPrintableAsAscii(input: number): boolean { return input >= 32 && input < (128 - 1); diff --git a/src/webview/columns/column-contribution-service.ts b/src/webview/columns/column-contribution-service.ts index 42fa494..1d4ada6 100644 --- a/src/webview/columns/column-contribution-service.ts +++ b/src/webview/columns/column-contribution-service.ts @@ -14,10 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { DebugProtocol } from '@vscode/debugprotocol'; import type * as React from 'react'; import { BigIntMemoryRange } from '../../common/memory-range'; -import type { Disposable, Memory, MemoryState, SerializedTableRenderOptions, UpdateExecutor } from '../utils/view-types'; +import type { Disposable, MemoryState, SerializedTableRenderOptions, UpdateExecutor } from '../utils/view-types'; +import { Memory } from '../../common/memory'; +import { ReadMemoryArguments } from '../../common/messaging'; export type ColumnFittingType = 'content-width'; @@ -31,7 +32,7 @@ export interface ColumnContribution { priority?: number; render(range: BigIntMemoryRange, memory: Memory, options: TableRenderOptions): React.ReactNode /** Called when fetching new memory or when activating the column. */ - fetchData?(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; + fetchData?(currentViewParameters: ReadMemoryArguments): Promise; /** Called when the user reveals the column */ activate?(memory: MemoryState): Promise; /** Called when the user hides the column */ diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx index 0389b3c..816705b 100644 --- a/src/webview/columns/data-column.tsx +++ b/src/webview/columns/data-column.tsx @@ -16,11 +16,12 @@ import * as React from 'react'; import { BigIntMemoryRange, Endianness, toOffset } from '../../common/memory-range'; -import { FullNodeAttributes, Memory } from '../utils/view-types'; +import { FullNodeAttributes } from '../utils/view-types'; import { ColumnContribution, TableRenderOptions } from './column-contribution-service'; import { decorationService } from '../decorations/decoration-service'; import type { MemorySizeOptions } from '../components/memory-table'; import { elementInnerWidth, characterWidthInContainer } from '../utils/window'; +import { Memory } from '../../common/memory'; export class DataColumn implements ColumnContribution { static CLASS_NAME = 'column-data'; diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index 908d2f2..a57f76c 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -14,7 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { DebugProtocol } from '@vscode/debugprotocol'; import memoize from 'memoize-one'; import { Column } from 'primereact/column'; import { DataTable, DataTableCellSelection, DataTableProps, DataTableSelectionCellChangeEvent } from 'primereact/datatable'; @@ -22,22 +21,24 @@ import { ProgressSpinner } from 'primereact/progressspinner'; import { Tooltip } from 'primereact/tooltip'; import React from 'react'; import { TableRenderOptions } from '../columns/column-contribution-service'; -import { Decoration, Memory, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types'; +import { Decoration, MemoryDisplayConfiguration, ScrollingBehavior, isTrigger } from '../utils/view-types'; import isDeepEqual from 'fast-deep-equal'; import { classNames } from 'primereact/utils'; import { tryToNumber } from '../../common/typescript'; import { DataColumn } from '../columns/data-column'; import { createColumnVscodeContext, createSectionVscodeContext } from '../utils/vscode-contexts'; import { WebviewSelection } from '../../common/messaging'; +import { MemoryOptions, ReadMemoryArguments } from '../../common/messaging'; +import { Memory } from '../../common/memory'; import { debounce } from 'lodash'; import type { HoverService } from '../hovers/hover-service'; import { TooltipEvent } from 'primereact/tooltip/tooltipoptions'; export interface MoreMemorySelectProps { - activeReadArguments: Required; + activeReadArguments: Required; options: number[]; direction: 'above' | 'below'; - fetchMemory(partialOptions?: Partial): Promise; + fetchMemory(partialOptions?: MemoryOptions): Promise; disabled: boolean } @@ -128,13 +129,13 @@ export const MoreMemorySelect: React.FC; - activeReadArguments: Required; + configuredReadArguments: Required; + activeReadArguments: Required; memory?: Memory; decorations: Decoration[]; effectiveAddressLength: number; hoverService: HoverService; - fetchMemory(partialOptions?: Partial): Promise; + fetchMemory(partialOptions?: Partial): Promise; isMemoryFetching: boolean; isFrozen: boolean; } @@ -441,7 +442,7 @@ export class MemoryTable extends React.PureComponent; - activeReadArguments: Required; + sessionContext: SessionContext; + configuredReadArguments: Required; + activeReadArguments: Required; memory?: Memory; title: string; decorations: Decoration[]; @@ -43,7 +45,9 @@ interface MemoryWidgetProps extends MemoryDisplayConfiguration { updateMemoryDisplayConfiguration: (memoryArguments: Partial) => void; resetMemoryDisplayConfiguration: () => void; updateTitle: (title: string) => void; - fetchMemory(partialOptions?: Partial): Promise + fetchMemory(partialOptions?: MemoryOptions): Promise; + storeMemory(): void; + applyMemory(): void; } interface MemoryWidgetState { @@ -67,6 +71,7 @@ export class MemoryWidget extends React.Component { - configuredReadArguments: Required; - activeReadArguments: Required; + sessionContext: SessionContext; + configuredReadArguments: Required; + activeReadArguments: Required; title: string; updateRenderOptions: (options: Partial) => void; resetRenderOptions: () => void; updateTitle: (title: string) => void; updateMemoryState: (state: Partial) => void; - fetchMemory(partialOptions?: Partial): Promise + fetchMemory(partialOptions?: MemoryOptions): Promise toggleColumn(id: string, isVisible: boolean): void; toggleFrozen: () => void; isFrozen: boolean; + storeMemory(): void; + applyMemory(): void; } interface OptionsWidgetState { @@ -101,36 +105,18 @@ export class OptionsWidget extends React.Component { const errors: FormikErrors = {}; - - if (values.address.trim().length === 0) { - errors.address = 'Required'; - } else { - const address = +values.address; - if (!isNaN(address) && address < 0) { - errors.address = 'Value needs to be >= 0'; - } + const addressError = values.address.trim().length === 0 ? 'Required' : validateMemoryReference(values.address); + if (addressError) { + errors.address = addressError; } - - if (values.offset.trim().length === 0) { - errors.offset = 'Required'; - } else { - const offset = +values.offset; - if (isNaN(offset)) { - errors.offset = 'No number provided'; - } + const offsetError = values.offset.trim().length === 0 ? 'Required' : validateOffset(values.offset); + if (offsetError) { + errors.offset = offsetError; } - - if (values.count.trim().length === 0) { - errors.count = 'Required'; - } else { - const count = +values.count; - if (isNaN(count)) { - errors.count = 'No number provided'; - } else if (count <= 0) { - errors.count = 'Value needs to be > 0'; - } + const countError = values.count.trim().length === 0 ? 'Required' : validateCount(values.count); + if (countError) { + errors.count = countError; } - return errors; }; @@ -145,6 +131,7 @@ export class OptionsWidget extends React.Component { if (userValue !== memoryValue) { @@ -176,16 +163,40 @@ export class OptionsWidget extends React.Component{this.props.title} )} {!isLabelEditing && ( - @@ -406,7 +417,7 @@ export class OptionsWidget extends React.Component) => void = e => this.doHandleKeyDown(e); protected doHandleKeyDown(event: KeyboardEvent): void { - if (event.code === 'Enter') { + if (event.key === 'Enter') { const id = event.currentTarget.id as InputId; const value = event.currentTarget.value; diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index f410aea..679c2b2 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -14,43 +14,51 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import 'primeflex/primeflex.css'; +import { PrimeReactProvider } from 'primereact/api'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { HOST_EXTENSION, WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import { Memory, createMemoryFromRead } from '../common/memory'; +import { BigIntMemoryRange, Endianness, WrittenMemory, doOverlap, getAddressLength, getAddressString } from '../common/memory-range'; import { - readyType, + MemoryOptions, + ReadMemoryArguments, + WebviewSelection, + applyMemoryType, + getWebviewSelectionType, logMessageType, - setOptionsType, + memoryWrittenType, readMemoryType, - setTitleType, - setMemoryViewSettingsType, + readyType, resetMemoryViewSettingsType, + setMemoryViewSettingsType, + setOptionsType, + setTitleType, showAdvancedOptionsType, - getWebviewSelectionType, - WebviewSelection, + storeMemoryType, + sessionContextChangedType, + SessionContext, } from '../common/messaging'; -import type { DebugProtocol } from '@vscode/debugprotocol'; -import { Decoration, Memory, MemoryDisplayConfiguration, MemoryState } from './utils/view-types'; -import { MemoryWidget } from './components/memory-widget'; -import { messenger } from './view-messenger'; +import { AddressColumn } from './columns/address-column'; +import { AsciiColumn } from './columns/ascii-column'; import { ColumnStatus, columnContributionService } from './columns/column-contribution-service'; +import { DataColumn } from './columns/data-column'; +import { MemoryWidget } from './components/memory-widget'; import { decorationService } from './decorations/decoration-service'; +import { Decoration, MemoryDisplayConfiguration, MemoryState } from './utils/view-types'; import { variableDecorator } from './variables/variable-decorations'; -import { AsciiColumn } from './columns/ascii-column'; -import { AddressColumn } from './columns/address-column'; -import { DataColumn } from './columns/data-column'; -import { PrimeReactProvider } from 'primereact/api'; -import 'primeflex/primeflex.css'; -import { getAddressLength, getAddressString } from '../common/memory-range'; -import { Endianness } from '../common/memory-range'; +import { messenger } from './view-messenger'; import { hoverService, HoverService } from './hovers/hover-service'; import { AddressHover } from './hovers/address-hover'; import { DataHover } from './hovers/data-hover'; import { VariableHover } from './hovers/variable-hover'; +import { debounce } from 'lodash'; export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration { messageParticipant: WebviewIdMessageParticipant; title: string; + sessionContext: SessionContext; effectiveAddressLength: number; decorations: Decoration[]; hoverService: HoverService; @@ -58,6 +66,11 @@ export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration isFrozen: boolean; } +const DEFAULT_SESSION_CONTEXT: SessionContext = { + canRead: false, + canWrite: false +}; + const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = { bytesPerWord: 1, wordsPerGroup: 1, @@ -68,7 +81,8 @@ const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = { addressRadix: 16, showRadixPrefix: true, }; -const DEFAULT_READ_ARGUMENTS: Required = { + +const DEFAULT_READ_ARGUMENTS: Required = { memoryReference: '', offset: 0, count: 256, @@ -90,6 +104,7 @@ class App extends React.Component<{}, MemoryAppState> { this.state = { messageParticipant: { type: 'webview', webviewId: '' }, title: 'Memory', + sessionContext: DEFAULT_SESSION_CONTEXT, memory: undefined, effectiveAddressLength: 0, configuredReadArguments: DEFAULT_READ_ARGUMENTS, @@ -105,6 +120,8 @@ class App extends React.Component<{}, MemoryAppState> { public componentDidMount(): void { messenger.onRequest(setOptionsType, options => this.setOptions(options)); + messenger.onNotification(memoryWrittenType, writtenMemory => this.memoryWritten(writtenMemory)); + messenger.onNotification(sessionContextChangedType, sessionContext => this.sessionContextChanged(sessionContext)); messenger.onNotification(setMemoryViewSettingsType, config => { if (config.visibleColumns) { for (const column of columnContributionService.getColumns()) { @@ -133,11 +150,51 @@ class App extends React.Component<{}, MemoryAppState> { hoverService.setMemoryState(this.state); } + // use a slight debounce as the same event may come in short succession + protected memoryWritten = debounce((writtenMemory: WrittenMemory): void => { + if (!this.state.memory) { + return; + } + if (this.state.activeReadArguments.memoryReference === writtenMemory.memoryReference) { + // catch simple case + this.fetchMemory(); + return; + } + try { + // If we are dealing with numeric addresses (and not expressions) then we can determine the overlap. + // Note that we use big int arithmetic here to determine the overlap for (start address + length) vs (memory state address + length), i.e., + // we do not actually determine the end address may need to consider the size of a word in bytes + const written: BigIntMemoryRange = { + startAddress: BigInt(writtenMemory.memoryReference), + endAddress: BigInt(writtenMemory.memoryReference) + BigInt(writtenMemory.count ?? 0) + }; + const shown: BigIntMemoryRange = { + startAddress: this.state.memory.address, + endAddress: this.state.memory.address + BigInt(this.state.memory.bytes.length) + }; + if (doOverlap(written, shown)) { + this.fetchMemory(); + return; + } + } catch (error) { + // ignore and fall through + } + + // we could try to convert any expression we may have to an address by sending an evaluation request to the DA + // but for now we just go with a pessimistic approach: if we are unsure, we refresh the memory + this.fetchMemory(); + }, 100); + + protected sessionContextChanged(sessionContext: SessionContext): void { + this.setState({ sessionContext }); + } + public render(): React.ReactNode { return { addressPadding={this.state.addressPadding} addressRadix={this.state.addressRadix} showRadixPrefix={this.state.showRadixPrefix} + storeMemory={this.storeMemory} + applyMemory={this.applyMemory} /> ; } - protected updateMemoryState = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); + protected updateMemoryState = (newState?: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); protected updateMemoryDisplayConfiguration = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); protected resetMemoryDisplayConfiguration = () => messenger.sendNotification(resetMemoryViewSettingsType, HOST_EXTENSION, undefined); protected updateTitle = (title: string) => { @@ -175,7 +234,7 @@ class App extends React.Component<{}, MemoryAppState> { messenger.sendNotification(setTitleType, HOST_EXTENSION, title); }; - protected async setOptions(options?: Partial): Promise { + protected async setOptions(options?: MemoryOptions): Promise { messenger.sendRequest(logMessageType, HOST_EXTENSION, `Setting options: ${JSON.stringify(options)}`); if (this.state.configuredReadArguments.memoryReference === '') { // Only update if we have no user configured read arguments @@ -185,8 +244,8 @@ class App extends React.Component<{}, MemoryAppState> { return this.fetchMemory(options); } - protected fetchMemory = async (partialOptions?: Partial): Promise => this.doFetchMemory(partialOptions); - protected async doFetchMemory(partialOptions?: Partial): Promise { + protected fetchMemory = async (partialOptions?: MemoryOptions): Promise => this.doFetchMemory(partialOptions); + protected async doFetchMemory(partialOptions?: MemoryOptions): Promise { if (this.state.isFrozen) { return; } @@ -204,7 +263,8 @@ class App extends React.Component<{}, MemoryAppState> { executor => executor.fetchData(completeOptions) )); - const memory = this.convertMemory(completeOptions, response); + const memory = createMemoryFromRead(response); + this.setState(prev => ({ ...prev, decorations: decorationService.decorations, @@ -231,17 +291,6 @@ class App extends React.Component<{}, MemoryAppState> { } - protected convertMemory(request: Required, result: DebugProtocol.ReadMemoryResponse['body']): Memory { - if (!result?.data) { - const message = `No memory provided for address ${request.memoryReference}` - + `, offset ${request.offset} and count ${request.count}!`; - throw new Error(message); - } - const address = BigInt(result.address); - const bytes = Uint8Array.from(Buffer.from(result.data, 'base64')); - return { bytes, address }; - } - protected getEffectiveAddressLength(memory?: Memory): number { const { addressRadix, addressPadding } = this.state; return addressPadding === 'Min' ? this.getLastAddressLength(memory) : getAddressLength(addressPadding, addressRadix); @@ -275,6 +324,14 @@ class App extends React.Component<{}, MemoryAppState> { protected getWebviewSelection(): WebviewSelection { return this.memoryWidget.current?.getWebviewSelection() ?? {}; } + + protected storeMemory = async (): Promise => { + await messenger.sendRequest(storeMemoryType, HOST_EXTENSION, { ...this.state.activeReadArguments }); + }; + + protected applyMemory = async (): Promise => { + await messenger.sendRequest(applyMemoryType, HOST_EXTENSION, undefined); + }; } const container = document.getElementById('root') as Element; diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 762f9cd..a6d78de 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -14,17 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import type { DebugProtocol } from '@vscode/debugprotocol'; import deepequal from 'fast-deep-equal'; import type * as React from 'react'; import { areRangesEqual, BigIntMemoryRange, Endianness, Radix } from '../../common/memory-range'; import { GroupsPerRowOption } from '../../plugin/manifest'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; - -export interface Memory { - address: bigint; - bytes: Uint8Array; -} +import { Memory } from '../../common/memory'; +import { ReadMemoryArguments } from '../../common/messaging'; export interface SerializedTableRenderOptions extends MemoryDisplayConfiguration { columnOptions: Array<{ label: string, doRender: boolean }>; @@ -54,17 +50,17 @@ export interface MemoryState { /** * The user configured memory read arguments */ - configuredReadArguments: Required; + configuredReadArguments: Required; /** * The active memory read arguments used to load the memory */ - activeReadArguments: Required; + activeReadArguments: Required; memory?: Memory; isMemoryFetching: boolean; } export interface UpdateExecutor { - fetchData(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; + fetchData(currentViewParameters: ReadMemoryArguments): Promise; } export interface StylableNodeAttributes { diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index 98c9fc0..226ae8c 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -14,9 +14,8 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import type { DebugProtocol } from '@vscode/debugprotocol'; import { HOST_EXTENSION } from 'vscode-messenger-common'; -import { getVariables } from '../../common/messaging'; +import { ReadMemoryArguments, getVariablesType } from '../../common/messaging'; import { messenger } from '../view-messenger'; import { Decoration, MemoryState } from '../utils/view-types'; import { EventEmitter, IEvent } from '../utils/events'; @@ -26,6 +25,7 @@ import { ReactNode } from 'react'; import { areVariablesEqual, compareBigInt, BigIntMemoryRange, BigIntVariableRange, doOverlap } from '../../common/memory-range'; import * as React from 'react'; import { createVariableVscodeContext } from '../utils/vscode-contexts'; +import { stringifyWithBigInts } from '../../common/typescript'; const NON_HC_COLORS = [ 'var(--vscode-terminal-ansiBlue)', @@ -46,9 +46,9 @@ export class VariableDecorator implements ColumnContribution, Decorator { get onDidChange(): IEvent { return this.onDidChangeEmitter.event; } - async fetchData(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise { + async fetchData(currentViewParameters: ReadMemoryArguments): Promise { if (!this.active || !currentViewParameters.memoryReference || !currentViewParameters.count) { return; } - const visibleVariables = (await messenger.sendRequest(getVariables, HOST_EXTENSION, currentViewParameters)) + const visibleVariables = (await messenger.sendRequest(getVariablesType, HOST_EXTENSION, currentViewParameters)) .map(transmissible => { const startAddress = BigInt(transmissible.startAddress); return { @@ -138,9 +138,4 @@ export class VariableDecorator implements ColumnContribution, Decorator { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function stringifyWithBigInts(object: any): any { - return JSON.stringify(object, (_key, value) => typeof value === 'bigint' ? value.toString() : value); -} - export const variableDecorator = new VariableDecorator(); diff --git a/yarn.lock b/yarn.lock index 47b95bc..003239c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2306,6 +2306,11 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +nrf-intel-hex@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/nrf-intel-hex/-/nrf-intel-hex-1.4.0.tgz#f14d5d89a09437407536652ca3a377cef915be9e" + integrity sha512-q3+GGRIpe0VvCjUP1zaqW5rk6IpCZzhD0lu7Sguo1bgWwFcA9kZRjsaKUb0jBQMnefyOl5o0BBGAxvqMqYx8Sg== + nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -3379,6 +3384,11 @@ vscode-messenger@^0.4.3: dependencies: vscode-messenger-common "^0.4.3" +vscode-uri@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"