diff --git a/media/options-widget.css b/media/options-widget.css index 2aa5624..b2e8c1f 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -120,7 +120,8 @@ flex-direction: column; } -.advanced-options-dropdown { +.advanced-options-dropdown, +.advanced-options-input { width: 100%; } diff --git a/package.json b/package.json index c4c8bca..1969593 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prepare": "yarn build", "clean": "git clean -f -x ./node_modules ./dist", "build": "webpack --mode production && yarn lint", - "watch": "webpack -w", + "watch": "webpack -w --mode development ", "lint": "eslint . --ext .ts,.tsx", "package": "vsce package --yarn", "serve": "serve --cors -p 3333" @@ -78,6 +78,7 @@ { "command": "memory-inspector.show", "title": "Show Memory Inspector", + "icon": "$(file-binary)", "category": "Memory" }, { @@ -262,11 +263,32 @@ "off" ], "enumDescriptions": [ - "Refresh memory views when when debugger stops (e.g. a breakpoint is hit)", - "Memory view data is manually refreshed by user" + "Refresh Memory Inspector when the debugger stops (e.g. a breakpoint is hit)", + "Do not automatically refresh when the debugger stops" ], "default": "on", - "description": "Refresh memory views when debugger stops" + "description": "Refresh Memory Inspector windows when the debugger stops" + }, + "memory-inspector.periodicRefresh": { + "type": "string", + "enum": [ + "always", + "while running", + "off" + ], + "markdownEnumDescriptions": [ + "Always refresh automatically after the configured `#memory-inspector.periodicRefreshInterval#`", + "Refresh automatically after the configured `#memory-inspector.periodicRefreshInterval#` while the CPU is running", + "Do not automatically refresh after the configured delay" + ], + "default": "off", + "markdownDescription": "Refresh Memory Inspectors after the configured `#memory-inspector.periodicRefreshInterval#`." + }, + "memory-inspector.periodicRefreshInterval": { + "type": "number", + "default": 500, + "minimum": 500, + "markdownDescription": "Controls the delay in milliseconds after which a Memory Inspector is refrehsed automatically. Only applies when `#memory-inspector.periodicRefresh#` is enabled." }, "memory-inspector.groupings.bytesPerMAU": { "type": "number", diff --git a/src/common/debug-requests.ts b/src/common/debug-requests.ts index 3623fb2..319eba2 100644 --- a/src/common/debug-requests.ts +++ b/src/common/debug-requests.ts @@ -29,8 +29,9 @@ export interface DebugRequestTypes { export interface DebugEvents { 'memory': DebugProtocol.MemoryEvent, + 'continued': DebugProtocol.ContinuedEvent, 'stopped': DebugProtocol.StoppedEvent, - 'output': DebugProtocol.OutputEvent, + 'output': DebugProtocol.OutputEvent } export type DebugRequest = Omit & { command: C, arguments: A }; diff --git a/src/plugin/manifest.ts b/src/common/manifest.ts similarity index 78% rename from src/plugin/manifest.ts rename to src/common/manifest.ts index 98f76fc..be9a6d5 100644 --- a/src/plugin/manifest.ts +++ b/src/common/manifest.ts @@ -14,8 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Endianness } from '../common/memory-range'; - // Common export const PACKAGE_NAME = 'memory-inspector'; export const DISPLAY_NAME = 'Memory Inspector'; @@ -26,8 +24,6 @@ export const CONFIG_LOGGING_VERBOSITY = 'loggingVerbosity'; export const DEFAULT_LOGGING_VERBOSITY = 'warn'; export const CONFIG_DEBUG_TYPES = 'debugTypes'; export const DEFAULT_DEBUG_TYPES = ['gdb', 'embedded-debug', 'arm-debugger']; -export const CONFIG_REFRESH_ON_STOP = 'refreshOnStop'; -export const DEFAULT_REFRESH_ON_STOP = 'on'; // MAUs (Minimum Addressable Units) // - Bytes per MAU @@ -49,7 +45,23 @@ export const DEFAULT_GROUPS_PER_ROW: GroupsPerRowOption = 4; // - Group Endianness export const CONFIG_ENDIANNESS = 'endianness'; -export const DEFAULT_ENDIANNESS = Endianness.Little; +export const ENDIANNESS_CHOICES = ['Little Endian', 'Big Endian'] as const; +export type Endianness = (typeof ENDIANNESS_CHOICES)[number]; +export const DEFAULT_ENDIANNESS: Endianness = 'Little Endian'; + +// Refresh: On Stop +export const CONFIG_REFRESH_ON_STOP = 'refreshOnStop'; +export const REFRESH_ON_STOP = ['on', 'off'] as const; +export type RefreshOnStop = (typeof REFRESH_ON_STOP)[number]; +export const DEFAULT_REFRESH_ON_STOP = 'on'; + +// Refresh: Periodic +export const CONFIG_PERIODIC_REFRESH = 'periodicRefresh'; +export const PERIODIC_REFRESH_CHOICES = ['always', 'while running', 'off'] as const; +export type PeriodicRefresh = (typeof PERIODIC_REFRESH_CHOICES)[number]; +export const DEFAULT_PERIODIC_REFRESH: PeriodicRefresh = 'always'; +export const CONFIG_PERIODIC_REFRESH_INTERVAL = 'periodicRefreshInterval'; +export const DEFAULT_PERIODIC_REFRESH_INTERVAL = 500; // Scroll export const CONFIG_SCROLLING_BEHAVIOR = 'scrollingBehavior'; diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 5f1e766..2f0d6c6 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -126,8 +126,3 @@ export function areVariablesEqual(one: BigIntVariableRange, other: BigIntVariabl export function toOffset(startAddress: bigint, targetAddress: bigint, mauSize: number): number { return Number(targetAddress - startAddress) * (mauSize / 8); } - -export enum Endianness { - Little = 'Little Endian', - Big = 'Big Endian' -} diff --git a/src/common/messaging.ts b/src/common/messaging.ts index 030086f..9b23854 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -18,9 +18,9 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import type { NotificationType, RequestType } from 'vscode-messenger-common'; import { URI } from 'vscode-uri'; import { VariablesView } from '../plugin/external-views'; -import { MemoryViewSettings } from '../webview/utils/view-types'; import { DebugRequestTypes } from './debug-requests'; import type { VariableRange, WrittenMemory } from './memory-range'; +import { MemoryViewSettings } from './webview-configuration'; import { WebviewContext } from './webview-context'; // convenience types for easier readability and better semantics @@ -42,6 +42,7 @@ export interface SessionContext { sessionId?: string; canRead: boolean; canWrite: boolean; + stopped?: boolean; } // Notifications diff --git a/src/common/typescript.ts b/src/common/typescript.ts index d8ef137..e20b326 100644 --- a/src/common/typescript.ts +++ b/src/common/typescript.ts @@ -24,3 +24,16 @@ export function tryToNumber(value?: string | number): number | undefined { export function stringifyWithBigInts(object: any, space?: string | number): any { return JSON.stringify(object, (_key, value) => typeof value === 'bigint' ? value.toString() : value, space); } + +export interface Change { + from: T; + to: T; +} + +export function hasChanged(change: Change, prop: P): boolean { + return change.from[prop] !== change.to[prop]; +} + +export function hasChangedTo(change: Change, prop: P, value: T[P]): boolean { + return change.from[prop] !== change.to[prop] && change.to[prop] === value; +} diff --git a/src/common/webview-configuration.ts b/src/common/webview-configuration.ts new file mode 100644 index 0000000..5532e65 --- /dev/null +++ b/src/common/webview-configuration.ts @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (C) 2024 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import { Endianness, GroupsPerRowOption, PeriodicRefresh, RefreshOnStop } from './manifest'; +import { Radix } from './memory-range'; + +/** The memory display configuration that can be specified for the memory widget. */ +export interface MemoryDisplayConfiguration { + bytesPerMau: number; + mausPerGroup: number; + groupsPerRow: GroupsPerRowOption; + endianness: Endianness; + scrollingBehavior: ScrollingBehavior; + addressPadding: AddressPadding; + addressRadix: Radix; + showRadixPrefix: boolean; + refreshOnStop: RefreshOnStop; + periodicRefresh: PeriodicRefresh; + periodicRefreshInterval: number; +} + +export type ScrollingBehavior = 'Paginate' | 'Grow' | 'Auto-Append'; + +export type AddressPadding = 'Minimal' | number; + +export interface ColumnVisibilityStatus { + visibleColumns: string[]; +} + +/** All settings related to memory view that can be specified for the webview from the extension "main". */ +export interface MemoryViewSettings extends ColumnVisibilityStatus, MemoryDisplayConfiguration { + title: string + messageParticipant: WebviewIdMessageParticipant; +} diff --git a/src/entry-points/browser/extension.ts b/src/entry-points/browser/extension.ts index 26a0713..d8c6905 100644 --- a/src/entry-points/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -17,18 +17,23 @@ import * as vscode from 'vscode'; import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; +import { ContextTracker } from '../../plugin/context-tracker'; import { MemoryProvider } from '../../plugin/memory-provider'; import { MemoryStorage } from '../../plugin/memory-storage'; import { MemoryWebview } from '../../plugin/memory-webview-main'; +import { SessionTracker } from '../../plugin/session-tracker'; 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 sessionTracker = new SessionTracker(); + new ContextTracker(sessionTracker); + const memoryProvider = new MemoryProvider(registry, sessionTracker); + const memoryView = new MemoryWebview(context.extensionUri, memoryProvider, sessionTracker); const memoryStorage = new MemoryStorage(memoryProvider); const cAdapter = new CAdapter(registry); registry.activate(context); + sessionTracker.activate(context); memoryProvider.activate(context); memoryView.activate(context); memoryStorage.activate(context); diff --git a/src/entry-points/desktop/extension.ts b/src/entry-points/desktop/extension.ts index 73c0fa3..d8c6905 100644 --- a/src/entry-points/desktop/extension.ts +++ b/src/entry-points/desktop/extension.ts @@ -17,19 +17,24 @@ import * as vscode from 'vscode'; import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry'; import { CAdapter } from '../../plugin/adapter-registry/c-adapter'; +import { ContextTracker } from '../../plugin/context-tracker'; import { MemoryProvider } from '../../plugin/memory-provider'; import { MemoryStorage } from '../../plugin/memory-storage'; import { MemoryWebview } from '../../plugin/memory-webview-main'; +import { SessionTracker } from '../../plugin/session-tracker'; 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 sessionTracker = new SessionTracker(); + new ContextTracker(sessionTracker); + const memoryProvider = new MemoryProvider(registry, sessionTracker); + const memoryView = new MemoryWebview(context.extensionUri, memoryProvider, sessionTracker); const memoryStorage = new MemoryStorage(memoryProvider); const cAdapter = new CAdapter(registry); - memoryProvider.activate(context); registry.activate(context); + sessionTracker.activate(context); + memoryProvider.activate(context); memoryView.activate(context); memoryStorage.activate(context); cAdapter.activate(context); diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index 0afd107..d3509e5 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -74,7 +74,7 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { onDidSendMessage(message: unknown): void { if (isDebugResponse('scopes', message)) { this.variablesTree = {}; // Scopes request implies that all scopes will be queried again. - for (const scope of message.body.scopes) { + for (const scope of message.body?.scopes) { if (this.isDesiredScope(scope)) { if (!this.variablesTree[scope.variablesReference] || this.variablesTree[scope.variablesReference].name !== scope.name) { this.variablesTree[scope.variablesReference] = { ...scope }; @@ -86,7 +86,7 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { const parentReference = this.pendingMessages.get(message.request_seq)!; this.pendingMessages.delete(message.request_seq); if (parentReference in this.variablesTree) { - this.variablesTree[parentReference].children = message.body.variables; + this.variablesTree[parentReference].children = message.body?.variables; } } } diff --git a/src/plugin/adapter-registry/c-adapter.ts b/src/plugin/adapter-registry/c-adapter.ts index 26b35fd..686ceb6 100644 --- a/src/plugin/adapter-registry/c-adapter.ts +++ b/src/plugin/adapter-registry/c-adapter.ts @@ -15,8 +15,8 @@ ********************************************************************************/ import * as vscode from 'vscode'; +import * as manifest from '../../common/manifest'; import { outputChannelLogger } from '../logger'; -import * as manifest from '../manifest'; import { VariableTracker } from './adapter-capabilities'; import { AdapterRegistry } from './adapter-registry'; import { CTracker } from './c-tracker'; diff --git a/src/plugin/context-tracker.ts b/src/plugin/context-tracker.ts new file mode 100644 index 0000000..30b27a7 --- /dev/null +++ b/src/plugin/context-tracker.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (C) 2022 Ericsson, Arm and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as vscode from 'vscode'; +import * as manifest from '../common/manifest'; +import { isSessionEvent, SessionEvent, SessionTracker } from './session-tracker'; + +export class ContextTracker { + public static ReadKey = `${manifest.PACKAGE_NAME}.canRead`; + public static WriteKey = `${manifest.PACKAGE_NAME}.canWrite`; + + constructor(protected sessionTracker: SessionTracker) { + this.sessionTracker.onSessionEvent(event => this.onSessionEvent(event)); + } + + onSessionEvent(event: SessionEvent): void { + if (isSessionEvent('active', event)) { + vscode.commands.executeCommand('setContext', ContextTracker.ReadKey, !!event.session?.debugCapabilities?.supportsReadMemoryRequest); + vscode.commands.executeCommand('setContext', ContextTracker.WriteKey, !!event.session?.debugCapabilities?.supportsWriteMemoryRequest); + } + } +} diff --git a/src/plugin/logger.ts b/src/plugin/logger.ts index b8490cc..3d80c71 100644 --- a/src/plugin/logger.ts +++ b/src/plugin/logger.ts @@ -15,8 +15,8 @@ ********************************************************************************/ import * as vscode from 'vscode'; +import * as manifest from '../common/manifest'; import { stringifyWithBigInts } from '../common/typescript'; -import * as manifest from './manifest'; export enum Verbosity { off = 0, diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index 7a77996..10b3e75 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -16,170 +16,75 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import * as vscode from 'vscode'; -import { isDebugEvent, isDebugRequest, isDebugResponse, sendRequest } from '../common/debug-requests'; +import { sendRequest } from '../common/debug-requests'; import { stringToBytesMemory } from '../common/memory'; -import { VariableRange, WrittenMemory } from '../common/memory-range'; -import { ReadMemoryResult, SessionContext, WriteMemoryResult } from '../common/messaging'; +import { VariableRange } from '../common/memory-range'; +import { ReadMemoryResult, WriteMemoryResult } from '../common/messaging'; import { AdapterRegistry } from './adapter-registry/adapter-registry'; -import * as manifest from './manifest'; +import { isSessionEvent, SessionTracker } from './session-tracker'; export interface LabeledUint8Array extends Uint8Array { label?: string; } export class MemoryProvider { - public static ReadKey = `${manifest.PACKAGE_NAME}.canRead`; - public static WriteKey = `${manifest.PACKAGE_NAME}.canWrite`; + protected scheduledOnDidMemoryWriteEvents: { [sessionidmemoryReference: string]: ((response: WriteMemoryResult) => void) | undefined } = {}; - private _onDidStopDebug = new vscode.EventEmitter(); - public readonly onDidStopDebug = this._onDidStopDebug.event; - - private _onDidWriteMemory = new vscode.EventEmitter(); - public readonly onDidWriteMemory = this._onDidWriteMemory.event; - - private _onDidChangeSessionContext = new vscode.EventEmitter(); - public readonly onDidChangeSessionContext = this._onDidChangeSessionContext.event; - - protected readonly sessionDebugCapabilities = new Map(); - protected readonly sessionClientCapabilities = new Map(); - protected scheduledOnDidMemoryWriteEvents: { [memoryReference: string]: ((response: WriteMemoryResult) => void) | undefined } = {}; - - constructor(protected adapterRegistry: AdapterRegistry) { + constructor(protected adapterRegistry: AdapterRegistry, protected sessionTracker: SessionTracker) { + this.sessionTracker.onSessionEvent(event => { + if (isSessionEvent('memory-written', event)) { + delete this.scheduledOnDidMemoryWriteEvents[event.session.raw.id + '_' + event.data.memoryReference]; + } + }); } public activate(context: vscode.ExtensionContext): void { - const createDebugAdapterTracker = (session: vscode.DebugSession): Required => { + const createDebugAdapterTracker = (session: vscode.DebugSession): vscode.ProviderResult => { const handlerForSession = this.adapterRegistry.getHandlerForSession(session.type); const contributedTracker = handlerForSession?.initializeAdapterTracker?.(session); - - return ({ - onWillStartSession: () => { - this.debugSessionStarted(session); - contributedTracker?.onWillStartSession?.(); - }, - onWillStopSession: () => { - this.debugSessionTerminated(session); - contributedTracker?.onWillStopSession?.(); - }, - onDidSendMessage: message => { - if (isDebugResponse('initialize', message)) { - // Check for right capabilities in the adapter - this.sessionDebugCapabilities.set(session.id, message.body); - if (vscode.debug.activeDebugSession?.id === session.id) { - this.setContext(session); - } - } else if (isDebugEvent('stopped', message)) { - this._onDidStopDebug.fire(session); - } else if (isDebugEvent('memory', message)) { - delete this.scheduledOnDidMemoryWriteEvents[message.body.memoryReference]; - this._onDidWriteMemory.fire(message.body); - } - contributedTracker?.onDidSendMessage?.(message); - }, - onError: error => { contributedTracker?.onError?.(error); }, - onExit: (code, signal) => { contributedTracker?.onExit?.(code, signal); }, - 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 => this.setContext(session)) - ); - } - - protected async debugSessionStarted(_session: vscode.DebugSession): Promise { - // Do nothing for now - } - - protected debugSessionTerminated(session: vscode.DebugSession): void { - 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 + return contributedTracker; }; - } - - 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.hasDebugCapability(session, capability)) { - throw new Error(`Cannot ${action}. Session does not have capability ${capability}.`); - } - return session; - } - - protected hasDebugCapability(session: vscode.DebugSession, capability: keyof DebugProtocol.Capabilities): boolean { - return !!this.sessionDebugCapabilities.get(session.id)?.[capability]; - } - - protected hasClientCapability(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.`); - } - return vscode.debug.activeDebugSession; + context.subscriptions.push(vscode.debug.registerDebugAdapterTrackerFactory('*', { createDebugAdapterTracker })); } public async readMemory(args: DebugProtocol.ReadMemoryArguments): Promise { - return sendRequest(this.assertCapability('supportsReadMemoryRequest', 'read memory'), 'readMemory', args); + return sendRequest(this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsReadMemoryRequest', 'read memory'), 'readMemory', args); } public async writeMemory(args: DebugProtocol.WriteMemoryArguments): Promise { - const session = this.assertCapability('supportsWriteMemoryRequest', 'write memory'); + const session = this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsWriteMemoryRequest', 'write memory'); // Schedule a emit in case we don't retrieve a memory event - this.scheduledOnDidMemoryWriteEvents[args.memoryReference] = response => { + this.scheduledOnDidMemoryWriteEvents[session.id + '_' + args.memoryReference] = response => { // 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 }); + // if our custom handler is active, let's fire the event ourselves + this.sessionTracker.fireSessionEvent(session, 'memory-written', { memoryReference: args.memoryReference, offset, count }); }; return sendRequest(session, 'writeMemory', args).then(response => { // The memory event is handled before we got here, if the scheduled event still exists, we need to handle it - this.scheduledOnDidMemoryWriteEvents[args.memoryReference]?.(response); + this.scheduledOnDidMemoryWriteEvents[session.id + '_' + args.memoryReference]?.(response); return response; }); } public async getVariables(variableArguments: DebugProtocol.ReadMemoryArguments): Promise { - const session = this.assertActiveSession('get variables'); + const session = this.sessionTracker.assertActiveSession('get variables'); const handler = this.adapterRegistry?.getHandlerForSession(session.type); 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 session = this.sessionTracker.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 session = this.sessionTracker.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 index 064f25b..8604036 100644 --- a/src/plugin/memory-storage.ts +++ b/src/plugin/memory-storage.ts @@ -18,6 +18,7 @@ import MemoryMap from 'nrf-intel-hex'; import * as vscode from 'vscode'; import { URI, Utils } from 'vscode-uri'; import { IntelHEX } from '../common/intel-hex'; +import * as manifest from '../common/manifest'; import { bytesToStringMemory, createMemoryFromRead, validateCount, validateMemoryReference, validateOffset @@ -26,7 +27,6 @@ import { toHexStringWithRadixMarker } from '../common/memory-range'; import { ApplyMemoryArguments, ApplyMemoryResult, MemoryOptions, StoreMemoryArguments } from '../common/messaging'; import { isWebviewContext } from '../common/webview-context'; import { isVariablesContext } from './external-views'; -import * as manifest from './manifest'; import { MemoryProvider } from './memory-provider'; export const StoreCommandType = `${manifest.PACKAGE_NAME}.store-file`; diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 6fb65d9..9439ca6 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -17,7 +17,8 @@ import * as vscode from 'vscode'; import { Messenger } from 'vscode-messenger'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; -import { Endianness, VariableRange } from '../common/memory-range'; +import * as manifest from '../common/manifest'; +import { VariableRange } from '../common/memory-range'; import { applyMemoryType, getVariablesType, @@ -43,18 +44,13 @@ import { WriteMemoryResult, writeMemoryType, } from '../common/messaging'; +import { MemoryViewSettings, ScrollingBehavior } from '../common/webview-configuration'; import { getVisibleColumns, isWebviewVariableContext, WebviewContext } from '../common/webview-context'; -import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types'; import { isVariablesContext } from './external-views'; import { outputChannelLogger } from './logger'; -import * as manifest from './manifest'; import { MemoryProvider } from './memory-provider'; import { ApplyCommandType, StoreCommandType } from './memory-storage'; - -enum RefreshEnum { - off = 0, - on = 1 -} +import { isSessionEvent, SessionEvent, SessionTracker } from './session-tracker'; const CONFIGURABLE_COLUMNS = [ manifest.CONFIG_SHOW_ASCII_COLUMN, @@ -73,19 +69,11 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { public static GetWebviewSelectionCommandType = `${manifest.PACKAGE_NAME}.get-webview-selection`; protected messenger: Messenger; - protected refreshOnStop: RefreshEnum; protected panelIndices: number = 1; - public constructor(protected extensionUri: vscode.Uri, protected memoryProvider: MemoryProvider) { + public constructor(protected extensionUri: vscode.Uri, protected memoryProvider: MemoryProvider, protected sessionTracker: SessionTracker) { this.messenger = new Messenger(); - - this.refreshOnStop = this.getRefresh(); - vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(`${manifest.PACKAGE_NAME}.${manifest.CONFIG_REFRESH_ON_STOP}`)) { - this.refreshOnStop = this.getRefresh(); - } - }); } public activate(context: vscode.ExtensionContext): void { @@ -171,11 +159,6 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.setWebviewMessageListener(panel, initialMemory); } - protected getRefresh(): RefreshEnum { - const config = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_REFRESH_ON_STOP) || manifest.DEFAULT_REFRESH_ON_STOP; - return RefreshEnum[config as keyof typeof RefreshEnum]; - } - protected async getWebviewContent(panel: vscode.WebviewPanel): Promise { const mainUri = panel.webview.asWebviewUri(vscode.Uri.joinPath( this.extensionUri, @@ -208,16 +191,9 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { 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 => { - options = { ...options, ...o }; - }, { sender: participant }), + this.messenger.onNotification(readyType, () => this.initialize(participant, panel, options), { sender: participant }), + this.messenger.onRequest(setOptionsType, newOptions => { options = { ...options, ...newOptions }; }, { sender: participant }), 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 }), @@ -226,23 +202,17 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.onNotification(setTitleType, title => { panel.title = title; }, { sender: participant }), this.messenger.onRequest(storeMemoryType, args => this.storeMemory(args), { sender: participant }), this.messenger.onRequest(applyMemoryType, () => this.applyMemory(), { sender: participant }), - - this.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)) + this.sessionTracker.onSessionEvent(event => this.handleSessionEvent(participant, event)) ]; - panel.onDidChangeViewState(newState => { - if (newState.webviewPanel.visible) { - this.refresh(participant, options); - } - }); panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); } + protected async initialize(participant: WebviewIdMessageParticipant, panel: vscode.WebviewPanel, options?: MemoryOptions): Promise { + this.setSessionContext(participant, this.createContext()); + this.setInitialSettings(participant, panel.title); + this.refresh(participant, options); + } + protected async refresh(participant: WebviewIdMessageParticipant, options: MemoryOptions = {}): Promise { this.messenger.sendRequest(setOptionsType, participant, options); } @@ -264,17 +234,47 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { const bytesPerMau = memoryInspectorConfiguration.get(manifest.CONFIG_BYTES_PER_MAU, manifest.DEFAULT_BYTES_PER_MAU); const mausPerGroup = memoryInspectorConfiguration.get(manifest.CONFIG_MAUS_PER_GROUP, manifest.DEFAULT_MAUS_PER_GROUP); const groupsPerRow = memoryInspectorConfiguration.get(manifest.CONFIG_GROUPS_PER_ROW, manifest.DEFAULT_GROUPS_PER_ROW); - const endianness = memoryInspectorConfiguration.get(manifest.CONFIG_ENDIANNESS, manifest.DEFAULT_ENDIANNESS); + const endianness = memoryInspectorConfiguration.get(manifest.CONFIG_ENDIANNESS, manifest.DEFAULT_ENDIANNESS); const scrollingBehavior = memoryInspectorConfiguration.get(manifest.CONFIG_SCROLLING_BEHAVIOR, manifest.DEFAULT_SCROLLING_BEHAVIOR); const visibleColumns = CONFIGURABLE_COLUMNS .filter(column => vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(column, false)) .map(columnId => columnId.replace('columns.', '')); - const addressPadding = AddressPaddingOptions[memoryInspectorConfiguration.get(manifest.CONFIG_ADDRESS_PADDING, manifest.DEFAULT_ADDRESS_PADDING)]; + const addressPadding = memoryInspectorConfiguration.get(manifest.CONFIG_ADDRESS_PADDING, manifest.DEFAULT_ADDRESS_PADDING); const addressRadix = memoryInspectorConfiguration.get(manifest.CONFIG_ADDRESS_RADIX, manifest.DEFAULT_ADDRESS_RADIX); const showRadixPrefix = memoryInspectorConfiguration.get(manifest.CONFIG_SHOW_RADIX_PREFIX, manifest.DEFAULT_SHOW_RADIX_PREFIX); + const refreshOnStop = memoryInspectorConfiguration.get(manifest.CONFIG_REFRESH_ON_STOP, manifest.DEFAULT_REFRESH_ON_STOP); + const periodicRefresh = memoryInspectorConfiguration.get(manifest.CONFIG_PERIODIC_REFRESH, manifest.DEFAULT_PERIODIC_REFRESH); + const periodicRefreshInterval = memoryInspectorConfiguration.get(manifest.CONFIG_PERIODIC_REFRESH_INTERVAL, manifest.DEFAULT_PERIODIC_REFRESH_INTERVAL); return { messageParticipant, title, bytesPerMau, mausPerGroup, groupsPerRow, - endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix + endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix, + refreshOnStop, periodicRefresh, periodicRefreshInterval + }; + } + + protected handleSessionEvent(participant: WebviewIdMessageParticipant, event: SessionEvent): void { + if (isSessionEvent('active', event)) { + this.setSessionContext(participant, this.createContext(event.session?.raw)); + return; + } + // we are only interested in the events of the active session + if (!event.session?.active) { + return; + } + if (isSessionEvent('memory-written', event)) { + this.messenger.sendNotification(memoryWrittenType, participant, event.data); + } else { + this.setSessionContext(participant, this.createContext(event.session.raw)); + } + } + + protected createContext(session = this.sessionTracker.activeSession): SessionContext { + const sessionId = session?.id; + return { + sessionId, + canRead: !!this.sessionTracker.hasDebugCapabilitiy(session, 'supportsReadMemoryRequest'), + canWrite: !!this.sessionTracker.hasDebugCapabilitiy(session, 'supportsWriteMemoryRequest'), + stopped: this.sessionTracker.isStopped(session) }; } @@ -322,18 +322,14 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { 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.'); - } + this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsReadMemoryRequest', 'store memory'); 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.'); - } + this.sessionTracker.assertDebugCapability(this.sessionTracker.activeSession, 'supportsWriteMemoryRequest', 'apply memory'); return vscode.commands.executeCommand(ApplyCommandType); } diff --git a/src/plugin/session-tracker.ts b/src/plugin/session-tracker.ts new file mode 100644 index 0000000..3ca95b4 --- /dev/null +++ b/src/plugin/session-tracker.ts @@ -0,0 +1,182 @@ +/******************************************************************************** + * 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 { isDebugEvent, isDebugRequest, isDebugResponse } from '../common/debug-requests'; +import { WrittenMemory } from '../common/memory-range'; + +export interface SessionInfo { + raw: vscode.DebugSession; + debugCapabilities?: DebugProtocol.Capabilities; + clientCapabilities?: DebugProtocol.InitializeRequestArguments; + active?: boolean; + stopped?: boolean; +} + +export interface SessionEvent { + event: string; + session?: SessionInfo; + data?: unknown; +} + +export interface ActiveSessionChangedEvent extends SessionEvent { + event: 'active'; +} + +export interface SessionMemoryWrittenEvent extends SessionEvent { + event: 'memory-written'; + session: SessionInfo; + data: WrittenMemory; +} + +export interface SessionStoppedEvent extends SessionEvent { + event: 'stopped'; + session: SessionInfo; +} + +export interface SessionContinuedEvent extends SessionEvent { + event: 'continued'; + session: SessionInfo; +} + +export interface SessionEvents { + 'active': ActiveSessionChangedEvent, + 'memory-written': SessionMemoryWrittenEvent, + 'continued': SessionContinuedEvent, + 'stopped': SessionStoppedEvent +} + +export type DebugCapability = keyof DebugProtocol.Capabilities; +export type ClientCapability = keyof DebugProtocol.InitializeRequestArguments; + +export function isSessionEvent(event: K, message: unknown): message is SessionEvents[K] { + const assumed = message ? message as SessionEvent : undefined; + return !!assumed && assumed.event === event; +} + +export class SessionTracker implements vscode.DebugAdapterTrackerFactory { + protected toDispose: vscode.Disposable[] = []; + + protected readonly _sessionInfo = new Map(); + + private _onSessionEvent = new vscode.EventEmitter(); + public readonly onSessionEvent = this._onSessionEvent.event; + + activate(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.debug.registerDebugAdapterTrackerFactory('*', this), + vscode.debug.onDidChangeActiveDebugSession(session => this.activeSessionChanged(session)) + ); + } + + createDebugAdapterTracker(session: vscode.DebugSession): vscode.ProviderResult { + return ({ + onWillStartSession: () => this.sessionWillStart(session), + onWillStopSession: () => this.sessionWillStop(session), + onDidSendMessage: message => this.adapterMessageReceived(session, message), + onWillReceiveMessage: message => this.willSendClientMessage(session, message) + }); + } + + protected sessionInfo(session: vscode.DebugSession): SessionInfo { + let info = this._sessionInfo.get(session.id); + if (!info) { + info = { raw: session }; + this._sessionInfo.set(session.id, info); + } + return info; + } + + protected async activeSessionChanged(session?: vscode.DebugSession): Promise { + for (const [sessionId, info] of this._sessionInfo) { + info.active = sessionId === session?.id; + } + this._onSessionEvent.fire({ event: 'active', session: session ? this.sessionInfo(session) : undefined }); + } + + fireSessionEvent>(session: vscode.DebugSession, event: K, data: SessionEvents[K]['data']): void { + this._onSessionEvent.fire({ event, session: this.sessionInfo(session), data }); + } + + protected async sessionWillStart(session: vscode.DebugSession): Promise { + this._sessionInfo.set(session.id, { raw: session }); + } + + protected sessionWillStop(session: vscode.DebugSession): void { + this._sessionInfo.delete(session.id); + } + + protected willSendClientMessage(session: vscode.DebugSession, message: unknown): void { + if (isDebugRequest('initialize', message)) { + this.sessionInfo(session).clientCapabilities = message.arguments; + } + } + + protected adapterMessageReceived(session: vscode.DebugSession, message: unknown): void { + if (isDebugResponse('initialize', message)) { + this.sessionInfo(session).debugCapabilities = message.body; + } else if (isDebugEvent('stopped', message)) { + this.sessionInfo(session).stopped = true; + this.fireSessionEvent(session, 'stopped', undefined); + } else if (isDebugEvent('continued', message)) { + this.sessionInfo(session).stopped = false; + this.fireSessionEvent(session, 'continued', undefined); + } else if (isDebugEvent('memory', message)) { + this.fireSessionEvent(session, 'memory-written', message.body); + } + } + + get activeSession(): vscode.DebugSession | undefined { + return vscode.debug.activeDebugSession; + } + + assertActiveSession(action: string = 'get session'): vscode.DebugSession { + if (!this.activeSession) { + throw new Error(`Cannot ${action}. No active debug session.`); + } + return this.activeSession; + } + + isActive(session = this.activeSession): boolean { + return !!session && vscode.debug.activeDebugSession?.id === session?.id; + } + + isStopped(session = this.activeSession): boolean { + return !!session && !!this.sessionInfo(session).stopped; + } + + hasDebugCapabilitiy(session = this.activeSession, capability: DebugCapability): boolean { + return !!session && !!this.sessionInfo(session).debugCapabilities?.[capability]; + } + + assertDebugCapability(session = this.assertActiveSession(), capability: DebugCapability, action: string = 'execute action'): vscode.DebugSession { + if (!this.hasDebugCapabilitiy(session, capability)) { + throw new Error(`Cannot ${action}. Session does not have capability '${capability}'.`); + } + return session; + } + + hasClientCapabilitiy(session: vscode.DebugSession | undefined, capability: ClientCapability): boolean { + return !!session && !!this.sessionInfo(session).clientCapabilities?.[capability]; + } + + assertClientCapability(session = this.assertActiveSession(), capability: ClientCapability, action: string = 'execute action'): vscode.DebugSession { + if (!this.hasClientCapabilitiy(session, capability)) { + throw new Error(`Cannot ${action}. Client does not have capability '${capability}'.`); + } + return session; + } +} diff --git a/src/webview/columns/data-column.tsx b/src/webview/columns/data-column.tsx index 4c47f61..c6a8b59 100644 --- a/src/webview/columns/data-column.tsx +++ b/src/webview/columns/data-column.tsx @@ -18,7 +18,7 @@ import { InputText } from 'primereact/inputtext'; import * as React from 'react'; import { HOST_EXTENSION } from 'vscode-messenger-common'; import { Memory } from '../../common/memory'; -import { BigIntMemoryRange, Endianness, isWithin, toHexStringWithRadixMarker, toOffset } from '../../common/memory-range'; +import { BigIntMemoryRange, isWithin, toHexStringWithRadixMarker, toOffset } from '../../common/memory-range'; import { writeMemoryType } from '../../common/messaging'; import type { MemorySizeOptions } from '../components/memory-table'; import { decorationService } from '../decorations/decoration-service'; @@ -131,7 +131,7 @@ export class EditableDataColumnRow extends React.Component(group: T[], options: TableRenderOptions): T[] { // Assume data from the DAP comes in Big Endian so we need to revert the order if we use Little Endian - return options.endianness === Endianness.Big ? group : group.reverse(); + return options.endianness === 'Big Endian' ? group : group.reverse(); } protected renderEditingGroup(editedRange: BigIntMemoryRange): React.ReactNode { @@ -213,7 +213,7 @@ export class EditableDataColumnRow extends React.Component; protected extendedOptions = React.createRef(); protected labelEditInput = React.createRef(); + protected refreshRateInput = React.createRef(); protected coreOptionsDiv = React.createRef(); protected optionsMenuContext = createSectionVscodeContext('optionsWidget'); protected advancedOptionsContext = createSectionVscodeContext('advancedOptionsOverlay'); @@ -96,9 +101,7 @@ export class OptionsWidget extends React.Component { - this.props.fetchMemory(this.props.configuredReadArguments); - }, + onSubmit: () => this.props.fetchMemory(this.props.configuredReadArguments), }; this.state = { isTitleEditing: false }; } @@ -365,7 +368,7 @@ export class OptionsWidget extends React.Component

Address Format

@@ -409,6 +412,41 @@ export class OptionsWidget extends React.Component + +

Refresh

+
+ + +
+ + + + +
+ + +
@@ -499,6 +537,12 @@ export class OptionsWidget extends React.Component | React.KeyboardEvent) => void = + event => this.doHandlePeriodicRefreshIntervalChange(event); + doHandlePeriodicRefreshIntervalChange(event: React.FocusEvent | React.KeyboardEvent): void { + if (!('key' in event) || event.key === 'Enter') { + const periodicRefreshInterval = tryToNumber(event.currentTarget.value) ?? DEFAULT_MEMORY_DISPLAY_CONFIGURATION.periodicRefreshInterval; + this.props.updateRenderOptions({ periodicRefreshInterval }); + } + } + protected handleResetAdvancedOptions: MouseEventHandler | undefined = () => this.props.resetRenderOptions(); protected enableTitleEditing = () => this.doEnableTitleEditing(); diff --git a/src/webview/hovers/hover-service.tsx b/src/webview/hovers/hover-service.tsx index 9c9ed5e..aea3aab 100644 --- a/src/webview/hovers/hover-service.tsx +++ b/src/webview/hovers/hover-service.tsx @@ -17,8 +17,9 @@ import * as React from 'react'; import { HOST_EXTENSION } from 'vscode-messenger-common'; import { logMessageType } from '../../common/messaging'; +import { MemoryDisplayConfiguration } from '../../common/webview-configuration'; import { MemoryAppState } from '../memory-webview-view'; -import { Disposable, MemoryDisplayConfiguration } from '../utils/view-types'; +import { Disposable } from '../utils/view-types'; import { messenger } from '../view-messenger'; export interface HoverableDetails { diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index 32bb5f6..34c04bb 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -15,13 +15,15 @@ ********************************************************************************/ import 'primeflex/primeflex.css'; + import { debounce } from 'lodash'; import { PrimeReactProvider } from 'primereact/api'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { HOST_EXTENSION, WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import * as manifest from '../common/manifest'; import { createMemoryFromRead, Memory } from '../common/memory'; -import { BigIntMemoryRange, doOverlap, Endianness, getAddressLength, getAddressString, WrittenMemory } from '../common/memory-range'; +import { BigIntMemoryRange, doOverlap, getAddressLength, getAddressString, WrittenMemory } from '../common/memory-range'; import { applyMemoryType, getWebviewSelectionType, @@ -40,6 +42,8 @@ import { storeMemoryType, WebviewSelection, } from '../common/messaging'; +import { Change, hasChanged, hasChangedTo } from '../common/typescript'; +import { MemoryDisplayConfiguration } from '../common/webview-configuration'; import { AddressColumn } from './columns/address-column'; import { AsciiColumn } from './columns/ascii-column'; import { columnContributionService, ColumnStatus } from './columns/column-contribution-service'; @@ -48,9 +52,9 @@ import { MemoryWidget } from './components/memory-widget'; import { decorationService } from './decorations/decoration-service'; import { AddressHover } from './hovers/address-hover'; import { DataHover } from './hovers/data-hover'; -import { hoverService, HoverService } from './hovers/hover-service'; +import { HoverService, hoverService } from './hovers/hover-service'; import { VariableHover } from './hovers/variable-hover'; -import { Decoration, DEFAULT_READ_ARGUMENTS, MemoryDisplayConfiguration, MemoryState } from './utils/view-types'; +import { Decoration, DEFAULT_READ_ARGUMENTS, MemoryState } from './utils/view-types'; import { variableDecorator } from './variables/variable-decorations'; import { messenger } from './view-messenger'; @@ -65,24 +69,28 @@ export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration isFrozen: boolean; } -const DEFAULT_SESSION_CONTEXT: SessionContext = { +export const DEFAULT_SESSION_CONTEXT: SessionContext = { canRead: false, canWrite: false }; -const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = { - bytesPerMau: 1, - mausPerGroup: 1, - groupsPerRow: 4, - endianness: Endianness.Little, - scrollingBehavior: 'Paginate', - addressPadding: 'Min', - addressRadix: 16, - showRadixPrefix: true, +export const DEFAULT_MEMORY_DISPLAY_CONFIGURATION: MemoryDisplayConfiguration = { + bytesPerMau: manifest.DEFAULT_BYTES_PER_MAU, + mausPerGroup: manifest.DEFAULT_MAUS_PER_GROUP, + groupsPerRow: manifest.DEFAULT_GROUPS_PER_ROW, + endianness: manifest.DEFAULT_ENDIANNESS, + scrollingBehavior: manifest.DEFAULT_SCROLLING_BEHAVIOR, + addressPadding: manifest.DEFAULT_ADDRESS_PADDING, + addressRadix: manifest.DEFAULT_ADDRESS_RADIX, + showRadixPrefix: manifest.DEFAULT_SHOW_RADIX_PREFIX, + refreshOnStop: manifest.DEFAULT_REFRESH_ON_STOP, + periodicRefresh: manifest.DEFAULT_PERIODIC_REFRESH, + periodicRefreshInterval: manifest.DEFAULT_PERIODIC_REFRESH_INTERVAL }; class App extends React.Component<{}, MemoryAppState> { protected memoryWidget = React.createRef(); + protected refreshTimer?: NodeJS.Timeout | number; public constructor(props: {}) { super(props); @@ -107,7 +115,7 @@ class App extends React.Component<{}, MemoryAppState> { columns: columnContributionService.getColumns(), isMemoryFetching: false, isFrozen: false, - ...MEMORY_DISPLAY_CONFIGURATION_DEFAULTS + ...DEFAULT_MEMORY_DISPLAY_CONFIGURATION }; } @@ -128,21 +136,47 @@ class App extends React.Component<{}, MemoryAppState> { messenger.onRequest(getWebviewSelectionType, () => this.getWebviewSelection()); messenger.onNotification(showAdvancedOptionsType, () => this.showAdvancedOptions()); messenger.sendNotification(readyType, HOST_EXTENSION, undefined); + this.updatePeriodicRefresh(); } - public componentDidUpdate(_: {}, prevState: MemoryAppState): void { - const addressPaddingNeedsUpdate = - (this.state.addressPadding === 'Min' && this.state.memory !== prevState.memory) - || this.state.addressPadding !== prevState.addressPadding; - if (addressPaddingNeedsUpdate) { + public componentDidUpdate(_: {}, from: MemoryAppState): void { + const current = this.state; + const stateChange: Change = { from, to: current }; + const sessionContextChange: Change = { from: from.sessionContext, to: current.sessionContext }; + + if (hasChanged(stateChange, 'addressPadding') || (this.state.addressPadding === 'Minimal' && hasChanged(stateChange, 'memory'))) { const effectiveAddressLength = this.getEffectiveAddressLength(this.state.memory); if (this.state.effectiveAddressLength !== effectiveAddressLength) { this.setState({ effectiveAddressLength }); } } + if (hasChanged(stateChange, 'periodicRefresh') || hasChanged(stateChange, 'periodicRefreshInterval') || hasChanged(sessionContextChange, 'stopped')) { + this.updatePeriodicRefresh(); + } + + if (current.refreshOnStop === 'on' && hasChangedTo(sessionContextChange, 'stopped', true)) { + this.fetchMemory(); + } + hoverService.setMemoryState(this.state); } + componentWillUnmount(): void { + clearTimeout(this.refreshTimer); + } + + protected updatePeriodicRefresh = (): void => { + clearTimeout(this.refreshTimer); + + if (this.state.periodicRefreshInterval && this.state.periodicRefreshInterval > 0 && + this.state.periodicRefresh === 'always' || (this.state.periodicRefresh === 'while running' && !this.state.sessionContext.stopped)) { + // we do not use an interval here as we only want to schedule another refresh AFTER the previous execution AND the delay has passed + // and not strictly every n milliseconds. Even if 'fetchMemory' fails here, we schedule another auto-refresh. + const scheduleRefresh = () => this.fetchMemory().finally(() => this.updatePeriodicRefresh()); + this.refreshTimer = setTimeout(scheduleRefresh, this.state.periodicRefreshInterval); + } + }; + // use a slight debounce as the same event may come in short succession protected memoryWritten = debounce((writtenMemory: WrittenMemory): void => { if (!this.state.memory) { @@ -215,6 +249,9 @@ class App extends React.Component<{}, MemoryAppState> { showRadixPrefix={this.state.showRadixPrefix} storeMemory={this.storeMemory} applyMemory={this.applyMemory} + refreshOnStop={this.state.refreshOnStop} + periodicRefresh={this.state.periodicRefresh} + periodicRefreshInterval={this.state.periodicRefreshInterval} /> ; } @@ -222,6 +259,7 @@ class App extends React.Component<{}, MemoryAppState> { 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) => { this.setState({ title }); messenger.sendNotification(setTitleType, HOST_EXTENSION, title); @@ -229,17 +267,12 @@ class App extends React.Component<{}, MemoryAppState> { 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 - this.setState(prevState => ({ ...prevState, configuredReadArguments: { ...this.state.configuredReadArguments, ...options } })); - } - + this.setState({ configuredReadArguments: { ...this.state.configuredReadArguments, ...options } }); return this.fetchMemory(options); } - protected fetchMemory = async (partialOptions?: MemoryOptions): Promise => this.doFetchMemory(partialOptions); - protected async doFetchMemory(partialOptions?: MemoryOptions): Promise { - if (this.state.isFrozen) { + protected fetchMemory = async (partialOptions?: MemoryOptions): Promise => { + if (this.state.isFrozen || !this.state.sessionContext.canRead) { return; } const completeOptions = { @@ -247,52 +280,41 @@ class App extends React.Component<{}, MemoryAppState> { offset: partialOptions?.offset ?? this.state.activeReadArguments.offset, count: partialOptions?.count ?? this.state.activeReadArguments.count }; - // Don't fetch memory if we have an incomplete memory reference if (completeOptions.memoryReference === '') { return; } + return this.doFetchMemory(completeOptions); + }; - this.setState(prev => ({ ...prev, isMemoryFetching: true })); + protected async doFetchMemory(memoryOptions: Required): Promise { + this.setState({ isMemoryFetching: true, activeReadArguments: memoryOptions }); try { - const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions); + const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, memoryOptions); await Promise.all(Array.from( new Set(columnContributionService.getUpdateExecutors().concat(decorationService.getUpdateExecutors())), - executor => executor.fetchData(completeOptions) + executor => executor.fetchData(memoryOptions) )); const memory = createMemoryFromRead(response); - - this.setState(prev => ({ - ...prev, - decorations: decorationService.decorations, - memory, - activeReadArguments: completeOptions, - isMemoryFetching: false - })); - - messenger.sendRequest(setOptionsType, HOST_EXTENSION, completeOptions); + this.setState({ memory, decorations: decorationService.decorations }); + messenger.sendRequest(setOptionsType, HOST_EXTENSION, memoryOptions); } catch (ex) { // Do not show old results if the current search provided no memory - this.setState(prev => ({ - ...prev, - memory: undefined, - activeReadArguments: completeOptions, - })); + this.setState({ memory: undefined }); if (ex instanceof Error) { console.error(ex); } } finally { - this.setState(prev => ({ ...prev, isMemoryFetching: false })); + this.setState({ isMemoryFetching: false }); } - } protected getEffectiveAddressLength(memory?: Memory): number { const { addressRadix, addressPadding } = this.state; - return addressPadding === 'Min' ? this.getLastAddressLength(memory) : getAddressLength(addressPadding, addressRadix); + return addressPadding === 'Minimal' ? this.getLastAddressLength(memory) : getAddressLength(addressPadding, addressRadix); } protected getLastAddressLength(memory?: Memory): number { @@ -308,12 +330,12 @@ class App extends React.Component<{}, MemoryAppState> { protected toggleColumn = (id: string, active: boolean): void => { this.doToggleColumn(id, active); }; protected async doToggleColumn(id: string, isVisible: boolean): Promise { const columns = isVisible ? await columnContributionService.show(id, this.state) : columnContributionService.hide(id); - this.setState(prevState => ({ ...prevState, columns })); + this.setState({ columns }); } protected toggleFrozen = (): void => { this.doToggleFrozen(); }; protected doToggleFrozen(): void { - this.setState(prevState => ({ ...prevState, isFrozen: !prevState.isFrozen })); + this.setState(prevState => ({ isFrozen: !prevState.isFrozen })); } protected showAdvancedOptions(): void { diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 4e674e4..f6967f7 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -16,11 +16,11 @@ import deepequal from 'fast-deep-equal'; import type * as React from 'react'; -import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import { Endianness } from '../../common/manifest'; import { Memory } from '../../common/memory'; -import { areRangesEqual, BigIntMemoryRange, Endianness, Radix } from '../../common/memory-range'; +import { areRangesEqual, BigIntMemoryRange } from '../../common/memory-range'; import { ReadMemoryArguments } from '../../common/messaging'; -import { GroupsPerRowOption } from '../../plugin/manifest'; +import { MemoryDisplayConfiguration } from '../../common/webview-configuration'; export interface SerializedTableRenderOptions extends MemoryDisplayConfiguration { columnOptions: Array<{ label: string, doRender: boolean }>; @@ -79,37 +79,13 @@ export interface FullNodeAttributes extends StylableNodeAttributes { content: string; } -/** All settings related to memory view that can be specified for the webview from the extension "main". */ -export interface MemoryViewSettings extends ColumnVisibilityStatus, MemoryDisplayConfiguration { - title: string - messageParticipant: WebviewIdMessageParticipant; -} - -/** The memory display configuration that can be specified for the memory widget. */ -export interface MemoryDisplayConfiguration { - bytesPerMau: number; - mausPerGroup: number; - groupsPerRow: GroupsPerRowOption; - endianness: Endianness; - scrollingBehavior: ScrollingBehavior; - addressPadding: AddressPadding; - addressRadix: Radix; - showRadixPrefix: boolean; -} -export type ScrollingBehavior = 'Paginate' | 'Grow' | 'Auto-Append'; - -export type AddressPadding = 'Min' | number; export const AddressPaddingOptions = { - 'Minimal': 'Min', + 'Minimal': 'Minimal', 'Unpadded': 0, '32bit': 32, '64bit': 64, } as const; -export interface ColumnVisibilityStatus { - visibleColumns: string[]; -} - export type ReactInteraction = React.MouseEvent | React.KeyboardEvent; export function isTrigger(event: ReactInteraction): boolean { diff --git a/webpack.config.js b/webpack.config.js index f9dd75f..624a925 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,7 +8,7 @@ const webpack = require('webpack'); /** @type {WebpackConfig} */ const common = { mode: 'development', - devtool: 'source-map', + devtool: 'inline-source-map', module: { rules: [ {