-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Variables and Variable Decorations #16
Changes from 7 commits
c80caa3
79f325d
83d0a6d
3df454a
9d977b1
8a0ec06
b587554
f5ee09f
0e05f6d
9bb81f7
c0a3533
ac340ee
83dd93e
0d00dee
81b83fd
22e83a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/******************************************************************************** | ||
* 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 | ||
********************************************************************************/ | ||
|
||
/** 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 BigIntMemoryRange { | ||
startAddress: bigint; | ||
endAddress: bigint; | ||
} | ||
|
||
export function isWithin(candidate: bigint, container: BigIntMemoryRange): boolean { | ||
return container.startAddress <= candidate && container.endAddress > candidate; | ||
} | ||
|
||
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: BigIntMemoryRange, other: BigIntMemoryRange): boolean { | ||
return one.startAddress === other.startAddress && one.endAddress === other.endAddress; | ||
} | ||
|
||
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?: BigIntMemoryRange): RangeRelationship { | ||
if (range === undefined) { return RangeRelationship.None; } | ||
if (candidate < range.startAddress) { return RangeRelationship.Before; } | ||
if (candidate >= range.endAddress) { return RangeRelationship.Past; } | ||
return RangeRelationship.Within; | ||
} | ||
|
||
export function toHexStringWithRadixMarker(target: bigint): string { | ||
return `0x${target.toString(16)}`; | ||
} | ||
|
||
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 BigIntVariableRange extends BigIntMemoryRange, VariableMetadata { } | ||
|
||
export function areVariablesEqual(one: BigIntVariableRange, other: BigIntVariableRange): boolean { | ||
return areRangesEqual(one, other) | ||
&& one.name === other.name | ||
&& one.type === other.type | ||
&& one.value === other.value; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,15 +15,20 @@ | |
********************************************************************************/ | ||
|
||
import * as vscode from 'vscode'; | ||
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<void> => { | ||
export const activate = async (context: vscode.ExtensionContext): Promise<AdapterRegistry> => { | ||
const memoryProvider = new MemoryProvider(); | ||
const memoryView = new MemoryWebview(context.extensionUri, memoryProvider); | ||
|
||
await memoryProvider.activate(context); | ||
const registry = new AdapterRegistry(); | ||
context.subscriptions.push(registry); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer the architecture of having an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer not to have a two-step lifecycle of construction and then activation if it can be avoided, so if the class is going to handle its own lifecycle, I'd rather it do it in the constructor. What do you think of that approach? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My rationale for isolation is it keeps the activation needs along with the module being added. It makes it trivial to add/remove/change these modules and keeps the extension activation function from growing too large (a common problem I've seen in many extensions). A couple of reasons why the two-step approach works better:
I'm not too precious about this, so happy to try adding the context to all constructors if you'd prefer? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds fair. Consistency has some value, too, so I'll switch the registry to use an |
||
context.subscriptions.push(registry.registerAdapter('gdb', new GdbCapabilities)); | ||
await memoryProvider.activate(context, registry); | ||
await memoryView.activate(context); | ||
return registry; | ||
}; | ||
|
||
export const deactivate = async (): Promise<void> => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/******************************************************************************** | ||
* 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 * as vscode from 'vscode'; | ||
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<VariableRange[]>; | ||
/** Resolve symbols resident in the memory at the specified range. Will be preferred to {@link getVariables} if present. */ | ||
getResidents?(session: vscode.DebugSession, params: DebugProtocol.ReadMemoryArguments): Promise<VariableRange[]>; | ||
initializeAdapterTracker?(session: vscode.DebugSession): vscode.DebugAdapterTracker | undefined; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
protected handlers = new Map<string, AdapterCapabilities>(); | ||
protected 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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/******************************************************************************** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our use case doesn't use gdb (and gdb doesn't work in the browser), so I'd be keen to see a basic adaptertracker implemented which does as much as possible, perhaps with an example gdb one extending the base one? For example, scopes, variables and evaluation requests are all supported in DAP (we could check the capabilities request?) and even the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good - I think the only GDB specific bit in the current implementation is the actual evaluate request, in the sense that even though the evaluate request is part of the DAP, what expressions get sent and their syntax may depend on the language and adapter being used. Another tricky thing, though, is that the GDB adapter (currently) sends its memory references formatted as pointers rather than addresses, so without the evaluate request, it isn't possible to match the variables to memory at all. Other adapters may work around the specification a little less and provide addresses in that field, but we're shooting in the dark a bit. |
||
* 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 } from './adapter-capabilities'; | ||
import { toHexStringWithRadixMarker, VariableRange } from '../../common/memory-range'; | ||
|
||
type WithChildren<Original> = Original & { children?: Array<WithChildren<DebugProtocol.Variable>> }; | ||
type VariablesTree = Record<number, WithChildren<DebugProtocol.Scope | DebugProtocol.Variable>>; | ||
|
||
class GdbAdapterTracker implements vscode.DebugAdapterTracker { | ||
protected currentFrame?: number; | ||
protected variablesTree: VariablesTree = {}; | ||
protected readonly pendingMessages = new Map<number, number>(); | ||
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; | ||
} | ||
} | ||
} | ||
} | ||
onExit(): void { | ||
this.onEnd.dispose(); | ||
this.pendingMessages.clear(); | ||
} | ||
|
||
async getLocals(session: vscode.DebugSession): Promise<VariableRange[]> { | ||
if (this.currentFrame === undefined) { return []; } | ||
const maybeRanges = await Promise.all(Object.values(this.variablesTree).reduce<Array<Promise<VariableRange | undefined>>>((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<VariableRange | undefined> { | ||
if (variable.memoryReference === undefined || this.currentFrame === undefined) { return undefined; } | ||
try { | ||
const [addressResponse, sizeResponse] = await Promise.all([ | ||
session.customRequest('evaluate', <DebugProtocol.EvaluateArguments>{ expression: `&(${variable.name})`, context: 'watch', frameId: this.currentFrame }), | ||
session.customRequest('evaluate', <DebugProtocol.EvaluateArguments>{ 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); | ||
return { | ||
name: variable.name, | ||
startAddress: toHexStringWithRadixMarker(startAddress), | ||
endAddress: endAddress === undefined ? undefined : toHexStringWithRadixMarker(endAddress), | ||
value: variable.value, | ||
}; | ||
} catch (err) { | ||
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<string, GdbAdapterTracker>(); | ||
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<VariableRange[]> { | ||
return Promise.resolve(this.sessions.get(session.id)?.getLocals(session) ?? []); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are returning an API, it could be good to publish the API to npm (or even on the GitHub npm registry).
We do this internally by having an api folder with the interface exposed (which is published as a separate package) and then have the main class implement this interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a good idea, since it will make it easier to inform people of changes. On the one hand, I think that allowing others to contribute handling for their own adapters is valuable, on the other hand managing the API when VSCode's dependency resolution is fairly crude could be tricky. I know that we would like to support at least memory watchpoints on the plugin side - is there anything that falls on the margins of the DAP that you'd like to support? Perhaps we can start releasing the API once we've got at least a first draft of the functionality we know we want?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using our browser debugger and the gdb debugger as test adapters should really help us uncover the correct abstraction points and any common code we don't need to duplicate in plugins. Let's let this evolve as we add features and see where it goes :)