From c80caa3eea2430695b9019f217d63c6e09ccd17e Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Fri, 24 Mar 2023 15:34:41 -0600 Subject: [PATCH 01/16] Most of the infrastructure --- .vscode/memory-inspector.code-snippets | 16 +++ src/adapter-registry/adapter-capabilities.ts | 45 ++++++ src/adapter-registry/adapter-registry.ts | 42 ++++++ src/adapter-registry/gdb-capabilities.ts | 137 +++++++++++++++++++ src/browser/extension.ts | 11 +- src/desktop/extension.ts | 11 +- src/memory-provider.ts | 42 ++++-- 7 files changed, 285 insertions(+), 19 deletions(-) create mode 100644 .vscode/memory-inspector.code-snippets create mode 100644 src/adapter-registry/adapter-capabilities.ts create mode 100644 src/adapter-registry/adapter-registry.ts create mode 100644 src/adapter-registry/gdb-capabilities.ts diff --git a/.vscode/memory-inspector.code-snippets b/.vscode/memory-inspector.code-snippets new file mode 100644 index 0000000..1747aff --- /dev/null +++ b/.vscode/memory-inspector.code-snippets @@ -0,0 +1,16 @@ +{ + "Copyright": { + "prefix": [ + "header", + "copyright" + ], + "body": "/********************************************************************************\n * Copyright (C) $CURRENT_YEAR ${YourCompany} and others.\n *\n * This program and the accompanying materials are made available under the\n * terms of the Eclipse Public License v. 2.0 which is available at\n * http://www.eclipse.org/legal/epl-2.0.\n *\n * This Source Code may also be made available under the following Secondary\n * Licenses when the conditions for such availability set forth in the Eclipse\n * Public License v. 2.0 are satisfied: GNU General Public License, version 2\n * with the GNU Classpath Exception which is available at\n * https://www.gnu.org/software/classpath/license.html.\n *\n * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0\n ********************************************************************************/\n\n$0", + "description": "Adds the copyright...", + "scope": "css,javascript,javascriptreact,typescript,typescriptreact" + }, + "Import VSCode": { + "prefix": "codeimport", + "body": "import * as vscode from 'vscode';", + "scope": "typescript,javascript" + } +} diff --git a/src/adapter-registry/adapter-capabilities.ts b/src/adapter-registry/adapter-capabilities.ts new file mode 100644 index 0000000..35a5947 --- /dev/null +++ b/src/adapter-registry/adapter-capabilities.ts @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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'; + +export interface MemoryRange { + /** String representation of the address at which the range begins. May exceed maximum safe JS integer. */ + startAddress: string; + /** + * String representation of the address at which the range ends, exclusive. I.e. this should be the first address not included in the range. + * May exceed maximum safe JS integer. + * + * If absent, the UI will indicate the first address at which the variable can be found but not its extent. + */ + endAddress?: string; +} + +export interface VariableRange extends MemoryRange { + name: string; + type?: string; + /** If applicable, a string representation of the variable's value */ + value?: string; +} + +/** Represents capabilities that may be achieved with particular debug adapters but are not part of the DAP */ +export interface AdapterCapabilities { + /** Resolve variables known to the adapter to their locations. Fallback if {@link getResidents} is not present */ + getVariables?(session: vscode.DebugSession): Promise; + /** Resolve symbols resident in the memory at the specified range. Will be preferred to {@link getVariables} if present. */ + getResidents?(session: vscode.DebugSession, range: MemoryRange): Promise; + initializeAdapterTracker?(session: vscode.DebugSession): vscode.DebugAdapterTracker | undefined; +} diff --git a/src/adapter-registry/adapter-registry.ts b/src/adapter-registry/adapter-registry.ts new file mode 100644 index 0000000..f189de5 --- /dev/null +++ b/src/adapter-registry/adapter-registry.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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 { AdapterCapabilities } from './adapter-capabilities'; + +export class AdapterRegistry implements vscode.Disposable { + private handlers = new Map(); + private isDisposed = false; + registerAdapter(debugType: string, handlerToRegister: AdapterCapabilities): vscode.Disposable { + if (this.isDisposed) { return new vscode.Disposable(() => { }); } + this.handlers.set(debugType, handlerToRegister); + return new vscode.Disposable(() => { + const currentlyRegisteredHandler = this.handlers.get(debugType); + if (currentlyRegisteredHandler === handlerToRegister) { + this.handlers.delete(debugType); + } + }); + }; + + getHandlerForSession(session: vscode.DebugSession): AdapterCapabilities | undefined { + return this.handlers.get(session.type); + } + + dispose(): void { + this.isDisposed = true; + this.handlers.clear(); + } +} diff --git a/src/adapter-registry/gdb-capabilities.ts b/src/adapter-registry/gdb-capabilities.ts new file mode 100644 index 0000000..45455f2 --- /dev/null +++ b/src/adapter-registry/gdb-capabilities.ts @@ -0,0 +1,137 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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 { DebugProtocol } from '@vscode/debugprotocol'; +import { AdapterCapabilities, VariableRange } from './adapter-capabilities'; + +type WithChildren = Original & { children?: Array> }; +type VariablesTree = Record>; + +class GdbAdapterTracker implements vscode.DebugAdapterTracker { + private variablesTree: VariablesTree = {}; + private readonly pendingMessages = new Map(); + private static hexAddress = /0x[0-9a-f]+/i; + private static notADigit = /[^0-9]/; + + constructor(private readonly onEnd: vscode.Disposable) { } + + onWillReceiveMessage(message: unknown): void { + if (isScopesRequest(message)) { + console.log('Sending a fun scopes request!', message); + } else if (isVariableRequest(message)) { + if (message.arguments.variablesReference in this.variablesTree) { + this.pendingMessages.set(message.seq, message.arguments.variablesReference); + } + console.log('Sending a fun variable request!', message); + } + } + onDidSendMessage(message: unknown): void { + if (isScopesResponse(message)) { + console.log('Got a fun scope message!', message); + for (const scope of message.body.scopes) { + if (scope.name === 'Local') { + if (!this.variablesTree[scope.variablesReference] || this.variablesTree[scope.variablesReference].name !== 'Local') { + this.variablesTree = { [scope.variablesReference]: { ...scope } }; + } + return; + } + } + } else if (isVariableResponse(message)) { + if (this.pendingMessages.has(message.request_seq)) { + 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; + } + } + console.log('Got a fun variable message!', message); + } + } + onExit(): void { + this.onEnd.dispose(); + this.pendingMessages.clear(); + } + + async getLocals(session: vscode.DebugSession): Promise { + const maybeRanges = await Promise.all(Object.values(this.variablesTree).reduce>>((previous, parent) => { + if (parent.name === 'Local' && parent.children?.length) { + parent.children.forEach(child => { + previous.push(this.variableToVariableRange(child, session)); + }); + } + return previous; + }, [])); + return maybeRanges.filter((candidate): candidate is VariableRange => !!candidate); + } + + private async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { + if (variable.memoryReference === undefined) { return undefined; } + try { + const [addressResponse, sizeResponse] = await Promise.all([ + session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch' }), + session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'watch' }), + ]) as DebugProtocol.EvaluateResponse['body'][]; + const addressPart = GdbAdapterTracker.hexAddress.exec(addressResponse.result); + if (!addressPart) { return undefined; } + const startAddress = BigInt(addressPart[0]); + const endAddress = GdbAdapterTracker.notADigit.test(sizeResponse.result) ? undefined : startAddress + BigInt(sizeResponse.result); + return { + name: variable.name, + startAddress: startAddress.toString(16), + endAddress: endAddress === undefined ? undefined : endAddress.toString(16), + value: variable.value, + }; + } catch { + return undefined; + } + } +} + +function isScopesRequest(message: unknown): message is DebugProtocol.ScopesRequest { + const candidate = message as DebugProtocol.ScopesRequest; + return !!candidate && candidate.command === 'scopes'; +} + +function isVariableRequest(message: unknown): message is DebugProtocol.VariablesRequest { + const candidate = message as DebugProtocol.VariablesRequest; + return !!candidate && candidate.command === 'variables'; +} + +function isScopesResponse(message: unknown): message is DebugProtocol.ScopesResponse { + const candidate = message as DebugProtocol.ScopesResponse; + return !!candidate && candidate.command === 'scopes' && Array.isArray(candidate.body.scopes); +} + +function isVariableResponse(message: unknown): message is DebugProtocol.VariablesResponse { + const candidate = message as DebugProtocol.VariablesResponse; + return !!candidate && candidate.command === 'variables' && Array.isArray(candidate.body.variables); +} + +export class GdbCapabilities implements AdapterCapabilities { + private sessions = new Map(); + initializeAdapterTracker(session: vscode.DebugSession): GdbAdapterTracker | undefined { + if (session.type === 'gdb') { + const sessionTracker = new GdbAdapterTracker(new vscode.Disposable(() => this.sessions.delete(session.id))); + this.sessions.set(session.id, sessionTracker); + return sessionTracker; + } + } + + getVariables(session: vscode.DebugSession): Promise { + return Promise.resolve(this.sessions.get(session.id)?.getLocals(session) ?? []); + } +} diff --git a/src/browser/extension.ts b/src/browser/extension.ts index 6449ad1..707d919 100644 --- a/src/browser/extension.ts +++ b/src/browser/extension.ts @@ -15,15 +15,20 @@ ********************************************************************************/ import * as vscode from 'vscode'; +import { AdapterRegistry } from '../adapter-registry/adapter-registry'; +import { GdbCapabilities } from '../adapter-registry/gdb-capabilities'; import { MemoryProvider } from '../memory-provider'; import { MemoryWebview } from '../views/memory-webview-main'; -export const activate = async (context: vscode.ExtensionContext): Promise => { +export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); - - await memoryProvider.activate(context); + const registry = new AdapterRegistry(); + context.subscriptions.push(registry); + context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); + await memoryProvider.activate(context, registry); await memoryView.activate(context); + return registry; }; export const deactivate = async (): Promise => { diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index 6449ad1..707d919 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -15,15 +15,20 @@ ********************************************************************************/ import * as vscode from 'vscode'; +import { AdapterRegistry } from '../adapter-registry/adapter-registry'; +import { GdbCapabilities } from '../adapter-registry/gdb-capabilities'; import { MemoryProvider } from '../memory-provider'; import { MemoryWebview } from '../views/memory-webview-main'; -export const activate = async (context: vscode.ExtensionContext): Promise => { +export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); - - await memoryProvider.activate(context); + const registry = new AdapterRegistry(); + context.subscriptions.push(registry); + context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); + await memoryProvider.activate(context, registry); await memoryView.activate(context); + return registry; }; export const deactivate = async (): Promise => { diff --git a/src/memory-provider.ts b/src/memory-provider.ts index 3bd625f..d7deec8 100644 --- a/src/memory-provider.ts +++ b/src/memory-provider.ts @@ -18,6 +18,7 @@ import * as vscode from 'vscode'; import * as manifest from './manifest'; import { DebugProtocol } from '@vscode/debugprotocol'; import { MemoryReadResult, MemoryWriteResult } from './views/memory-webview-common'; +import { AdapterRegistry } from './adapter-registry/adapter-registry'; export interface LabeledUint8Array extends Uint8Array { label?: string; @@ -32,20 +33,35 @@ export class MemoryProvider { protected readonly sessions = new Map(); - public async activate(context: vscode.ExtensionContext): Promise { - const createDebugAdapterTracker = (session: vscode.DebugSession): vscode.DebugAdapterTracker => ({ - onWillStartSession: () => this.debugSessionStarted(session), - onWillStopSession: () => this.debugSessionTerminated(session), - onDidSendMessage: message => { - if (isInitializeMessage(message)) { - // Check for right capabilities in the adapter - this.sessions.set(session.id, message.body); - if (vscode.debug.activeDebugSession?.id === session.id) { - this.setContext(message.body); + public async activate(context: vscode.ExtensionContext, registry: AdapterRegistry): Promise { + const createDebugAdapterTracker = (session: vscode.DebugSession): Required => { + const handlerForSession = registry.getHandlerForSession(session); + const contributedTracker = handlerForSession?.initializeAdapterTracker?.(session); + + return ({ + onWillStartSession: () => { + this.debugSessionStarted(session); + contributedTracker?.onWillStartSession?.(); + }, + onWillStopSession: () => { + this.debugSessionTerminated(session); + contributedTracker?.onWillStopSession?.(); + }, + onDidSendMessage: message => { + if (isInitializeMessage(message)) { + // Check for right capabilities in the adapter + this.sessions.set(session.id, message.body); + if (vscode.debug.activeDebugSession?.id === session.id) { + this.setContext(message.body); + } } - } - } - }); + 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 }), From 79f325dcf3cd3bd5d9a09df9f079fc8fc466520b Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Thu, 30 Mar 2023 13:45:12 -0600 Subject: [PATCH 02/16] Big reorganization --- src/common/memory-range.ts | 53 +++++++++++ .../messaging.ts} | 4 +- src/{ => entry-points}/browser/extension.ts | 8 +- src/{ => entry-points}/desktop/extension.ts | 8 +- .../adapter-registry/adapter-capabilities.ts | 20 +--- .../adapter-registry/adapter-registry.ts | 0 .../adapter-registry/gdb-capabilities.ts | 3 +- src/{ => plugin}/logger.ts | 0 src/{ => plugin}/manifest.ts | 0 src/{ => plugin}/memory-provider.ts | 2 +- src/{views => plugin}/memory-webview-main.ts | 8 +- .../columns/column-contribution-service.ts | 68 +++++++++++++ .../components/memory-table.tsx | 2 +- .../components/memory-widget.tsx | 2 +- .../components/options-widget.tsx | 2 +- src/webview/decorations/decoration-service.ts | 57 +++++++++++ .../memory-webview-view.tsx | 34 ++----- src/webview/tsconfig.json | 16 ++++ src/webview/utils/events.ts | 63 ++++++++++++ src/webview/utils/view-types.ts | 60 ++++++++++++ src/webview/variables/variable-decorations.ts | 95 +++++++++++++++++++ .../view-messenger.ts} | 24 +---- 22 files changed, 447 insertions(+), 82 deletions(-) create mode 100644 src/common/memory-range.ts rename src/{views/memory-webview-common.ts => common/messaging.ts} (86%) rename src/{ => entry-points}/browser/extension.ts (83%) rename src/{ => entry-points}/desktop/extension.ts (83%) rename src/{ => plugin}/adapter-registry/adapter-capabilities.ts (68%) rename src/{ => plugin}/adapter-registry/adapter-registry.ts (100%) rename src/{ => plugin}/adapter-registry/gdb-capabilities.ts (98%) rename src/{ => plugin}/logger.ts (100%) rename src/{ => plugin}/manifest.ts (100%) rename src/{ => plugin}/memory-provider.ts (98%) rename src/{views => plugin}/memory-webview-main.ts (97%) create mode 100644 src/webview/columns/column-contribution-service.ts rename src/{views => webview}/components/memory-table.tsx (99%) rename src/{views => webview}/components/memory-widget.tsx (98%) rename src/{views => webview}/components/options-widget.tsx (98%) create mode 100644 src/webview/decorations/decoration-service.ts rename src/{views => webview}/memory-webview-view.tsx (77%) create mode 100644 src/webview/tsconfig.json create mode 100644 src/webview/utils/events.ts create mode 100644 src/webview/utils/view-types.ts create mode 100644 src/webview/variables/variable-decorations.ts rename src/{views/components/view-types.ts => webview/view-messenger.ts} (64%) diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts new file mode 100644 index 0000000..4f8691f --- /dev/null +++ b/src/common/memory-range.ts @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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 type Long from 'long'; + +/** Suitable for transmission as JSON */ +export interface MemoryRange { + /** String representation of the address at which the range begins. May exceed maximum safe JS integer. */ + startAddress: string; + /** + * String representation of the address at which the range ends, exclusive. I.e. this should be the first address not included in the range. + * May exceed maximum safe JS integer. + * + * If absent, the UI will indicate the first address at which the variable can be found but not its extent. + */ + endAddress?: string; +} + +/** Suitable for arithemetic */ +export interface LongMemoryRange { + startAddress: Long; + endAddress?: Long; +} + +export function isWithin(candidate: Long, container: LongMemoryRange): boolean { + if (container.endAddress === undefined) { return container.startAddress.equals(candidate); } + return container.startAddress.lessThanOrEqual(candidate) && container.endAddress.greaterThan(candidate); +} + +export interface VariableMetadata { + name: string; + type?: string; + /** If applicable, a string representation of the variable's value */ + value?: string; +} + +/** Suitable for transmission as JSON */ +export interface VariableRange extends MemoryRange, VariableMetadata { } +/** Suitable for arithemetic */ +export interface LongVariableRange extends LongMemoryRange, VariableMetadata { } diff --git a/src/views/memory-webview-common.ts b/src/common/messaging.ts similarity index 86% rename from src/views/memory-webview-common.ts rename to src/common/messaging.ts index 1a3d251..166b40b 100644 --- a/src/views/memory-webview-common.ts +++ b/src/common/messaging.ts @@ -15,7 +15,8 @@ ********************************************************************************/ import type { DebugProtocol } from '@vscode/debugprotocol'; -import { NotificationType, RequestType } from 'vscode-messenger-common'; +import type { NotificationType, RequestType } from 'vscode-messenger-common'; +import type { VariableRange } from './memory-range'; export type MemoryReadResult = DebugProtocol.ReadMemoryResponse['body']; export type MemoryWriteResult = DebugProtocol.WriteMemoryResponse['body']; @@ -25,3 +26,4 @@ export const logMessageType: RequestType = { method: 'logMessage' export const setOptionsType: RequestType, void> = { method: 'setOptions' }; export const readMemoryType: RequestType = { method: 'readMemory' }; export const writeMemoryType: RequestType = { method: 'writeMemory' }; +export const getVariables: RequestType = { method: 'getVariables' }; diff --git a/src/browser/extension.ts b/src/entry-points/browser/extension.ts similarity index 83% rename from src/browser/extension.ts rename to src/entry-points/browser/extension.ts index 707d919..7f9187f 100644 --- a/src/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -15,10 +15,10 @@ ********************************************************************************/ import * as vscode from 'vscode'; -import { AdapterRegistry } from '../adapter-registry/adapter-registry'; -import { GdbCapabilities } from '../adapter-registry/gdb-capabilities'; -import { MemoryProvider } from '../memory-provider'; -import { MemoryWebview } from '../views/memory-webview-main'; +import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry'; +import { GdbCapabilities } from '../../plugin/adapter-registry/gdb-capabilities'; +import { MemoryProvider } from '../../plugin/memory-provider'; +import { MemoryWebview } from '../../plugin/memory-webview-main'; export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); diff --git a/src/desktop/extension.ts b/src/entry-points/desktop/extension.ts similarity index 83% rename from src/desktop/extension.ts rename to src/entry-points/desktop/extension.ts index 707d919..7f9187f 100644 --- a/src/desktop/extension.ts +++ b/src/entry-points/desktop/extension.ts @@ -15,10 +15,10 @@ ********************************************************************************/ import * as vscode from 'vscode'; -import { AdapterRegistry } from '../adapter-registry/adapter-registry'; -import { GdbCapabilities } from '../adapter-registry/gdb-capabilities'; -import { MemoryProvider } from '../memory-provider'; -import { MemoryWebview } from '../views/memory-webview-main'; +import { AdapterRegistry } from '../../plugin/adapter-registry/adapter-registry'; +import { GdbCapabilities } from '../../plugin/adapter-registry/gdb-capabilities'; +import { MemoryProvider } from '../../plugin/memory-provider'; +import { MemoryWebview } from '../../plugin/memory-webview-main'; export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); diff --git a/src/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts similarity index 68% rename from src/adapter-registry/adapter-capabilities.ts rename to src/plugin/adapter-registry/adapter-capabilities.ts index 35a5947..10d8591 100644 --- a/src/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -15,25 +15,7 @@ ********************************************************************************/ import * as vscode from 'vscode'; - -export interface MemoryRange { - /** String representation of the address at which the range begins. May exceed maximum safe JS integer. */ - startAddress: string; - /** - * String representation of the address at which the range ends, exclusive. I.e. this should be the first address not included in the range. - * May exceed maximum safe JS integer. - * - * If absent, the UI will indicate the first address at which the variable can be found but not its extent. - */ - endAddress?: string; -} - -export interface VariableRange extends MemoryRange { - name: string; - type?: string; - /** If applicable, a string representation of the variable's value */ - value?: string; -} +import { MemoryRange, VariableRange } from '../../common/memory-range'; /** Represents capabilities that may be achieved with particular debug adapters but are not part of the DAP */ export interface AdapterCapabilities { diff --git a/src/adapter-registry/adapter-registry.ts b/src/plugin/adapter-registry/adapter-registry.ts similarity index 100% rename from src/adapter-registry/adapter-registry.ts rename to src/plugin/adapter-registry/adapter-registry.ts diff --git a/src/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts similarity index 98% rename from src/adapter-registry/gdb-capabilities.ts rename to src/plugin/adapter-registry/gdb-capabilities.ts index 45455f2..64f5dfd 100644 --- a/src/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -16,7 +16,8 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { AdapterCapabilities, VariableRange } from './adapter-capabilities'; +import { AdapterCapabilities } from './adapter-capabilities'; +import { VariableRange } from '../../common/memory-range'; type WithChildren = Original & { children?: Array> }; type VariablesTree = Record>; diff --git a/src/logger.ts b/src/plugin/logger.ts similarity index 100% rename from src/logger.ts rename to src/plugin/logger.ts diff --git a/src/manifest.ts b/src/plugin/manifest.ts similarity index 100% rename from src/manifest.ts rename to src/plugin/manifest.ts diff --git a/src/memory-provider.ts b/src/plugin/memory-provider.ts similarity index 98% rename from src/memory-provider.ts rename to src/plugin/memory-provider.ts index d7deec8..ccccf2e 100644 --- a/src/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -17,7 +17,7 @@ import * as vscode from 'vscode'; import * as manifest from './manifest'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { MemoryReadResult, MemoryWriteResult } from './views/memory-webview-common'; +import { MemoryReadResult, MemoryWriteResult } from './common/memory-webview-common'; import { AdapterRegistry } from './adapter-registry/adapter-registry'; export interface LabeledUint8Array extends Uint8Array { diff --git a/src/views/memory-webview-main.ts b/src/plugin/memory-webview-main.ts similarity index 97% rename from src/views/memory-webview-main.ts rename to src/plugin/memory-webview-main.ts index 0e09898..b506239 100644 --- a/src/views/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -16,7 +16,7 @@ import * as vscode from 'vscode'; import type { DebugProtocol } from '@vscode/debugprotocol'; -import * as manifest from '../manifest'; +import * as manifest from './manifest'; import { Messenger } from 'vscode-messenger'; import { WebviewIdMessageParticipant } from 'vscode-messenger-common'; import { @@ -27,9 +27,9 @@ import { writeMemoryType, MemoryReadResult, MemoryWriteResult -} from './memory-webview-common'; -import { MemoryProvider } from '../memory-provider'; -import { logger } from '../logger'; +} from '../common/messaging'; +import { MemoryProvider } from './memory-provider'; +import { logger } from './logger'; interface Variable { name: string; diff --git a/src/webview/columns/column-contribution-service.ts b/src/webview/columns/column-contribution-service.ts new file mode 100644 index 0000000..14ab48c --- /dev/null +++ b/src/webview/columns/column-contribution-service.ts @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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 { DebugProtocol } from '@vscode/debugprotocol'; +import type * as React from 'react'; +import { LongMemoryRange } from '../../common/memory-range'; +import type { Disposable, MemoryState, UpdateExecutor } from '../utils/view-types'; + +export interface ColumnContribution { + readonly label: string; + readonly id: string; + render(memory: MemoryState, range: LongMemoryRange): React.ReactElement + /** Called when fetching new memory or when activating the column. */ + fetchData?(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; + /** Called when the user reveals the column */ + activate?(memory: MemoryState): Promise; + /** Called when the user hides the column */ + deactivate?(): void; +} + +class ColumnContributionService { + private activeColumns = new Array(); + private registeredColumns = new Map; + register(contribution: ColumnContribution): Disposable { + this.registeredColumns.set(contribution.id, contribution); + return { + dispose: () => { + this.hide(contribution.id); + this.registeredColumns.delete(contribution.id); + } + }; + } + async show(id: string, memoryState: MemoryState): Promise { + const contribution = this.registeredColumns.get(id); + if (contribution) { + await contribution.activate?.(memoryState); + this.activeColumns.push(contribution); + this.activeColumns.sort((left, right) => left.id.localeCompare(right.id)); + } + return this.activeColumns.slice(); + } + hide(id: string): ColumnContribution[] { + const contribution = this.registeredColumns.get(id); + let index; + if (contribution && (index = this.activeColumns.findIndex(candidate => candidate === contribution)) !== -1) { + this.activeColumns.splice(index, 1); + } + return this.activeColumns.slice(); + } + getUpdateExecutors(): UpdateExecutor[] { + return this.activeColumns.filter((candidate): candidate is ColumnContribution & UpdateExecutor => candidate.fetchData !== undefined); + } +} + +export const columnContributionService = new ColumnContributionService(); diff --git a/src/views/components/memory-table.tsx b/src/webview/components/memory-table.tsx similarity index 99% rename from src/views/components/memory-table.tsx rename to src/webview/components/memory-table.tsx index 085bd48..e10b06e 100644 --- a/src/views/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -21,7 +21,7 @@ import { VSCodeDataGridRow, VSCodeDataGridCell } from '@vscode/webview-ui-toolkit/react'; -import { Endianness, Memory } from './view-types'; +import { Endianness, Memory } from '../utils/view-types'; interface VariableDecoration { name: string; diff --git a/src/views/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx similarity index 98% rename from src/views/components/memory-widget.tsx rename to src/webview/components/memory-widget.tsx index fbd6608..fc1f532 100644 --- a/src/views/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -18,7 +18,7 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import React from 'react'; import { MemoryTable } from './memory-table'; import { OptionsWidget } from './options-widget'; -import { Endianness, Memory } from './view-types'; +import { Endianness, Memory } from '../utils/view-types'; interface MemoryWidgetProps { memory?: Memory; diff --git a/src/views/components/options-widget.tsx b/src/webview/components/options-widget.tsx similarity index 98% rename from src/views/components/options-widget.tsx rename to src/webview/components/options-widget.tsx index 074719d..cc48dcf 100644 --- a/src/views/components/options-widget.tsx +++ b/src/webview/components/options-widget.tsx @@ -16,7 +16,7 @@ import React from 'react'; import type { DebugProtocol } from '@vscode/debugprotocol'; -import { Endianness, TableRenderOptions } from './view-types'; +import { Endianness, TableRenderOptions } from '../utils/view-types'; import { VSCodeButton, VSCodeDivider, VSCodeDropdown, VSCodeOption, VSCodeTextField } from '@vscode/webview-ui-toolkit/react'; interface OptionsWidgetProps { diff --git a/src/webview/decorations/decoration-service.ts b/src/webview/decorations/decoration-service.ts new file mode 100644 index 0000000..8b8d5d0 --- /dev/null +++ b/src/webview/decorations/decoration-service.ts @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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 { EventEmitter, IEvent } from '../utils/events'; +import { Decoration, Disposable, UpdateExecutor } from '../utils/view-types'; + +export interface Decorator extends Partial { + readonly id: string; + readonly onDidChange: IEvent; +} + +class DecorationService { + private onDidChangeEmitter = new EventEmitter(); + private decorations = new Map(); + private decorators = new Map(); + /** Represents the aggregation of all contributed decorations */ + private currentDecorations = new Array(); + register(contribution: Decorator): Disposable { + this.decorators.set(contribution.id, contribution); + const changeListener = contribution.onDidChange(newDecorations => { + const oldDecorations = this.decorations.get(contribution.id); + this.reconcileDecorations(contribution.id, oldDecorations, newDecorations); + }); + return { + dispose: () => { + changeListener.dispose(); + this.decorators.delete(contribution.id); + const currentDecorations = this.decorations.get(contribution.id); + this.decorations.delete(contribution.id); + this.reconcileDecorations(contribution.id, currentDecorations, []); + } + }; + } + + private reconcileDecorations(affectedDecorator: string, oldDecorations: Decoration[] | undefined, newDecorations: Decoration[]): void { + + } + + getUpdateExecutors(): UpdateExecutor[] { + return Array.from(this.decorators.values()).filter((candidate): candidate is Decorator & UpdateExecutor => candidate.fetchData !== undefined); + } +} + +export const decorationService = new DecorationService(); diff --git a/src/views/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx similarity index 77% rename from src/views/memory-webview-view.tsx rename to src/webview/memory-webview-view.tsx index c43a562..55bfaf4 100644 --- a/src/views/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -17,38 +17,20 @@ import Long from 'long'; import React from 'react'; import { createRoot } from 'react-dom/client'; -import { Messenger } from 'vscode-messenger-webview'; import { HOST_EXTENSION } from 'vscode-messenger-common'; import { readyType, logMessageType, setOptionsType, readMemoryType -} from './memory-webview-common'; +} from '../common/messaging'; import type { DebugProtocol } from '@vscode/debugprotocol'; -import { Memory } from './components/view-types'; +import { Memory, MemoryState } from './utils/view-types'; import { MemoryWidget } from './components/memory-widget'; - -interface MemoryState { - memory?: Memory; - memoryReference: string; - offset: number; - count: number; -} +import { messenger } from './view-messenger'; class App extends React.Component<{}, MemoryState> { - private _messenger: Messenger | undefined; - protected get messenger(): Messenger { - if (!this._messenger) { - const vscode = acquireVsCodeApi(); - this._messenger = new Messenger(vscode); - this._messenger.start(); - } - - return this._messenger; - } - public constructor(props: {}) { super(props); this.state = { @@ -60,15 +42,15 @@ class App extends React.Component<{}, MemoryState> { } public componentDidMount(): void { - this.messenger.onRequest(setOptionsType, options => this.setOptions(options)); - this.messenger.sendNotification(readyType, HOST_EXTENSION, undefined); + messenger.onRequest(setOptionsType, options => this.setOptions(options)); + messenger.sendNotification(readyType, HOST_EXTENSION, undefined); } public render(): React.ReactNode { return { protected updateMemoryState = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); protected async setOptions(options?: Partial): Promise { - this.messenger.sendRequest(logMessageType, HOST_EXTENSION, JSON.stringify(options)); + messenger.sendRequest(logMessageType, HOST_EXTENSION, JSON.stringify(options)); this.setState(prevState => ({ ...prevState, ...options })); return this.fetchMemory(options); } @@ -92,7 +74,7 @@ class App extends React.Component<{}, MemoryState> { count: partialOptions?.count ?? this.state.count }; - const response = await this.messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions); + const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions); this.setState({ memory: this.convertMemory(response) diff --git a/src/webview/tsconfig.json b/src/webview/tsconfig.json new file mode 100644 index 0000000..c11fb43 --- /dev/null +++ b/src/webview/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "jsx": "react", + "target": "es2020", + "lib": [ + "es2020", + "dom" + ], + "outDir": "../../out/frontend", + "sourceMap": true, + "strict": true, + "rootDir": "../", + "esModuleInterop": true, + "moduleResolution": "node" + }, +} diff --git a/src/webview/utils/events.ts b/src/webview/utils/events.ts new file mode 100644 index 0000000..9bd7093 --- /dev/null +++ b/src/webview/utils/events.ts @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (C) 2023 YourCompany 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 { Disposable, dispose } from './view-types'; + +export class EventEmitter { + private emitter = new EventTarget(); + private toDispose = new Array(); + + event(externalHandler: (event: T) => unknown): Disposable { + const internalHandler = (event: Event) => { + const handlerEvent = event as Event & { data: T }; + externalHandler(handlerEvent.data); + }; + this.emitter.addEventListener('fire', internalHandler); + let disposed = false; + const toDispose = () => { + if (!disposed) { + disposed = true; + this.emitter.removeEventListener('fire', internalHandler); + } + }; + const result = { + dispose: () => { + if (!disposed) { + toDispose(); + const locationInArray = this.toDispose.findIndex(disposable => disposable.dispose === toDispose); + if (locationInArray !== -1) { this.toDispose.splice(locationInArray, 1); } + } + } + }; + this.toDispose.push({ dispose: toDispose }); + return result; + } + + fire(event: T): void { + const domEvent = new Event('fire') as Event & { data: T }; + domEvent.data = event; + this.emitter.dispatchEvent(domEvent); + } + + dispose(): void { + this.toDispose.forEach(dispose); + this.toDispose.length = 0; + } +} + +export interface IEvent { + (handler: (event: T) => unknown): Disposable; +} diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts new file mode 100644 index 0000000..6db8e03 --- /dev/null +++ b/src/webview/utils/view-types.ts @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (C) 2023 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 type { DebugProtocol } from '@vscode/debugprotocol'; +import type Long from 'long'; +import type * as React from 'react'; +import type { LongMemoryRange } from '../../plugin/adapter-registry/adapter-capabilities'; + +export enum Endianness { + Little = 'Little Endian', + Big = 'Big Endian' +} + +export interface Memory { + address: Long; + bytes: Uint8Array; +} + +export interface TableRenderOptions { + columnOptions: Array<{ label: string, doRender: boolean }>; + endianness: Endianness; + bytesPerGroup: number; + groupsPerRow: number; + byteSize: number; +} + +export interface Event { + (handler: (event: T) => unknown): Disposable; +} + +export interface Disposable { dispose(): unknown }; +export function dispose(disposable: { dispose(): unknown }): void { + disposable.dispose(); +} + +export interface Decoration { + range: LongMemoryRange; + style: React.StyleHTMLAttributes; +} + +export interface MemoryState extends DebugProtocol.ReadMemoryArguments { + memory?: Memory; +} + +export interface UpdateExecutor { + fetchData(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; +} diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts new file mode 100644 index 0000000..fbd2c42 --- /dev/null +++ b/src/webview/variables/variable-decorations.ts @@ -0,0 +1,95 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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 type { DebugProtocol } from '@vscode/debugprotocol'; +import { HOST_EXTENSION } from 'vscode-messenger-common'; +import type { LongVariableRange } from '../../plugin/adapter-registry/adapter-capabilities'; +import { getVariables } from '../../common/messaging'; +import { messenger } from '../view-messenger'; +import { Decoration } from '../utils/view-types'; +import { EventEmitter, IEvent } from '../utils/events'; +import Long from 'long'; +import { ColumnContribution } from '../columns/column-contribution-service'; +import { Decorator } from '../decorations/decoration-service'; + +const NON_HC_COLORS = [ + 'var(--vscode-terminal-ansiBlue)', + 'var(--vscode-terminal-ansiGreen)', + 'var(--vscode-terminal-ansiRed)', + 'var(--vscode-terminal-ansiYellow)', + 'var(--vscode-terminal-ansiMagenta)', +] as const; + +export class VariableDecorator implements ColumnContribution, Decorator { + private onDidChangeEmitter = new EventEmitter(); + /** We expect this to always be sorted from lowest to highest start address */ + private currentVariables?: LongVariableRange[]; + + get onDidChange(): IEvent { return this.onDidChangeEmitter.event.bind(this.onDidChangeEmitter); } + + async fetchData(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise { + const visibleVariables = (await messenger.sendRequest(getVariables, HOST_EXTENSION, currentViewParameters)) + .map(transmissible => ({ + ...transmissible, + startAddress: Long.fromString(transmissible.startAddress), + endAddress: transmissible.endAddress ? Long.fromString(transmissible.endAddress) : undefined + })); + visibleVariables.sort((left, right) => left.startAddress.compare(right.startAddress)); + if (this.didVariableChange(visibleVariables)) { + this.currentVariables = visibleVariables; + this.onDidChangeEmitter.fire(this.toDecorations()); + } + } + + private didVariableChange(visibleVariables: LongVariableRange[]): boolean { + return visibleVariables.length !== this.currentVariables?.length + || visibleVariables.some((item, index) => !this.areEqual(item, this.currentVariables![index])); + } + + private areEqual(one: LongVariableRange, other: LongVariableRange): boolean { + return one.startAddress.equals(other.startAddress) + && one.name === other.name + && one.type === other.type + && one.value === other.value + && this.compareUndefinedOrLong(one.endAddress, other.endAddress); + } + + private compareUndefinedOrLong(one: Long | undefined, other: Long | undefined): boolean { + return one === other || (one !== undefined && other !== undefined && one?.equals(other)); + } + + private toDecorations(): Decoration[] { + const decorations: Decoration[] = []; + let colorIndex = 0; + for (const variable of this.currentVariables ?? []) { + if (variable.endAddress) { + decorations.push({ + range: { + startAddress: variable.startAddress, + endAddress: variable.endAddress + }, + style: { color: NON_HC_COLORS[colorIndex++] } + }); + } + } + return decorations; + } + + dispose(): void { + this.onDidChangeEmitter.dispose(); + } +} + diff --git a/src/views/components/view-types.ts b/src/webview/view-messenger.ts similarity index 64% rename from src/views/components/view-types.ts rename to src/webview/view-messenger.ts index 52e082a..6d40c29 100644 --- a/src/views/components/view-types.ts +++ b/src/webview/view-messenger.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2023 Ericsson, Arm and others. + * Copyright (C) 2023 Ericsson 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 @@ -14,22 +14,8 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import Long from 'long'; +import { Messenger } from 'vscode-messenger-webview'; -export enum Endianness { - Little = 'Little Endian', - Big = 'Big Endian' -} - -export interface Memory { - address: Long; - bytes: Uint8Array; -} - -export interface TableRenderOptions { - columnOptions: Array<{ label: string, doRender: boolean }>; - endianness: Endianness; - bytesPerGroup: number; - groupsPerRow: number; - byteSize: number; -} +export const vscode = acquireVsCodeApi(); +export const messenger = new Messenger(vscode); +messenger.start(); From 83d0a6dcaef5ecac820b3db5f74d6334c984b1b4 Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Thu, 30 Mar 2023 15:03:47 -0600 Subject: [PATCH 03/16] BigInts are going to be better --- package.json | 1 + src/common/memory-range.ts | 20 ++++++++++++++++ .../columns/column-contribution-service.ts | 2 +- src/webview/decorations/decoration-service.ts | 4 ++-- src/webview/utils/view-types.ts | 7 +++++- src/webview/variables/variable-decorations.ts | 23 ++++++++----------- tsconfig.json | 2 ++ 7 files changed, 41 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index edb594e..c361b19 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dependencies": { "@vscode/codicons": "^0.0.32", "@vscode/webview-ui-toolkit": "^1.2.0", + "fast-deep-equal": "^3.1.3", "long": "^5.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 4f8691f..86fe91a 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -40,6 +40,19 @@ export function isWithin(candidate: Long, container: LongMemoryRange): boolean { return container.startAddress.lessThanOrEqual(candidate) && container.endAddress.greaterThan(candidate); } +export function doOverlap(one: LongMemoryRange, other: LongMemoryRange): boolean { + // If they overlap, they either start in the same place, or one starts in the other. + return isWithin(one.startAddress, other) || isWithin(other.startAddress, one); +} + +export function areRangesEqual(one: LongMemoryRange, other: LongMemoryRange): boolean { + return one.startAddress.equals(other.startAddress) && compareUndefinedOrLong(one.endAddress, other.endAddress); +} + +function compareUndefinedOrLong(one: Long | undefined, other: Long | undefined): boolean { + return one === other || (one !== undefined && other !== undefined && one?.equals(other)); +} + export interface VariableMetadata { name: string; type?: string; @@ -51,3 +64,10 @@ export interface VariableMetadata { export interface VariableRange extends MemoryRange, VariableMetadata { } /** Suitable for arithemetic */ export interface LongVariableRange extends LongMemoryRange, VariableMetadata { } + +export function areVariablesEqual(one: LongVariableRange, other: LongVariableRange): boolean { + return areRangesEqual(one, other) + && one.name === other.name + && one.type === other.type + && one.value === other.value; +} diff --git a/src/webview/columns/column-contribution-service.ts b/src/webview/columns/column-contribution-service.ts index 14ab48c..2362c6a 100644 --- a/src/webview/columns/column-contribution-service.ts +++ b/src/webview/columns/column-contribution-service.ts @@ -22,7 +22,7 @@ import type { Disposable, MemoryState, UpdateExecutor } from '../utils/view-type export interface ColumnContribution { readonly label: string; readonly id: string; - render(memory: MemoryState, range: LongMemoryRange): React.ReactElement + render(range: LongMemoryRange, memory: MemoryState): React.ReactNode /** Called when fetching new memory or when activating the column. */ fetchData?(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; /** Called when the user reveals the column */ diff --git a/src/webview/decorations/decoration-service.ts b/src/webview/decorations/decoration-service.ts index 8b8d5d0..e7c8428 100644 --- a/src/webview/decorations/decoration-service.ts +++ b/src/webview/decorations/decoration-service.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { EventEmitter, IEvent } from '../utils/events'; -import { Decoration, Disposable, UpdateExecutor } from '../utils/view-types'; +import { areDecorationsEqual, Decoration, Disposable, UpdateExecutor } from '../utils/view-types'; export interface Decorator extends Partial { readonly id: string; @@ -46,7 +46,7 @@ class DecorationService { } private reconcileDecorations(affectedDecorator: string, oldDecorations: Decoration[] | undefined, newDecorations: Decoration[]): void { - + if (oldDecorations?.length === newDecorations.length && oldDecorations.every((old, index) => areDecorationsEqual(old, newDecorations[index]))) { return; } } getUpdateExecutors(): UpdateExecutor[] { diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 6db8e03..580913f 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -17,7 +17,8 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import type Long from 'long'; import type * as React from 'react'; -import type { LongMemoryRange } from '../../plugin/adapter-registry/adapter-capabilities'; +import { areRangesEqual, LongMemoryRange } from '../../common/memory-range'; +import deepequal from 'fast-deep-equal'; export enum Endianness { Little = 'Little Endian', @@ -51,6 +52,10 @@ export interface Decoration { style: React.StyleHTMLAttributes; } +export function areDecorationsEqual(one: Decoration, other: Decoration): boolean { + return areRangesEqual(one.range, other.range) && deepequal(one.style, other.style); +} + export interface MemoryState extends DebugProtocol.ReadMemoryArguments { memory?: Memory; } diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index fbd2c42..b687d5c 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -16,7 +16,6 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import { HOST_EXTENSION } from 'vscode-messenger-common'; -import type { LongVariableRange } from '../../plugin/adapter-registry/adapter-capabilities'; import { getVariables } from '../../common/messaging'; import { messenger } from '../view-messenger'; import { Decoration } from '../utils/view-types'; @@ -24,6 +23,8 @@ import { EventEmitter, IEvent } from '../utils/events'; import Long from 'long'; import { ColumnContribution } from '../columns/column-contribution-service'; import { Decorator } from '../decorations/decoration-service'; +import { ReactNode } from 'react'; +import { areVariablesEqual, doOverlap, LongMemoryRange, LongVariableRange } from '../../common/memory-range'; const NON_HC_COLORS = [ 'var(--vscode-terminal-ansiBlue)', @@ -34,6 +35,8 @@ const NON_HC_COLORS = [ ] as const; export class VariableDecorator implements ColumnContribution, Decorator { + readonly id = 'variables'; + readonly label = 'Variables'; private onDidChangeEmitter = new EventEmitter(); /** We expect this to always be sorted from lowest to highest start address */ private currentVariables?: LongVariableRange[]; @@ -54,21 +57,13 @@ export class VariableDecorator implements ColumnContribution, Decorator { } } - private didVariableChange(visibleVariables: LongVariableRange[]): boolean { - return visibleVariables.length !== this.currentVariables?.length - || visibleVariables.some((item, index) => !this.areEqual(item, this.currentVariables![index])); - } - - private areEqual(one: LongVariableRange, other: LongVariableRange): boolean { - return one.startAddress.equals(other.startAddress) - && one.name === other.name - && one.type === other.type - && one.value === other.value - && this.compareUndefinedOrLong(one.endAddress, other.endAddress); + render(range: LongMemoryRange): ReactNode { + return this.currentVariables?.filter(candidate => doOverlap(candidate, range)).map(variables => variables.name).join(', '); } - private compareUndefinedOrLong(one: Long | undefined, other: Long | undefined): boolean { - return one === other || (one !== undefined && other !== undefined && one?.equals(other)); + private didVariableChange(visibleVariables: LongVariableRange[]): boolean { + return visibleVariables.length !== this.currentVariables?.length + || visibleVariables.some((item, index) => !areVariablesEqual(item, this.currentVariables![index])); } private toDecorations(): Decoration[] { diff --git a/tsconfig.json b/tsconfig.json index 574f9e8..639c77a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "target": "ES2020", "module": "commonjs", + "noUnusedLocals": true, + "noUnusedParameters": true, "strict": true, "sourceMap": true, "esModuleInterop": true, From 3df454a5a1b29640e356a5156901d91a32247c3d Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Thu, 30 Mar 2023 15:22:01 -0600 Subject: [PATCH 04/16] BigInts are better --- package.json | 5 ++--- src/common/memory-range.ts | 18 ++++++------------ src/plugin/memory-provider.ts | 2 +- src/webview/components/memory-table.tsx | 19 +++++++++---------- src/webview/memory-webview-view.tsx | 5 +---- src/webview/utils/view-types.ts | 3 +-- src/webview/variables/variable-decorations.ts | 10 ++++++---- webpack.config.js | 6 +++--- yarn.lock | 5 ----- 9 files changed, 29 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index c361b19..0439003 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "publisher": "Eclipse", "author": "Rob Moran ", "license": "EPL-2.0", - "main": "dist/desktop/extension.js", - "browser": "dist/browser/extension.js", + "main": "dist/entry-points/desktop/extension.js", + "browser": "dist/entry-points/extension.js", "repository": "https://github.com/eclipse-cdt-cloud/vscode-memory-inspector", "qna": "https://github.com/eclipse-cdt-cloud/vscode-memory-inspector/issues", "icon": "media/cdtcloud.png", @@ -35,7 +35,6 @@ "@vscode/codicons": "^0.0.32", "@vscode/webview-ui-toolkit": "^1.2.0", "fast-deep-equal": "^3.1.3", - "long": "^5.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", "vscode-messenger": "^0.4.3", diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 86fe91a..00f30b0 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -14,8 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import type Long from 'long'; - /** Suitable for transmission as JSON */ export interface MemoryRange { /** String representation of the address at which the range begins. May exceed maximum safe JS integer. */ @@ -31,13 +29,13 @@ export interface MemoryRange { /** Suitable for arithemetic */ export interface LongMemoryRange { - startAddress: Long; - endAddress?: Long; + startAddress: bigint; + endAddress?: bigint; } -export function isWithin(candidate: Long, container: LongMemoryRange): boolean { - if (container.endAddress === undefined) { return container.startAddress.equals(candidate); } - return container.startAddress.lessThanOrEqual(candidate) && container.endAddress.greaterThan(candidate); +export function isWithin(candidate: bigint, container: LongMemoryRange): boolean { + if (container.endAddress === undefined) { return container.startAddress === candidate; } + return container.startAddress <= candidate && container.endAddress > candidate; } export function doOverlap(one: LongMemoryRange, other: LongMemoryRange): boolean { @@ -46,11 +44,7 @@ export function doOverlap(one: LongMemoryRange, other: LongMemoryRange): boolean } export function areRangesEqual(one: LongMemoryRange, other: LongMemoryRange): boolean { - return one.startAddress.equals(other.startAddress) && compareUndefinedOrLong(one.endAddress, other.endAddress); -} - -function compareUndefinedOrLong(one: Long | undefined, other: Long | undefined): boolean { - return one === other || (one !== undefined && other !== undefined && one?.equals(other)); + return one.startAddress === other.startAddress && one.endAddress === other.endAddress; } export interface VariableMetadata { diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index ccccf2e..b03478a 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -17,7 +17,7 @@ import * as vscode from 'vscode'; import * as manifest from './manifest'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { MemoryReadResult, MemoryWriteResult } from './common/memory-webview-common'; +import { MemoryReadResult, MemoryWriteResult } from '../common/messaging'; import { AdapterRegistry } from './adapter-registry/adapter-registry'; export interface LabeledUint8Array extends Uint8Array { diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index e10b06e..67c8935 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -15,7 +15,6 @@ ********************************************************************************/ import React from 'react'; -import Long from 'long'; import { VSCodeDataGrid, VSCodeDataGridRow, @@ -114,7 +113,7 @@ export class MemoryTable extends React.Component { return [...this.renderRows(this.props.memory.bytes, this.props.memory.address)]; } - protected *renderRows(iteratee: Uint8Array, address: Long): IterableIterator { + protected *renderRows(iteratee: Uint8Array, address: bigint): IterableIterator { const bytesPerRow = this.props.bytesPerGroup * this.props.groupsPerRow; let rowsYielded = 0; let groups = []; @@ -127,7 +126,7 @@ export class MemoryTable extends React.Component { variables.push(...groupVariables); isRowHighlighted = isRowHighlighted || isHighlighted; if (groups.length === this.props.groupsPerRow || index === iteratee.length - 1) { - const rowAddress = address.add(bytesPerRow * rowsYielded); + const rowAddress = address + BigInt(bytesPerRow * rowsYielded); const options = { address: `0x${rowAddress.toString(16)}`, doShowDivider: (rowsYielded % 4) === 3, @@ -147,7 +146,7 @@ export class MemoryTable extends React.Component { } } - protected *renderGroups(iteratee: Uint8Array, address: Long): IterableIterator { + protected *renderGroups(iteratee: Uint8Array, address: bigint): IterableIterator { let bytesInGroup: React.ReactNode[] = []; let ascii = ''; let variables = []; @@ -158,7 +157,7 @@ export class MemoryTable extends React.Component { variables.push(...byteVariables); isGroupHighlighted = isGroupHighlighted || isHighlighted; if (bytesInGroup.length === this.props.bytesPerGroup || index === iteratee.length - 1) { - const itemID = address.add(index); + const itemID = address + BigInt(index); if (this.props.endianness === Endianness.Little) { bytesInGroup.reverse(); } @@ -177,7 +176,7 @@ export class MemoryTable extends React.Component { } } - protected *renderBytes(iteratee: Uint8Array, address: Long): IterableIterator { + protected *renderBytes(iteratee: Uint8Array, address: bigint): IterableIterator { const itemsPerByte = this.props.byteSize / 8; let currentByte = 0; let chunksInByte: React.ReactNode[] = []; @@ -192,7 +191,7 @@ export class MemoryTable extends React.Component { variables.push(variable); } if (chunksInByte.length === itemsPerByte || index === iteratee.length - 1) { - const itemID = address.add(index); + const itemID = address + BigInt(index); const ascii = this.getASCIIForSingleByte(currentByte); yield { node: {chunksInByte}, @@ -209,10 +208,10 @@ export class MemoryTable extends React.Component { } } - protected *renderArrayItems(iteratee: Uint8Array, address: Long): IterableIterator { + protected *renderArrayItems(iteratee: Uint8Array, address: bigint): IterableIterator { const getBitAttributes = this.getBitAttributes.bind(this); for (let i = 0; i < iteratee.length; i += 1) { - const itemID = address.add(i).toString(16); + const itemID = (address + BigInt(i)).toString(16); const { content = '', className, style, variable, title, isHighlighted } = getBitAttributes(i, iteratee, address); const node = ( { } } - protected getBitAttributes(arrayOffset: number, iteratee: Uint8Array, _address: Long): Partial { + protected getBitAttributes(arrayOffset: number, iteratee: Uint8Array, _address: bigint): Partial { const classNames = ['eight-bits']; return { className: classNames.join(' '), diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index 55bfaf4..ce98a18 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -14,7 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import Long from 'long'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { HOST_EXTENSION } from 'vscode-messenger-common'; @@ -83,9 +82,7 @@ class App extends React.Component<{}, MemoryState> { protected convertMemory(result: DebugProtocol.ReadMemoryResponse['body']): Memory { if (!result?.data) { throw new Error('No memory provided!'); } - const address = result.address.startsWith('0x') - ? Long.fromString(result.address, true, 16) - : Long.fromString(result.address, true, 10); + const address = BigInt(result.address); const bytes = Uint8Array.from(Buffer.from(result.data, 'base64')); return { bytes, address }; } diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 580913f..87c4847 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -15,7 +15,6 @@ ********************************************************************************/ import type { DebugProtocol } from '@vscode/debugprotocol'; -import type Long from 'long'; import type * as React from 'react'; import { areRangesEqual, LongMemoryRange } from '../../common/memory-range'; import deepequal from 'fast-deep-equal'; @@ -26,7 +25,7 @@ export enum Endianness { } export interface Memory { - address: Long; + address: bigint; bytes: Uint8Array; } diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index b687d5c..68f849d 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -20,7 +20,6 @@ import { getVariables } from '../../common/messaging'; import { messenger } from '../view-messenger'; import { Decoration } from '../utils/view-types'; import { EventEmitter, IEvent } from '../utils/events'; -import Long from 'long'; import { ColumnContribution } from '../columns/column-contribution-service'; import { Decorator } from '../decorations/decoration-service'; import { ReactNode } from 'react'; @@ -47,10 +46,13 @@ export class VariableDecorator implements ColumnContribution, Decorator { const visibleVariables = (await messenger.sendRequest(getVariables, HOST_EXTENSION, currentViewParameters)) .map(transmissible => ({ ...transmissible, - startAddress: Long.fromString(transmissible.startAddress), - endAddress: transmissible.endAddress ? Long.fromString(transmissible.endAddress) : undefined + startAddress: BigInt(transmissible.startAddress), + endAddress: transmissible.endAddress ? BigInt(transmissible.endAddress) : undefined })); - visibleVariables.sort((left, right) => left.startAddress.compare(right.startAddress)); + visibleVariables.sort((left, right) => { + const difference = left.startAddress - right.startAddress; + return difference === BigInt(0) ? 0 : difference > 0 ? 1 : -1; + }); if (this.didVariableChange(visibleVariables)) { this.currentVariables = visibleVariables; this.onDidChangeEmitter.fire(this.toDecorations()); diff --git a/webpack.config.js b/webpack.config.js index f1f16d4..ab18cd5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -35,7 +35,7 @@ module.exports = [ ...common, target: 'node', entry: { - extension: './src/desktop/extension.ts' + extension: './src/entry-poinst/desktop/extension.ts' }, output: { filename: '[name].js', @@ -50,7 +50,7 @@ module.exports = [ ...common, target: 'webworker', entry: { - extension: './src/browser/extension.ts' + extension: './src/entry-points/browser/extension.ts' }, output: { filename: '[name].js', @@ -65,7 +65,7 @@ module.exports = [ ...common, target: 'web', entry: { - memory: './src/views/memory-webview-view.tsx' + memory: './src/webview/memory-webview-view.tsx' }, output: { filename: '[name].js', diff --git a/yarn.lock b/yarn.lock index f4f4f94..095cfb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2070,11 +2070,6 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -long@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.1.tgz#e27595d0083d103d2fa2c20c7699f8e0c92b897f" - integrity sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A== - loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" From 9d977b1a6978c78b4847318599c66b805d69425a Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Thu, 30 Mar 2023 19:18:16 -0600 Subject: [PATCH 05/16] Memory decorations working --- package.json | 4 +- src/common/memory-range.ts | 27 ++++++++ .../adapter-registry/adapter-capabilities.ts | 5 +- .../adapter-registry/gdb-capabilities.ts | 17 +++-- src/plugin/memory-provider.ts | 10 +++ src/plugin/memory-webview-main.ts | 9 ++- src/webview/components/memory-table.tsx | 14 ++-- src/webview/components/memory-widget.tsx | 4 +- src/webview/decorations/decoration-service.ts | 64 +++++++++++++++++-- src/webview/memory-webview-view.tsx | 21 +++++- src/webview/variables/variable-decorations.ts | 9 +-- webpack.config.js | 2 +- 12 files changed, 154 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 0439003..8165dc4 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "publisher": "Eclipse", "author": "Rob Moran ", "license": "EPL-2.0", - "main": "dist/entry-points/desktop/extension.js", - "browser": "dist/entry-points/extension.js", + "main": "dist/desktop/extension.js", + "browser": "dist/browser/extension.js", "repository": "https://github.com/eclipse-cdt-cloud/vscode-memory-inspector", "qna": "https://github.com/eclipse-cdt-cloud/vscode-memory-inspector/issues", "icon": "media/cdtcloud.png", diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 00f30b0..266ca20 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -47,6 +47,33 @@ export function areRangesEqual(one: LongMemoryRange, other: LongMemoryRange): bo return one.startAddress === other.startAddress && one.endAddress === other.endAddress; } +export function ensureEndAddress(range: LongMemoryRange): bigint { + return range.endAddress ?? range.startAddress + BigInt(1); +} + +export function compareBigInt(left: bigint, right: bigint): number { + const difference = left - right; + return difference === BigInt(0) ? 0 : difference > 0 ? 1 : -1; +} + +export enum RangeRelationship { + Before, + Within, + Past, + None, +} + +export function determineRelationship(candidate: bigint, range?: LongMemoryRange): RangeRelationship { + if (range === undefined) { return RangeRelationship.None; } + if (candidate < range.startAddress) { return RangeRelationship.Before; } + if (candidate >= ensureEndAddress(range)) { return RangeRelationship.Past; } + return RangeRelationship.Within; +} + +export function toHexStringWithRadixMarker(target: bigint): string { + return `0x${target.toString(16)}`; +} + export interface VariableMetadata { name: string; type?: string; diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index 10d8591..28eea26 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -14,14 +14,15 @@ * 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 { MemoryRange, VariableRange } from '../../common/memory-range'; +import { VariableRange } from '../../common/memory-range'; /** Represents capabilities that may be achieved with particular debug adapters but are not part of the DAP */ export interface AdapterCapabilities { /** Resolve variables known to the adapter to their locations. Fallback if {@link getResidents} is not present */ getVariables?(session: vscode.DebugSession): Promise; /** Resolve symbols resident in the memory at the specified range. Will be preferred to {@link getVariables} if present. */ - getResidents?(session: vscode.DebugSession, range: MemoryRange): Promise; + getResidents?(session: vscode.DebugSession, params: DebugProtocol.ReadMemoryArguments): Promise; initializeAdapterTracker?(session: vscode.DebugSession): vscode.DebugAdapterTracker | undefined; } diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index 64f5dfd..28d46a3 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -17,12 +17,13 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; import { AdapterCapabilities } from './adapter-capabilities'; -import { VariableRange } from '../../common/memory-range'; +import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; type WithChildren = Original & { children?: Array> }; type VariablesTree = Record>; class GdbAdapterTracker implements vscode.DebugAdapterTracker { + private currentFrame?: number; private variablesTree: VariablesTree = {}; private readonly pendingMessages = new Map(); private static hexAddress = /0x[0-9a-f]+/i; @@ -32,6 +33,7 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { onWillReceiveMessage(message: unknown): void { if (isScopesRequest(message)) { + this.currentFrame = message.arguments.frameId; console.log('Sending a fun scopes request!', message); } else if (isVariableRequest(message)) { if (message.arguments.variablesReference in this.variablesTree) { @@ -68,6 +70,7 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { } async getLocals(session: vscode.DebugSession): Promise { + if (this.currentFrame === undefined) { return []; } const maybeRanges = await Promise.all(Object.values(this.variablesTree).reduce>>((previous, parent) => { if (parent.name === 'Local' && parent.children?.length) { parent.children.forEach(child => { @@ -80,11 +83,11 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { } private async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { - if (variable.memoryReference === undefined) { return undefined; } + if (variable.memoryReference === undefined || this.currentFrame === undefined) { return undefined; } try { const [addressResponse, sizeResponse] = await Promise.all([ - session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch' }), - session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'watch' }), + session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch', frameId: this.currentFrame }), + session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'watch', frameId: this.currentFrame }), ]) as DebugProtocol.EvaluateResponse['body'][]; const addressPart = GdbAdapterTracker.hexAddress.exec(addressResponse.result); if (!addressPart) { return undefined; } @@ -92,11 +95,11 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { const endAddress = GdbAdapterTracker.notADigit.test(sizeResponse.result) ? undefined : startAddress + BigInt(sizeResponse.result); return { name: variable.name, - startAddress: startAddress.toString(16), - endAddress: endAddress === undefined ? undefined : endAddress.toString(16), + startAddress: toHexStringWithRadixMarker(startAddress), + endAddress: endAddress === undefined ? undefined : toHexStringWithRadixMarker(endAddress), value: variable.value, }; - } catch { + } catch (err) { return undefined; } } diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index b03478a..20547b4 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -19,6 +19,7 @@ import * as manifest from './manifest'; import { DebugProtocol } from '@vscode/debugprotocol'; import { MemoryReadResult, MemoryWriteResult } from '../common/messaging'; import { AdapterRegistry } from './adapter-registry/adapter-registry'; +import { VariableRange } from '../common/memory-range'; export interface LabeledUint8Array extends Uint8Array { label?: string; @@ -32,8 +33,10 @@ export class MemoryProvider { public static WriteKey = `${manifest.PACKAGE_NAME}.canWrite`; protected readonly sessions = new Map(); + protected adapterRegistry?: AdapterRegistry; public async activate(context: vscode.ExtensionContext, registry: AdapterRegistry): Promise { + this.adapterRegistry = registry; const createDebugAdapterTracker = (session: vscode.DebugSession): Required => { const handlerForSession = registry.getHandlerForSession(session); const contributedTracker = handlerForSession?.initializeAdapterTracker?.(session); @@ -108,4 +111,11 @@ export class MemoryProvider { public async writeMemory(writeMemoryArguments: DebugProtocol.WriteMemoryArguments): Promise { return this.assertCapability('supportsWriteMemoryRequest', 'write memory').customRequest('writeMemory', writeMemoryArguments); } + + public async getVariables(variableArguments: DebugProtocol.ReadMemoryArguments): Promise { + const session = this.assertActiveSession('get variables'); + const handler = this.adapterRegistry?.getHandlerForSession(session); + if (handler?.getResidents) { return handler.getResidents(session, variableArguments); } + return handler?.getVariables?.(session) ?? []; + } } diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index b506239..170675c 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -26,10 +26,12 @@ import { readMemoryType, writeMemoryType, MemoryReadResult, - MemoryWriteResult + MemoryWriteResult, + getVariables } from '../common/messaging'; import { MemoryProvider } from './memory-provider'; import { logger } from './logger'; +import { VariableRange } from '../common/memory-range'; interface Variable { name: string; @@ -120,6 +122,7 @@ export class MemoryWebview { this.messenger.onRequest(logMessageType, message => logger.info(message), { sender: participant }), this.messenger.onRequest(readMemoryType, request => this.readMemory(request), { sender: participant }), this.messenger.onRequest(writeMemoryType, request => this.writeMemory(request), { sender: participant }), + this.messenger.onRequest(getVariables, request => this.getVariables(request), { sender: participant }), ]; panel.onDidDispose(() => disposables.forEach(disposible => disposible.dispose())); @@ -136,4 +139,8 @@ export class MemoryWebview { protected async writeMemory(request: DebugProtocol.WriteMemoryArguments): Promise { return this.memoryProvider.writeMemory(request); } + + protected async getVariables(request: DebugProtocol.ReadMemoryArguments): Promise { + return this.memoryProvider.getVariables(request); + } } diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index 67c8935..13efd6b 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -20,7 +20,9 @@ import { VSCodeDataGridRow, VSCodeDataGridCell } from '@vscode/webview-ui-toolkit/react'; -import { Endianness, Memory } from '../utils/view-types'; +import { Decoration, Endianness, Memory } from '../utils/view-types'; +import { toHexStringWithRadixMarker } from '../../common/memory-range'; +import { decorationService } from '../decorations/decoration-service'; interface VariableDecoration { name: string; @@ -74,6 +76,7 @@ interface RowOptions { interface MemoryTableProps { memory?: Memory; + decorations: Decoration[]; endianness: Endianness; byteSize: number; bytesPerGroup: number; @@ -128,7 +131,7 @@ export class MemoryTable extends React.Component { if (groups.length === this.props.groupsPerRow || index === iteratee.length - 1) { const rowAddress = address + BigInt(bytesPerRow * rowsYielded); const options = { - address: `0x${rowAddress.toString(16)}`, + address: toHexStringWithRadixMarker(rowAddress), doShowDivider: (rowsYielded % 4) === 3, isHighlighted: isRowHighlighted, ascii, @@ -211,7 +214,7 @@ export class MemoryTable extends React.Component { protected *renderArrayItems(iteratee: Uint8Array, address: bigint): IterableIterator { const getBitAttributes = this.getBitAttributes.bind(this); for (let i = 0; i < iteratee.length; i += 1) { - const itemID = (address + BigInt(i)).toString(16); + const itemID = toHexStringWithRadixMarker(address + BigInt(i)); const { content = '', className, style, variable, title, isHighlighted } = getBitAttributes(i, iteratee, address); const node = ( { } } - protected getBitAttributes(arrayOffset: number, iteratee: Uint8Array, _address: bigint): Partial { + protected getBitAttributes(arrayOffset: number, iteratee: Uint8Array, address: bigint): Partial { + const currentCellAddress = (address + BigInt(arrayOffset)); const classNames = ['eight-bits']; return { className: classNames.join(' '), variable: undefined, - style: { color: undefined }, + style: decorationService.getDecoration(currentCellAddress)?.style, content: iteratee[arrayOffset].toString(16).padStart(2, '0') }; } diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index fc1f532..e01d218 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -18,10 +18,11 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import React from 'react'; import { MemoryTable } from './memory-table'; import { OptionsWidget } from './options-widget'; -import { Endianness, Memory } from '../utils/view-types'; +import { Decoration, Endianness, Memory } from '../utils/view-types'; interface MemoryWidgetProps { memory?: Memory; + decorations: Decoration[]; memoryReference: string; offset: number; count: number; @@ -64,6 +65,7 @@ export class MemoryWidget extends React.Component { class DecorationService { private onDidChangeEmitter = new EventEmitter(); - private decorations = new Map(); + private contributedDecorations = new Map(); private decorators = new Map(); /** Represents the aggregation of all contributed decorations */ private currentDecorations = new Array(); + get decorations(): Decoration[] { + return this.currentDecorations; + } register(contribution: Decorator): Disposable { this.decorators.set(contribution.id, contribution); const changeListener = contribution.onDidChange(newDecorations => { - const oldDecorations = this.decorations.get(contribution.id); + const oldDecorations = this.contributedDecorations.get(contribution.id); this.reconcileDecorations(contribution.id, oldDecorations, newDecorations); }); return { dispose: () => { changeListener.dispose(); this.decorators.delete(contribution.id); - const currentDecorations = this.decorations.get(contribution.id); - this.decorations.delete(contribution.id); + const currentDecorations = this.contributedDecorations.get(contribution.id); + this.contributedDecorations.delete(contribution.id); this.reconcileDecorations(contribution.id, currentDecorations, []); } }; @@ -47,6 +51,58 @@ class DecorationService { private reconcileDecorations(affectedDecorator: string, oldDecorations: Decoration[] | undefined, newDecorations: Decoration[]): void { if (oldDecorations?.length === newDecorations.length && oldDecorations.every((old, index) => areDecorationsEqual(old, newDecorations[index]))) { return; } + // TODO: Could be more surgical and figure out the changed ranges. For now, we just rebuild everything. + if (newDecorations.length) { + this.contributedDecorations.set(affectedDecorator, newDecorations); + } else { this.contributedDecorations.delete(affectedDecorator); } + if (this.decorators.size < 2) { + this.currentDecorations = newDecorations; + } else { + const termini = new Set(); + for (const decorationContributions of this.contributedDecorations.values()) { + decorationContributions.forEach(decoration => { + termini.add(decoration.range.startAddress); + termini.add(ensureEndAddress(decoration.range)); + }); + } + const decorations = new Array(); + const contributions = Array.from(this.contributedDecorations.values(), array => array.values()); + const currentSubDecorations = contributions.map(contribution => contribution.next().value); + const terminiInOrder = Array.from(termini).sort(compareBigInt); + terminiInOrder.forEach((terminus, index) => { + if (index === terminiInOrder.length - 1) { return; } + const decoration: Decoration = { + range: { startAddress: terminus, endAddress: terminiInOrder[index + 1] }, + style: {} + }; + decorations.push(decoration); + currentSubDecorations.forEach((subDecoration, subDecorationIndex) => { + switch (determineRelationship(terminus, subDecoration?.range)) { + case RangeRelationship.Within: Object.assign(decoration.style, subDecoration?.style); + break; + case RangeRelationship.Past: { + const newSubDecoration = currentSubDecorations[subDecorationIndex] = contributions[subDecorationIndex].next().value; + if (determineRelationship(terminus, newSubDecoration.range) === RangeRelationship.Within) { Object.assign(decoration.style, newSubDecoration.style); } + } + } + }); + }); + this.currentDecorations = decorations; + } + this.onDidChangeEmitter.fire(this.currentDecorations); + } + + private currentDecorationIndex = 0; + private lastCall?: bigint; + getDecoration(address: bigint): Decoration | undefined { + if (this.currentDecorations.length === 0) { return undefined; } + if (this.lastCall === undefined || address < this.lastCall) { this.currentDecorationIndex = 0; } + this.lastCall = address; + if (address < this.currentDecorations[this.currentDecorationIndex].range.startAddress) { return undefined; } + while (this.currentDecorationIndex < this.currentDecorations.length + && address >= ensureEndAddress(this.currentDecorations[this.currentDecorationIndex].range)) { this.currentDecorationIndex++; } + this.currentDecorationIndex = Math.min(this.currentDecorationIndex, this.currentDecorations.length - 1); + return isWithin(address, this.currentDecorations[this.currentDecorationIndex].range) ? this.currentDecorations[this.currentDecorationIndex] : undefined; } getUpdateExecutors(): UpdateExecutor[] { diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index ce98a18..ac384aa 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -24,19 +24,29 @@ import { readMemoryType } from '../common/messaging'; import type { DebugProtocol } from '@vscode/debugprotocol'; -import { Memory, MemoryState } from './utils/view-types'; +import { Decoration, Memory, MemoryState } from './utils/view-types'; import { MemoryWidget } from './components/memory-widget'; import { messenger } from './view-messenger'; +import { columnContributionService } from './columns/column-contribution-service'; +import { decorationService } from './decorations/decoration-service'; +import { variableDecorator } from './variables/variable-decorations'; -class App extends React.Component<{}, MemoryState> { +export interface MemoryAppState extends MemoryState { + decorations: Decoration[]; +} + +class App extends React.Component<{}, MemoryAppState> { public constructor(props: {}) { super(props); + columnContributionService.register(variableDecorator); + decorationService.register(variableDecorator); this.state = { memory: undefined, memoryReference: '', offset: 0, count: 256, + decorations: [] }; } @@ -48,6 +58,7 @@ class App extends React.Component<{}, MemoryState> { public render(): React.ReactNode { return { }; const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions); - + await Promise.all(Array.from( + new Set(columnContributionService.getUpdateExecutors().concat(decorationService.getUpdateExecutors())), + execututor => execututor.fetchData(completeOptions) + )); this.setState({ + decorations: decorationService.decorations, memory: this.convertMemory(response) }); } diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index 68f849d..06c0c95 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -23,7 +23,7 @@ import { EventEmitter, IEvent } from '../utils/events'; import { ColumnContribution } from '../columns/column-contribution-service'; import { Decorator } from '../decorations/decoration-service'; import { ReactNode } from 'react'; -import { areVariablesEqual, doOverlap, LongMemoryRange, LongVariableRange } from '../../common/memory-range'; +import { areVariablesEqual, compareBigInt, doOverlap, LongMemoryRange, LongVariableRange } from '../../common/memory-range'; const NON_HC_COLORS = [ 'var(--vscode-terminal-ansiBlue)', @@ -49,10 +49,7 @@ export class VariableDecorator implements ColumnContribution, Decorator { startAddress: BigInt(transmissible.startAddress), endAddress: transmissible.endAddress ? BigInt(transmissible.endAddress) : undefined })); - visibleVariables.sort((left, right) => { - const difference = left.startAddress - right.startAddress; - return difference === BigInt(0) ? 0 : difference > 0 ? 1 : -1; - }); + visibleVariables.sort((left, right) => compareBigInt(left.startAddress, right.startAddress)); if (this.didVariableChange(visibleVariables)) { this.currentVariables = visibleVariables; this.onDidChangeEmitter.fire(this.toDecorations()); @@ -89,4 +86,4 @@ export class VariableDecorator implements ColumnContribution, Decorator { this.onDidChangeEmitter.dispose(); } } - +export const variableDecorator = new VariableDecorator(); diff --git a/webpack.config.js b/webpack.config.js index ab18cd5..88b5b7f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -35,7 +35,7 @@ module.exports = [ ...common, target: 'node', entry: { - extension: './src/entry-poinst/desktop/extension.ts' + extension: './src/entry-points/desktop/extension.ts' }, output: { filename: '[name].js', From 8a0ec065d7366b0b9905f9bceacdf19d18dc2bcf Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Fri, 31 Mar 2023 11:51:23 -0600 Subject: [PATCH 06/16] Working pretty well --- src/common/memory-range.ts | 9 +- .../adapter-registry/adapter-registry.ts | 4 +- .../adapter-registry/gdb-capabilities.ts | 20 +-- src/plugin/memory-provider.ts | 2 +- .../columns/column-contribution-service.ts | 50 ++++--- src/webview/components/memory-table.tsx | 30 +++- src/webview/components/memory-widget.tsx | 6 + src/webview/components/multi-select-bar.tsx | 140 ++++++++++++++++++ src/webview/components/options-widget.tsx | 10 ++ src/webview/decorations/decoration-service.ts | 20 +-- src/webview/memory-webview-view.tsx | 15 +- src/webview/utils/events.ts | 6 +- src/webview/utils/view-types.ts | 2 +- src/webview/variables/variable-decorations.ts | 65 ++++++-- 14 files changed, 303 insertions(+), 76 deletions(-) create mode 100644 src/webview/components/multi-select-bar.tsx diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index 266ca20..c522547 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -30,11 +30,10 @@ export interface MemoryRange { /** Suitable for arithemetic */ export interface LongMemoryRange { startAddress: bigint; - endAddress?: bigint; + endAddress: bigint; } export function isWithin(candidate: bigint, container: LongMemoryRange): boolean { - if (container.endAddress === undefined) { return container.startAddress === candidate; } return container.startAddress <= candidate && container.endAddress > candidate; } @@ -47,10 +46,6 @@ export function areRangesEqual(one: LongMemoryRange, other: LongMemoryRange): bo return one.startAddress === other.startAddress && one.endAddress === other.endAddress; } -export function ensureEndAddress(range: LongMemoryRange): bigint { - return range.endAddress ?? range.startAddress + BigInt(1); -} - export function compareBigInt(left: bigint, right: bigint): number { const difference = left - right; return difference === BigInt(0) ? 0 : difference > 0 ? 1 : -1; @@ -66,7 +61,7 @@ export enum RangeRelationship { export function determineRelationship(candidate: bigint, range?: LongMemoryRange): RangeRelationship { if (range === undefined) { return RangeRelationship.None; } if (candidate < range.startAddress) { return RangeRelationship.Before; } - if (candidate >= ensureEndAddress(range)) { return RangeRelationship.Past; } + if (candidate >= range.endAddress) { return RangeRelationship.Past; } return RangeRelationship.Within; } diff --git a/src/plugin/adapter-registry/adapter-registry.ts b/src/plugin/adapter-registry/adapter-registry.ts index f189de5..78dda8e 100644 --- a/src/plugin/adapter-registry/adapter-registry.ts +++ b/src/plugin/adapter-registry/adapter-registry.ts @@ -18,8 +18,8 @@ import * as vscode from 'vscode'; import { AdapterCapabilities } from './adapter-capabilities'; export class AdapterRegistry implements vscode.Disposable { - private handlers = new Map(); - private isDisposed = false; + protected handlers = new Map(); + protected isDisposed = false; registerAdapter(debugType: string, handlerToRegister: AdapterCapabilities): vscode.Disposable { if (this.isDisposed) { return new vscode.Disposable(() => { }); } this.handlers.set(debugType, handlerToRegister); diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index 28d46a3..50336b5 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -23,28 +23,25 @@ type WithChildren = Original & { children?: Array>; class GdbAdapterTracker implements vscode.DebugAdapterTracker { - private currentFrame?: number; - private variablesTree: VariablesTree = {}; - private readonly pendingMessages = new Map(); - private static hexAddress = /0x[0-9a-f]+/i; - private static notADigit = /[^0-9]/; + protected currentFrame?: number; + protected variablesTree: VariablesTree = {}; + protected readonly pendingMessages = new Map(); + protected static hexAddress = /0x[0-9a-f]+/i; + protected static notADigit = /[^0-9]/; - constructor(private readonly onEnd: vscode.Disposable) { } + constructor(protected readonly onEnd: vscode.Disposable) { } onWillReceiveMessage(message: unknown): void { if (isScopesRequest(message)) { this.currentFrame = message.arguments.frameId; - console.log('Sending a fun scopes request!', message); } else if (isVariableRequest(message)) { if (message.arguments.variablesReference in this.variablesTree) { this.pendingMessages.set(message.seq, message.arguments.variablesReference); } - console.log('Sending a fun variable request!', message); } } onDidSendMessage(message: unknown): void { if (isScopesResponse(message)) { - console.log('Got a fun scope message!', message); for (const scope of message.body.scopes) { if (scope.name === 'Local') { if (!this.variablesTree[scope.variablesReference] || this.variablesTree[scope.variablesReference].name !== 'Local') { @@ -61,7 +58,6 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { this.variablesTree[parentReference].children = message.body.variables; } } - console.log('Got a fun variable message!', message); } } onExit(): void { @@ -82,7 +78,7 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { return maybeRanges.filter((candidate): candidate is VariableRange => !!candidate); } - private async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { + protected async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { if (variable.memoryReference === undefined || this.currentFrame === undefined) { return undefined; } try { const [addressResponse, sizeResponse] = await Promise.all([ @@ -126,7 +122,7 @@ function isVariableResponse(message: unknown): message is DebugProtocol.Variable } export class GdbCapabilities implements AdapterCapabilities { - private sessions = new Map(); + protected sessions = new Map(); initializeAdapterTracker(session: vscode.DebugSession): GdbAdapterTracker | undefined { if (session.type === 'gdb') { const sessionTracker = new GdbAdapterTracker(new vscode.Disposable(() => this.sessions.delete(session.id))); diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index 20547b4..4af6400 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -97,7 +97,7 @@ export class MemoryProvider { return session; } - private assertActiveSession(action: string): vscode.DebugSession { + protected assertActiveSession(action: string): vscode.DebugSession { if (!vscode.debug.activeDebugSession) { throw new Error(`Cannot ${action}. No active debug session.`); } diff --git a/src/webview/columns/column-contribution-service.ts b/src/webview/columns/column-contribution-service.ts index 2362c6a..7490a95 100644 --- a/src/webview/columns/column-contribution-service.ts +++ b/src/webview/columns/column-contribution-service.ts @@ -17,12 +17,12 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import type * as React from 'react'; import { LongMemoryRange } from '../../common/memory-range'; -import type { Disposable, MemoryState, UpdateExecutor } from '../utils/view-types'; +import type { Disposable, Memory, MemoryState, UpdateExecutor } from '../utils/view-types'; export interface ColumnContribution { readonly label: string; readonly id: string; - render(range: LongMemoryRange, memory: MemoryState): React.ReactNode + render(range: LongMemoryRange, memory: Memory): React.ReactNode /** Called when fetching new memory or when activating the column. */ fetchData?(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; /** Called when the user reveals the column */ @@ -31,37 +31,49 @@ export interface ColumnContribution { deactivate?(): void; } +export interface ColumnStatus { + contribution: ColumnContribution; + active: boolean; +} + class ColumnContributionService { - private activeColumns = new Array(); - private registeredColumns = new Map; + protected columnArray = new Array(); + protected registeredColumns = new Map; register(contribution: ColumnContribution): Disposable { - this.registeredColumns.set(contribution.id, contribution); + if (this.registeredColumns.has(contribution.id)) { return { dispose: () => { } }; } + const wrapper = { contribution, active: false }; + this.registeredColumns.set(contribution.id, wrapper); + this.columnArray.push(wrapper); + this.columnArray.sort((left, right) => left.contribution.id.localeCompare(right.contribution.id)); return { dispose: () => { this.hide(contribution.id); this.registeredColumns.delete(contribution.id); + this.columnArray = this.columnArray.filter(candidate => wrapper !== candidate); } }; } - async show(id: string, memoryState: MemoryState): Promise { - const contribution = this.registeredColumns.get(id); - if (contribution) { - await contribution.activate?.(memoryState); - this.activeColumns.push(contribution); - this.activeColumns.sort((left, right) => left.id.localeCompare(right.id)); + async show(id: string, memoryState: MemoryState): Promise { + const wrapper = this.registeredColumns.get(id); + if (wrapper) { + await wrapper.contribution.activate?.(memoryState); + wrapper.active = true; } - return this.activeColumns.slice(); + return this.columnArray.slice(); } - hide(id: string): ColumnContribution[] { - const contribution = this.registeredColumns.get(id); - let index; - if (contribution && (index = this.activeColumns.findIndex(candidate => candidate === contribution)) !== -1) { - this.activeColumns.splice(index, 1); + hide(id: string): ColumnStatus[] { + const wrapper = this.registeredColumns.get(id); + if (wrapper?.active) { + wrapper.active = false; + wrapper.contribution.deactivate?.(); } - return this.activeColumns.slice(); + return this.columnArray.slice(); + } + getColumns(): ColumnStatus[] { + return this.columnArray.slice(); } getUpdateExecutors(): UpdateExecutor[] { - return this.activeColumns.filter((candidate): candidate is ColumnContribution & UpdateExecutor => candidate.fetchData !== undefined); + return this.columnArray.map(({ contribution }) => contribution).filter((candidate): candidate is ColumnContribution & UpdateExecutor => candidate.fetchData !== undefined); } } diff --git a/src/webview/components/memory-table.tsx b/src/webview/components/memory-table.tsx index 13efd6b..461bf78 100644 --- a/src/webview/components/memory-table.tsx +++ b/src/webview/components/memory-table.tsx @@ -23,6 +23,7 @@ import { import { Decoration, Endianness, Memory } from '../utils/view-types'; import { toHexStringWithRadixMarker } from '../../common/memory-range'; import { decorationService } from '../decorations/decoration-service'; +import { ColumnStatus } from '../columns/column-contribution-service'; interface VariableDecoration { name: string; @@ -65,8 +66,9 @@ interface FullNodeAttributes extends StylableNodeAttributes { } interface RowOptions { - address: string; + address: bigint; groups: React.ReactNode; + gridTemplateColumns: string; ascii?: string; variables?: VariableDecoration[]; doShowDivider?: boolean; @@ -77,6 +79,7 @@ interface RowOptions { interface MemoryTableProps { memory?: Memory; decorations: Decoration[]; + columns: ColumnStatus[]; endianness: Endianness; byteSize: number; bytesPerGroup: number; @@ -89,13 +92,16 @@ export class MemoryTable extends React.Component { return (
- + Address Groups + {this.props.columns.map(({ contribution }, index) => + {contribution.label} + )} {rows} @@ -106,9 +112,10 @@ export class MemoryTable extends React.Component { protected getTableRows(): React.ReactNode { if (!this.props.memory) { return ( - + No Data No Data + {this.props.columns.map((column, index) => column.active && No Data)} ); } @@ -118,6 +125,7 @@ export class MemoryTable extends React.Component { protected *renderRows(iteratee: Uint8Array, address: bigint): IterableIterator { const bytesPerRow = this.props.bytesPerGroup * this.props.groupsPerRow; + const gridTemplateColumns = new Array(this.props.columns.length + 2).fill('1fr').join(' '); let rowsYielded = 0; let groups = []; let ascii = ''; @@ -131,13 +139,14 @@ export class MemoryTable extends React.Component { if (groups.length === this.props.groupsPerRow || index === iteratee.length - 1) { const rowAddress = address + BigInt(bytesPerRow * rowsYielded); const options = { - address: toHexStringWithRadixMarker(rowAddress), + address: rowAddress, doShowDivider: (rowsYielded % 4) === 3, isHighlighted: isRowHighlighted, ascii, groups, variables, index, + gridTemplateColumns }; yield this.renderRow(options); ascii = ''; @@ -256,18 +265,25 @@ export class MemoryTable extends React.Component { } protected renderRow(options: RowOptions, getRowAttributes = this.getRowAttributes.bind(this)): React.ReactNode { - const { address, groups } = options; + const { address, groups, gridTemplateColumns } = options; + const addressString = toHexStringWithRadixMarker(address); const { className, style, title } = getRowAttributes(options); + const range = { startAddress: address, endAddress: address + BigInt(this.props.bytesPerGroup * this.props.groupsPerRow) }; return ( - {address} + {addressString} {groups} + {this.props.columns.map((column, index) => + {column.contribution.render(range, this.props.memory!)} + + )} ); } diff --git a/src/webview/components/memory-widget.tsx b/src/webview/components/memory-widget.tsx index e01d218..258a941 100644 --- a/src/webview/components/memory-widget.tsx +++ b/src/webview/components/memory-widget.tsx @@ -19,15 +19,18 @@ import React from 'react'; import { MemoryTable } from './memory-table'; import { OptionsWidget } from './options-widget'; import { Decoration, Endianness, Memory } from '../utils/view-types'; +import { ColumnStatus } from '../columns/column-contribution-service'; interface MemoryWidgetProps { memory?: Memory; decorations: Decoration[]; + columns: ColumnStatus[]; memoryReference: string; offset: number; count: number; refreshMemory: () => void; updateMemoryArguments: (memoryArguments: Partial) => void; + toggleColumn(id: string, active: boolean): void; } interface MemoryWidgetState { @@ -53,6 +56,7 @@ export class MemoryWidget extends React.Component candidate.active)} memory={this.props.memory} endianness={this.state.endianness} byteSize={this.state.byteSize} diff --git a/src/webview/components/multi-select-bar.tsx b/src/webview/components/multi-select-bar.tsx new file mode 100644 index 0000000..7da37ef --- /dev/null +++ b/src/webview/components/multi-select-bar.tsx @@ -0,0 +1,140 @@ +/******************************************************************************** + * Copyright (C) 2023 Ericsson 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 React from 'react'; + +export const MultiSelectBarStyle: React.CSSProperties = { + display: 'flex', + flexFlow: 'row nowrap', + userSelect: 'none', + boxSizing: 'border-box', + msUserSelect: 'none', + MozUserSelect: 'none', + WebkitUserSelect: 'none', +}; + +export const MultiSelectCheckboxWrapperStyle: React.CSSProperties = { + display: 'flex', + position: 'relative', + flex: 'auto', + textAlign: 'center', +}; + +export const MultiSelectLabelStyle: React.CSSProperties = { + height: '100%', + flex: 'auto', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + border: '1px solid', + padding: '0 6', + backgroundColor: 'var(--vscode-editor-background)', + borderColor: 'var(--vscode-dropdown-border)', + boxSizing: 'border-box', + textTransform: 'uppercase', +}; + +export const MultiSelectActiveLabelStyle: React.CSSProperties = { + ...MultiSelectLabelStyle, + backgroundColor: 'var(--vscode-input-background)', + borderColor: 'var(--vscode-sideBar-foreground)', + textDecoration: 'underline', + fontWeight: 'bold', +}; + +export const MultiSelectInactiveLabelStyle: React.CSSProperties = { + ...MultiSelectActiveLabelStyle, + fontStyle: 'italic', + opacity: '0.7', +}; + +export const MultiSelectCheckboxStyle: React.CSSProperties = { + appearance: 'none', + WebkitAppearance: 'none', + position: 'absolute', + left: '0', + top: '0', + margin: '0', + height: '100%', + width: '100%', + cursor: 'pointer', +}; + +export interface SingleSelectItemProps { + id: string; + label: string; + checked: boolean; +} + +interface MultiSelectBarProps { + items: SingleSelectItemProps[]; + id?: string; + onSelectionChanged: (labelSelected: string, newSelectionState: boolean) => unknown; +} + +export const MultiSelectBar: React.FC = ({ items, onSelectionChanged, id }) => { + const changeHandler: React.ChangeEventHandler = React.useCallback(e => { + onSelectionChanged(e.target.id, e.target.checked); + }, [onSelectionChanged]); + + return ( +
+ {items.map(({ label, id: itemId, checked }) => ())} +
+ ); +}; + +interface LabeledCheckboxProps { + label: string; + id: string; + onChange: React.ChangeEventHandler; + checked: boolean; +} + +export interface LabelProps { id: string; label: string; disabled?: boolean; classNames?: string[], style?: React.CSSProperties } + +export const Label: React.FC = ({ id, label, disabled, classNames, style }) => { + const additionalClassNames = classNames ? classNames.join(' ') : ''; + return ; +}; + +const LabeledCheckbox: React.FC = ({ checked, label, onChange, id }) => ( +
+ +
+); + +export const MultiSelectWithLabel: React.FC = ({ id, label, items, onSelectionChanged }) => ( + <> + + + +); diff --git a/src/webview/components/options-widget.tsx b/src/webview/components/options-widget.tsx index cc48dcf..3f19132 100644 --- a/src/webview/components/options-widget.tsx +++ b/src/webview/components/options-widget.tsx @@ -18,11 +18,14 @@ import React from 'react'; import type { DebugProtocol } from '@vscode/debugprotocol'; import { Endianness, TableRenderOptions } from '../utils/view-types'; import { VSCodeButton, VSCodeDivider, VSCodeDropdown, VSCodeOption, VSCodeTextField } from '@vscode/webview-ui-toolkit/react'; +import { ColumnStatus } from '../columns/column-contribution-service'; +import { MultiSelectWithLabel } from './multi-select-bar'; interface OptionsWidgetProps { updateRenderOptions: (options: Partial) => void; updateMemoryArguments: (memoryArguments: Partial) => void; refreshMemory: () => void; + toggleColumn(id: string, active: boolean): void; memoryReference: string; offset: number; count: number; @@ -30,6 +33,7 @@ interface OptionsWidgetProps { byteSize: number; bytesPerGroup: number; groupsPerRow: number; + columns: ColumnStatus[]; } interface OptionsWidgetState { @@ -120,6 +124,12 @@ export class OptionsWidget extends React.Component16 32 + {!!this.props.columns.length && ({ id: column.contribution.id, label: column.contribution.label, checked: column.active }))} + onSelectionChanged={this.props.toggleColumn} + />}
} diff --git a/src/webview/decorations/decoration-service.ts b/src/webview/decorations/decoration-service.ts index 7ee1a39..b3335d3 100644 --- a/src/webview/decorations/decoration-service.ts +++ b/src/webview/decorations/decoration-service.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { compareBigInt, determineRelationship, ensureEndAddress, isWithin, RangeRelationship } from '../../common/memory-range'; +import { compareBigInt, determineRelationship, isWithin, RangeRelationship } from '../../common/memory-range'; import { EventEmitter, IEvent } from '../utils/events'; import { areDecorationsEqual, Decoration, Disposable, UpdateExecutor } from '../utils/view-types'; @@ -24,11 +24,11 @@ export interface Decorator extends Partial { } class DecorationService { - private onDidChangeEmitter = new EventEmitter(); - private contributedDecorations = new Map(); - private decorators = new Map(); + protected onDidChangeEmitter = new EventEmitter(); + protected contributedDecorations = new Map(); + protected decorators = new Map(); /** Represents the aggregation of all contributed decorations */ - private currentDecorations = new Array(); + protected currentDecorations = new Array(); get decorations(): Decoration[] { return this.currentDecorations; } @@ -49,7 +49,7 @@ class DecorationService { }; } - private reconcileDecorations(affectedDecorator: string, oldDecorations: Decoration[] | undefined, newDecorations: Decoration[]): void { + protected reconcileDecorations(affectedDecorator: string, oldDecorations: Decoration[] | undefined, newDecorations: Decoration[]): void { if (oldDecorations?.length === newDecorations.length && oldDecorations.every((old, index) => areDecorationsEqual(old, newDecorations[index]))) { return; } // TODO: Could be more surgical and figure out the changed ranges. For now, we just rebuild everything. if (newDecorations.length) { @@ -62,7 +62,7 @@ class DecorationService { for (const decorationContributions of this.contributedDecorations.values()) { decorationContributions.forEach(decoration => { termini.add(decoration.range.startAddress); - termini.add(ensureEndAddress(decoration.range)); + termini.add(decoration.range.endAddress); }); } const decorations = new Array(); @@ -92,15 +92,15 @@ class DecorationService { this.onDidChangeEmitter.fire(this.currentDecorations); } - private currentDecorationIndex = 0; - private lastCall?: bigint; + protected currentDecorationIndex = 0; + protected lastCall?: bigint; getDecoration(address: bigint): Decoration | undefined { if (this.currentDecorations.length === 0) { return undefined; } if (this.lastCall === undefined || address < this.lastCall) { this.currentDecorationIndex = 0; } this.lastCall = address; if (address < this.currentDecorations[this.currentDecorationIndex].range.startAddress) { return undefined; } while (this.currentDecorationIndex < this.currentDecorations.length - && address >= ensureEndAddress(this.currentDecorations[this.currentDecorationIndex].range)) { this.currentDecorationIndex++; } + && address >= this.currentDecorations[this.currentDecorationIndex].range.endAddress) { this.currentDecorationIndex++; } this.currentDecorationIndex = Math.min(this.currentDecorationIndex, this.currentDecorations.length - 1); return isWithin(address, this.currentDecorations[this.currentDecorationIndex].range) ? this.currentDecorations[this.currentDecorationIndex] : undefined; } diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index ac384aa..e056dd7 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -27,12 +27,13 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import { Decoration, Memory, MemoryState } from './utils/view-types'; import { MemoryWidget } from './components/memory-widget'; import { messenger } from './view-messenger'; -import { columnContributionService } from './columns/column-contribution-service'; +import { columnContributionService, ColumnStatus } from './columns/column-contribution-service'; import { decorationService } from './decorations/decoration-service'; import { variableDecorator } from './variables/variable-decorations'; export interface MemoryAppState extends MemoryState { decorations: Decoration[]; + columns: ColumnStatus[]; } class App extends React.Component<{}, MemoryAppState> { @@ -46,7 +47,8 @@ class App extends React.Component<{}, MemoryAppState> { memoryReference: '', offset: 0, count: 256, - decorations: [] + decorations: [], + columns: columnContributionService.getColumns(), }; } @@ -59,11 +61,13 @@ class App extends React.Component<{}, MemoryAppState> { return ; } @@ -101,6 +105,13 @@ class App extends React.Component<{}, MemoryAppState> { const bytes = Uint8Array.from(Buffer.from(result.data, 'base64')); return { bytes, address }; } + + protected toggleColumn = (id: string, active: boolean): void => { this.doToggleColumn(id, active); }; + + protected async doToggleColumn(id: string, active: boolean): Promise { + const columns = active ? await columnContributionService.show(id, this.state) : columnContributionService.hide(id); + this.setState({ columns }); + } } const container = document.getElementById('root') as Element; diff --git a/src/webview/utils/events.ts b/src/webview/utils/events.ts index 9bd7093..fc3535d 100644 --- a/src/webview/utils/events.ts +++ b/src/webview/utils/events.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2023 YourCompany and others. + * Copyright (C) 2023 Ericsson 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 @@ -17,8 +17,8 @@ import { Disposable, dispose } from './view-types'; export class EventEmitter { - private emitter = new EventTarget(); - private toDispose = new Array(); + protected emitter = new EventTarget(); + protected toDispose = new Array(); event(externalHandler: (event: T) => unknown): Disposable { const internalHandler = (event: Event) => { diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 87c4847..0d2f9d4 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -48,7 +48,7 @@ export function dispose(disposable: { dispose(): unknown }): void { export interface Decoration { range: LongMemoryRange; - style: React.StyleHTMLAttributes; + style: React.CSSProperties; } export function areDecorationsEqual(one: Decoration, other: Decoration): boolean { diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index 06c0c95..ca1a8e9 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -18,12 +18,13 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import { HOST_EXTENSION } from 'vscode-messenger-common'; import { getVariables } from '../../common/messaging'; import { messenger } from '../view-messenger'; -import { Decoration } from '../utils/view-types'; +import { Decoration, MemoryState } from '../utils/view-types'; import { EventEmitter, IEvent } from '../utils/events'; import { ColumnContribution } from '../columns/column-contribution-service'; import { Decorator } from '../decorations/decoration-service'; import { ReactNode } from 'react'; -import { areVariablesEqual, compareBigInt, doOverlap, LongMemoryRange, LongVariableRange } from '../../common/memory-range'; +import { areVariablesEqual, compareBigInt, isWithin, LongMemoryRange, LongVariableRange } from '../../common/memory-range'; +import * as React from 'react'; const NON_HC_COLORS = [ 'var(--vscode-terminal-ansiBlue)', @@ -36,19 +37,24 @@ const NON_HC_COLORS = [ export class VariableDecorator implements ColumnContribution, Decorator { readonly id = 'variables'; readonly label = 'Variables'; - private onDidChangeEmitter = new EventEmitter(); + protected active = false; + protected onDidChangeEmitter = new EventEmitter(); /** We expect this to always be sorted from lowest to highest start address */ - private currentVariables?: LongVariableRange[]; + protected currentVariables?: LongVariableRange[]; get onDidChange(): IEvent { return this.onDidChangeEmitter.event.bind(this.onDidChangeEmitter); } async fetchData(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise { + if (!this.active) { return; } const visibleVariables = (await messenger.sendRequest(getVariables, HOST_EXTENSION, currentViewParameters)) - .map(transmissible => ({ - ...transmissible, - startAddress: BigInt(transmissible.startAddress), - endAddress: transmissible.endAddress ? BigInt(transmissible.endAddress) : undefined - })); + .map(transmissible => { + const startAddress = BigInt(transmissible.startAddress); + return { + ...transmissible, + startAddress, + endAddress: transmissible.endAddress ? BigInt(transmissible.endAddress) : startAddress + BigInt(1) + }; + }); visibleVariables.sort((left, right) => compareBigInt(left.startAddress, right.startAddress)); if (this.didVariableChange(visibleVariables)) { this.currentVariables = visibleVariables; @@ -56,16 +62,50 @@ export class VariableDecorator implements ColumnContribution, Decorator { } } + async activate(memory: MemoryState): Promise { + this.active = true; + await this.fetchData({ count: memory.count, offset: memory.offset, memoryReference: memory.memoryReference }); + } + + deactivate(): void { + this.active = false; + const currentVariablesPopulated = !!this.currentVariables?.length; + if (currentVariablesPopulated) { this.onDidChangeEmitter.fire(this.currentVariables = []); } + } + render(range: LongMemoryRange): ReactNode { - return this.currentVariables?.filter(candidate => doOverlap(candidate, range)).map(variables => variables.name).join(', '); + return this.getVariablesInRange(range)?.reduce((result, current, index) => { + if (index > 0) { result.push(','); } + result.push(React.createElement('span', { style: { color: current.color } }, current.variable.name)); + return result; + }, []); } - private didVariableChange(visibleVariables: LongVariableRange[]): boolean { + /** Returns variables that start in the given range. */ + protected lastCall?: BigInt; + protected currentIndex = 0; + protected getVariablesInRange(range: LongMemoryRange): Array<{ variable: LongVariableRange, color: string }> | undefined { + if (!this.currentVariables?.length) { return undefined; } + if (this.currentIndex === this.currentVariables.length - 1 && this.currentVariables[this.currentIndex].startAddress < range.startAddress) { return undefined; } + if (this.lastCall === undefined || range.startAddress < this.lastCall) { this.currentIndex = 0; } + this.lastCall = range.startAddress; + const result = []; + while (this.currentIndex < this.currentVariables.length && this.currentVariables[this.currentIndex].startAddress < range.endAddress) { + if (isWithin(this.currentVariables[this.currentIndex].startAddress, range)) { + result.push({ color: NON_HC_COLORS[this.currentIndex % 5], variable: this.currentVariables[this.currentIndex] }); + } + this.currentIndex++; + } + this.currentIndex = Math.min(this.currentVariables.length - 1, this.currentIndex); + return result; + } + + protected didVariableChange(visibleVariables: LongVariableRange[]): boolean { return visibleVariables.length !== this.currentVariables?.length || visibleVariables.some((item, index) => !areVariablesEqual(item, this.currentVariables![index])); } - private toDecorations(): Decoration[] { + protected toDecorations(): Decoration[] { const decorations: Decoration[] = []; let colorIndex = 0; for (const variable of this.currentVariables ?? []) { @@ -86,4 +126,5 @@ export class VariableDecorator implements ColumnContribution, Decorator { this.onDidChangeEmitter.dispose(); } } + export const variableDecorator = new VariableDecorator(); From b5875546a255386f4edf0a9871ce9ad0151a7235 Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Fri, 31 Mar 2023 20:06:41 +0200 Subject: [PATCH 07/16] Rename old Long references --- src/common/memory-range.ts | 14 +++++++------- src/webview/columns/column-contribution-service.ts | 4 ++-- src/webview/utils/view-types.ts | 4 ++-- src/webview/variables/variable-decorations.ts | 12 ++++++------ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/common/memory-range.ts b/src/common/memory-range.ts index c522547..8947248 100644 --- a/src/common/memory-range.ts +++ b/src/common/memory-range.ts @@ -28,21 +28,21 @@ export interface MemoryRange { } /** Suitable for arithemetic */ -export interface LongMemoryRange { +export interface BigIntMemoryRange { startAddress: bigint; endAddress: bigint; } -export function isWithin(candidate: bigint, container: LongMemoryRange): boolean { +export function isWithin(candidate: bigint, container: BigIntMemoryRange): boolean { return container.startAddress <= candidate && container.endAddress > candidate; } -export function doOverlap(one: LongMemoryRange, other: LongMemoryRange): boolean { +export function doOverlap(one: BigIntMemoryRange, other: BigIntMemoryRange): boolean { // If they overlap, they either start in the same place, or one starts in the other. return isWithin(one.startAddress, other) || isWithin(other.startAddress, one); } -export function areRangesEqual(one: LongMemoryRange, other: LongMemoryRange): boolean { +export function areRangesEqual(one: BigIntMemoryRange, other: BigIntMemoryRange): boolean { return one.startAddress === other.startAddress && one.endAddress === other.endAddress; } @@ -58,7 +58,7 @@ export enum RangeRelationship { None, } -export function determineRelationship(candidate: bigint, range?: LongMemoryRange): RangeRelationship { +export function determineRelationship(candidate: bigint, range?: BigIntMemoryRange): RangeRelationship { if (range === undefined) { return RangeRelationship.None; } if (candidate < range.startAddress) { return RangeRelationship.Before; } if (candidate >= range.endAddress) { return RangeRelationship.Past; } @@ -79,9 +79,9 @@ export interface VariableMetadata { /** Suitable for transmission as JSON */ export interface VariableRange extends MemoryRange, VariableMetadata { } /** Suitable for arithemetic */ -export interface LongVariableRange extends LongMemoryRange, VariableMetadata { } +export interface BigIntVariableRange extends BigIntMemoryRange, VariableMetadata { } -export function areVariablesEqual(one: LongVariableRange, other: LongVariableRange): boolean { +export function areVariablesEqual(one: BigIntVariableRange, other: BigIntVariableRange): boolean { return areRangesEqual(one, other) && one.name === other.name && one.type === other.type diff --git a/src/webview/columns/column-contribution-service.ts b/src/webview/columns/column-contribution-service.ts index 7490a95..c3bb283 100644 --- a/src/webview/columns/column-contribution-service.ts +++ b/src/webview/columns/column-contribution-service.ts @@ -16,13 +16,13 @@ import { DebugProtocol } from '@vscode/debugprotocol'; import type * as React from 'react'; -import { LongMemoryRange } from '../../common/memory-range'; +import { BigIntMemoryRange } from '../../common/memory-range'; import type { Disposable, Memory, MemoryState, UpdateExecutor } from '../utils/view-types'; export interface ColumnContribution { readonly label: string; readonly id: string; - render(range: LongMemoryRange, memory: Memory): React.ReactNode + render(range: BigIntMemoryRange, memory: Memory): React.ReactNode /** Called when fetching new memory or when activating the column. */ fetchData?(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise; /** Called when the user reveals the column */ diff --git a/src/webview/utils/view-types.ts b/src/webview/utils/view-types.ts index 0d2f9d4..a037604 100644 --- a/src/webview/utils/view-types.ts +++ b/src/webview/utils/view-types.ts @@ -16,7 +16,7 @@ import type { DebugProtocol } from '@vscode/debugprotocol'; import type * as React from 'react'; -import { areRangesEqual, LongMemoryRange } from '../../common/memory-range'; +import { areRangesEqual, BigIntMemoryRange } from '../../common/memory-range'; import deepequal from 'fast-deep-equal'; export enum Endianness { @@ -47,7 +47,7 @@ export function dispose(disposable: { dispose(): unknown }): void { } export interface Decoration { - range: LongMemoryRange; + range: BigIntMemoryRange; style: React.CSSProperties; } diff --git a/src/webview/variables/variable-decorations.ts b/src/webview/variables/variable-decorations.ts index ca1a8e9..81b2416 100644 --- a/src/webview/variables/variable-decorations.ts +++ b/src/webview/variables/variable-decorations.ts @@ -23,7 +23,7 @@ import { EventEmitter, IEvent } from '../utils/events'; import { ColumnContribution } from '../columns/column-contribution-service'; import { Decorator } from '../decorations/decoration-service'; import { ReactNode } from 'react'; -import { areVariablesEqual, compareBigInt, isWithin, LongMemoryRange, LongVariableRange } from '../../common/memory-range'; +import { areVariablesEqual, compareBigInt, isWithin, BigIntMemoryRange, BigIntVariableRange } from '../../common/memory-range'; import * as React from 'react'; const NON_HC_COLORS = [ @@ -40,14 +40,14 @@ export class VariableDecorator implements ColumnContribution, Decorator { protected active = false; protected onDidChangeEmitter = new EventEmitter(); /** We expect this to always be sorted from lowest to highest start address */ - protected currentVariables?: LongVariableRange[]; + protected currentVariables?: BigIntVariableRange[]; get onDidChange(): IEvent { return this.onDidChangeEmitter.event.bind(this.onDidChangeEmitter); } async fetchData(currentViewParameters: DebugProtocol.ReadMemoryArguments): Promise { if (!this.active) { return; } const visibleVariables = (await messenger.sendRequest(getVariables, HOST_EXTENSION, currentViewParameters)) - .map(transmissible => { + .map(transmissible => { const startAddress = BigInt(transmissible.startAddress); return { ...transmissible, @@ -73,7 +73,7 @@ export class VariableDecorator implements ColumnContribution, Decorator { if (currentVariablesPopulated) { this.onDidChangeEmitter.fire(this.currentVariables = []); } } - render(range: LongMemoryRange): ReactNode { + render(range: BigIntMemoryRange): ReactNode { return this.getVariablesInRange(range)?.reduce((result, current, index) => { if (index > 0) { result.push(','); } result.push(React.createElement('span', { style: { color: current.color } }, current.variable.name)); @@ -84,7 +84,7 @@ export class VariableDecorator implements ColumnContribution, Decorator { /** Returns variables that start in the given range. */ protected lastCall?: BigInt; protected currentIndex = 0; - protected getVariablesInRange(range: LongMemoryRange): Array<{ variable: LongVariableRange, color: string }> | undefined { + protected getVariablesInRange(range: BigIntMemoryRange): Array<{ variable: BigIntVariableRange, color: string }> | undefined { if (!this.currentVariables?.length) { return undefined; } if (this.currentIndex === this.currentVariables.length - 1 && this.currentVariables[this.currentIndex].startAddress < range.startAddress) { return undefined; } if (this.lastCall === undefined || range.startAddress < this.lastCall) { this.currentIndex = 0; } @@ -100,7 +100,7 @@ export class VariableDecorator implements ColumnContribution, Decorator { return result; } - protected didVariableChange(visibleVariables: LongVariableRange[]): boolean { + protected didVariableChange(visibleVariables: BigIntVariableRange[]): boolean { return visibleVariables.length !== this.currentVariables?.length || visibleVariables.some((item, index) => !areVariablesEqual(item, this.currentVariables![index])); } From f5ee09feb14d8730f1c17f2ece66ba2419077e1b Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Mon, 3 Apr 2023 15:09:13 -0600 Subject: [PATCH 08/16] RM Comments - Add logging to variable retrieval - Refactor into abstract variable tracker class --- src/entry-points/browser/extension.ts | 7 +- src/entry-points/desktop/extension.ts | 7 +- .../adapter-registry/adapter-capabilities.ts | 116 +++++++++++++++++- .../adapter-registry/adapter-registry.ts | 9 +- .../adapter-registry/gdb-capabilities.ts | 105 ++-------------- src/plugin/logger.ts | 20 ++- src/plugin/memory-provider.ts | 6 +- src/plugin/memory-webview-main.ts | 4 +- src/webview/memory-webview-view.tsx | 2 +- 9 files changed, 153 insertions(+), 123 deletions(-) diff --git a/src/entry-points/browser/extension.ts b/src/entry-points/browser/extension.ts index 7f9187f..7c05a0a 100644 --- a/src/entry-points/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -23,11 +23,10 @@ import { MemoryWebview } from '../../plugin/memory-webview-main'; export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); - const registry = new AdapterRegistry(); - context.subscriptions.push(registry); + const registry = new AdapterRegistry(context); context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); - await memoryProvider.activate(context, registry); - await memoryView.activate(context); + memoryProvider.activate(context, registry); + memoryView.activate(context); return registry; }; diff --git a/src/entry-points/desktop/extension.ts b/src/entry-points/desktop/extension.ts index 7f9187f..7c05a0a 100644 --- a/src/entry-points/desktop/extension.ts +++ b/src/entry-points/desktop/extension.ts @@ -23,11 +23,10 @@ import { MemoryWebview } from '../../plugin/memory-webview-main'; export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); - const registry = new AdapterRegistry(); - context.subscriptions.push(registry); + const registry = new AdapterRegistry(context); context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); - await memoryProvider.activate(context, registry); - await memoryView.activate(context); + memoryProvider.activate(context, registry); + memoryView.activate(context); return registry; }; diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index 28eea26..09251b0 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -14,9 +14,10 @@ * 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 { DebugProtocol } from '@vscode/debugprotocol'; import { VariableRange } from '../../common/memory-range'; +import { Logger } from '../logger'; /** Represents capabilities that may be achieved with particular debug adapters but are not part of the DAP */ export interface AdapterCapabilities { @@ -26,3 +27,116 @@ export interface AdapterCapabilities { getResidents?(session: vscode.DebugSession, params: DebugProtocol.ReadMemoryArguments): Promise; initializeAdapterTracker?(session: vscode.DebugSession): vscode.DebugAdapterTracker | undefined; } + +export type WithChildren = Original & { children?: Array> }; +export type VariablesTree = Record>; + +/** This class implements some of the basic elements of tracking adapter sessions in order to maintain a list of variables. */ +export class AdapterVariableTracker implements vscode.DebugAdapterTracker { + protected currentFrame?: number; + protected variablesTree: VariablesTree = {}; + protected readonly pendingMessages = new Map(); + protected static hexAddress = /0x[0-9a-f]+/i; + protected static notADigit = /[^0-9]/; + + constructor(protected readonly onEnd: vscode.Disposable, protected logger: Logger) { } + + onWillReceiveMessage(message: unknown): void { + if (isScopesRequest(message)) { + this.currentFrame = message.arguments.frameId; + } else if (isVariableRequest(message)) { + if (message.arguments.variablesReference in this.variablesTree) { + this.pendingMessages.set(message.seq, message.arguments.variablesReference); + } + } + } + + onDidSendMessage(message: unknown): void { + if (isScopesResponse(message)) { + for (const scope of message.body.scopes) { + if (scope.name === 'Local' || scope.presentationHint === 'locals') { + if (!this.variablesTree[scope.variablesReference] || this.variablesTree[scope.variablesReference].name !== 'Local') { + this.variablesTree = { [scope.variablesReference]: { ...scope } }; + } + return; + } + } + } else if (isVariableResponse(message)) { + if (this.pendingMessages.has(message.request_seq)) { + 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; + } + } + } + } + + onExit(): void { + this.onEnd.dispose(); + this.pendingMessages.clear(); + } + + async getLocals(session: vscode.DebugSession): Promise { + this.logger.debug('Retrieving local variables in', session.name + ' Current variables:\n', this.variablesTree); + if (this.currentFrame === undefined) { return []; } + const maybeRanges = await Promise.all(Object.values(this.variablesTree).reduce>>((previous, parent) => { + if ((parent.name === 'Local' || parent.presentationHint === 'locals') && parent.children?.length) { + this.logger.debug('Resolving children of', parent.name); + parent.children.forEach(child => { + previous.push(this.variableToVariableRange(child, session)); + }); + } else { + this.logger.debug('Ignoring', parent.name); + } + return previous; + }, [])); + return maybeRanges.filter((candidate): candidate is VariableRange => !!candidate); + } + + protected variableToVariableRange(_variable: DebugProtocol.Variable, _session: vscode.DebugSession): Promise { + throw new Error('To be implemented by derived classes!'); + } +} + +export class VariableTracker { + protected sessions = new Map(); + protected types: string[]; + + // Include `type` in addition to the rest parameter to indicate that at least one is required + constructor(protected TrackerConstructor: typeof AdapterVariableTracker, protected logger: Logger, type: string, ...otherTypes: string[]) { + this.types = otherTypes.concat(type); + } + + initializeAdapterTracker(session: vscode.DebugSession): AdapterVariableTracker | undefined { + if (session.type === 'gdb') { + const sessionTracker = new this.TrackerConstructor(new vscode.Disposable(() => this.sessions.delete(session.id)), this.logger); + this.sessions.set(session.id, sessionTracker); + return sessionTracker; + } + } + + getVariables(session: vscode.DebugSession): Promise { + return Promise.resolve(this.sessions.get(session.id)?.getLocals(session) ?? []); + } +} + +export function isScopesRequest(message: unknown): message is DebugProtocol.ScopesRequest { + const candidate = message as DebugProtocol.ScopesRequest; + return !!candidate && candidate.command === 'scopes'; +} + +export function isVariableRequest(message: unknown): message is DebugProtocol.VariablesRequest { + const candidate = message as DebugProtocol.VariablesRequest; + return !!candidate && candidate.command === 'variables'; +} + +export function isScopesResponse(message: unknown): message is DebugProtocol.ScopesResponse { + const candidate = message as DebugProtocol.ScopesResponse; + return !!candidate && candidate.command === 'scopes' && Array.isArray(candidate.body.scopes); +} + +export function isVariableResponse(message: unknown): message is DebugProtocol.VariablesResponse { + const candidate = message as DebugProtocol.VariablesResponse; + return !!candidate && candidate.command === 'variables' && Array.isArray(candidate.body.variables); +} diff --git a/src/plugin/adapter-registry/adapter-registry.ts b/src/plugin/adapter-registry/adapter-registry.ts index 78dda8e..73d9350 100644 --- a/src/plugin/adapter-registry/adapter-registry.ts +++ b/src/plugin/adapter-registry/adapter-registry.ts @@ -20,6 +20,11 @@ import { AdapterCapabilities } from './adapter-capabilities'; export class AdapterRegistry implements vscode.Disposable { protected handlers = new Map(); protected isDisposed = false; + + constructor(context: vscode.ExtensionContext) { + context.subscriptions.push(this); + } + registerAdapter(debugType: string, handlerToRegister: AdapterCapabilities): vscode.Disposable { if (this.isDisposed) { return new vscode.Disposable(() => { }); } this.handlers.set(debugType, handlerToRegister); @@ -31,8 +36,8 @@ export class AdapterRegistry implements vscode.Disposable { }); }; - getHandlerForSession(session: vscode.DebugSession): AdapterCapabilities | undefined { - return this.handlers.get(session.type); + getHandlerForSession(sessionType: string): AdapterCapabilities | undefined { + return this.handlers.get(sessionType); } dispose(): void { diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index 50336b5..4282e56 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -16,70 +16,16 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { AdapterCapabilities } from './adapter-capabilities'; +import { AdapterVariableTracker, VariableTracker } from './adapter-capabilities'; import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; +import { logger } from '../logger'; -type WithChildren = Original & { children?: Array> }; -type VariablesTree = Record>; - -class GdbAdapterTracker implements vscode.DebugAdapterTracker { - protected currentFrame?: number; - protected variablesTree: VariablesTree = {}; - protected readonly pendingMessages = new Map(); - protected static hexAddress = /0x[0-9a-f]+/i; - protected static notADigit = /[^0-9]/; - - constructor(protected readonly onEnd: vscode.Disposable) { } - - onWillReceiveMessage(message: unknown): void { - if (isScopesRequest(message)) { - this.currentFrame = message.arguments.frameId; - } else if (isVariableRequest(message)) { - if (message.arguments.variablesReference in this.variablesTree) { - this.pendingMessages.set(message.seq, message.arguments.variablesReference); - } - } - } - onDidSendMessage(message: unknown): void { - if (isScopesResponse(message)) { - for (const scope of message.body.scopes) { - if (scope.name === 'Local') { - if (!this.variablesTree[scope.variablesReference] || this.variablesTree[scope.variablesReference].name !== 'Local') { - this.variablesTree = { [scope.variablesReference]: { ...scope } }; - } - return; - } - } - } else if (isVariableResponse(message)) { - if (this.pendingMessages.has(message.request_seq)) { - 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; - } - } +class GdbAdapterTracker extends AdapterVariableTracker { + protected override async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { + if (!variable.memoryReference || this.currentFrame === undefined) { + this.logger.debug('Unable to resolve', variable.name, { noMemoryReference: !variable.memoryReference, noFrame: this.currentFrame === undefined }); + return undefined; } - } - onExit(): void { - this.onEnd.dispose(); - this.pendingMessages.clear(); - } - - async getLocals(session: vscode.DebugSession): Promise { - if (this.currentFrame === undefined) { return []; } - const maybeRanges = await Promise.all(Object.values(this.variablesTree).reduce>>((previous, parent) => { - if (parent.name === 'Local' && parent.children?.length) { - parent.children.forEach(child => { - previous.push(this.variableToVariableRange(child, session)); - }); - } - return previous; - }, [])); - return maybeRanges.filter((candidate): candidate is VariableRange => !!candidate); - } - - protected async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { - if (variable.memoryReference === undefined || this.currentFrame === undefined) { return undefined; } try { const [addressResponse, sizeResponse] = await Promise.all([ session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch', frameId: this.currentFrame }), @@ -89,6 +35,7 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { if (!addressPart) { return undefined; } const startAddress = BigInt(addressPart[0]); const endAddress = GdbAdapterTracker.notADigit.test(sizeResponse.result) ? undefined : startAddress + BigInt(sizeResponse.result); + this.logger.debug('Resolved', variable.name, { start: addressPart[0], size: sizeResponse.result }); return { name: variable.name, startAddress: toHexStringWithRadixMarker(startAddress), @@ -96,42 +43,12 @@ class GdbAdapterTracker implements vscode.DebugAdapterTracker { value: variable.value, }; } catch (err) { + this.logger.warn('Unable to resolve location and size of', variable.name + (err instanceof Error ? ':\n\t' + err.message : '')); return undefined; } } } -function isScopesRequest(message: unknown): message is DebugProtocol.ScopesRequest { - const candidate = message as DebugProtocol.ScopesRequest; - return !!candidate && candidate.command === 'scopes'; -} - -function isVariableRequest(message: unknown): message is DebugProtocol.VariablesRequest { - const candidate = message as DebugProtocol.VariablesRequest; - return !!candidate && candidate.command === 'variables'; -} - -function isScopesResponse(message: unknown): message is DebugProtocol.ScopesResponse { - const candidate = message as DebugProtocol.ScopesResponse; - return !!candidate && candidate.command === 'scopes' && Array.isArray(candidate.body.scopes); -} - -function isVariableResponse(message: unknown): message is DebugProtocol.VariablesResponse { - const candidate = message as DebugProtocol.VariablesResponse; - return !!candidate && candidate.command === 'variables' && Array.isArray(candidate.body.variables); -} - -export class GdbCapabilities implements AdapterCapabilities { - protected sessions = new Map(); - initializeAdapterTracker(session: vscode.DebugSession): GdbAdapterTracker | undefined { - if (session.type === 'gdb') { - const sessionTracker = new GdbAdapterTracker(new vscode.Disposable(() => this.sessions.delete(session.id))); - this.sessions.set(session.id, sessionTracker); - return sessionTracker; - } - } - - getVariables(session: vscode.DebugSession): Promise { - return Promise.resolve(this.sessions.get(session.id)?.getLocals(session) ?? []); - } +export class GdbCapabilities extends VariableTracker { + constructor() { super(GdbAdapterTracker, logger, 'gdb'); } } diff --git a/src/plugin/logger.ts b/src/plugin/logger.ts index 42df911..141328f 100644 --- a/src/plugin/logger.ts +++ b/src/plugin/logger.ts @@ -46,28 +46,24 @@ export class Logger { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public log(verbosity: Verbosity, message: string | any): void { - if (this.logVerbosity === Verbosity.off) { + public log(verbosity: Verbosity, ...messages: Array): void { + if (this.logVerbosity === Verbosity.off || verbosity > this.logVerbosity) { return; } - if (typeof message !== 'string') { - message = JSON.stringify(message, undefined, '\t'); - } + const result = messages.map(message => typeof message === 'string' ? message : JSON.stringify(message, undefined, '\t')).join(' '); - if (verbosity <= this.logVerbosity) { - this.outputChannel.appendLine(message); - } + this.outputChannel.appendLine(result); } // eslint-disable-next-line @typescript-eslint/no-explicit-any - public error = (message: string | any): void => this.log(Verbosity.error, message); + public error = (...messages: Array): void => this.log(Verbosity.error, ...messages); // eslint-disable-next-line @typescript-eslint/no-explicit-any - public warn = (message: string | any): void => this.log(Verbosity.warn, message); + public warn = (...messages: Array): void => this.log(Verbosity.warn, ...messages); // eslint-disable-next-line @typescript-eslint/no-explicit-any - public info = (message: string | any): void => this.log(Verbosity.info, message); + public info = (...messages: Array): void => this.log(Verbosity.info, ...messages); // eslint-disable-next-line @typescript-eslint/no-explicit-any - public debug = (message: string | any): void => this.log(Verbosity.debug, message); + public debug = (...messages: Array): void => this.log(Verbosity.debug, ...messages); } export const logger = Logger.instance; diff --git a/src/plugin/memory-provider.ts b/src/plugin/memory-provider.ts index 4af6400..2b5c9bd 100644 --- a/src/plugin/memory-provider.ts +++ b/src/plugin/memory-provider.ts @@ -35,10 +35,10 @@ export class MemoryProvider { protected readonly sessions = new Map(); protected adapterRegistry?: AdapterRegistry; - public async activate(context: vscode.ExtensionContext, registry: AdapterRegistry): Promise { + public activate(context: vscode.ExtensionContext, registry: AdapterRegistry): void { this.adapterRegistry = registry; const createDebugAdapterTracker = (session: vscode.DebugSession): Required => { - const handlerForSession = registry.getHandlerForSession(session); + const handlerForSession = registry.getHandlerForSession(session.type); const contributedTracker = handlerForSession?.initializeAdapterTracker?.(session); return ({ @@ -114,7 +114,7 @@ export class MemoryProvider { public async getVariables(variableArguments: DebugProtocol.ReadMemoryArguments): Promise { const session = this.assertActiveSession('get variables'); - const handler = this.adapterRegistry?.getHandlerForSession(session); + const handler = this.adapterRegistry?.getHandlerForSession(session.type); if (handler?.getResidents) { return handler.getResidents(session, variableArguments); } return handler?.getVariables?.(session) ?? []; } diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 170675c..2cbdeee 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -53,7 +53,7 @@ export class MemoryWebview { this.messenger = new Messenger(); } - public async activate(context: vscode.ExtensionContext): Promise { + public activate(context: vscode.ExtensionContext): void { context.subscriptions.push( vscode.commands.registerCommand(MemoryWebview.ShowCommandType, () => this.show()), vscode.commands.registerCommand(MemoryWebview.VariableCommandType, node => { @@ -119,7 +119,7 @@ export class MemoryWebview { const disposables = [ this.messenger.onNotification(readyType, () => this.refresh(participant, options), { sender: participant }), - this.messenger.onRequest(logMessageType, message => logger.info(message), { sender: participant }), + this.messenger.onRequest(logMessageType, message => logger.info('[webview]:', message), { sender: participant }), this.messenger.onRequest(readMemoryType, request => this.readMemory(request), { sender: participant }), this.messenger.onRequest(writeMemoryType, request => this.writeMemory(request), { sender: participant }), this.messenger.onRequest(getVariables, request => this.getVariables(request), { sender: participant }), diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index e056dd7..a88aedb 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -74,7 +74,7 @@ class App extends React.Component<{}, MemoryAppState> { protected updateMemoryState = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); protected async setOptions(options?: Partial): Promise { - messenger.sendRequest(logMessageType, HOST_EXTENSION, JSON.stringify(options)); + messenger.sendRequest(logMessageType, HOST_EXTENSION, `[webview] Setting options: ${JSON.stringify(options)}`); this.setState(prevState => ({ ...prevState, ...options })); return this.fetchMemory(options); } From 0e05f6d3e514f7bee8de6453d731f7cdc3d5d817 Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Mon, 3 Apr 2023 16:18:07 -0600 Subject: [PATCH 09/16] Fix JSDoc type comments --- webpack.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 88b5b7f..41d3666 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,7 @@ const path = require('path'); const webpack = require('webpack'); /** @typedef {import('webpack').Configuration} WebpackConfig **/ -/** @type WebpackConfig */ +/** @type {WebpackConfig} */ const common = { mode: 'development', devtool: 'source-map', @@ -29,7 +29,7 @@ const common = { } }; -/** @type WebpackConfig[] */ +/** @type {WebpackConfig[]} */ module.exports = [ { ...common, From 9bb81f73938c11653f0ab87ed569724f074011db Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Tue, 4 Apr 2023 08:19:05 -0600 Subject: [PATCH 10/16] Use activate method --- src/entry-points/browser/extension.ts | 5 +++-- src/entry-points/desktop/extension.ts | 5 +++-- src/plugin/adapter-registry/adapter-registry.ts | 2 +- src/webview/memory-webview-view.tsx | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/entry-points/browser/extension.ts b/src/entry-points/browser/extension.ts index 7c05a0a..a476d82 100644 --- a/src/entry-points/browser/extension.ts +++ b/src/entry-points/browser/extension.ts @@ -23,10 +23,11 @@ import { MemoryWebview } from '../../plugin/memory-webview-main'; export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); - const registry = new AdapterRegistry(context); - context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); + const registry = new AdapterRegistry(); + registry.activate(context); memoryProvider.activate(context, registry); memoryView.activate(context); + context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); return registry; }; diff --git a/src/entry-points/desktop/extension.ts b/src/entry-points/desktop/extension.ts index 7c05a0a..18e1e96 100644 --- a/src/entry-points/desktop/extension.ts +++ b/src/entry-points/desktop/extension.ts @@ -23,10 +23,11 @@ import { MemoryWebview } from '../../plugin/memory-webview-main'; export const activate = async (context: vscode.ExtensionContext): Promise => { const memoryProvider = new MemoryProvider(); const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); - const registry = new AdapterRegistry(context); - context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); + const registry = new AdapterRegistry(); memoryProvider.activate(context, registry); + registry.activate(context); memoryView.activate(context); + context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); return registry; }; diff --git a/src/plugin/adapter-registry/adapter-registry.ts b/src/plugin/adapter-registry/adapter-registry.ts index 73d9350..06b1130 100644 --- a/src/plugin/adapter-registry/adapter-registry.ts +++ b/src/plugin/adapter-registry/adapter-registry.ts @@ -21,7 +21,7 @@ export class AdapterRegistry implements vscode.Disposable { protected handlers = new Map(); protected isDisposed = false; - constructor(context: vscode.ExtensionContext) { + activate(context: vscode.ExtensionContext): void { context.subscriptions.push(this); } diff --git a/src/webview/memory-webview-view.tsx b/src/webview/memory-webview-view.tsx index a88aedb..d1aa958 100644 --- a/src/webview/memory-webview-view.tsx +++ b/src/webview/memory-webview-view.tsx @@ -74,7 +74,7 @@ class App extends React.Component<{}, MemoryAppState> { protected updateMemoryState = (newState: Partial) => this.setState(prevState => ({ ...prevState, ...newState })); protected async setOptions(options?: Partial): Promise { - messenger.sendRequest(logMessageType, HOST_EXTENSION, `[webview] Setting options: ${JSON.stringify(options)}`); + messenger.sendRequest(logMessageType, HOST_EXTENSION, `Setting options: ${JSON.stringify(options)}`); this.setState(prevState => ({ ...prevState, ...options })); return this.fetchMemory(options); } From c0a3533605924cf03a9ed06bd5e5f5ae6bd9a11f Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Thu, 13 Apr 2023 11:43:01 -0600 Subject: [PATCH 11/16] Use all scopes other than registers --- .../adapter-registry/adapter-capabilities.ts | 16 +++-- .../adapter-registry/gdb-capabilities.ts | 60 ++++++++++--------- src/plugin/logger.ts | 2 +- src/plugin/memory-webview-main.ts | 4 +- src/webview/tsconfig.json | 2 +- 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/plugin/adapter-registry/adapter-capabilities.ts b/src/plugin/adapter-registry/adapter-capabilities.ts index 09251b0..0d2a3b0 100644 --- a/src/plugin/adapter-registry/adapter-capabilities.ts +++ b/src/plugin/adapter-registry/adapter-capabilities.ts @@ -30,14 +30,14 @@ export interface AdapterCapabilities { export type WithChildren = Original & { children?: Array> }; export type VariablesTree = Record>; +export const hexAddress = /0x[0-9a-f]+/i; +export const notADigit = /[^0-9]/; /** This class implements some of the basic elements of tracking adapter sessions in order to maintain a list of variables. */ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { protected currentFrame?: number; protected variablesTree: VariablesTree = {}; protected readonly pendingMessages = new Map(); - protected static hexAddress = /0x[0-9a-f]+/i; - protected static notADigit = /[^0-9]/; constructor(protected readonly onEnd: vscode.Disposable, protected logger: Logger) { } @@ -51,14 +51,14 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { } } + /** Produces a two-level tree of scopes and their immediate children. Does not handle expansion of complex variables. */ onDidSendMessage(message: unknown): void { if (isScopesResponse(message)) { for (const scope of message.body.scopes) { - if (scope.name === 'Local' || scope.presentationHint === 'locals') { - if (!this.variablesTree[scope.variablesReference] || this.variablesTree[scope.variablesReference].name !== 'Local') { - this.variablesTree = { [scope.variablesReference]: { ...scope } }; + if (this.isDesiredScope(scope)) { + if (!this.variablesTree[scope.variablesReference] || this.variablesTree[scope.variablesReference].name !== scope.name) { + this.variablesTree[scope.variablesReference] = { ...scope }; } - return; } } } else if (isVariableResponse(message)) { @@ -72,6 +72,10 @@ export class AdapterVariableTracker implements vscode.DebugAdapterTracker { } } + protected isDesiredScope(scope: DebugProtocol.Scope): boolean { + return scope.name !== 'Registers'; + } + onExit(): void { this.onEnd.dispose(); this.pendingMessages.clear(); diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index 4282e56..303d6df 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -16,39 +16,45 @@ import * as vscode from 'vscode'; import { DebugProtocol } from '@vscode/debugprotocol'; -import { AdapterVariableTracker, VariableTracker } from './adapter-capabilities'; +import { AdapterVariableTracker, hexAddress, notADigit, VariableTracker } from './adapter-capabilities'; import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; -import { logger } from '../logger'; +import { Logger, outputChannelLogger } from '../logger'; class GdbAdapterTracker extends AdapterVariableTracker { protected override async variableToVariableRange(variable: DebugProtocol.Variable, session: vscode.DebugSession): Promise { - if (!variable.memoryReference || this.currentFrame === undefined) { - this.logger.debug('Unable to resolve', variable.name, { noMemoryReference: !variable.memoryReference, noFrame: this.currentFrame === undefined }); - return undefined; - } - try { - const [addressResponse, sizeResponse] = await Promise.all([ - session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch', frameId: this.currentFrame }), - session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'watch', frameId: this.currentFrame }), - ]) as DebugProtocol.EvaluateResponse['body'][]; - const addressPart = GdbAdapterTracker.hexAddress.exec(addressResponse.result); - if (!addressPart) { return undefined; } - const startAddress = BigInt(addressPart[0]); - const endAddress = GdbAdapterTracker.notADigit.test(sizeResponse.result) ? undefined : startAddress + BigInt(sizeResponse.result); - this.logger.debug('Resolved', variable.name, { start: addressPart[0], size: sizeResponse.result }); - return { - name: variable.name, - startAddress: toHexStringWithRadixMarker(startAddress), - endAddress: endAddress === undefined ? undefined : toHexStringWithRadixMarker(endAddress), - value: variable.value, - }; - } catch (err) { - this.logger.warn('Unable to resolve location and size of', variable.name + (err instanceof Error ? ':\n\t' + err.message : '')); - return undefined; - } + return cVariableToVariableRange(variable, session, this.currentFrame, this.logger); + } +} + +export async function cVariableToVariableRange( + variable: DebugProtocol.Variable, session: vscode.DebugSession, currentFrame: number | undefined, logger: Logger +): Promise { + if (!variable.memoryReference || currentFrame === undefined) { + logger.debug('Unable to resolve', variable.name, { noMemoryReference: !variable.memoryReference, noFrame: currentFrame === undefined }); + return undefined; + } + try { + const [addressResponse, sizeResponse] = await Promise.all([ + session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'variables', frameId: currentFrame }), + session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'variables', frameId: currentFrame }), + ]) as DebugProtocol.EvaluateResponse['body'][]; + const addressPart = hexAddress.exec(addressResponse.result); + if (!addressPart) { return undefined; } + const startAddress = BigInt(addressPart[0]); + const endAddress = notADigit.test(sizeResponse.result) ? undefined : startAddress + BigInt(sizeResponse.result); + logger.debug('Resolved', variable.name, { start: addressPart[0], size: sizeResponse.result }); + return { + name: variable.name, + startAddress: toHexStringWithRadixMarker(startAddress), + endAddress: endAddress === undefined ? undefined : toHexStringWithRadixMarker(endAddress), + value: variable.value, + }; + } catch (err) { + logger.warn('Unable to resolve location and size of', variable.name + (err instanceof Error ? ':\n\t' + err.message : '')); + return undefined; } } export class GdbCapabilities extends VariableTracker { - constructor() { super(GdbAdapterTracker, logger, 'gdb'); } + constructor() { super(GdbAdapterTracker, outputChannelLogger, 'gdb'); } } diff --git a/src/plugin/logger.ts b/src/plugin/logger.ts index 141328f..b03afed 100644 --- a/src/plugin/logger.ts +++ b/src/plugin/logger.ts @@ -66,4 +66,4 @@ export class Logger { public debug = (...messages: Array): void => this.log(Verbosity.debug, ...messages); } -export const logger = Logger.instance; +export const outputChannelLogger = Logger.instance; diff --git a/src/plugin/memory-webview-main.ts b/src/plugin/memory-webview-main.ts index 2cbdeee..37042fe 100644 --- a/src/plugin/memory-webview-main.ts +++ b/src/plugin/memory-webview-main.ts @@ -30,7 +30,7 @@ import { getVariables } from '../common/messaging'; import { MemoryProvider } from './memory-provider'; -import { logger } from './logger'; +import { outputChannelLogger } from './logger'; import { VariableRange } from '../common/memory-range'; interface Variable { @@ -119,7 +119,7 @@ export class MemoryWebview { const disposables = [ this.messenger.onNotification(readyType, () => this.refresh(participant, options), { sender: participant }), - this.messenger.onRequest(logMessageType, message => logger.info('[webview]:', message), { 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 }), this.messenger.onRequest(getVariables, request => this.getVariables(request), { sender: participant }), diff --git a/src/webview/tsconfig.json b/src/webview/tsconfig.json index c11fb43..4bc97cc 100644 --- a/src/webview/tsconfig.json +++ b/src/webview/tsconfig.json @@ -6,7 +6,7 @@ "es2020", "dom" ], - "outDir": "../../out/frontend", + "outDir": "../../out/webview", "sourceMap": true, "strict": true, "rootDir": "../", From ac340ee1740fd7fc564a8c97c7135eb8e3494e35 Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Thu, 13 Apr 2023 20:13:57 +0200 Subject: [PATCH 12/16] Add name check --- src/plugin/adapter-registry/gdb-capabilities.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index 303d6df..aed7006 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -30,7 +30,8 @@ export async function cVariableToVariableRange( variable: DebugProtocol.Variable, session: vscode.DebugSession, currentFrame: number | undefined, logger: Logger ): Promise { if (!variable.memoryReference || currentFrame === undefined) { - logger.debug('Unable to resolve', variable.name, { noMemoryReference: !variable.memoryReference, noFrame: currentFrame === undefined }); + logger.debug('Unable to resolve', variable.name || variable.memoryReference, + { noName: !variable.name, noMemoryReference: !variable.memoryReference, noFrame: currentFrame === undefined }); return undefined; } try { From 83dd93e4b023c371d9ec3b874b0fafb65ed05693 Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Thu, 13 Apr 2023 20:14:15 +0200 Subject: [PATCH 13/16] Really add name check :-) --- src/plugin/adapter-registry/gdb-capabilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index aed7006..dcd62d5 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -29,7 +29,7 @@ class GdbAdapterTracker extends AdapterVariableTracker { export async function cVariableToVariableRange( variable: DebugProtocol.Variable, session: vscode.DebugSession, currentFrame: number | undefined, logger: Logger ): Promise { - if (!variable.memoryReference || currentFrame === undefined) { + if (!variable.memoryReference || currentFrame === undefined || !variable.name) { logger.debug('Unable to resolve', variable.name || variable.memoryReference, { noName: !variable.name, noMemoryReference: !variable.memoryReference, noFrame: currentFrame === undefined }); return undefined; From 0d00dee2a858340472698431fc1a23b58696c37e Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Fri, 14 Apr 2023 07:18:50 -0600 Subject: [PATCH 14/16] Use only name; ignore memoryReference --- src/plugin/adapter-registry/gdb-capabilities.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index dcd62d5..320cc9d 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -26,12 +26,16 @@ class GdbAdapterTracker extends AdapterVariableTracker { } } +/** + * Resolves memory location and sice using evaluate requests for `$(variable.name)` and `sizeof(variable.name)` + * Ignores the presence or absence of variable.memoryReference. + */ export async function cVariableToVariableRange( variable: DebugProtocol.Variable, session: vscode.DebugSession, currentFrame: number | undefined, logger: Logger ): Promise { - if (!variable.memoryReference || currentFrame === undefined || !variable.name) { - logger.debug('Unable to resolve', variable.name || variable.memoryReference, - { noName: !variable.name, noMemoryReference: !variable.memoryReference, noFrame: currentFrame === undefined }); + if (currentFrame === undefined || !variable.name) { + logger.debug('Unable to resolve', variable.name, + { noName: !variable.name, noFrame: currentFrame === undefined }); return undefined; } try { From 81b83fd9efd124fa21b298e1f49293a529c07728 Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Fri, 14 Apr 2023 07:28:23 -0600 Subject: [PATCH 15/16] Typo --- src/plugin/adapter-registry/gdb-capabilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index 320cc9d..b723c38 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -27,7 +27,7 @@ class GdbAdapterTracker extends AdapterVariableTracker { } /** - * Resolves memory location and sice using evaluate requests for `$(variable.name)` and `sizeof(variable.name)` + * Resolves memory location and size using evaluate requests for `$(variable.name)` and `sizeof(variable.name)` * Ignores the presence or absence of variable.memoryReference. */ export async function cVariableToVariableRange( From 22e83a88d7fb5d6e0f62d6ae3c564fc7b992997d Mon Sep 17 00:00:00 2001 From: Colin Grant Date: Fri, 14 Apr 2023 07:35:40 -0600 Subject: [PATCH 16/16] Go back to watch, since they're expressions --- src/plugin/adapter-registry/gdb-capabilities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugin/adapter-registry/gdb-capabilities.ts b/src/plugin/adapter-registry/gdb-capabilities.ts index b723c38..acd1247 100644 --- a/src/plugin/adapter-registry/gdb-capabilities.ts +++ b/src/plugin/adapter-registry/gdb-capabilities.ts @@ -40,8 +40,8 @@ export async function cVariableToVariableRange( } try { const [addressResponse, sizeResponse] = await Promise.all([ - session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'variables', frameId: currentFrame }), - session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'variables', frameId: currentFrame }), + session.customRequest('evaluate', { expression: `&(${variable.name})`, context: 'watch', frameId: currentFrame }), + session.customRequest('evaluate', { expression: `sizeof(${variable.name})`, context: 'watch', frameId: currentFrame }), ]) as DebugProtocol.EvaluateResponse['body'][]; const addressPart = hexAddress.exec(addressResponse.result); if (!addressPart) { return undefined; }