From 9322f7a5dfd71006c05f683baef4d58369b6aab9 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Tue, 5 Mar 2024 17:32:43 +0100 Subject: [PATCH] Extend Auto Refresh capabilities with 'After Delay' and 'On Focus' - Rework 'refreshOnStop' boolean setting to 'autoRefresh' enumerable -- On Stop (previously: 'on' for 'refreshOnStop' -- On Focus (previously: always implicit on view state change) -- After Delay (new) -- Off (previously: 'off' for 'refreshOnStop') - On Stop -- Rework global setting to be local for each Memory Inspector -- Listen to debug session stopped event and propagate to view - On Focus -- Rework implicit refresh update to option in setting -- Listen to view state changes and propagate to view - After Delay -- New option to explicitly define a delay when re-fetching the memory -- Minimum: 500ms, default: 500, input step size: 250ms Refactoring: - Split debug session tracking into dedicated class with session events -- Convert debug events into session events with additional data - Split context tracking from memory provider into dedicated class - Move manifest to common for default values and avoid duplication -- Align 'Min' with 'Minimal' value from manifest Minor: - Add title toolbar item for C/C++ file to access open memory inspector - Improve debugging experience by using inline source maps - Align creation of option enums to use const objects - Additionally guard 'body' on debug responses for safety - Avoid functional React state update where unnecessary Fixes #91 --- media/options-widget.css | 3 +- package.json | 34 +++- src/common/debug-requests.ts | 1 + src/{plugin => common}/manifest.ts | 16 +- src/common/memory-range.ts | 5 - src/common/messaging.ts | 7 + src/common/typescript.ts | 13 ++ src/entry-points/browser/extension.ts | 9 +- src/entry-points/desktop/extension.ts | 11 +- .../adapter-registry/adapter-capabilities.ts | 4 +- src/plugin/adapter-registry/c-adapter.ts | 2 +- src/plugin/context-tracker.ts | 35 ++++ src/plugin/logger.ts | 2 +- src/plugin/memory-provider.ts | 141 +++----------- src/plugin/memory-storage.ts | 2 +- src/plugin/memory-webview-main.ts | 112 +++++------ src/plugin/session-tracker.ts | 182 ++++++++++++++++++ src/webview/columns/data-column.tsx | 6 +- src/webview/components/memory-widget.tsx | 4 + src/webview/components/options-widget.tsx | 70 ++++++- src/webview/memory-webview-view.tsx | 139 ++++++++----- src/webview/utils/view-types.ts | 10 +- webpack.config.js | 2 +- 23 files changed, 548 insertions(+), 262 deletions(-) rename src/{plugin => common}/manifest.ts (82%) create mode 100644 src/plugin/context-tracker.ts create mode 100644 src/plugin/session-tracker.ts diff --git a/media/options-widget.css b/media/options-widget.css index 4cd3874..450f546 100644 --- a/media/options-widget.css +++ b/media/options-widget.css @@ -120,7 +120,8 @@ gap: 8px; } -.advanced-options-dropdown { +.advanced-options-dropdown, +.advanced-options-input { width: 100%; } diff --git a/package.json b/package.json index b95ba39..9c64ed6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,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" @@ -79,6 +79,7 @@ { "command": "memory-inspector.show", "title": "Show Memory Inspector", + "icon": "$(file-binary)", "category": "Memory" }, { @@ -201,6 +202,13 @@ "group": "display@6", "when": "webviewId === memory-inspector.memory" } + ], + "editor/title": [ + { + "command": "memory-inspector.show", + "group": "navigation", + "when": "memory-inspector.canRead && (resourceLangId === c || resourceLangId === cpp)" + } ] }, "customEditors": [ @@ -242,18 +250,28 @@ ], "description": "C-based debuggers to activate (requires debug session restart)" }, - "memory-inspector.refreshOnStop": { + "memory-inspector.autoRefresh": { "type": "string", "enum": [ - "on", - "off" + "On Stop", + "On Focus", + "After Delay", + "Off" ], "enumDescriptions": [ - "Refresh memory views when when debugger stops (e.g. a breakpoint is hit)", - "Memory view data is manually refreshed by user" + "Refresh when the debugger stops (e.g. a breakpoint is hit)", + "Refresh automatically when the view is newly focussed.", + "Refresh automatically after the configured `#memory-inspector.autoRefreshDelay#`", + "Memory Inspector needs to be manually refreshed by the user" ], - "default": "on", - "description": "Refresh memory views when debugger stops" + "default": "On Stop", + "description": "Controls when the Memory Inspector is refreshed." + }, + "memory-inspector.autoRefreshDelay": { + "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.autoRefresh#` is set to `afterDelay`." }, "memory-inspector.groupings.bytesPerMAU": { "type": "number", diff --git a/src/common/debug-requests.ts b/src/common/debug-requests.ts index dd3d78a..9c723fc 100644 --- a/src/common/debug-requests.ts +++ b/src/common/debug-requests.ts @@ -29,6 +29,7 @@ export interface DebugRequestTypes { export interface DebugEvents { 'memory': DebugProtocol.MemoryEvent, + 'continued': DebugProtocol.ContinuedEvent, 'stopped': DebugProtocol.StoppedEvent } diff --git a/src/plugin/manifest.ts b/src/common/manifest.ts similarity index 82% rename from src/plugin/manifest.ts rename to src/common/manifest.ts index 98f76fc..2b094dd 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,17 @@ 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'; + +// Auto Refresh +export const CONFIG_AUTO_REFRESH = 'autoRefresh'; +export const AUTO_REFRESH_CHOICES = ['On Stop', 'On Focus', 'After Delay', 'Off'] as const; +export type AutoRefresh = (typeof AUTO_REFRESH_CHOICES)[number]; +export const DEFAULT_AUTO_REFRESH: AutoRefresh = 'On Stop'; +export const CONFIG_AUTO_REFRESH_DELAY = 'autoRefreshDelay'; +export const DEFAULT_AUTO_REFRESH_DELAY = 500; // Scroll export const CONFIG_SCROLLING_BEHAVIOR = 'scrollingBehavior'; diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 45ec989..44ddbed 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -125,8 +125,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..d433d15 100644 --- a/src/common/messaging.ts +++ b/src/common/messaging.ts @@ -42,6 +42,12 @@ export interface SessionContext { sessionId?: string; canRead: boolean; canWrite: boolean; + stopped?: boolean; +} + +export interface ViewState { + active: boolean; + visible: boolean; } // Notifications @@ -51,6 +57,7 @@ export const resetMemoryViewSettingsType: NotificationType = { method: 're export const setTitleType: NotificationType = { method: 'setTitle' }; export const memoryWrittenType: NotificationType = { method: 'memoryWritten' }; export const sessionContextChangedType: NotificationType = { method: 'sessionContextChanged' }; +export const viewStateChangedType: NotificationType = { method: 'viewStateChanged' }; // Requests export const setOptionsType: RequestType = { method: 'setOptions' }; 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/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 cd35d0e..2b87d84 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 0f122d6..ef3cf65 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -16,35 +16,26 @@ 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 { @@ -53,133 +44,55 @@ export class MemoryProvider { 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); - } + onWillStartSession: () => contributedTracker?.onWillStartSession?.(), + onWillStopSession: () => contributedTracker?.onWillStopSession?.(), + onDidSendMessage: message => contributedTracker?.onDidSendMessage?.(message), + onError: error => contributedTracker?.onError?.(error), + onExit: (code, signal) => contributedTracker?.onExit?.(code, signal), + onWillReceiveMessage: message => 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 - }; - } - - 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.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.`); - } - 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 38f9473..58f128c 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, @@ -38,23 +39,20 @@ import { showAdvancedOptionsType, StoreMemoryArguments, storeMemoryType, + ViewState, + viewStateChangedType, WebviewSelection, WriteMemoryArguments, WriteMemoryResult, writeMemoryType, } from '../common/messaging'; import { getVisibleColumns, WebviewContext } from '../common/webview-context'; -import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types'; +import type { 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, @@ -72,19 +70,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 { @@ -165,11 +155,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, @@ -200,18 +185,12 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { `; } - protected setWebviewMessageListener(panel: vscode.WebviewPanel, options?: MemoryOptions): void { + protected setWebviewMessageListener(panel: vscode.WebviewPanel, initialOptions?: MemoryOptions): void { const participant = this.messenger.registerWebviewPanel(panel); - + let memoryOptions = initialOptions; 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, memoryOptions), { sender: participant }), + this.messenger.onRequest(setOptionsType, newOptions => { memoryOptions = { ...memoryOptions, ...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 }), @@ -220,23 +199,19 @@ 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.onDidChangeViewState(newState => this.setViewState(participant, { active: newState.webviewPanel.active, visible: newState.webviewPanel.visible })); panel.onDidDispose(() => disposables.forEach(disposable => disposable.dispose())); } + protected async initialize(participant: WebviewIdMessageParticipant, panel: vscode.WebviewPanel, options?: MemoryOptions): Promise { + this.setSessionContext(participant, this.createContext()); + this.setViewState(participant, { active: panel.active, visible: panel.visible }); + this.setInitialSettings(participant, panel.title); + this.refresh(participant, options); + } + protected async refresh(participant: WebviewIdMessageParticipant, options: MemoryOptions = {}): Promise { this.messenger.sendRequest(setOptionsType, participant, options); } @@ -253,22 +228,55 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider { this.messenger.sendNotification(sessionContextChangedType, webviewParticipant, context); } + protected setViewState(webviewParticipant: WebviewIdMessageParticipant, state: ViewState): void { + this.messenger.sendNotification(viewStateChangedType, webviewParticipant, state); + } + protected getMemoryViewSettings(messageParticipant: WebviewIdMessageParticipant, title: string): MemoryViewSettings { const memoryInspectorConfiguration = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME); const bytesPerMau = memoryInspectorConfiguration.get(manifest.CONFIG_BYTES_PER_MAU, manifest.DEFAULT_BYTES_PER_MAU); 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 autoRefresh = memoryInspectorConfiguration.get(manifest.CONFIG_AUTO_REFRESH, manifest.DEFAULT_AUTO_REFRESH); + const autoRefreshDelay = memoryInspectorConfiguration.get(manifest.CONFIG_AUTO_REFRESH_DELAY, manifest.DEFAULT_AUTO_REFRESH_DELAY); return { messageParticipant, title, bytesPerMau, mausPerGroup, groupsPerRow, - endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix + endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix, + autoRefresh, autoRefreshDelay + }; + } + + 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) }; } @@ -316,18 +324,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..1d3b680 --- /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 }); + } + + protected 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 b84dc5e..41f7990 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'; @@ -134,7 +134,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 { @@ -221,7 +221,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,11 +101,9 @@ export class OptionsWidget extends React.Component { - this.props.fetchMemory(this.props.configuredReadArguments); - }, + onSubmit: () => this.props.fetchMemory(this.props.configuredReadArguments), }; - this.state = { isTitleEditing: false }; + this.state = { isTitleEditing: false, isEnablingAutoRefreshDelay: false }; } protected validate = (values: OptionsForm) => { @@ -125,6 +128,11 @@ export class OptionsWidget extends React.Component

Address Format

@@ -404,6 +412,42 @@ export class OptionsWidget extends React.Component + +

Auto-Refresh

+ + + +
+ + +
@@ -494,6 +538,10 @@ export class OptionsWidget extends React.Component | React.KeyboardEvent) => void = event => this.doHandleRefreshRateChange(event); + doHandleRefreshRateChange(event: React.FocusEvent | React.KeyboardEvent): void { + if (!('key' in event) || event.key === 'Enter') { + const autoRefreshDelay = tryToNumber(event.currentTarget.value) ?? DEFAULT_MEMORY_DISPLAY_CONFIGURATION.autoRefreshDelay; + this.props.updateRenderOptions({ autoRefreshDelay }); + } + } + protected handleResetAdvancedOptions: MouseEventHandler | undefined = () => this.props.resetRenderOptions(); protected enableTitleEditing = () => this.doEnableTitleEditing(); diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index e115d1a..7c2c083 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, @@ -39,8 +41,11 @@ import { setTitleType, showAdvancedOptionsType, storeMemoryType, + ViewState, + viewStateChangedType, WebviewSelection, } from '../common/messaging'; +import { Change, hasChanged, hasChangedTo } from '../common/typescript'; import { AddressColumn } from './columns/address-column'; import { AsciiColumn } from './columns/ascii-column'; import { columnContributionService, ColumnStatus } from './columns/column-contribution-service'; @@ -49,7 +54,7 @@ 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, MemoryDisplayConfiguration, MemoryState } from './utils/view-types'; import { variableDecorator } from './variables/variable-decorations'; @@ -58,6 +63,7 @@ import { messenger } from './view-messenger'; export interface MemoryAppState extends MemoryState, MemoryDisplayConfiguration { messageParticipant: WebviewIdMessageParticipant; title: string; + viewState: ViewState; sessionContext: SessionContext; effectiveAddressLength: number; decorations: Decoration[]; @@ -66,23 +72,30 @@ 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_VIEW_STATE: ViewState = { + active: false, + visible: false +}; + +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, + autoRefresh: manifest.DEFAULT_AUTO_REFRESH, + autoRefreshDelay: manifest.DEFAULT_AUTO_REFRESH_DELAY }; -const DEFAULT_READ_ARGUMENTS: Required = { +export const DEFAULT_READ_ARGUMENTS: Required = { memoryReference: '', offset: 0, count: 256, @@ -90,6 +103,7 @@ const DEFAULT_READ_ARGUMENTS: Required = { class App extends React.Component<{}, MemoryAppState> { protected memoryWidget = React.createRef(); + protected refreshTimer?: NodeJS.Timeout | number; public constructor(props: {}) { super(props); @@ -104,6 +118,7 @@ class App extends React.Component<{}, MemoryAppState> { this.state = { messageParticipant: { type: 'webview', webviewId: '' }, title: 'Memory', + viewState: DEFAULT_VIEW_STATE, sessionContext: DEFAULT_SESSION_CONTEXT, memory: undefined, effectiveAddressLength: 0, @@ -114,7 +129,7 @@ class App extends React.Component<{}, MemoryAppState> { columns: columnContributionService.getColumns(), isMemoryFetching: false, isFrozen: false, - ...MEMORY_DISPLAY_CONFIGURATION_DEFAULTS + ...DEFAULT_MEMORY_DISPLAY_CONFIGURATION }; } @@ -122,6 +137,7 @@ class App extends React.Component<{}, MemoryAppState> { messenger.onRequest(setOptionsType, options => this.setOptions(options)); messenger.onNotification(memoryWrittenType, writtenMemory => this.memoryWritten(writtenMemory)); messenger.onNotification(sessionContextChangedType, sessionContext => this.sessionContextChanged(sessionContext)); + messenger.onNotification(viewStateChangedType, viewState => this.viewStateChanged(viewState)); messenger.onNotification(setMemoryViewSettingsType, config => { if (config.visibleColumns) { for (const column of columnContributionService.getColumns()) { @@ -135,21 +151,48 @@ class App extends React.Component<{}, MemoryAppState> { messenger.onRequest(getWebviewSelectionType, () => this.getWebviewSelection()); messenger.onNotification(showAdvancedOptionsType, () => this.showAdvancedOptions()); messenger.sendNotification(readyType, HOST_EXTENSION, undefined); + this.updateAutoRefresh(); } - 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 }; + const viewStateChange: Change = { from: from.viewState, to: current.viewState }; + + 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, 'autoRefresh') || hasChanged(stateChange, 'autoRefreshDelay')) { + this.updateAutoRefresh(); + } + + if (current.autoRefresh === 'On Stop' && hasChangedTo(sessionContextChange, 'stopped', true) || + current.autoRefresh === 'On Focus' && hasChangedTo(viewStateChange, 'active', true)) { + this.fetchMemory(); + } + hoverService.setMemoryState(this.state); } + componentWillUnmount(): void { + clearTimeout(this.refreshTimer); + } + + protected updateAutoRefresh = (): void => { + clearTimeout(this.refreshTimer); + + if (this.state.autoRefresh === 'After Delay' && this.state.autoRefreshDelay && this.state.autoRefreshDelay > 0) { + // 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.updateAutoRefresh()); + this.refreshTimer = setTimeout(scheduleRefresh, this.state.autoRefreshDelay); + } + }; + // use a slight debounce as the same event may come in short succession protected memoryWritten = debounce((writtenMemory: WrittenMemory): void => { if (!this.state.memory) { @@ -189,6 +232,10 @@ class App extends React.Component<{}, MemoryAppState> { this.setState({ sessionContext }); } + protected viewStateChanged(viewState: ViewState): void { + this.setState({ viewState }); + } + public render(): React.ReactNode { return { showRadixPrefix={this.state.showRadixPrefix} storeMemory={this.storeMemory} applyMemory={this.applyMemory} + autoRefresh={this.state.autoRefresh} + autoRefreshDelay={this.state.autoRefreshDelay} /> ; } @@ -229,6 +278,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); @@ -236,64 +286,53 @@ 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 } })); - } - - return this.fetchMemory(options); + this.setState({ configuredReadArguments: { ...this.state.configuredReadArguments, ...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; } - this.setState(prev => ({ ...prev, isMemoryFetching: true })); const completeOptions = { memoryReference: partialOptions?.memoryReference || this.state.activeReadArguments.memoryReference, offset: partialOptions?.offset ?? this.state.activeReadArguments.offset, count: partialOptions?.count ?? this.state.activeReadArguments.count }; + if (completeOptions.memoryReference === '') { + // may happen when we initialize empty + return; + } + this.doFetchMemory(completeOptions); + }; + 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 { @@ -309,12 +348,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 a4dca3e..94f0cb2 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -17,10 +17,10 @@ import deepequal from 'fast-deep-equal'; import type * as React from 'react'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; +import { AutoRefresh, Endianness, GroupsPerRowOption } from '../../common/manifest'; import { Memory } from '../../common/memory'; -import { areRangesEqual, BigIntMemoryRange, Endianness, Radix } from '../../common/memory-range'; +import { areRangesEqual, BigIntMemoryRange, Radix } from '../../common/memory-range'; import { ReadMemoryArguments } from '../../common/messaging'; -import { GroupsPerRowOption } from '../../plugin/manifest'; export interface SerializedTableRenderOptions extends MemoryDisplayConfiguration { columnOptions: Array<{ label: string, doRender: boolean }>; @@ -89,12 +89,14 @@ export interface MemoryDisplayConfiguration { addressPadding: AddressPadding; addressRadix: Radix; showRadixPrefix: boolean; + autoRefresh: AutoRefresh; + autoRefreshDelay: number; } export type ScrollingBehavior = 'Paginate' | 'Grow' | 'Auto-Append'; -export type AddressPadding = 'Min' | number; +export type AddressPadding = 'Minimal' | number; export const AddressPaddingOptions = { - 'Minimal': 'Min', + 'Minimal': 'Minimal', 'Unpadded': 0, '32bit': 32, '64bit': 64, 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: [ {