diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 7e4686b7..916ccb81 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -683,6 +683,8 @@ export class DebugProtocolAdapter { let evaluateName: string; if (!parentEvaluateName?.trim()) { evaluateName = name?.toString(); + } else if (variable.isVirtual) { + evaluateName = `${parentEvaluateName}.${name}`; } else if (typeof name === 'string') { evaluateName = `${parentEvaluateName}["${name}"]`; } else if (typeof name === 'number') { @@ -715,6 +717,12 @@ export class DebugProtocolAdapter { container.children.push(childContainer); } } + + //show virtual variables in the UI + if (variable.isVirtual) { + container.presentationHint = 'virtual'; + } + return container; } diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index 4e75b474..8ce8652e 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -632,7 +632,7 @@ export class TelnetAdapter { //the array/associative array print is a loop of every value, so handle that children = this.getForLoopPrintedChildren(expression, data); children.push({ - name: '[[count]]', + name: '$count', value: children.length.toString(), type: 'integer', highLevelType: HighLevelType.primative, @@ -662,7 +662,7 @@ export class TelnetAdapter { //add a computed `[[children]]` property to allow expansion of node children if (lowerExpressionType === 'rosgnode') { let nodeChildren = { - name: '[[children]]', + name: '$children', type: 'roArray', highLevelType: 'array', presentationHint: 'virtual', @@ -677,12 +677,12 @@ export class TelnetAdapter { children.push({ //look up the name of the xml element ...await this.getVariable(`${expression}.GetName()`), - name: '[[name]]', + name: '$name', presentationHint: 'virtual' }); children.push({ - name: '[[attributes]]', + name: '$attributes', type: 'roAssociativeArray', highLevelType: HighLevelType.array, evaluateName: `${expression}.GetAttributes()`, @@ -692,7 +692,7 @@ export class TelnetAdapter { //add a computed `[[children]]` property to allow expansion of child elements children.push({ - name: '[[children]]', + name: '$children', type: 'roArray', highLevelType: HighLevelType.array, evaluateName: `${expression}.GetChildNodes()`, diff --git a/src/debugProtocol/client/DebugProtocolClient.spec.ts b/src/debugProtocol/client/DebugProtocolClient.spec.ts index f09f87a2..37c34cb2 100644 --- a/src/debugProtocol/client/DebugProtocolClient.spec.ts +++ b/src/debugProtocol/client/DebugProtocolClient.spec.ts @@ -664,6 +664,7 @@ describe('DebugProtocolClient', () => { childCount: 0, isConst: false, isContainer: false, + isVirtual: false, refCount: 0 } as Variable); }); @@ -743,6 +744,7 @@ describe('DebugProtocolClient', () => { childCount: 0, isConst: false, isContainer: false, + isVirtual: false, refCount: 0 } as Variable); }); @@ -1027,14 +1029,18 @@ describe('DebugProtocolClient', () => { requestId: 1, command: Command.Variables, enableForceCaseInsensitivity: false, + getVirtualKeys: false, + includesVirtualPath: false, getChildKeys: true, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [{ name: 'm', + isVirtual: false, forceCaseInsensitive: false }, { name: 'top', + isVirtual: false, forceCaseInsensitive: false }] } as VariablesRequest['data']); @@ -1055,14 +1061,18 @@ describe('DebugProtocolClient', () => { requestId: 2, command: Command.Variables, enableForceCaseInsensitivity: true, + getVirtualKeys: false, + includesVirtualPath: false, getChildKeys: true, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [{ name: 'm', + isVirtual: false, forceCaseInsensitive: true }, { name: 'top', + isVirtual: false, forceCaseInsensitive: false }] } as VariablesRequest['data']); diff --git a/src/debugProtocol/client/DebugProtocolClient.ts b/src/debugProtocol/client/DebugProtocolClient.ts index 5c50fb32..8f1372f9 100644 --- a/src/debugProtocol/client/DebugProtocolClient.ts +++ b/src/debugProtocol/client/DebugProtocolClient.ts @@ -156,6 +156,10 @@ export class DebugProtocolClient { return semver.satisfies(this.protocolVersion, '>=3.2.0'); } + public get supportsVirtualVariables() { + return semver.satisfies(this.protocolVersion, '>=3.3.0'); + } + public get supportsExceptionBreakpoints() { return semver.satisfies(this.protocolVersion, '>=3.3.0'); } @@ -493,10 +497,13 @@ export class DebugProtocolClient { threadIndex: threadIndex, stackFrameIndex: stackFrameIndex, getChildKeys: true, + getVirtualKeys: this.supportsVirtualVariables, variablePathEntries: variablePathEntries.map(x => ({ //remove leading and trailing quotes name: x.replace(/^"/, '').replace(/"$/, ''), - forceCaseInsensitive: !x.startsWith('"') && !x.endsWith('"') + forceCaseInsensitive: !x.startsWith('"') && !x.endsWith('"'), + //vars that start with `'$'` are virtual (AA keys will wrapped in quotes so would start with `"$` + isVirtual: x.startsWith('$') // || x.startsWith('"$') })), //starting in protocol v3.1.0, it supports marking certain path items as case-insensitive (i.e. parts of DottedGet expressions) enableForceCaseInsensitivity: semver.satisfies(this.protocolVersion, '>=3.1.0') && variablePathEntries.length > 0 diff --git a/src/debugProtocol/events/requests/VariablesRequest.spec.ts b/src/debugProtocol/events/requests/VariablesRequest.spec.ts index a852bb6e..dcf1e874 100644 --- a/src/debugProtocol/events/requests/VariablesRequest.spec.ts +++ b/src/debugProtocol/events/requests/VariablesRequest.spec.ts @@ -8,12 +8,13 @@ describe('VariablesRequest', () => { requestId: 3, getChildKeys: true, enableForceCaseInsensitivity: false, + getVirtualKeys: false, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [ - { name: 'a', forceCaseInsensitive: true }, - { name: 'b', forceCaseInsensitive: true }, - { name: 'c', forceCaseInsensitive: true } + { name: 'a', forceCaseInsensitive: true, isVirtual: false }, + { name: 'b', forceCaseInsensitive: true, isVirtual: false }, + { name: 'c', forceCaseInsensitive: true, isVirtual: false } ] }); @@ -24,12 +25,14 @@ describe('VariablesRequest', () => { getChildKeys: true, enableForceCaseInsensitivity: false, + getVirtualKeys: false, + includesVirtualPath: false, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [ - { name: 'a', forceCaseInsensitive: false }, - { name: 'b', forceCaseInsensitive: false }, - { name: 'c', forceCaseInsensitive: false } + { name: 'a', forceCaseInsensitive: false, isVirtual: false }, + { name: 'b', forceCaseInsensitive: false, isVirtual: false }, + { name: 'c', forceCaseInsensitive: false, isVirtual: false } ] }); @@ -43,13 +46,15 @@ describe('VariablesRequest', () => { //variable_request_flags // 1 byte getChildKeys: true, // 0 bytes enableForceCaseInsensitivity: false, // 0 bytes + getVirtualKeys: false, + includesVirtualPath: false, stackFrameIndex: 1, // 4 bytes threadIndex: 2, // 4 bytes // variable_path_len // 4 bytes variablePathEntries: [ - { name: 'a', forceCaseInsensitive: false }, // 2 bytes - { name: 'b', forceCaseInsensitive: false }, // 2 bytes - { name: 'c', forceCaseInsensitive: false } // 2 bytes + { name: 'a', forceCaseInsensitive: false, isVirtual: false }, // 2 bytes + { name: 'b', forceCaseInsensitive: false, isVirtual: false }, // 2 bytes + { name: 'c', forceCaseInsensitive: false, isVirtual: false } // 2 bytes ] }); }); @@ -59,12 +64,13 @@ describe('VariablesRequest', () => { requestId: 3, getChildKeys: false, enableForceCaseInsensitivity: true, + getVirtualKeys: false, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [ - { name: 'a', forceCaseInsensitive: true }, - { name: 'b', forceCaseInsensitive: false }, - { name: 'c', forceCaseInsensitive: true } + { name: 'a', forceCaseInsensitive: true, isVirtual: false }, + { name: 'b', forceCaseInsensitive: false, isVirtual: false }, + { name: 'c', forceCaseInsensitive: true, isVirtual: false } ] }); @@ -75,12 +81,14 @@ describe('VariablesRequest', () => { getChildKeys: false, enableForceCaseInsensitivity: true, + getVirtualKeys: false, + includesVirtualPath: false, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [ - { name: 'a', forceCaseInsensitive: true }, - { name: 'b', forceCaseInsensitive: false }, - { name: 'c', forceCaseInsensitive: true } + { name: 'a', forceCaseInsensitive: true, isVirtual: false }, + { name: 'b', forceCaseInsensitive: false, isVirtual: false }, + { name: 'c', forceCaseInsensitive: true, isVirtual: false } ] }); @@ -94,21 +102,94 @@ describe('VariablesRequest', () => { //variable_request_flags // 1 byte getChildKeys: false, // 0 bytes enableForceCaseInsensitivity: true, // 0 bytes + getVirtualKeys: false, + includesVirtualPath: false, stackFrameIndex: 1, // 4 bytes threadIndex: 2, // 4 bytes // variable_path_len // 4 bytes variablePathEntries: [ { name: 'a', // 2 bytes - forceCaseInsensitive: true // 1 byte + forceCaseInsensitive: true, // 1 byte + isVirtual: false // 0 byte }, // ? { name: 'b', // 2 bytes - forceCaseInsensitive: false // 1 byte + forceCaseInsensitive: false, // 1 byte + isVirtual: false // 0 byte }, // ? { name: 'c', // 2 bytes - forceCaseInsensitive: true // 1 byte + forceCaseInsensitive: true, // 1 byte + isVirtual: false // 0 byte + } // ? + ] + }); + }); + + it('serializes and deserializes properly for case isVirtual lookups', () => { + const command = VariablesRequest.fromJson({ + requestId: 3, + getChildKeys: false, + enableForceCaseInsensitivity: true, + getVirtualKeys: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [ + { name: 'a', forceCaseInsensitive: true, isVirtual: true }, + { name: 'b', forceCaseInsensitive: false, isVirtual: false }, + { name: 'c', forceCaseInsensitive: true, isVirtual: true } + ] + }); + + expect(command.data).to.eql({ + packetLength: undefined, + requestId: 3, + command: Command.Variables, + + getChildKeys: false, + enableForceCaseInsensitivity: true, + getVirtualKeys: true, + includesVirtualPath: true, + stackFrameIndex: 1, + threadIndex: 2, + variablePathEntries: [ + { name: 'a', forceCaseInsensitive: true, isVirtual: true }, + { name: 'b', forceCaseInsensitive: false, isVirtual: false }, + { name: 'c', forceCaseInsensitive: true, isVirtual: true } + ] + }); + + expect( + VariablesRequest.fromBuffer(command.toBuffer()).data + ).to.eql({ + packetLength: 37, // 4 bytes + requestId: 3, // 4 bytes + command: Command.Variables, // 4 bytes, + + //variable_request_flags // 1 byte + getChildKeys: false, // 0 bytes + enableForceCaseInsensitivity: true, // 0 bytes + getVirtualKeys: true, // 0 bytes + includesVirtualPath: true, // 0 bytes + stackFrameIndex: 1, // 4 bytes + threadIndex: 2, // 4 bytes + // variable_path_len // 4 bytes + variablePathEntries: [ + { + name: 'a', // 2 bytes + forceCaseInsensitive: true, // 1 byte + isVirtual: true // 1 byte + }, // ? + { + name: 'b', // 2 bytes + forceCaseInsensitive: false, // 1 byte + isVirtual: false // 1 byte + }, // ? + { + name: 'c', // 2 bytes + forceCaseInsensitive: true, // 1 byte + isVirtual: true // 1 byte } // ? ] }); @@ -119,6 +200,7 @@ describe('VariablesRequest', () => { requestId: 3, getChildKeys: false, enableForceCaseInsensitivity: true, + getVirtualKeys: false, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [] @@ -131,6 +213,8 @@ describe('VariablesRequest', () => { getChildKeys: false, enableForceCaseInsensitivity: true, + getVirtualKeys: false, + includesVirtualPath: false, stackFrameIndex: 1, threadIndex: 2, variablePathEntries: [] @@ -146,6 +230,8 @@ describe('VariablesRequest', () => { //variable_request_flags // 1 byte getChildKeys: false, // 0 bytes enableForceCaseInsensitivity: true, // 0 bytes + getVirtualKeys: false, + includesVirtualPath: false, stackFrameIndex: 1, // 4 bytes threadIndex: 2, // 4 bytes // variable_path_len // 4 bytes diff --git a/src/debugProtocol/events/requests/VariablesRequest.ts b/src/debugProtocol/events/requests/VariablesRequest.ts index e5081432..910f29d0 100644 --- a/src/debugProtocol/events/requests/VariablesRequest.ts +++ b/src/debugProtocol/events/requests/VariablesRequest.ts @@ -10,16 +10,19 @@ export class VariablesRequest implements ProtocolRequest { requestId: number; getChildKeys: boolean; enableForceCaseInsensitivity: boolean; + getVirtualKeys: boolean; threadIndex: number; stackFrameIndex: number; variablePathEntries: Array<{ name: string; forceCaseInsensitive: boolean; + isVirtual: boolean; }>; }) { const request = new VariablesRequest(); protocolUtil.loadJson(request, data); request.data.variablePathEntries ??= []; + request.data.includesVirtualPath = request.data.variablePathEntries.some((entry) => entry.isVirtual); // all variables will be case sensitive if the flag is disabled for (const entry of request.data.variablePathEntries) { if (request.data.enableForceCaseInsensitivity !== true) { @@ -28,6 +31,9 @@ export class VariablesRequest implements ProtocolRequest { //default any missing values to false entry.forceCaseInsensitive ??= false; } + + //default any missing values to false + entry.isVirtual ??= false; } return request; } @@ -41,6 +47,8 @@ export class VariablesRequest implements ProtocolRequest { request.data.getChildKeys = !!(variableRequestFlags & VariableRequestFlag.GetChildKeys); request.data.enableForceCaseInsensitivity = !!(variableRequestFlags & VariableRequestFlag.CaseSensitivityOptions); + request.data.getVirtualKeys = !!(variableRequestFlags & VariableRequestFlag.GetVirtualKeys); + request.data.includesVirtualPath = !!(variableRequestFlags & VariableRequestFlag.VirtualPathIncluded); request.data.threadIndex = smartBuffer.readUInt32LE(); // thread_index request.data.stackFrameIndex = smartBuffer.readUInt32LE(); // stack_frame_index const variablePathLength = smartBuffer.readUInt32LE(); // variable_path_len @@ -50,7 +58,8 @@ export class VariablesRequest implements ProtocolRequest { request.data.variablePathEntries.push({ name: protocolUtil.readStringNT(smartBuffer), // variable_path_entries - optional //by default, all variable lookups are case SENSITIVE - forceCaseInsensitive: false + forceCaseInsensitive: false, + isVirtual: false }); } @@ -61,6 +70,13 @@ export class VariablesRequest implements ProtocolRequest { request.data.variablePathEntries[i].forceCaseInsensitive = smartBuffer.readUInt8() === 0 ? false : true; } } + + if (request.data.includesVirtualPath) { + for (let i = 0; i < variablePathLength; i++) { + //0 means this is not a virtual variable, 1 means it's a virtual variable and must be request as such when making a variables request + request.data.variablePathEntries[i].isVirtual = smartBuffer.readUInt8() === 0 ? false : true; + } + } } }); return request; @@ -68,11 +84,14 @@ export class VariablesRequest implements ProtocolRequest { public toBuffer(): Buffer { const smartBuffer = new SmartBuffer(); + const includesVirtualPath = this.data.variablePathEntries.some((entry) => entry.isVirtual); //build the flags var let variableRequestFlags = 0; variableRequestFlags |= this.data.getChildKeys ? VariableRequestFlag.GetChildKeys : 0; variableRequestFlags |= this.data.enableForceCaseInsensitivity ? VariableRequestFlag.CaseSensitivityOptions : 0; + variableRequestFlags |= this.data.getVirtualKeys ? VariableRequestFlag.GetVirtualKeys : 0; + variableRequestFlags |= includesVirtualPath ? VariableRequestFlag.VirtualPathIncluded : 0; smartBuffer.writeUInt8(variableRequestFlags); // variable_request_flags smartBuffer.writeUInt32LE(this.data.threadIndex); // thread_index @@ -86,6 +105,13 @@ export class VariablesRequest implements ProtocolRequest { //0 means case SENSITIVE lookup, 1 means force case INsensitive lookup smartBuffer.writeUInt8(entry.forceCaseInsensitive !== true ? 0 : 1); } + + } + + if (includesVirtualPath) { + for (const entry of this.data.variablePathEntries) { + smartBuffer.writeUInt8(entry.isVirtual !== true ? 0 : 1); + } } protocolUtil.insertCommonRequestFields(this, smartBuffer); @@ -109,6 +135,16 @@ export class VariablesRequest implements ProtocolRequest { */ enableForceCaseInsensitivity: undefined as boolean, + /** + * Indicates whether the VARIABLES response should include virtual keys for the requested path + */ + getVirtualKeys: undefined as boolean, + + /** + * Enables the client application to send path_is_virtual data + */ + includesVirtualPath: undefined as boolean, + /** * The index of the thread containing the variable. */ @@ -128,6 +164,7 @@ export class VariablesRequest implements ProtocolRequest { variablePathEntries: undefined as Array<{ name: string; forceCaseInsensitive: boolean; + isVirtual?: boolean; }>, //common props @@ -138,6 +175,21 @@ export class VariablesRequest implements ProtocolRequest { } export enum VariableRequestFlag { + /** + * Indicates whether the VARIABLES response includes the child keys for container types like + * lists and associative arrays. If this is set to true (0x01), the VARIABLES response include the child keys. + */ GetChildKeys = 1, - CaseSensitivityOptions = 2 + /** + * Enables the client application to send path_force_case_insensitive data + */ + CaseSensitivityOptions = 2, + /** + * Indicates whether the VARIABLES response should include virtual keys for the requested path + */ + GetVirtualKeys = 4, + /** + * Enables the client application to send path_is_virtual data + */ + VirtualPathIncluded = 8 } diff --git a/src/debugProtocol/events/responses/VariablesResponse.spec.ts b/src/debugProtocol/events/responses/VariablesResponse.spec.ts index a12eaeb6..79b2306b 100644 --- a/src/debugProtocol/events/responses/VariablesResponse.spec.ts +++ b/src/debugProtocol/events/responses/VariablesResponse.spec.ts @@ -165,6 +165,7 @@ describe('VariablesResponse', () => { refCount: 2, isConst: false, isContainer: true, + isVirtual: false, type: VariableType.AssociativeArray, keyType: 'String', value: undefined, @@ -173,6 +174,7 @@ describe('VariablesResponse', () => { refCount: 1, value: 'Bob', type: VariableType.String, + isVirtual: false, isContainer: false, isConst: false }, { @@ -180,6 +182,7 @@ describe('VariablesResponse', () => { refCount: 1, value: undefined, isContainer: false, + isVirtual: false, type: VariableType.Invalid, isConst: false }] @@ -203,6 +206,7 @@ describe('VariablesResponse', () => { refCount: 2, // 4 bytes isConst: false, // 0 bytes -- part of flags isContainer: true, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags type: VariableType.AssociativeArray, // 1 byte keyType: 'String', // 1 byte // element_count // 4 bytes @@ -210,6 +214,7 @@ describe('VariablesResponse', () => { // flags // 1 byte isContainer: false, // 0 bytes --part of flags isConst: false, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags type: VariableType.String, // 1 byte name: 'firstName', // 10 bytes refCount: 1, // 4 bytes @@ -218,6 +223,7 @@ describe('VariablesResponse', () => { // flags // 1 byte isContainer: false, // 0 bytes -- part of flags isConst: false, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags type: VariableType.Invalid, // 1 byte name: 'lastName', // 9 bytes refCount: 1 // 4 bytes @@ -226,6 +232,202 @@ describe('VariablesResponse', () => { }); }); + it('handles parent var with children and virtual variables', () => { + let response = VariablesResponse.fromJson({ + requestId: 2, + variables: [{ + name: 'person', + refCount: 2, + isConst: false, + isContainer: true, + type: VariableType.AssociativeArray, + keyType: VariableType.String, + value: undefined, + children: [{ + name: 'firstName', + refCount: 1, + value: 'Bob', + type: VariableType.String, + isContainer: false, + isConst: false + }, { + name: 'lastName', + refCount: 1, + value: undefined, + isContainer: false, + type: VariableType.Invalid, + isConst: false + }, { + name: '$children', + refCount: 1, + value: undefined, + isContainer: true, + isConst: false, + isVirtual: true, + childCount: 4, + keyType: VariableType.Integer, + type: VariableType.Array + }, { + name: '$parent', + refCount: 1, + value: undefined, + isContainer: true, + isConst: false, + isVirtual: true, + childCount: 4, + keyType: VariableType.String, + type: VariableType.SubtypedObject + }, { + name: '$threadinfo', + refCount: 1, + value: undefined, + isContainer: true, + isConst: false, + isVirtual: true, + childCount: 4, + keyType: VariableType.String, + type: VariableType.AssociativeArray + }] + }] + }); + + expect(response.data).to.eql({ + packetLength: undefined, + errorCode: ErrorCode.OK, + requestId: 2, + variables: [{ + name: 'person', + refCount: 2, + isConst: false, + isContainer: true, + isVirtual: false, + type: VariableType.AssociativeArray, + keyType: 'String', + value: undefined, + children: [{ + name: 'firstName', + refCount: 1, + value: 'Bob', + type: VariableType.String, + isVirtual: false, + isContainer: false, + isConst: false + }, { + name: 'lastName', + refCount: 1, + value: undefined, + isContainer: false, + isVirtual: false, + type: VariableType.Invalid, + isConst: false + }, { + name: '$children', + refCount: 1, + value: undefined, + isContainer: true, + isVirtual: true, + isConst: false, + childCount: 4, + type: VariableType.Array, + keyType: VariableType.Integer + }, { + name: '$parent', + refCount: 1, + value: undefined, + isContainer: true, + isVirtual: true, + isConst: false, + childCount: 4, + type: VariableType.SubtypedObject, + keyType: VariableType.String + }, { + name: '$threadinfo', + refCount: 1, + value: undefined, + isContainer: true, + isVirtual: true, + isConst: false, + childCount: 4, + type: VariableType.AssociativeArray, + keyType: VariableType.String + }] + }] + }); + + response = VariablesResponse.fromBuffer(response.toBuffer()); + + expect(response.success).to.be.true; + + + expect( + response.data + ).to.eql({ + packetLength: 132, // 4 bytes + errorCode: ErrorCode.OK, // 4 bytes + requestId: 2, // 4 bytes + // num_variables // 4 bytes + variables: [{ + // flags // 1 byte + name: 'person', // 7 bytes + refCount: 2, // 4 bytes + isConst: false, // 0 bytes -- part of flags + isContainer: true, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags + type: VariableType.AssociativeArray, // 1 byte + keyType: 'String', // 1 byte + // element_count // 4 bytes + children: [{ + // flags // 1 byte + isContainer: false, // 0 bytes --part of flags + isConst: false, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags + type: VariableType.String, // 1 byte + name: 'firstName', // 10 bytes + refCount: 1, // 4 bytes + value: 'Bob' // 4 bytes + }, { + // flags // 1 byte + isContainer: false, // 0 bytes -- part of flags + isConst: false, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags + type: VariableType.Invalid, // 1 byte + name: 'lastName', // 9 bytes + refCount: 1 // 4 bytes + }, { + // flags // 1 byte + isContainer: true, // 0 bytes -- part of flags + isConst: false, // 0 bytes -- part of flags + isVirtual: true, // 0 bytes -- part of flags + childCount: 4, // 4 bytes + type: VariableType.Array, // 1 byte + keyType: VariableType.Integer, // 1 byte + name: '$children', // 10 bytes + refCount: 1 // 4 bytes + }, { + // flags // 1 byte + isContainer: true, // 0 bytes -- part of flags + isConst: false, // 0 bytes -- part of flags + isVirtual: true, // 0 bytes -- part of flags + childCount: 4, // 4 bytes + type: VariableType.SubtypedObject, // 1 byte + keyType: VariableType.String, // 1 byte + name: '$parent', // 8 bytes + refCount: 1 // 4 bytes + }, { + // flags // 1 byte + isContainer: true, // 0 bytes -- part of flags + isConst: false, // 0 bytes -- part of flags + isVirtual: true, // 0 bytes -- part of flags + childCount: 4, // 4 bytes + type: VariableType.AssociativeArray, // 1 byte + keyType: VariableType.String, // 1 byte + name: '$threadinfo', // 12 bytes + refCount: 1 // 4 bytes + }] + }] + }); + }); + it('handles every variable type', () => { let response = VariablesResponse.fromBuffer( VariablesResponse.fromJson({ @@ -387,6 +589,7 @@ describe('VariablesResponse', () => { variables: [{ isConst: false, isContainer: true, + isVirtual: false, type: VariableType.AssociativeArray, name: 'm', refCount: 2, @@ -396,6 +599,7 @@ describe('VariablesResponse', () => { }, { isConst: false, isContainer: true, + isVirtual: false, type: VariableType.Array, name: 'nodes', refCount: 2, @@ -405,6 +609,7 @@ describe('VariablesResponse', () => { }, { isConst: false, isContainer: false, + isVirtual: false, type: VariableType.String, name: 'message', refCount: 2, @@ -427,6 +632,7 @@ describe('VariablesResponse', () => { // flags // 1 byte isConst: false, // 0 bytes -- part of flags isContainer: true, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags type: VariableType.AssociativeArray, // 1 byte name: 'm', // 2 bytes refCount: 2, // 4 bytes @@ -436,6 +642,7 @@ describe('VariablesResponse', () => { // flags // 1 byte isConst: false, // 0 bytes -- part of flags isContainer: true, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags type: VariableType.Array, // 1 byte name: 'nodes', // 6 bytes refCount: 2, // 4 bytes @@ -445,6 +652,7 @@ describe('VariablesResponse', () => { // flags // 1 byte isConst: false, // 0 bytes -- part of flags isContainer: false, // 0 bytes -- part of flags + isVirtual: false, // 0 bytes -- part of flags type: VariableType.String, // 1 byte name: 'message', // 8 bytes refCount: 2, // 4 bytes diff --git a/src/debugProtocol/events/responses/VariablesResponse.ts b/src/debugProtocol/events/responses/VariablesResponse.ts index f8d02777..f5cddce6 100644 --- a/src/debugProtocol/events/responses/VariablesResponse.ts +++ b/src/debugProtocol/events/responses/VariablesResponse.ts @@ -30,6 +30,7 @@ export class VariablesResponse implements ProtocolResponse { if (variable.isContainer && util.isNullish(variable.childCount) && !hasChildrenArray) { throw new Error('Container variable must have one of these properties defined: childCount, children'); } + variable.isVirtual ??= false; } return response; } @@ -81,6 +82,7 @@ export class VariablesResponse implements ProtocolResponse { const isNameHere = (flags & VariableFlags.isNameHere) > 0; const isRefCounted = (flags & VariableFlags.isRefCounted) > 0; const isValueHere = (flags & VariableFlags.isValueHere) > 0; + variable.isVirtual = (flags & VariableFlags.isVirtual) > 0; const variableTypeCode = smartBuffer.readUInt8(); variable.type = VariableTypeCode[variableTypeCode] as VariableType; // variable_type @@ -185,6 +187,7 @@ export class VariablesResponse implements ProtocolResponse { flags |= Array.isArray(variable.children) ? 0 : VariableFlags.isChildKey; flags |= variable.isConst ? VariableFlags.isConst : 0; flags |= variable.isContainer ? VariableFlags.isContainer : 0; + flags |= variable.isVirtual ? VariableFlags.isVirtual : 0; const isNameHere = !util.isNullish(variable.name); flags |= isNameHere ? VariableFlags.isNameHere : 0; @@ -312,7 +315,12 @@ export enum VariableFlags { * Value is container, key lookup is case sensitive * @since protocol 3.1.0 */ - isKeysCaseSensitive = 64 + isKeysCaseSensitive = 64, + /** + * Indicates whether the associated variable is virtual or not. + * @since protocol 3.3.0 + */ + isVirtual = 128 } /** @@ -391,6 +399,10 @@ export interface Variable { * Is this variable a container var (i.e. an array or object with children) */ isContainer: boolean; + /** + * Indicates whether the associated variable is virtual or not. + */ + isVirtual?: boolean; /** * If the variable is a container, it will have child elements. this is the number of those children. If `.children` is set, this field will be set to undefined * (meaning it will be ignored during serialization) diff --git a/src/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index 4b7ae61e..083644c8 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -413,7 +413,7 @@ describe('BrightScriptDebugSession', () => { variablesReference: 3 }, { - name: '[[count]]', + name: '$count', value: '3', variablesReference: 0 } diff --git a/src/util.spec.ts b/src/util.spec.ts index cd751122..dc93372d 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -277,6 +277,11 @@ describe('Util', () => { expect(util.getVariablePath(`m.["that"]`)).to.eql(['m', '"that"']); }); + it('detects valid virtual patterns', () => { + expect(util.getVariablePath('m["top"].$children')).to.eql(['m', '"top"', '$children']); + expect(util.getVariablePath('m["top"]["$children"]')).to.eql(['m', '"top"', '"$children"']); + }); + it('rejects invalid patterns', () => { expect(util.getVariablePath('[0]')).undefined; expect(util.getVariablePath('m.global.initialInputEvent.0')).undefined;