From d085740202ab4fe3abcf6a1205482ed727fe783f Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Thu, 14 Nov 2024 14:39:03 -0400 Subject: [PATCH 1/8] Updated VariablesRequest and tests with new values --- .../events/requests/VariablesRequest.spec.ts | 122 +++++++++++++++--- .../events/requests/VariablesRequest.ts | 56 +++++++- 2 files changed, 158 insertions(+), 20 deletions(-) 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..6374e37f 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 case SENSITIVE lookup, 1 means forced case INsensitive lookup + 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 } From 6add72776828c7c4c9d590f96c545c5da2be17c8 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Thu, 14 Nov 2024 15:35:28 -0400 Subject: [PATCH 2/8] Inital pass at updated VariablesResponse to support vertial variables --- .../responses/VariablesResponse.spec.ts | 208 ++++++++++++++++++ .../events/responses/VariablesResponse.ts | 14 +- 2 files changed, 221 insertions(+), 1 deletion(-) 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) From e48948b745013913a5085c03c64fa07cec2d7b41 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Thu, 14 Nov 2024 16:41:07 -0400 Subject: [PATCH 3/8] attempting to ask for and use virtual variables --- src/debugProtocol/client/DebugProtocolClient.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/debugProtocol/client/DebugProtocolClient.ts b/src/debugProtocol/client/DebugProtocolClient.ts index 16d21aac..84064208 100644 --- a/src/debugProtocol/client/DebugProtocolClient.ts +++ b/src/debugProtocol/client/DebugProtocolClient.ts @@ -152,6 +152,10 @@ export class DebugProtocolClient { return semver.satisfies(this.protocolVersion, '>=3.2.0'); } + public get supportsVirtualVariables() { + return semver.satisfies(this.protocolVersion, '>=3.3.0'); + } + /** * Get a promise that resolves after an event occurs exactly once */ @@ -474,10 +478,12 @@ 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('"'), + isVirtual: 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 From a3bd0b4439044b9a471464e39300d53163095a40 Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Thu, 14 Nov 2024 16:18:38 -0500 Subject: [PATCH 4/8] Slightly better handling of virtual variables (aa keys are good, can't expand virtuals atm) --- src/adapters/DebugProtocolAdapter.ts | 8 ++++++++ src/debugProtocol/client/DebugProtocolClient.ts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 8bc0b39d..21c8a2f5 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -659,6 +659,8 @@ export class DebugProtocolAdapter { let evaluateName: string; if (!parentEvaluateName?.trim()) { evaluateName = name; + } else if (variable.isVirtual) { + evaluateName = `${parentEvaluateName}[${name}]`; } else if (typeof name === 'string') { evaluateName = `${parentEvaluateName}["${name}"]`; } else if (typeof name === 'number') { @@ -691,6 +693,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/debugProtocol/client/DebugProtocolClient.ts b/src/debugProtocol/client/DebugProtocolClient.ts index 84064208..e9cdf4c7 100644 --- a/src/debugProtocol/client/DebugProtocolClient.ts +++ b/src/debugProtocol/client/DebugProtocolClient.ts @@ -483,7 +483,8 @@ export class DebugProtocolClient { //remove leading and trailing quotes name: x.replace(/^"/, '').replace(/"$/, ''), forceCaseInsensitive: !x.startsWith('"') && !x.endsWith('"'), - isVirtual: x.startsWith('$') + //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 From 08d92e55367e4aef53c6e6b2e1c0515594dc6c81 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Fri, 15 Nov 2024 16:29:47 -0400 Subject: [PATCH 5/8] Fixed virtual vars not being inspectable and some more tests --- src/adapters/DebugProtocolAdapter.ts | 2 +- src/debugProtocol/client/DebugProtocolClient.spec.ts | 10 ++++++++++ src/util.spec.ts | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/adapters/DebugProtocolAdapter.ts b/src/adapters/DebugProtocolAdapter.ts index 21c8a2f5..b2ef8b15 100644 --- a/src/adapters/DebugProtocolAdapter.ts +++ b/src/adapters/DebugProtocolAdapter.ts @@ -660,7 +660,7 @@ export class DebugProtocolAdapter { if (!parentEvaluateName?.trim()) { evaluateName = name; } else if (variable.isVirtual) { - evaluateName = `${parentEvaluateName}[${name}]`; + evaluateName = `${parentEvaluateName}.${name}`; } else if (typeof name === 'string') { evaluateName = `${parentEvaluateName}["${name}"]`; } else if (typeof name === 'number') { diff --git a/src/debugProtocol/client/DebugProtocolClient.spec.ts b/src/debugProtocol/client/DebugProtocolClient.spec.ts index 1a8e6127..06a1a4ea 100644 --- a/src/debugProtocol/client/DebugProtocolClient.spec.ts +++ b/src/debugProtocol/client/DebugProtocolClient.spec.ts @@ -641,6 +641,7 @@ describe('DebugProtocolClient', () => { childCount: 0, isConst: false, isContainer: false, + isVirtual: false, refCount: 0 } as Variable); }); @@ -720,6 +721,7 @@ describe('DebugProtocolClient', () => { childCount: 0, isConst: false, isContainer: false, + isVirtual: false, refCount: 0 } as Variable); }); @@ -1004,14 +1006,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']); @@ -1032,14 +1038,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/util.spec.ts b/src/util.spec.ts index db96563a..6e388940 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; From 33a902ecd9b46c3738696737e40e39a6363ac687 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Mon, 18 Nov 2024 10:04:23 -0400 Subject: [PATCH 6/8] Updated virtual variable naming convention in telnet adapter --- src/adapters/TelnetAdapter.ts | 10 +++++----- src/debugSession/BrightScriptDebugSession.spec.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/adapters/TelnetAdapter.ts b/src/adapters/TelnetAdapter.ts index b740b67a..320dc6b9 100644 --- a/src/adapters/TelnetAdapter.ts +++ b/src/adapters/TelnetAdapter.ts @@ -621,7 +621,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, @@ -651,7 +651,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', @@ -666,12 +666,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()`, @@ -681,7 +681,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/debugSession/BrightScriptDebugSession.spec.ts b/src/debugSession/BrightScriptDebugSession.spec.ts index 9b12dda9..852b2f4a 100644 --- a/src/debugSession/BrightScriptDebugSession.spec.ts +++ b/src/debugSession/BrightScriptDebugSession.spec.ts @@ -411,7 +411,7 @@ describe('BrightScriptDebugSession', () => { variablesReference: 3 }, { - name: '[[count]]', + name: '$count', value: '3', variablesReference: 0 } From 7179126430c6beba7a2c0c9e677c880efb8e0671 Mon Sep 17 00:00:00 2001 From: Christopher Dwyer-Perkins Date: Mon, 18 Nov 2024 10:06:08 -0400 Subject: [PATCH 7/8] Update src/debugProtocol/events/requests/VariablesRequest.ts --- src/debugProtocol/events/requests/VariablesRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/debugProtocol/events/requests/VariablesRequest.ts b/src/debugProtocol/events/requests/VariablesRequest.ts index 6374e37f..a76df49a 100644 --- a/src/debugProtocol/events/requests/VariablesRequest.ts +++ b/src/debugProtocol/events/requests/VariablesRequest.ts @@ -73,7 +73,7 @@ export class VariablesRequest implements ProtocolRequest { if (request.data.includesVirtualPath) { for (let i = 0; i < variablePathLength; i++) { - //0 means case SENSITIVE lookup, 1 means forced case INsensitive lookup + //0 means this is not a virtual variable, 1 means it a virtual variable and must be request as such when making a variables request request.data.variablePathEntries[i].isVirtual = smartBuffer.readUInt8() === 0 ? false : true; } } From 3d47183fdc1b467cb1a317ccf92374060e1a125f Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Wed, 8 Jan 2025 14:33:19 -0500 Subject: [PATCH 8/8] Fix typo --- src/debugProtocol/events/requests/VariablesRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/debugProtocol/events/requests/VariablesRequest.ts b/src/debugProtocol/events/requests/VariablesRequest.ts index a76df49a..910f29d0 100644 --- a/src/debugProtocol/events/requests/VariablesRequest.ts +++ b/src/debugProtocol/events/requests/VariablesRequest.ts @@ -73,7 +73,7 @@ export class VariablesRequest implements ProtocolRequest { if (request.data.includesVirtualPath) { for (let i = 0; i < variablePathLength; i++) { - //0 means this is not a virtual variable, 1 means it a virtual variable and must be request as such when making a variables request + //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; } }