Эх сурвалжийг харах

Add automatic project formatting (#814)

* Add biome as a dev dependency and add "npm format" script

* Align new debugger code with project style
David Kincaid 4 сар өмнө
parent
commit
f4ae73c9a0

+ 4 - 1
biome.json

@@ -1,4 +1,7 @@
 {
 {
+	"vcs": {
+		"defaultBranch": "master"
+	},
 	"formatter": {
 	"formatter": {
 		"enabled": true,
 		"enabled": true,
 		"formatWithErrors": false,
 		"formatWithErrors": false,
@@ -16,7 +19,7 @@
 		"rules": {
 		"rules": {
 			"style": {
 			"style": {
 				"noUselessElse": "off",
 				"noUselessElse": "off",
-                "useImportType": "off"
+				"useImportType": "off"
 			}
 			}
 		}
 		}
 	}
 	}

+ 156 - 0
package-lock.json

@@ -25,6 +25,7 @@
 				"ya-bbcode": "^4.0.0"
 				"ya-bbcode": "^4.0.0"
 			},
 			},
 			"devDependencies": {
 			"devDependencies": {
+				"@biomejs/biome": "^1.9.4",
 				"@types/chai": "^4.3.11",
 				"@types/chai": "^4.3.11",
 				"@types/chai-as-promised": "^8.0.1",
 				"@types/chai-as-promised": "^8.0.1",
 				"@types/chai-subset": "^1.3.5",
 				"@types/chai-subset": "^1.3.5",
@@ -391,6 +392,161 @@
 			"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
 			"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
 			"dev": true
 			"dev": true
 		},
 		},
+		"node_modules/@biomejs/biome": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz",
+			"integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==",
+			"dev": true,
+			"hasInstallScript": true,
+			"bin": {
+				"biome": "bin/biome"
+			},
+			"engines": {
+				"node": ">=14.21.3"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/biome"
+			},
+			"optionalDependencies": {
+				"@biomejs/cli-darwin-arm64": "1.9.4",
+				"@biomejs/cli-darwin-x64": "1.9.4",
+				"@biomejs/cli-linux-arm64": "1.9.4",
+				"@biomejs/cli-linux-arm64-musl": "1.9.4",
+				"@biomejs/cli-linux-x64": "1.9.4",
+				"@biomejs/cli-linux-x64-musl": "1.9.4",
+				"@biomejs/cli-win32-arm64": "1.9.4",
+				"@biomejs/cli-win32-x64": "1.9.4"
+			}
+		},
+		"node_modules/@biomejs/cli-darwin-arm64": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz",
+			"integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==",
+			"cpu": [
+				"arm64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
+		"node_modules/@biomejs/cli-darwin-x64": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz",
+			"integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==",
+			"cpu": [
+				"x64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
+		"node_modules/@biomejs/cli-linux-arm64": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz",
+			"integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==",
+			"cpu": [
+				"arm64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
+		"node_modules/@biomejs/cli-linux-arm64-musl": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz",
+			"integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==",
+			"cpu": [
+				"arm64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
+		"node_modules/@biomejs/cli-linux-x64": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
+			"integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==",
+			"cpu": [
+				"x64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
+		"node_modules/@biomejs/cli-linux-x64-musl": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz",
+			"integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==",
+			"cpu": [
+				"x64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"linux"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
+		"node_modules/@biomejs/cli-win32-arm64": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz",
+			"integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==",
+			"cpu": [
+				"arm64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
+		"node_modules/@biomejs/cli-win32-x64": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz",
+			"integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==",
+			"cpu": [
+				"x64"
+			],
+			"dev": true,
+			"optional": true,
+			"os": [
+				"win32"
+			],
+			"engines": {
+				"node": ">=14.21.3"
+			}
+		},
 		"node_modules/@cspotcode/source-map-support": {
 		"node_modules/@cspotcode/source-map-support": {
 			"version": "0.8.1",
 			"version": "0.8.1",
 			"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
 			"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",

+ 3 - 1
package.json

@@ -31,6 +31,7 @@
 	],
 	],
 	"main": "./out/extension.js",
 	"main": "./out/extension.js",
 	"scripts": {
 	"scripts": {
+		"format": "biome format --write --changed src",
 		"compile": "tsc -p ./",
 		"compile": "tsc -p ./",
 		"lint": "eslint ./src --quiet",
 		"lint": "eslint ./src --quiet",
 		"watch": "tsc -watch -p ./",
 		"watch": "tsc -watch -p ./",
@@ -263,7 +264,7 @@
 					"maximum": 200,
 					"maximum": 200,
 					"description": "Scale factor (%) to apply to the Godot documentation viewer."
 					"description": "Scale factor (%) to apply to the Godot documentation viewer."
 				},
 				},
-				"godotTools.documentation.displayMinimap":{
+				"godotTools.documentation.displayMinimap": {
 					"type": "boolean",
 					"type": "boolean",
 					"default": true,
 					"default": true,
 					"description": "Whether to display the minimap for the Godot documentation viewer."
 					"description": "Whether to display the minimap for the Godot documentation viewer."
@@ -875,6 +876,7 @@
 		}
 		}
 	},
 	},
 	"devDependencies": {
 	"devDependencies": {
+		"@biomejs/biome": "^1.9.4",
 		"@types/chai": "^4.3.11",
 		"@types/chai": "^4.3.11",
 		"@types/chai-as-promised": "^8.0.1",
 		"@types/chai-as-promised": "^8.0.1",
 		"@types/chai-subset": "^1.3.5",
 		"@types/chai-subset": "^1.3.5",

+ 16 - 10
src/debugger/godot4/debug_session.ts

@@ -29,7 +29,7 @@ export class GodotDebugSession extends LoggingDebugSession {
 
 
 	public variables_manager: VariablesManager;
 	public variables_manager: VariablesManager;
 
 
-	public constructor(projectVersion : string) {
+	public constructor(projectVersion: string) {
 		super();
 		super();
 
 
 		this.setDebuggerLinesStartAt1(false);
 		this.setDebuggerLinesStartAt1(false);
@@ -233,14 +233,14 @@ export class GodotDebugSession extends LoggingDebugSession {
 
 
 		// TODO: create scopes dynamically for a given frame
 		// TODO: create scopes dynamically for a given frame
 		const vscode_scope_ids = this.variables_manager.get_or_create_frame_scopes(args.frameId);
 		const vscode_scope_ids = this.variables_manager.get_or_create_frame_scopes(args.frameId);
-		const scopes_with_references =  [
-      {name: "Locals", variablesReference: vscode_scope_ids.Locals, expensive: false},
-			{name: "Members", variablesReference: vscode_scope_ids.Members, expensive: false},
-			{name: "Globals", variablesReference: vscode_scope_ids.Globals, expensive: false},
-    ];
+		const scopes_with_references = [
+			{ name: "Locals", variablesReference: vscode_scope_ids.Locals, expensive: false },
+			{ name: "Members", variablesReference: vscode_scope_ids.Members, expensive: false },
+			{ name: "Globals", variablesReference: vscode_scope_ids.Globals, expensive: false },
+		];
 
 
 		response.body = {
 		response.body = {
-			scopes: scopes_with_references
+			scopes: scopes_with_references,
 			// scopes: [
 			// scopes: [
 			// 	{ name: "Locals", variablesReference: 1, expensive: false },
 			// 	{ name: "Locals", variablesReference: 1, expensive: false },
 			// 	{ name: "Members", variablesReference: 2, expensive: false },
 			// 	{ name: "Members", variablesReference: 2, expensive: false },
@@ -252,7 +252,10 @@ export class GodotDebugSession extends LoggingDebugSession {
 		this.sendResponse(response);
 		this.sendResponse(response);
 	}
 	}
 
 
-	protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments) {
+	protected async variablesRequest(
+		response: DebugProtocol.VariablesResponse,
+		args: DebugProtocol.VariablesArguments,
+	) {
 		log.info("variablesRequest", args);
 		log.info("variablesRequest", args);
 		try {
 		try {
 			const variables = await this.variables_manager.get_vscode_object(args.variablesReference);
 			const variables = await this.variables_manager.get_vscode_object(args.variablesReference);
@@ -274,10 +277,13 @@ export class GodotDebugSession extends LoggingDebugSession {
 		log.info("evaluateRequest", args);
 		log.info("evaluateRequest", args);
 
 
 		try {
 		try {
-			const parsed_variable = await this.variables_manager.get_vscode_variable_by_name(args.expression, args.frameId);
+			const parsed_variable = await this.variables_manager.get_vscode_variable_by_name(
+				args.expression,
+				args.frameId,
+			);
 			response.body = {
 			response.body = {
 				result: parsed_variable.value,
 				result: parsed_variable.value,
-				variablesReference: parsed_variable.variablesReference
+				variablesReference: parsed_variable.variablesReference,
 			};
 			};
 		} catch (error) {
 		} catch (error) {
 			response.success = false;
 			response.success = false;

+ 7 - 4
src/debugger/godot4/helpers.ts

@@ -1,4 +1,4 @@
-import { GodotVariable, } from "../debug_runtime";
+import { GodotVariable } from "../debug_runtime";
 import { SceneNode } from "../scene_tree_provider";
 import { SceneNode } from "../scene_tree_provider";
 import { ObjectId } from "./variables/variants";
 import { ObjectId } from "./variables/variants";
 
 
@@ -43,9 +43,12 @@ export function get_sub_values(value: any): GodotVariable[] {
 		} else if (value instanceof Map) {
 		} else if (value instanceof Map) {
 			subValues = [];
 			subValues = [];
 			for (const [key, val] of value.entries()) {
 			for (const [key, val] of value.entries()) {
-				const name = typeof key["stringify_value"] === "function" ? `${key.type_name()}${key.stringify_value()}` : `${key}`;
+				const name =
+					typeof key["stringify_value"] === "function"
+						? `${key.type_name()}${key.stringify_value()}`
+						: `${key}`;
 				const godot_id = val instanceof ObjectId ? val.id : undefined;
 				const godot_id = val instanceof ObjectId ? val.id : undefined;
-				subValues.push({id: godot_id, name, value: val } as GodotVariable);
+				subValues.push({ id: godot_id, name, value: val } as GodotVariable);
 			}
 			}
 		} else if (typeof value["sub_values"] === "function") {
 		} else if (typeof value["sub_values"] === "function") {
 			subValues = value.sub_values()?.map((sva) => {
 			subValues = value.sub_values()?.map((sva) => {
@@ -59,4 +62,4 @@ export function get_sub_values(value: any): GodotVariable[] {
 	}
 	}
 
 
 	return subValues;
 	return subValues;
-}
+}

+ 47 - 24
src/debugger/godot4/server_controller.ts

@@ -24,7 +24,7 @@ import { VariantDecoder } from "./variables/variant_decoder";
 import { VariantEncoder } from "./variables/variant_encoder";
 import { VariantEncoder } from "./variables/variant_encoder";
 import { RawObject } from "./variables/variants";
 import { RawObject } from "./variables/variants";
 import { VariablesManager } from "./variables/variables_manager";
 import { VariablesManager } from "./variables/variables_manager";
-import BBCodeToAnsi from 'bbcode-to-ansi';
+import BBCodeToAnsi from "bbcode-to-ansi";
 
 
 const log = createLogger("debugger.controller", { output: "Godot Debugger" });
 const log = createLogger("debugger.controller", { output: "Godot Debugger" });
 const socketLog = createLogger("debugger.socket");
 const socketLog = createLogger("debugger.socket");
@@ -42,7 +42,7 @@ class Command {
 class GodotPartialStackVars {
 class GodotPartialStackVars {
 	Locals: GodotVariable[] = [];
 	Locals: GodotVariable[] = [];
 	Members: GodotVariable[] = [];
 	Members: GodotVariable[] = [];
-	Globals: GodotVariable [] = [];
+	Globals: GodotVariable[] = [];
 	public remaining: number;
 	public remaining: number;
 	public stack_frame_id: number;
 	public stack_frame_id: number;
 	constructor(stack_frame_id: number) {
 	constructor(stack_frame_id: number) {
@@ -56,7 +56,7 @@ class GodotPartialStackVars {
 		this.Globals = [];
 		this.Globals = [];
 	}
 	}
 
 
-	public append(name: string, godotScopeIndex: 0|1|2, type: number, value: any, sub_values?: GodotVariable[]) {
+	public append(name: string, godotScopeIndex: 0 | 1 | 2, type: number, value: any, sub_values?: GodotVariable[]) {
 		const scopeName = ["Locals", "Members", "Globals"][godotScopeIndex];
 		const scopeName = ["Locals", "Members", "Globals"][godotScopeIndex];
 		const scope = this[scopeName];
 		const scope = this[scopeName];
 		// const objectId = value instanceof ObjectId ? value : undefined; // won't work, unless the value is re-created through new ObjectId(godot_id)
 		// const objectId = value instanceof ObjectId ? value : undefined; // won't work, unless the value is re-created through new ObjectId(godot_id)
@@ -79,13 +79,13 @@ export class ServerController {
 	private didFirstOutput = false;
 	private didFirstOutput = false;
 	private partialStackVars: GodotPartialStackVars;
 	private partialStackVars: GodotPartialStackVars;
 	private projectVersionMajor: number;
 	private projectVersionMajor: number;
-	private projectVersionMinor : number;
-	private projectVersionPoint : number;
+	private projectVersionMinor: number;
+	private projectVersionPoint: number;
 
 
 	public constructor(public session: GodotDebugSession) {}
 	public constructor(public session: GodotDebugSession) {}
 
 
 	public setProjectVersion(projectVersion: string) {
 	public setProjectVersion(projectVersion: string) {
-		const versionParts = projectVersion.split('.').map(Number);
+		const versionParts = projectVersion.split(".").map(Number);
 		this.projectVersionMajor = versionParts[0] || 0;
 		this.projectVersionMajor = versionParts[0] || 0;
 		this.projectVersionMinor = versionParts[1] || 0;
 		this.projectVersionMinor = versionParts[1] || 0;
 		this.projectVersionPoint = versionParts[2] || 0;
 		this.projectVersionPoint = versionParts[2] || 0;
@@ -135,11 +135,12 @@ export class ServerController {
 
 
 	public request_stack_frame_vars(stack_frame_id: number) {
 	public request_stack_frame_vars(stack_frame_id: number) {
 		if (this.partialStackVars !== undefined) {
 		if (this.partialStackVars !== undefined) {
-			log.warn("Partial stack frames have been requested, while existing request hasn't been completed yet." +
-							`Remaining stack_frames: ${this.partialStackVars.remaining}` +
-							`Current stack_frame_id: ${this.partialStackVars.stack_frame_id}` + 
-							`Requested stack_frame_id: ${stack_frame_id}`
-						);
+			log.warn(
+				"Partial stack frames have been requested, while existing request hasn't been completed yet." +
+					`Remaining stack_frames: ${this.partialStackVars.remaining}` +
+					`Current stack_frame_id: ${this.partialStackVars.stack_frame_id}` +
+					`Requested stack_frame_id: ${stack_frame_id}`,
+			);
 		}
 		}
 		this.partialStackVars = new GodotPartialStackVars(stack_frame_id);
 		this.partialStackVars = new GodotPartialStackVars(stack_frame_id);
 		this.send_command("get_stack_frame_vars", [stack_frame_id]);
 		this.send_command("get_stack_frame_vars", [stack_frame_id]);
@@ -480,7 +481,9 @@ export class ServerController {
 			case "stack_frame_vars": {
 			case "stack_frame_vars": {
 				/** first response to {@link request_stack_frame_vars} */
 				/** first response to {@link request_stack_frame_vars} */
 				if (this.partialStackVars !== undefined) {
 				if (this.partialStackVars !== undefined) {
-					log.warn("'stack_frame_vars' received again from godot engine before all partial 'stack_frame_var' are received");
+					log.warn(
+						"'stack_frame_vars' received again from godot engine before all partial 'stack_frame_var' are received",
+					);
 				}
 				}
 				const remaining = command.parameters[0];
 				const remaining = command.parameters[0];
 				// init this.partialStackVars, which will be filled with "stack_frame_var" responses data
 				// init this.partialStackVars, which will be filled with "stack_frame_var" responses data
@@ -493,15 +496,27 @@ export class ServerController {
 					return;
 					return;
 				}
 				}
 				if (typeof command.parameters[0] !== "string") {
 				if (typeof command.parameters[0] !== "string") {
-					log.error("Unexpected parameter type for 'stack_frame_var'. Expected string for name, got " + typeof command.parameters[0]);
+					log.error(
+						"Unexpected parameter type for 'stack_frame_var'. Expected string for name, got " +
+							typeof command.parameters[0],
+					);
 					return;
 					return;
 				}
 				}
-				if (typeof command.parameters[1] !== "number" || command.parameters[1] !== 0 && command.parameters[1] !== 1 && command.parameters[1] !== 2) {
-					log.error("Unexpected parameter type for 'stack_frame_var'. Expected number for scope, got " + typeof command.parameters[1]);
+				if (
+					typeof command.parameters[1] !== "number" ||
+					(command.parameters[1] !== 0 && command.parameters[1] !== 1 && command.parameters[1] !== 2)
+				) {
+					log.error(
+						"Unexpected parameter type for 'stack_frame_var'. Expected number for scope, got " +
+							typeof command.parameters[1],
+					);
 					return;
 					return;
 				}
 				}
 				if (typeof command.parameters[2] !== "number") {
 				if (typeof command.parameters[2] !== "number") {
-					log.error("Unexpected parameter type for 'stack_frame_var'. Expected number for type, got " + typeof command.parameters[2]);
+					log.error(
+						"Unexpected parameter type for 'stack_frame_var'. Expected number for type, got " +
+							typeof command.parameters[2],
+					);
 					return;
 					return;
 				}
 				}
 				var name: string = command.parameters[0];
 				var name: string = command.parameters[0];
@@ -517,13 +532,21 @@ export class ServerController {
 					log.info("All partial 'stack_frame_var' are received.");
 					log.info("All partial 'stack_frame_var' are received.");
 					// godot server doesn't send the frame_id for the stack_vars, assume the remembered stack_frame_id:
 					// godot server doesn't send the frame_id for the stack_vars, assume the remembered stack_frame_id:
 					const frame_id = BigInt(stackVars.stack_frame_id);
 					const frame_id = BigInt(stackVars.stack_frame_id);
-					const local_scopes_godot_id = -frame_id*3n-1n;
-					const member_scopes_godot_id = -frame_id*3n-2n;
-					const global_scopes_godot_id = -frame_id*3n-3n;
-	
+					const local_scopes_godot_id = -frame_id * 3n - 1n;
+					const member_scopes_godot_id = -frame_id * 3n - 2n;
+					const global_scopes_godot_id = -frame_id * 3n - 3n;
+
 					this.session.variables_manager.resolve_variable(local_scopes_godot_id, "Locals", stackVars.Locals);
 					this.session.variables_manager.resolve_variable(local_scopes_godot_id, "Locals", stackVars.Locals);
-					this.session.variables_manager.resolve_variable(member_scopes_godot_id, "Members", stackVars.Members);
-					this.session.variables_manager.resolve_variable(global_scopes_godot_id, "Globals", stackVars.Globals);
+					this.session.variables_manager.resolve_variable(
+						member_scopes_godot_id,
+						"Members",
+						stackVars.Members,
+					);
+					this.session.variables_manager.resolve_variable(
+						global_scopes_godot_id,
+						"Globals",
+						stackVars.Globals,
+					);
 				}
 				}
 				break;
 				break;
 			}
 			}
@@ -532,8 +555,8 @@ export class ServerController {
 					this.didFirstOutput = true;
 					this.didFirstOutput = true;
 					// this.request_scene_tree();
 					// this.request_scene_tree();
 				}
 				}
-				for (const output of command.parameters[0]){
-					output.split("\n").forEach(line => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
+				for (const output of command.parameters[0]) {
+					output.split("\n").forEach((line) => debug.activeDebugConsole.appendLine(bbcodeParser.parse(line)));
 				}
 				}
 				break;
 				break;
 			}
 			}

+ 467 - 417
src/debugger/godot4/variables/debugger_variables.test.ts

@@ -1,417 +1,467 @@
-import { promises as fs } from "fs";
-import * as path from "path";
-import * as vscode from "vscode";
-import { DebugProtocol } from "@vscode/debugprotocol";
-import chai from "chai";
-import chaiSubset from "chai-subset";
-var chaiAsPromised = import("chai-as-promised");
-// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
-
-chaiAsPromised.then((module) => {
-  chai.use(module.default);
-});
-
-import { promisify } from "util";
-import { execFile } from "child_process";
-const execFileAsync = promisify(execFile);
-
-chai.use(chaiSubset);
-const { expect } = chai;
-
-async function sleep(ms) {
-  return new Promise((resolve) => setTimeout(resolve,  ms));
-}
-
-/**
- * Given a path to a script, returns an object where each key is the name of a
- * breakpoint (delimited by `breakpoint::`) and each value is the line number
- * where the breakpoint appears in the script.
- *
- * @param scriptPath The path to the script to scan.
- * @returns An object of breakpoint names to line numbers.
- */
-async function getBreakpointLocations(scriptPath: string): Promise<{ [key: string]: vscode.Location }> {
-  const script_content = await fs.readFile(scriptPath, "utf-8");
-  const breakpoints: { [key: string]: vscode.Location } = {};
-  const breakpointRegex = /\b(breakpoint::.*)\b/g;
-  let match: RegExpExecArray | null;
-  while ((match = breakpointRegex.exec(script_content)) !== null) {
-    const breakpointName = match[1];
-    const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1;
-    breakpoints[breakpointName] = new vscode.Location(vscode.Uri.file(scriptPath), new vscode.Position(line - 1, 0));
-  }
-  return breakpoints;
-}
-
-async function waitForActiveStackItemChange(ms: number = 10000): Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
-  const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((resolve, reject) => {
-      const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => {
-        debugListener.dispose();
-        resolve(vscode.debug.activeStackItem);
-      });
-
-      // Timeout fallback in case stack item never changes
-      setTimeout(() => {
-          debugListener.dispose();
-          console.warn();
-          reject(new Error(`The ActiveStackItem eventwas not changed within the timeout period of '${ms}'`));
-      }, ms);
-  });
-
-  return res;
-}
-
-async function getStackFrames(threadId: number = 1): Promise<DebugProtocol.StackFrame[]> {
-  // Ensure there is an active debug session
-  if (!vscode.debug.activeDebugSession) {
-      throw new Error("No active debug session found");
-  }
-
-  // corresponds to file://./debug_session.ts stackTraceRequest(...)
-  const stackTraceResponse = await vscode.debug.activeDebugSession.customRequest("stackTrace", {
-      threadId: threadId,
-  });
-
-  // Extract and return the stack frames
-  return stackTraceResponse.stackFrames || [];
-}
-
-async function waitForBreakpoint(breakpoint: vscode.SourceBreakpoint, timeoutMs: number, ctx?: Mocha.Context): Promise<void> {
-  const t0 = performance.now();
-  console.log(fmt(`Waiting for breakpoint ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}`));
-  const res = await waitForActiveStackItemChange(timeoutMs);
-  const t1 = performance.now();
-  console.log(fmt(`Waiting for breakpoint completed ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}, took ${t1 - t0}ms`));
-  const stackFrames = await getStackFrames();
-  if (stackFrames[0].source.path !== breakpoint.location.uri.fsPath || stackFrames[0].line != breakpoint.location.range.start.line+1) {
-    throw new Error(`Wrong breakpoint was hit. Expected: ${breakpoint.location.uri.fsPath}:${breakpoint.location.range.start.line+1}, Got: ${stackFrames[0].source.path}:${stackFrames[0].line}`);
-  }
-}
-
-enum VariableScope {
-  Locals,
-  Members,
-  Globals
-}
-
-async function getVariablesForVSCodeID(vscode_id: number): Promise<DebugProtocol.Variable[]> {
-  // corresponds to file://./debug_session.ts protected async variablesRequest
-  const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
-    variablesReference: vscode_id
-  });
-  return variablesResponse?.variables || [];
-}
-
-async function getVariablesForScope(scope: VariableScope, stack_frame_id: number = 0): Promise<DebugProtocol.Variable[]> {
-  const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: stack_frame_id});
-  const scope_name = VariableScope[scope];
-  const scope_res = res_scopes.scopes.find(s => s.name == scope_name);
-  if (scope_res === undefined) {
-    throw new Error(`No ${scope_name} scope found in responce from "scopes" request`);
-  }
-  const vscode_id = scope_res.variablesReference;
-  const variables  = await getVariablesForVSCodeID(vscode_id);
-  return variables;
-}
-
-async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise<any> {
-  // corresponds to file://./debug_session.ts protected async evaluateRequest
-  const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest("evaluate", {
-    context,
-    expression,
-    frameId
-  });
-  return evaluateResponse.body;
-}
-
-function formatMs(ms: number): string {
-  const seconds = Math.floor((ms / 1000) % 60);
-  const minutes = Math.floor((ms / (1000 * 60)) % 60);
-  return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${(Math.round(ms) % 1000).toString().padStart(3, "0")}`;
-}
-
-function formatMessage(this: Mocha.Context, msg: string): string {
-  return `[${formatMs(performance.now()-this.testStart)}] ${msg}`;
-}
-
-var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
-
-
-declare global {
-  // eslint-disable-next-line @typescript-eslint/no-namespace
-  namespace Chai {
-    interface Assertion {
-      unique: Assertion;
-    }
-  }
-}
-
-chai.Assertion.addProperty("unique", function() {
-  const actual = this._obj; // The object being tested
-  if (!Array.isArray(actual)) {
-    throw new chai.AssertionError("Expected value to be an array");
-  }
-  const uniqueArray = [...new Set(actual)];
-  this.assert(
-    actual.length === uniqueArray.length,
-    "expected #{this} to contain only unique elements",
-    "expected #{this} to not contain only unique elements",
-    uniqueArray,
-    actual
-  );
-});
-
-async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn"): Promise<void> {
-  const t0 = performance.now();
-  const debugConfig: vscode.DebugConfiguration = {
-    type: "godot",
-    request: "launch",
-    name: "Godot Debug",
-    scene: scene,
-    additional_options: "--headless"
-  };
-  console.log(fmt(`Starting debugger for scene ${scene}`));
-  const res = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfig);
-  const t1 = performance.now();
-  console.log(fmt(`Starting debugger for scene ${scene} completed, took ${t1 - t0}ms`));
-  if (!res) {
-    throw new Error(`Failed to start debugging for scene ${scene}`);
-  }
-}
-
-suite("DAP Integration Tests - Variable Scopes", () => {
-  // workspaceFolder should match `.vscode-test.js`::workspaceFolder
-  const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
-  if (!workspaceFolder || !workspaceFolder.endsWith("test-dap-project-godot4")) {
-    throw new Error(`workspaceFolder should contain 'test-dap-project-godot4' project, got: ${workspaceFolder}`);
-  }
-
-  suiteSetup(async function() {
-    this.timeout(20000); // enough time to do `godot --import`
-    console.log("Environment Variables:");
-    for (const [key, value] of Object.entries(process.env)) {
-      console.log(`${key}: ${value}`);
-    }
-
-    // init the godot project by importing it in godot engine:
-    const config = vscode.workspace.getConfiguration("godotTools");
-    // config.update("editorPath.godot4", "godot4", vscode.ConfigurationTarget.Workspace);
-    var godot4_path = config.get<string>("editorPath.godot4");
-    // get the path for currently opened project in vscode test instance:
-    console.log("Executing", [godot4_path, "--headless", "--import", workspaceFolder]);
-    const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", workspaceFolder], {shell: true, cwd: workspaceFolder});
-    if (exec_res.stderr !== "") {
-      throw new Error(exec_res.stderr);
-    }
-    console.log(exec_res.stdout);
-  });
-
-  setup(async function() {
-    console.log(`➤ Test '${this?.currentTest.title}' starting`);
-    await vscode.commands.executeCommand("workbench.action.closeAllEditors");
-    if (vscode.debug.breakpoints) {
-      await vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
-    }
-    this.testStart = performance.now();
-    fmt = formatMessage.bind(this);
-  });
-
-
-  teardown(async function() {
-    this.timeout(3000);
-    await sleep(1000);
-    if (vscode.debug.activeDebugSession !== undefined) {
-      console.log("Closing debug session");
-      await vscode.debug.stopDebugging();
-      await sleep(1000);
-    }
-    console.log(`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`);
-  });
-
-  // test("sample test", async function() {
-  //   expect(true).to.equal(true);
-  //   expect([1,2,3]).to.be.unique;
-  //   expect([1,1]).not.to.be.unique;
-  // });
-
-  test("should return correct scopes", async function() {
-    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
-    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::ClassFoo::test_function"]);
-    vscode.debug.addBreakpoints([breakpoint]);
-
-    await startDebugging("ScopeVars.tscn");
-    await waitForBreakpoint(breakpoint, 2000);
-
-    // TODO: current DAP needs a delay before it will return variables
-    console.log("Sleeping for 2 seconds");
-    await sleep(2000);
-
-    // corresponds to file://./debug_session.ts async scopesRequest
-    const stack_scopes_map: Map<number, {
-      "Locals": number;
-      "Members": number; 
-      "Globals": number;
-    }> = new Map();
-    for (var stack_frame_id = 0; stack_frame_id < 3; stack_frame_id++) {
-      const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: stack_frame_id});
-      expect(res_scopes).to.exist;
-      expect(res_scopes.scopes).to.exist;
-      expect(res_scopes.scopes.length).to.equal(3, "Expected 3 scopes");
-      expect(res_scopes.scopes[0].name).to.equal(VariableScope[VariableScope.Locals], "Expected Locals scope");
-      expect(res_scopes.scopes[1].name).to.equal(VariableScope[VariableScope.Members], "Expected Members scope");
-      expect(res_scopes.scopes[2].name).to.equal(VariableScope[VariableScope.Globals], "Expected Globals scope");
-      const vscode_ids = res_scopes.scopes.map(s => s.variablesReference);
-      expect(vscode_ids, "VSCode IDs should be unique for each scope").to.be.unique;
-      stack_scopes_map[stack_frame_id] = {
-        "Locals": vscode_ids[0],
-        "Members": vscode_ids[1], 
-        "Globals": vscode_ids[2]
-      };
-    }
-
-    const all_scopes_vscode_ids = Array.from(stack_scopes_map.values()).flatMap(s => Object.values(s));
-    expect(all_scopes_vscode_ids, "All scopes should be unique").to.be.unique;
-
-    const vars_frame0_locals = await getVariablesForVSCodeID(stack_scopes_map[0].Locals);
-    expect(vars_frame0_locals).to.containSubset([{name: "str_var", value: "ScopeVars::ClassFoo::test_function::local::str_var"}]);
-
-    const vars_frame1_locals = await getVariablesForVSCodeID(stack_scopes_map[1].Locals);
-    expect(vars_frame1_locals).to.containSubset([{name: "str_var", value: "ScopeVars::test::local::str_var"}]);
-
-    const vars_frame2_locals = await getVariablesForVSCodeID(stack_scopes_map[2].Locals);
-    expect(vars_frame2_locals).to.containSubset([{name: "str_var", value: "ScopeVars::_ready::local::str_var"}]);
-  })?.timeout(10000);
-
-  test("should return global variables", async function() {
-    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
-    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
-    vscode.debug.addBreakpoints([breakpoint]);
-
-    await startDebugging("ScopeVars.tscn");
-    await waitForBreakpoint(breakpoint, 2000);
-    
-    // TODO: current DAP needs a delay before it will return variables
-    console.log("Sleeping for 2 seconds");
-    await sleep(2000);
-
-    const variables = await getVariablesForScope(VariableScope.Globals);
-    expect(variables).to.containSubset([{name: "GlobalScript"}]);
-  })?.timeout(10000);
-
-  test("should return all local variables", async function() {
-    /** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
-    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
-    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
-    vscode.debug.addBreakpoints([breakpoint]);
-
-    await startDebugging("ScopeVars.tscn");
-    await waitForBreakpoint(breakpoint, 2000);
-
-    // TODO: current DAP needs a delay before it will return variables
-    console.log("Sleeping for 2 seconds");
-    await sleep(2000);
-
-    const variables = await getVariablesForScope(VariableScope.Locals);
-    expect(variables.length).to.equal(2);
-    expect(variables).to.containSubset([{name: "str_var"}]);
-    expect(variables).to.containSubset([{name: "self_var"}]);
-  })?.timeout(10000);
-
-  test("should return all member variables", async function() {
-    /** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
-    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
-    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
-    vscode.debug.addBreakpoints([breakpoint]);
-
-    await startDebugging("ScopeVars.tscn");
-    await waitForBreakpoint(breakpoint, 2000);
-
-    // TODO: current DAP needs a delay before it will return variables
-    console.log("Sleeping for 2 seconds");
-    await sleep(2000);
-
-    const variables = await getVariablesForScope(VariableScope.Members);
-    expect(variables.length).to.equal(4);
-    expect(variables).to.containSubset([{name: "self"}]);
-    expect(variables).to.containSubset([{name: "member1"}]);
-    expect(variables).to.containSubset([{name: "str_var", value: "ScopeVars::member::str_var"}]);
-    expect(variables).to.containSubset([{name: "str_var_member_only", value: "ScopeVars::member::str_var_member_only"}]);
-  })?.timeout(10000);
-
-  test("should retrieve all built-in types correctly", async function() {
-    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
-    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]);
-    vscode.debug.addBreakpoints([breakpoint]);
-
-    await startDebugging("BuiltInTypes.tscn");
-    await waitForBreakpoint(breakpoint, 2000);
-    
-    // TODO: current DAP needs a delay before it will return variables
-    console.log("Sleeping for 2 seconds");
-    await sleep(2000);
-    
-    const variables = await getVariablesForScope(VariableScope.Locals);
-
-    expect(variables).to.containSubset([{ name: "int_var", value: "42" }]);
-    expect(variables).to.containSubset([{ name: "float_var", value: "3.14" }]);
-    expect(variables).to.containSubset([{ name: "bool_var", value: "true" }]);
-    expect(variables).to.containSubset([{ name: "string_var", value: "Hello, Godot!" }]);
-    expect(variables).to.containSubset([{ name: "nil_var", value: "null" }]);
-    expect(variables).to.containSubset([{ name: "vector2", value: "Vector2(10, 20)" }]);
-    expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]);
-    expect(variables).to.containSubset([{ name: "rect2", value: "Rect2((0, 0) - (100, 50))" }]);
-    expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]);
-    expect(variables).to.containSubset([{ name: "simple_array", value: "(3) [1, 2, 3]" }]);
-    // expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]);
-    // expect(variables).to.containSubset([{ name: "nested_dict.sub_dict.sub_key", value: "99" }]);
-    expect(variables).to.containSubset([{ name: "nested_dict", value: "Dictionary(2)" }]);
-    expect(variables).to.containSubset([{ name: "byte_array", value: "(4) [0, 1, 2, 255]" }]);
-    expect(variables).to.containSubset([{ name: "int32_array", value: "(3) [100, 200, 300]" }]);
-    expect(variables).to.containSubset([{ name: "color_var", value: "Color(1, 0, 0, 1)" }]);
-    expect(variables).to.containSubset([{ name: "aabb_var", value: "AABB((0, 0, 0), (1, 1, 1))" }]);
-    expect(variables).to.containSubset([{ name: "plane_var", value: "Plane(0, 1, 0, -5)" }]);
-    expect(variables).to.containSubset([{ name: "callable_var", value: "Callable()" }]);
-    expect(variables).to.containSubset([{ name: "signal_var" }]);
-    const signal_var = variables.find(v => v.name === "signal_var");
-    expect(signal_var.value).to.match(/Signal\(member_signal\, <\d+>\)/, "Should be in format of 'Signal(member_signal, <28236055815>)'");
-  })?.timeout(10000);
-
-  test("should retrieve all complex variables correctly", async function() {
-    const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
-    const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]);
-    vscode.debug.addBreakpoints([breakpoint]);
-
-    await startDebugging("ExtensiveVars.tscn");
-    await waitForBreakpoint(breakpoint, 2000);
-
-    // TODO: current DAP needs a delay before it will return variables
-    console.log("Sleeping for 2 seconds");
-    await sleep(2000);
-
-    const memberVariables = await getVariablesForScope(VariableScope.Members);
-    
-    expect(memberVariables.length).to.equal(3, "Incorrect member variables count");
-    expect(memberVariables).to.containSubset([{name: "self"}]);
-    expect(memberVariables).to.containSubset([{name: "self_var"}]);
-    expect(memberVariables).to.containSubset([{name: "label"}]);
-    const self = memberVariables.find(v => v.name === "self");
-    const self_var = memberVariables.find(v => v.name === "self_var");
-    expect(self.value).to.deep.equal(self_var.value);
-    
-    const localVariables = await getVariablesForScope(VariableScope.Locals);
-    const expectedLocalVariables = [
-      { name: "local_label", value: /Label<\d+>/ },
-      { name: "local_self_var_through_label", value: /Node2D<\d+>/ },
-      { name: "local_classA", value: /RefCounted<\d+>/ },
-      { name: "local_classB", value: /RefCounted<\d+>/ },
-      { name: "str_var", value: /^ExtensiveVars::_ready::local::str_var$/ },
-    ];
-    expect(localVariables.length).to.equal(expectedLocalVariables.length, "Incorrect local variables count");
-    expect(localVariables).to.containSubset(expectedLocalVariables.map(v => ({ name: v.name })));
-    for (const expectedLocalVariable of expectedLocalVariables) {
-      const localVariable = localVariables.find(v => v.name === expectedLocalVariable.name);
-      expect(localVariable).to.exist;
-      expect(localVariable.value).to.match(expectedLocalVariable.value, `Variable '${expectedLocalVariable.name}' has incorrect value'`);
-    }
-  })?.timeout(15000);
-});
+import { promises as fs } from "fs";
+import * as path from "path";
+import * as vscode from "vscode";
+import { DebugProtocol } from "@vscode/debugprotocol";
+import chai from "chai";
+import chaiSubset from "chai-subset";
+var chaiAsPromised = import("chai-as-promised");
+// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
+
+chaiAsPromised.then((module) => {
+	chai.use(module.default);
+});
+
+import { promisify } from "util";
+import { execFile } from "child_process";
+const execFileAsync = promisify(execFile);
+
+chai.use(chaiSubset);
+const { expect } = chai;
+
+async function sleep(ms) {
+	return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+/**
+ * Given a path to a script, returns an object where each key is the name of a
+ * breakpoint (delimited by `breakpoint::`) and each value is the line number
+ * where the breakpoint appears in the script.
+ *
+ * @param scriptPath The path to the script to scan.
+ * @returns An object of breakpoint names to line numbers.
+ */
+async function getBreakpointLocations(scriptPath: string): Promise<{ [key: string]: vscode.Location }> {
+	const script_content = await fs.readFile(scriptPath, "utf-8");
+	const breakpoints: { [key: string]: vscode.Location } = {};
+	const breakpointRegex = /\b(breakpoint::.*)\b/g;
+	let match: RegExpExecArray | null;
+	while ((match = breakpointRegex.exec(script_content)) !== null) {
+		const breakpointName = match[1];
+		const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1;
+		breakpoints[breakpointName] = new vscode.Location(
+			vscode.Uri.file(scriptPath),
+			new vscode.Position(line - 1, 0),
+		);
+	}
+	return breakpoints;
+}
+
+async function waitForActiveStackItemChange(
+	ms: number = 10000,
+): Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
+	const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((resolve, reject) => {
+		const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => {
+			debugListener.dispose();
+			resolve(vscode.debug.activeStackItem);
+		});
+
+		// Timeout fallback in case stack item never changes
+		setTimeout(() => {
+			debugListener.dispose();
+			console.warn();
+			reject(new Error(`The ActiveStackItem eventwas not changed within the timeout period of '${ms}'`));
+		}, ms);
+	});
+
+	return res;
+}
+
+async function getStackFrames(threadId: number = 1): Promise<DebugProtocol.StackFrame[]> {
+	// Ensure there is an active debug session
+	if (!vscode.debug.activeDebugSession) {
+		throw new Error("No active debug session found");
+	}
+
+	// corresponds to file://./debug_session.ts stackTraceRequest(...)
+	const stackTraceResponse = await vscode.debug.activeDebugSession.customRequest("stackTrace", {
+		threadId: threadId,
+	});
+
+	// Extract and return the stack frames
+	return stackTraceResponse.stackFrames || [];
+}
+
+async function waitForBreakpoint(
+	breakpoint: vscode.SourceBreakpoint,
+	timeoutMs: number,
+	ctx?: Mocha.Context,
+): Promise<void> {
+	const t0 = performance.now();
+	console.log(
+		fmt(
+			`Waiting for breakpoint ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}`,
+		),
+	);
+	const res = await waitForActiveStackItemChange(timeoutMs);
+	const t1 = performance.now();
+	console.log(
+		fmt(
+			`Waiting for breakpoint completed ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}, took ${t1 - t0}ms`,
+		),
+	);
+	const stackFrames = await getStackFrames();
+	if (
+		stackFrames[0].source.path !== breakpoint.location.uri.fsPath ||
+		stackFrames[0].line != breakpoint.location.range.start.line + 1
+	) {
+		throw new Error(
+			`Wrong breakpoint was hit. Expected: ${breakpoint.location.uri.fsPath}:${breakpoint.location.range.start.line + 1}, Got: ${stackFrames[0].source.path}:${stackFrames[0].line}`,
+		);
+	}
+}
+
+enum VariableScope {
+	Locals,
+	Members,
+	Globals,
+}
+
+async function getVariablesForVSCodeID(vscode_id: number): Promise<DebugProtocol.Variable[]> {
+	// corresponds to file://./debug_session.ts protected async variablesRequest
+	const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", {
+		variablesReference: vscode_id,
+	});
+	return variablesResponse?.variables || [];
+}
+
+async function getVariablesForScope(
+	scope: VariableScope,
+	stack_frame_id: number = 0,
+): Promise<DebugProtocol.Variable[]> {
+	const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", { frameId: stack_frame_id });
+	const scope_name = VariableScope[scope];
+	const scope_res = res_scopes.scopes.find((s) => s.name == scope_name);
+	if (scope_res === undefined) {
+		throw new Error(`No ${scope_name} scope found in responce from "scopes" request`);
+	}
+	const vscode_id = scope_res.variablesReference;
+	const variables = await getVariablesForVSCodeID(vscode_id);
+	return variables;
+}
+
+async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise<any> {
+	// corresponds to file://./debug_session.ts protected async evaluateRequest
+	const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest(
+		"evaluate",
+		{
+			context,
+			expression,
+			frameId,
+		},
+	);
+	return evaluateResponse.body;
+}
+
+function formatMs(ms: number): string {
+	const seconds = Math.floor((ms / 1000) % 60);
+	const minutes = Math.floor((ms / (1000 * 60)) % 60);
+	return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${(Math.round(ms) % 1000).toString().padStart(3, "0")}`;
+}
+
+function formatMessage(this: Mocha.Context, msg: string): string {
+	return `[${formatMs(performance.now() - this.testStart)}] ${msg}`;
+}
+
+var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context
+
+declare global {
+	// eslint-disable-next-line @typescript-eslint/no-namespace
+	namespace Chai {
+		interface Assertion {
+			unique: Assertion;
+		}
+	}
+}
+
+chai.Assertion.addProperty("unique", function () {
+	const actual = this._obj; // The object being tested
+	if (!Array.isArray(actual)) {
+		throw new chai.AssertionError("Expected value to be an array");
+	}
+	const uniqueArray = [...new Set(actual)];
+	this.assert(
+		actual.length === uniqueArray.length,
+		"expected #{this} to contain only unique elements",
+		"expected #{this} to not contain only unique elements",
+		uniqueArray,
+		actual,
+	);
+});
+
+async function startDebugging(
+	scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn",
+): Promise<void> {
+	const t0 = performance.now();
+	const debugConfig: vscode.DebugConfiguration = {
+		type: "godot",
+		request: "launch",
+		name: "Godot Debug",
+		scene: scene,
+		additional_options: "--headless",
+	};
+	console.log(fmt(`Starting debugger for scene ${scene}`));
+	const res = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfig);
+	const t1 = performance.now();
+	console.log(fmt(`Starting debugger for scene ${scene} completed, took ${t1 - t0}ms`));
+	if (!res) {
+		throw new Error(`Failed to start debugging for scene ${scene}`);
+	}
+}
+
+suite("DAP Integration Tests - Variable Scopes", () => {
+	// workspaceFolder should match `.vscode-test.js`::workspaceFolder
+	const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
+	if (!workspaceFolder || !workspaceFolder.endsWith("test-dap-project-godot4")) {
+		throw new Error(`workspaceFolder should contain 'test-dap-project-godot4' project, got: ${workspaceFolder}`);
+	}
+
+	suiteSetup(async function () {
+		this.timeout(20000); // enough time to do `godot --import`
+		console.log("Environment Variables:");
+		for (const [key, value] of Object.entries(process.env)) {
+			console.log(`${key}: ${value}`);
+		}
+
+		// init the godot project by importing it in godot engine:
+		const config = vscode.workspace.getConfiguration("godotTools");
+		// config.update("editorPath.godot4", "godot4", vscode.ConfigurationTarget.Workspace);
+		var godot4_path = config.get<string>("editorPath.godot4");
+		// get the path for currently opened project in vscode test instance:
+		console.log("Executing", [godot4_path, "--headless", "--import", workspaceFolder]);
+		const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", workspaceFolder], {
+			shell: true,
+			cwd: workspaceFolder,
+		});
+		if (exec_res.stderr !== "") {
+			throw new Error(exec_res.stderr);
+		}
+		console.log(exec_res.stdout);
+	});
+
+	setup(async function () {
+		console.log(`➤ Test '${this?.currentTest.title}' starting`);
+		await vscode.commands.executeCommand("workbench.action.closeAllEditors");
+		if (vscode.debug.breakpoints) {
+			await vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
+		}
+		this.testStart = performance.now();
+		fmt = formatMessage.bind(this);
+	});
+
+	teardown(async function () {
+		this.timeout(3000);
+		await sleep(1000);
+		if (vscode.debug.activeDebugSession !== undefined) {
+			console.log("Closing debug session");
+			await vscode.debug.stopDebugging();
+			await sleep(1000);
+		}
+		console.log(
+			`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`,
+		);
+	});
+
+	// test("sample test", async function() {
+	//   expect(true).to.equal(true);
+	//   expect([1,2,3]).to.be.unique;
+	//   expect([1,1]).not.to.be.unique;
+	// });
+
+	test("should return correct scopes", async function () {
+		const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+		const breakpoint = new vscode.SourceBreakpoint(
+			breakpointLocations["breakpoint::ScopeVars::ClassFoo::test_function"],
+		);
+		vscode.debug.addBreakpoints([breakpoint]);
+
+		await startDebugging("ScopeVars.tscn");
+		await waitForBreakpoint(breakpoint, 2000);
+
+		// TODO: current DAP needs a delay before it will return variables
+		console.log("Sleeping for 2 seconds");
+		await sleep(2000);
+
+		// corresponds to file://./debug_session.ts async scopesRequest
+		const stack_scopes_map: Map<
+			number,
+			{
+				Locals: number;
+				Members: number;
+				Globals: number;
+			}
+		> = new Map();
+		for (var stack_frame_id = 0; stack_frame_id < 3; stack_frame_id++) {
+			const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {
+				frameId: stack_frame_id,
+			});
+			expect(res_scopes).to.exist;
+			expect(res_scopes.scopes).to.exist;
+			expect(res_scopes.scopes.length).to.equal(3, "Expected 3 scopes");
+			expect(res_scopes.scopes[0].name).to.equal(VariableScope[VariableScope.Locals], "Expected Locals scope");
+			expect(res_scopes.scopes[1].name).to.equal(VariableScope[VariableScope.Members], "Expected Members scope");
+			expect(res_scopes.scopes[2].name).to.equal(VariableScope[VariableScope.Globals], "Expected Globals scope");
+			const vscode_ids = res_scopes.scopes.map((s) => s.variablesReference);
+			expect(vscode_ids, "VSCode IDs should be unique for each scope").to.be.unique;
+			stack_scopes_map[stack_frame_id] = {
+				Locals: vscode_ids[0],
+				Members: vscode_ids[1],
+				Globals: vscode_ids[2],
+			};
+		}
+
+		const all_scopes_vscode_ids = Array.from(stack_scopes_map.values()).flatMap((s) => Object.values(s));
+		expect(all_scopes_vscode_ids, "All scopes should be unique").to.be.unique;
+
+		const vars_frame0_locals = await getVariablesForVSCodeID(stack_scopes_map[0].Locals);
+		expect(vars_frame0_locals).to.containSubset([
+			{ name: "str_var", value: "ScopeVars::ClassFoo::test_function::local::str_var" },
+		]);
+
+		const vars_frame1_locals = await getVariablesForVSCodeID(stack_scopes_map[1].Locals);
+		expect(vars_frame1_locals).to.containSubset([{ name: "str_var", value: "ScopeVars::test::local::str_var" }]);
+
+		const vars_frame2_locals = await getVariablesForVSCodeID(stack_scopes_map[2].Locals);
+		expect(vars_frame2_locals).to.containSubset([{ name: "str_var", value: "ScopeVars::_ready::local::str_var" }]);
+	})?.timeout(10000);
+
+	test("should return global variables", async function () {
+		const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+		const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+		vscode.debug.addBreakpoints([breakpoint]);
+
+		await startDebugging("ScopeVars.tscn");
+		await waitForBreakpoint(breakpoint, 2000);
+
+		// TODO: current DAP needs a delay before it will return variables
+		console.log("Sleeping for 2 seconds");
+		await sleep(2000);
+
+		const variables = await getVariablesForScope(VariableScope.Globals);
+		expect(variables).to.containSubset([{ name: "GlobalScript" }]);
+	})?.timeout(10000);
+
+	test("should return all local variables", async function () {
+		/** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
+		const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+		const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+		vscode.debug.addBreakpoints([breakpoint]);
+
+		await startDebugging("ScopeVars.tscn");
+		await waitForBreakpoint(breakpoint, 2000);
+
+		// TODO: current DAP needs a delay before it will return variables
+		console.log("Sleeping for 2 seconds");
+		await sleep(2000);
+
+		const variables = await getVariablesForScope(VariableScope.Locals);
+		expect(variables.length).to.equal(2);
+		expect(variables).to.containSubset([{ name: "str_var" }]);
+		expect(variables).to.containSubset([{ name: "self_var" }]);
+	})?.timeout(10000);
+
+	test("should return all member variables", async function () {
+		/** {@link file://./../../../../test_projects/test-dap-project-godot4/ScopeVars.gd"} */
+		const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd"));
+		const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]);
+		vscode.debug.addBreakpoints([breakpoint]);
+
+		await startDebugging("ScopeVars.tscn");
+		await waitForBreakpoint(breakpoint, 2000);
+
+		// TODO: current DAP needs a delay before it will return variables
+		console.log("Sleeping for 2 seconds");
+		await sleep(2000);
+
+		const variables = await getVariablesForScope(VariableScope.Members);
+		expect(variables.length).to.equal(4);
+		expect(variables).to.containSubset([{ name: "self" }]);
+		expect(variables).to.containSubset([{ name: "member1" }]);
+		expect(variables).to.containSubset([{ name: "str_var", value: "ScopeVars::member::str_var" }]);
+		expect(variables).to.containSubset([
+			{ name: "str_var_member_only", value: "ScopeVars::member::str_var_member_only" },
+		]);
+	})?.timeout(10000);
+
+	test("should retrieve all built-in types correctly", async function () {
+		const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd"));
+		const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]);
+		vscode.debug.addBreakpoints([breakpoint]);
+
+		await startDebugging("BuiltInTypes.tscn");
+		await waitForBreakpoint(breakpoint, 2000);
+
+		// TODO: current DAP needs a delay before it will return variables
+		console.log("Sleeping for 2 seconds");
+		await sleep(2000);
+
+		const variables = await getVariablesForScope(VariableScope.Locals);
+
+		expect(variables).to.containSubset([{ name: "int_var", value: "42" }]);
+		expect(variables).to.containSubset([{ name: "float_var", value: "3.14" }]);
+		expect(variables).to.containSubset([{ name: "bool_var", value: "true" }]);
+		expect(variables).to.containSubset([{ name: "string_var", value: "Hello, Godot!" }]);
+		expect(variables).to.containSubset([{ name: "nil_var", value: "null" }]);
+		expect(variables).to.containSubset([{ name: "vector2", value: "Vector2(10, 20)" }]);
+		expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]);
+		expect(variables).to.containSubset([{ name: "rect2", value: "Rect2((0, 0) - (100, 50))" }]);
+		expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]);
+		expect(variables).to.containSubset([{ name: "simple_array", value: "(3) [1, 2, 3]" }]);
+		// expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]);
+		// expect(variables).to.containSubset([{ name: "nested_dict.sub_dict.sub_key", value: "99" }]);
+		expect(variables).to.containSubset([{ name: "nested_dict", value: "Dictionary(2)" }]);
+		expect(variables).to.containSubset([{ name: "byte_array", value: "(4) [0, 1, 2, 255]" }]);
+		expect(variables).to.containSubset([{ name: "int32_array", value: "(3) [100, 200, 300]" }]);
+		expect(variables).to.containSubset([{ name: "color_var", value: "Color(1, 0, 0, 1)" }]);
+		expect(variables).to.containSubset([{ name: "aabb_var", value: "AABB((0, 0, 0), (1, 1, 1))" }]);
+		expect(variables).to.containSubset([{ name: "plane_var", value: "Plane(0, 1, 0, -5)" }]);
+		expect(variables).to.containSubset([{ name: "callable_var", value: "Callable()" }]);
+		expect(variables).to.containSubset([{ name: "signal_var" }]);
+		const signal_var = variables.find((v) => v.name === "signal_var");
+		expect(signal_var.value).to.match(
+			/Signal\(member_signal\, <\d+>\)/,
+			"Should be in format of 'Signal(member_signal, <28236055815>)'",
+		);
+	})?.timeout(10000);
+
+	test("should retrieve all complex variables correctly", async function () {
+		const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd"));
+		const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]);
+		vscode.debug.addBreakpoints([breakpoint]);
+
+		await startDebugging("ExtensiveVars.tscn");
+		await waitForBreakpoint(breakpoint, 2000);
+
+		// TODO: current DAP needs a delay before it will return variables
+		console.log("Sleeping for 2 seconds");
+		await sleep(2000);
+
+		const memberVariables = await getVariablesForScope(VariableScope.Members);
+
+		expect(memberVariables.length).to.equal(3, "Incorrect member variables count");
+		expect(memberVariables).to.containSubset([{ name: "self" }]);
+		expect(memberVariables).to.containSubset([{ name: "self_var" }]);
+		expect(memberVariables).to.containSubset([{ name: "label" }]);
+		const self = memberVariables.find((v) => v.name === "self");
+		const self_var = memberVariables.find((v) => v.name === "self_var");
+		expect(self.value).to.deep.equal(self_var.value);
+
+		const localVariables = await getVariablesForScope(VariableScope.Locals);
+		const expectedLocalVariables = [
+			{ name: "local_label", value: /Label<\d+>/ },
+			{ name: "local_self_var_through_label", value: /Node2D<\d+>/ },
+			{ name: "local_classA", value: /RefCounted<\d+>/ },
+			{ name: "local_classB", value: /RefCounted<\d+>/ },
+			{ name: "str_var", value: /^ExtensiveVars::_ready::local::str_var$/ },
+		];
+		expect(localVariables.length).to.equal(expectedLocalVariables.length, "Incorrect local variables count");
+		expect(localVariables).to.containSubset(expectedLocalVariables.map((v) => ({ name: v.name })));
+		for (const expectedLocalVariable of expectedLocalVariables) {
+			const localVariable = localVariables.find((v) => v.name === expectedLocalVariable.name);
+			expect(localVariable).to.exist;
+			expect(localVariable.value).to.match(
+				expectedLocalVariable.value,
+				`Variable '${expectedLocalVariable.name}' has incorrect value'`,
+			);
+		}
+	})?.timeout(15000);
+});

+ 58 - 58
src/debugger/godot4/variables/godot_id_to_vscode_id_mapper.test.ts

@@ -1,58 +1,58 @@
-import { expect } from "chai";
-import { GodotIdWithPath, GodotIdToVscodeIdMapper } from "./godot_id_to_vscode_id_mapper";
-
-suite("GodotIdToVscodeIdMapper", () => {
-  test("create_vscode_id assigns unique ID", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    const godotId = new GodotIdWithPath(BigInt(1), ["path1"]);
-    const vscodeId = mapper.create_vscode_id(godotId);
-    expect(vscodeId).to.equal(1);
-  });
-
-  test("create_vscode_id throws error on duplicate", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    const godotId = new GodotIdWithPath(BigInt(1), ["path1"]);
-    mapper.create_vscode_id(godotId);
-    expect(() => mapper.create_vscode_id(godotId)).to.throw("Duplicate godot_id: 1:path1");
-  });
-
-  test("get_godot_id_with_path returns correct object", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    const godotId = new GodotIdWithPath(BigInt(2), ["path2"]);
-    const vscodeId = mapper.create_vscode_id(godotId);
-    expect(mapper.get_godot_id_with_path(vscodeId)).to.deep.equal(godotId);
-  });
-
-  test("get_godot_id_with_path throws error if not found", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    expect(() => mapper.get_godot_id_with_path(999)).to.throw("Unknown vscode_id: 999");
-  });
-
-  test("get_vscode_id retrieves correct ID", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    const godotId = new GodotIdWithPath(BigInt(3), ["path3"]);
-    const vscodeId = mapper.create_vscode_id(godotId);
-    expect(mapper.get_vscode_id(godotId)).to.equal(vscodeId);
-  });
-
-  test("get_vscode_id throws error if not found", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    const godotId = new GodotIdWithPath(BigInt(4), ["path4"]);
-    expect(() => mapper.get_vscode_id(godotId)).to.throw("Unknown godot_id_with_path: 4:path4");
-  });
-
-  test("get_or_create_vscode_id creates new ID if not found", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    const godotId = new GodotIdWithPath(BigInt(5), ["path5"]);
-    const vscodeId = mapper.get_or_create_vscode_id(godotId);
-    expect(vscodeId).to.equal(1);
-  });
-
-  test("get_or_create_vscode_id retrieves existing ID if already created", () => {
-    const mapper = new GodotIdToVscodeIdMapper();
-    const godotId = new GodotIdWithPath(BigInt(6), ["path6"]);
-    const vscodeId1 = mapper.get_or_create_vscode_id(godotId);
-    const vscodeId2 = mapper.get_or_create_vscode_id(godotId);
-    expect(vscodeId1).to.equal(vscodeId2);
-  });
-});
+import { expect } from "chai";
+import { GodotIdWithPath, GodotIdToVscodeIdMapper } from "./godot_id_to_vscode_id_mapper";
+
+suite("GodotIdToVscodeIdMapper", () => {
+	test("create_vscode_id assigns unique ID", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		const godotId = new GodotIdWithPath(BigInt(1), ["path1"]);
+		const vscodeId = mapper.create_vscode_id(godotId);
+		expect(vscodeId).to.equal(1);
+	});
+
+	test("create_vscode_id throws error on duplicate", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		const godotId = new GodotIdWithPath(BigInt(1), ["path1"]);
+		mapper.create_vscode_id(godotId);
+		expect(() => mapper.create_vscode_id(godotId)).to.throw("Duplicate godot_id: 1:path1");
+	});
+
+	test("get_godot_id_with_path returns correct object", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		const godotId = new GodotIdWithPath(BigInt(2), ["path2"]);
+		const vscodeId = mapper.create_vscode_id(godotId);
+		expect(mapper.get_godot_id_with_path(vscodeId)).to.deep.equal(godotId);
+	});
+
+	test("get_godot_id_with_path throws error if not found", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		expect(() => mapper.get_godot_id_with_path(999)).to.throw("Unknown vscode_id: 999");
+	});
+
+	test("get_vscode_id retrieves correct ID", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		const godotId = new GodotIdWithPath(BigInt(3), ["path3"]);
+		const vscodeId = mapper.create_vscode_id(godotId);
+		expect(mapper.get_vscode_id(godotId)).to.equal(vscodeId);
+	});
+
+	test("get_vscode_id throws error if not found", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		const godotId = new GodotIdWithPath(BigInt(4), ["path4"]);
+		expect(() => mapper.get_vscode_id(godotId)).to.throw("Unknown godot_id_with_path: 4:path4");
+	});
+
+	test("get_or_create_vscode_id creates new ID if not found", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		const godotId = new GodotIdWithPath(BigInt(5), ["path5"]);
+		const vscodeId = mapper.get_or_create_vscode_id(godotId);
+		expect(vscodeId).to.equal(1);
+	});
+
+	test("get_or_create_vscode_id retrieves existing ID if already created", () => {
+		const mapper = new GodotIdToVscodeIdMapper();
+		const godotId = new GodotIdWithPath(BigInt(6), ["path6"]);
+		const vscodeId1 = mapper.get_or_create_vscode_id(godotId);
+		const vscodeId2 = mapper.get_or_create_vscode_id(godotId);
+		expect(vscodeId1).to.equal(vscodeId2);
+	});
+});

+ 78 - 79
src/debugger/godot4/variables/godot_object_promise.test.ts

@@ -1,79 +1,78 @@
-import sinon from "sinon";
-import chai from "chai";
-import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
-// import chaiAsPromised from "chai-as-promised";
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-var chaiAsPromised = import("chai-as-promised");
-// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
-
-chaiAsPromised.then((module) => {
-  chai.use(module.default);
-});
-const { expect } = chai;
-
-
-suite("GodotObjectPromise", () => {
-  let clock;
-
-  setup(() => {
-    clock = sinon.useFakeTimers(); // Use Sinon to control time
-  });
-
-  teardown(() => {
-    clock.restore(); // Restore the real timers after each test
-  });
-
-  test("resolves successfully with a valid GodotObject", async () => {
-    const godotObject: GodotObject = {
-      godot_id: BigInt(1),
-      type: "TestType",
-      sub_values: []
-    };
-
-    const promise = new GodotObjectPromise();
-    setTimeout(() => promise.resolve(godotObject), 10);
-    clock.tick(10); // Fast-forward time
-    await expect(promise.promise).to.eventually.equal(godotObject);
-  });
-
-  test("rejects with an error when explicitly called", async () => {
-    const promise = new GodotObjectPromise();
-    const error = new Error("Test rejection");
-    setTimeout(() => promise.reject(error), 10);
-    clock.tick(10); // Fast-forward time
-    await expect(promise.promise).to.be.rejectedWith("Test rejection");
-  });
-
-  test("rejects due to timeout", async () => {
-    const promise = new GodotObjectPromise(50);
-    clock.tick(50); // Fast-forward time
-    await expect(promise.promise).to.be.rejectedWith("GodotObjectPromise timed out");
-  });
-
-  test("does not reject if resolved before timeout", async () => {
-    const godotObject: GodotObject = {
-      godot_id: BigInt(2),
-      type: "AnotherTestType",
-      sub_values: []
-    };
-
-    const promise = new GodotObjectPromise(100);
-    setTimeout(() => promise.resolve(godotObject), 10);
-    clock.tick(10); // Fast-forward time
-    await expect(promise.promise).to.eventually.equal(godotObject);
-  });
-
-  test("clears timeout when resolved", async () => {
-    const promise = new GodotObjectPromise(1000);
-    promise.resolve({ godot_id: BigInt(3), type: "ResolvedType", sub_values: [] });
-    clock.tick(1000); // Fast-forward time
-    await expect(promise.promise).to.eventually.be.fulfilled;
-  });
-
-  test("clears timeout when rejected", async () => {
-    const promise = new GodotObjectPromise(1000);
-    promise.reject(new Error("Rejected"));
-    clock.tick(1000); // Fast-forward time
-    await expect(promise.promise).to.be.rejectedWith("Rejected");
-  });
-});
+import sinon from "sinon";
+import chai from "chai";
+import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
+// import chaiAsPromised from "chai-as-promised";
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+var chaiAsPromised = import("chai-as-promised");
+// const chaiAsPromised = await import("chai-as-promised"); // TODO: use after migration to ECMAScript modules
+
+chaiAsPromised.then((module) => {
+	chai.use(module.default);
+});
+const { expect } = chai;
+
+suite("GodotObjectPromise", () => {
+	let clock;
+
+	setup(() => {
+		clock = sinon.useFakeTimers(); // Use Sinon to control time
+	});
+
+	teardown(() => {
+		clock.restore(); // Restore the real timers after each test
+	});
+
+	test("resolves successfully with a valid GodotObject", async () => {
+		const godotObject: GodotObject = {
+			godot_id: BigInt(1),
+			type: "TestType",
+			sub_values: [],
+		};
+
+		const promise = new GodotObjectPromise();
+		setTimeout(() => promise.resolve(godotObject), 10);
+		clock.tick(10); // Fast-forward time
+		await expect(promise.promise).to.eventually.equal(godotObject);
+	});
+
+	test("rejects with an error when explicitly called", async () => {
+		const promise = new GodotObjectPromise();
+		const error = new Error("Test rejection");
+		setTimeout(() => promise.reject(error), 10);
+		clock.tick(10); // Fast-forward time
+		await expect(promise.promise).to.be.rejectedWith("Test rejection");
+	});
+
+	test("rejects due to timeout", async () => {
+		const promise = new GodotObjectPromise(50);
+		clock.tick(50); // Fast-forward time
+		await expect(promise.promise).to.be.rejectedWith("GodotObjectPromise timed out");
+	});
+
+	test("does not reject if resolved before timeout", async () => {
+		const godotObject: GodotObject = {
+			godot_id: BigInt(2),
+			type: "AnotherTestType",
+			sub_values: [],
+		};
+
+		const promise = new GodotObjectPromise(100);
+		setTimeout(() => promise.resolve(godotObject), 10);
+		clock.tick(10); // Fast-forward time
+		await expect(promise.promise).to.eventually.equal(godotObject);
+	});
+
+	test("clears timeout when resolved", async () => {
+		const promise = new GodotObjectPromise(1000);
+		promise.resolve({ godot_id: BigInt(3), type: "ResolvedType", sub_values: [] });
+		clock.tick(1000); // Fast-forward time
+		await expect(promise.promise).to.eventually.be.fulfilled;
+	});
+
+	test("clears timeout when rejected", async () => {
+		const promise = new GodotObjectPromise(1000);
+		promise.reject(new Error("Rejected"));
+		clock.tick(1000); // Fast-forward time
+		await expect(promise.promise).to.be.rejectedWith("Rejected");
+	});
+});

+ 52 - 52
src/debugger/godot4/variables/godot_object_promise.ts

@@ -1,52 +1,52 @@
-import { GodotVariable } from "../../debug_runtime";
-
-export interface GodotObject {
-  godot_id: bigint;
-  type: string;
-  sub_values: GodotVariable[];
-}
-
-/**
- * A promise that resolves to a {@link GodotObject}.
- *
- * This promise is used to handle the asynchronous nature of requesting a Godot object.
- * It is used as a placeholder until the actual object is received.
- *
- * When the object is received from the server, the promise is resolved with the object.
- * If the object is not received within a certain time, the promise is rejected with an error.
- */
-export class GodotObjectPromise {
-  private _resolve!: (value: GodotObject | PromiseLike<GodotObject>) => void;
-  private _reject!: (reason?: any) => void;
-  public promise: Promise<GodotObject>;
-  private timeoutId?: NodeJS.Timeout;
-
-  constructor(timeoutMs?: number) {
-    this.promise = new Promise<GodotObject>((resolve_arg, reject_arg) => {
-      this._resolve = resolve_arg;
-      this._reject = reject_arg;
-
-      if (timeoutMs !== undefined) {
-        this.timeoutId = setTimeout(() => {
-          reject_arg(new Error("GodotObjectPromise timed out"));
-        }, timeoutMs);
-      }
-    });
-  }
-
-  async resolve(value: GodotObject) {
-    if (this.timeoutId) {
-      clearTimeout(this.timeoutId);
-      this.timeoutId = undefined;
-    }
-    await this._resolve(value);
-  }
-
-  async reject(reason: Error) {
-    if (this.timeoutId) {
-      clearTimeout(this.timeoutId);
-      this.timeoutId = undefined;
-    }
-    await this._reject(reason);
-  }
-}
+import { GodotVariable } from "../../debug_runtime";
+
+export interface GodotObject {
+	godot_id: bigint;
+	type: string;
+	sub_values: GodotVariable[];
+}
+
+/**
+ * A promise that resolves to a {@link GodotObject}.
+ *
+ * This promise is used to handle the asynchronous nature of requesting a Godot object.
+ * It is used as a placeholder until the actual object is received.
+ *
+ * When the object is received from the server, the promise is resolved with the object.
+ * If the object is not received within a certain time, the promise is rejected with an error.
+ */
+export class GodotObjectPromise {
+	private _resolve!: (value: GodotObject | PromiseLike<GodotObject>) => void;
+	private _reject!: (reason?: any) => void;
+	public promise: Promise<GodotObject>;
+	private timeoutId?: NodeJS.Timeout;
+
+	constructor(timeoutMs?: number) {
+		this.promise = new Promise<GodotObject>((resolve_arg, reject_arg) => {
+			this._resolve = resolve_arg;
+			this._reject = reject_arg;
+
+			if (timeoutMs !== undefined) {
+				this.timeoutId = setTimeout(() => {
+					reject_arg(new Error("GodotObjectPromise timed out"));
+				}, timeoutMs);
+			}
+		});
+	}
+
+	async resolve(value: GodotObject) {
+		if (this.timeoutId) {
+			clearTimeout(this.timeoutId);
+			this.timeoutId = undefined;
+		}
+		await this._resolve(value);
+	}
+
+	async reject(reason: Error) {
+		if (this.timeoutId) {
+			clearTimeout(this.timeoutId);
+			this.timeoutId = undefined;
+		}
+		await this._reject(reason);
+	}
+}

+ 282 - 240
src/debugger/godot4/variables/variables_manager.ts

@@ -1,240 +1,282 @@
-import { DebugProtocol } from "@vscode/debugprotocol";
-import { ServerController } from "../server_controller";
-import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
-import { GodotVariable } from "../../debug_runtime";
-import { ObjectId } from "./variants";
-import { GodotIdToVscodeIdMapper, GodotIdWithPath } from "./godot_id_to_vscode_id_mapper";
-
-export interface VsCodeScopeIDs {
-  Locals: number;
-  Members: number;
-  Globals: number;
-}
-
-export class VariablesManager {
-  constructor(public controller: ServerController) {
-  }
-  
-  public godot_object_promises: Map<bigint, GodotObjectPromise>= new Map();
-	public godot_id_to_vscode_id_mapper = new GodotIdToVscodeIdMapper();
-	
-	// variablesFrameId: number;
-
-  private frame_id_to_scopes_map: Map<number, VsCodeScopeIDs> = new Map();
-
-  /**
-   * Returns Locals, Members, and Globals vscode_ids
-   * @param stack_frame_id the id of the stack frame
-   * @returns an object with Locals, Members, and Globals vscode_ids
-   */
-  public get_or_create_frame_scopes(stack_frame_id: number): VsCodeScopeIDs {
-    var scopes = this.frame_id_to_scopes_map.get(stack_frame_id);
-    if (scopes === undefined) {
-      const frame_id = BigInt(stack_frame_id);
-      scopes = {} as VsCodeScopeIDs;
-      scopes.Locals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(-frame_id*3n-1n, []));
-      scopes.Members = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(-frame_id*3n-2n, []));
-      scopes.Globals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(-frame_id*3n-3n, []));
-      this.frame_id_to_scopes_map.set(stack_frame_id, scopes);
-    }
-
-    return scopes;
-	}
-
-  /**
-   * Retrieves a Godot object from the cache or godot debug server
-   * @param godot_id the id of the object
-   * @returns a promise that resolves to the requested object
-   */
-  public async get_godot_object(godot_id: bigint, force_refresh = false) {
-    if (force_refresh) {
-      // delete the object
-      this.godot_object_promises.delete(godot_id);
-
-      // check if member scopes also need to be refreshed:
-      for (const [stack_frame_id, scopes] of this.frame_id_to_scopes_map) {
-        const members_godot_id = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(scopes.Members);
-        const scopes_object = await this.get_godot_object(members_godot_id.godot_id);
-        const self = scopes_object.sub_values.find((sv) => sv.name === "self");
-        if (self !== undefined && self.value instanceof ObjectId) {
-          if (self.value.id === godot_id) {
-            this.godot_object_promises.delete(members_godot_id.godot_id); // force refresh the member scope
-          }
-        }
-      }
-    }
-    var variable_promise = this.godot_object_promises.get(godot_id);
-    if (variable_promise === undefined) {
-      // variable not found, request one
-      if (godot_id < 0) {
-        // special case for scopes, which have godot_id below 0. see @this.get_or_create_frame_scopes
-        // all 3 scopes for current stackFrameId are retrieved at the same time, aka [-1,-2-,3], [-4,-5,-6], etc..
-        // init corresponding promises
-        const requested_stack_frame_id = (-godot_id-1n)/3n;
-        // this.variablesFrameId will be undefined when the debugger just stopped at breakpoint:
-        // evaluateRequest is called before scopesRequest
-        const local_scopes_godot_id = -requested_stack_frame_id*3n-1n;
-        const member_scopes_godot_id = -requested_stack_frame_id*3n-2n;
-        const global_scopes_godot_id = -requested_stack_frame_id*3n-3n;
-        this.godot_object_promises.set(local_scopes_godot_id, new GodotObjectPromise());
-        this.godot_object_promises.set(member_scopes_godot_id, new GodotObjectPromise());
-        this.godot_object_promises.set(global_scopes_godot_id, new GodotObjectPromise());
-        variable_promise = this.godot_object_promises.get(godot_id);
-        // request stack vars from godot server, which will resolve variable promises 1,2 & 3
-        // see file://../server_controller.ts 'case "stack_frame_vars":'
-        this.controller.request_stack_frame_vars(Number(requested_stack_frame_id));
-      } else {
-        this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(godot_id, []));
-        variable_promise = new GodotObjectPromise();
-        this.godot_object_promises.set(godot_id, variable_promise);
-        // request the object from godot server. Once godot server responds, the controller will resolve the variable_promise
-        this.controller.request_inspect_object(godot_id);
-      }
-    }
-    const godot_object = await variable_promise.promise;
-
-    return godot_object;
-  }
-
-  public async get_vscode_object(vscode_id: number): Promise<DebugProtocol.Variable[]> {
-    const godot_id_with_path = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id);
-    if (godot_id_with_path === undefined) {
-      throw new Error(`Unknown variablesReference ${vscode_id}`);
-    }
-    const godot_object = await this.get_godot_object(godot_id_with_path.godot_id);
-    if (godot_object === undefined) {
-      throw new Error(`Cannot retrieve path '${godot_id_with_path.toString()}'. Godot object with id ${godot_id_with_path.godot_id} not found.`);
-    }
-
-    let sub_values: GodotVariable[] = godot_object.sub_values;
-
-    // if the path is specified, walk the godot_object using it to access the requested variable:
-    for (const [idx, path] of godot_id_with_path.path.entries()) {
-      const sub_val = sub_values.find((sv) => sv.name === path);
-      if (sub_val === undefined) {
-        throw new Error(`Cannot retrieve path '${godot_id_with_path.toString()}'. Following subpath not found: '${godot_id_with_path.path.slice(0, idx+1).join("/")}'.`);
-      }
-      sub_values = sub_val.sub_values;				
-    }
-
-    const variables: DebugProtocol.Variable[] = [];
-    for (const va of sub_values) {
-      const godot_id_with_path_sub = va.id !== undefined ? new GodotIdWithPath(va.id, []) : undefined;
-      const vscode_id = godot_id_with_path_sub !== undefined ? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(godot_id_with_path_sub) : 0;
-      const variable: DebugProtocol.Variable = await this.parse_variable(va, vscode_id, godot_id_with_path.godot_id, godot_id_with_path.path, this.godot_id_to_vscode_id_mapper);
-      variables.push(variable);
-    }
-
-    return variables;
-  }
-
-  public async get_vscode_variable_by_name(variable_name: string, stack_frame_id: number): Promise<DebugProtocol.Variable> {
-    let variable: GodotVariable;
-
-    const variable_names = variable_name.split(".");
-
-    for (var i = 0; i < variable_names.length; i++) {
-      if (i === 0) {
-        // find the first part of variable_name in scopes. Locals first, then Members, then Globals
-        const vscode_scope_ids = this.get_or_create_frame_scopes(stack_frame_id);
-        const vscode_ids = [vscode_scope_ids.Locals, vscode_scope_ids.Members, vscode_scope_ids.Globals];
-        const godot_ids = vscode_ids.map(vscode_id => this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id))
-                                    .map(godot_id_with_path => godot_id_with_path.godot_id);
-        for (var godot_id of godot_ids) {
-          // check each scope for requested variable
-          const scope = await this.get_godot_object(godot_id);
-          variable = scope.sub_values.find((sv) => sv.name === variable_names[0]);
-          if (variable !== undefined) {
-            break;
-          }
-        }
-      } else {
-        // just look up the subpath using the current variable
-        if (variable.value instanceof ObjectId) {
-          const godot_object = await this.get_godot_object(variable.value.id);
-          variable = godot_object.sub_values.find((sv) => sv.name === variable_names[i]);
-        } else {
-          variable = variable.sub_values.find((sv) => sv.name === variable_names[i]);
-        }
-      }
-      if (variable === undefined) {
-        throw new Error(`Cannot retrieve path '${variable_name}'. Following subpath not found: '${variable_names.slice(0, i+1).join(".")}'`);
-      }
-    }
-
-    const parsed_variable = await this.parse_variable(variable, undefined, godot_id, [], this.godot_id_to_vscode_id_mapper);
-    if (parsed_variable.variablesReference === undefined) {
-      const objectId = variable.value instanceof ObjectId ? variable.value : undefined;
-      const vscode_id = objectId !== undefined ? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(objectId.id, [])) : 0;
-      parsed_variable.variablesReference = vscode_id;
-    }
-
-    return parsed_variable;
-  }
-
-  private async parse_variable(va: GodotVariable, vscode_id?: number, parent_godot_id?: bigint, relative_path?: string[], mapper?: GodotIdToVscodeIdMapper): Promise<DebugProtocol.Variable> {
-    const value = va.value;
-    let rendered_value = "";
-    let reference = 0;
-  
-    if (typeof value === "number") {
-      if (Number.isInteger(value)) {
-        rendered_value = `${value}`;
-      } else {
-        rendered_value = `${parseFloat(value.toFixed(5))}`;
-      }
-    } else if (
-      typeof value === "bigint" ||
-      typeof value === "boolean" ||
-      typeof value === "string"
-    ) {
-      rendered_value = `${value}`;
-    } else if (typeof value === "undefined") {
-      rendered_value = "null";
-    } else {
-      if (Array.isArray(value)) {
-        rendered_value = `(${value.length}) [${value.slice(0, 10).join(", ")}]`;
-        reference = mapper.get_or_create_vscode_id(new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]));
-      } else if (value instanceof Map) {
-        rendered_value = value["class_name"] ?? `Dictionary(${value.size})`;
-        reference = mapper.get_or_create_vscode_id(new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]));
-      } else if (value instanceof ObjectId) {
-        if (value.id === undefined) {
-          throw new Error("Invalid godot object: instanceof ObjectId but id is undefined");
-        }
-        // Godot returns only ID for the object.
-        // In order to retrieve the class name, we need to request the object
-        const godot_object = await this.get_godot_object(value.id);
-        rendered_value = `${godot_object.type}${value.stringify_value()}`;
-        // rendered_value = `${value.type_name()}${value.stringify_value()}`;
-        reference = vscode_id;
-      }
-      else {
-        try {
-          rendered_value = `${value.type_name()}${value.stringify_value()}`;
-        } catch (e) {
-          rendered_value = `${value}`;
-        }
-        reference = mapper.get_or_create_vscode_id(new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]));
-        // reference = vsode_id ? vsode_id : 0;
-      }
-    }
-  
-    const variable: DebugProtocol.Variable = {
-      name: va.name,
-      value: rendered_value,
-      variablesReference: reference
-    };
-    
-    return variable;
-  }  
-
-  public resolve_variable(godot_id: bigint, className: string, sub_values: GodotVariable[]) {
-    const variable_promise = this.godot_object_promises.get(godot_id);
-    if (variable_promise === undefined) {
-      throw new Error(`Received 'inspect_object' for godot_id ${godot_id} but no variable promise to resolve found`);
-    }
-
-    variable_promise.resolve({godot_id: godot_id, type: className, sub_values: sub_values} as GodotObject);
-  }
-}
+import { DebugProtocol } from "@vscode/debugprotocol";
+import { ServerController } from "../server_controller";
+import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
+import { GodotVariable } from "../../debug_runtime";
+import { ObjectId } from "./variants";
+import { GodotIdToVscodeIdMapper, GodotIdWithPath } from "./godot_id_to_vscode_id_mapper";
+
+export interface VsCodeScopeIDs {
+	Locals: number;
+	Members: number;
+	Globals: number;
+}
+
+export class VariablesManager {
+	constructor(public controller: ServerController) {}
+
+	public godot_object_promises: Map<bigint, GodotObjectPromise> = new Map();
+	public godot_id_to_vscode_id_mapper = new GodotIdToVscodeIdMapper();
+
+	// variablesFrameId: number;
+
+	private frame_id_to_scopes_map: Map<number, VsCodeScopeIDs> = new Map();
+
+	/**
+	 * Returns Locals, Members, and Globals vscode_ids
+	 * @param stack_frame_id the id of the stack frame
+	 * @returns an object with Locals, Members, and Globals vscode_ids
+	 */
+	public get_or_create_frame_scopes(stack_frame_id: number): VsCodeScopeIDs {
+		var scopes = this.frame_id_to_scopes_map.get(stack_frame_id);
+		if (scopes === undefined) {
+			const frame_id = BigInt(stack_frame_id);
+			scopes = {} as VsCodeScopeIDs;
+			scopes.Locals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(
+				new GodotIdWithPath(-frame_id * 3n - 1n, []),
+			);
+			scopes.Members = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(
+				new GodotIdWithPath(-frame_id * 3n - 2n, []),
+			);
+			scopes.Globals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(
+				new GodotIdWithPath(-frame_id * 3n - 3n, []),
+			);
+			this.frame_id_to_scopes_map.set(stack_frame_id, scopes);
+		}
+
+		return scopes;
+	}
+
+	/**
+	 * Retrieves a Godot object from the cache or godot debug server
+	 * @param godot_id the id of the object
+	 * @returns a promise that resolves to the requested object
+	 */
+	public async get_godot_object(godot_id: bigint, force_refresh = false) {
+		if (force_refresh) {
+			// delete the object
+			this.godot_object_promises.delete(godot_id);
+
+			// check if member scopes also need to be refreshed:
+			for (const [stack_frame_id, scopes] of this.frame_id_to_scopes_map) {
+				const members_godot_id = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(scopes.Members);
+				const scopes_object = await this.get_godot_object(members_godot_id.godot_id);
+				const self = scopes_object.sub_values.find((sv) => sv.name === "self");
+				if (self !== undefined && self.value instanceof ObjectId) {
+					if (self.value.id === godot_id) {
+						this.godot_object_promises.delete(members_godot_id.godot_id); // force refresh the member scope
+					}
+				}
+			}
+		}
+		var variable_promise = this.godot_object_promises.get(godot_id);
+		if (variable_promise === undefined) {
+			// variable not found, request one
+			if (godot_id < 0) {
+				// special case for scopes, which have godot_id below 0. see @this.get_or_create_frame_scopes
+				// all 3 scopes for current stackFrameId are retrieved at the same time, aka [-1,-2-,3], [-4,-5,-6], etc..
+				// init corresponding promises
+				const requested_stack_frame_id = (-godot_id - 1n) / 3n;
+				// this.variablesFrameId will be undefined when the debugger just stopped at breakpoint:
+				// evaluateRequest is called before scopesRequest
+				const local_scopes_godot_id = -requested_stack_frame_id * 3n - 1n;
+				const member_scopes_godot_id = -requested_stack_frame_id * 3n - 2n;
+				const global_scopes_godot_id = -requested_stack_frame_id * 3n - 3n;
+				this.godot_object_promises.set(local_scopes_godot_id, new GodotObjectPromise());
+				this.godot_object_promises.set(member_scopes_godot_id, new GodotObjectPromise());
+				this.godot_object_promises.set(global_scopes_godot_id, new GodotObjectPromise());
+				variable_promise = this.godot_object_promises.get(godot_id);
+				// request stack vars from godot server, which will resolve variable promises 1,2 & 3
+				// see file://../server_controller.ts 'case "stack_frame_vars":'
+				this.controller.request_stack_frame_vars(Number(requested_stack_frame_id));
+			} else {
+				this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(godot_id, []));
+				variable_promise = new GodotObjectPromise();
+				this.godot_object_promises.set(godot_id, variable_promise);
+				// request the object from godot server. Once godot server responds, the controller will resolve the variable_promise
+				this.controller.request_inspect_object(godot_id);
+			}
+		}
+		const godot_object = await variable_promise.promise;
+
+		return godot_object;
+	}
+
+	public async get_vscode_object(vscode_id: number): Promise<DebugProtocol.Variable[]> {
+		const godot_id_with_path = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id);
+		if (godot_id_with_path === undefined) {
+			throw new Error(`Unknown variablesReference ${vscode_id}`);
+		}
+		const godot_object = await this.get_godot_object(godot_id_with_path.godot_id);
+		if (godot_object === undefined) {
+			throw new Error(
+				`Cannot retrieve path '${godot_id_with_path.toString()}'. Godot object with id ${godot_id_with_path.godot_id} not found.`,
+			);
+		}
+
+		let sub_values: GodotVariable[] = godot_object.sub_values;
+
+		// if the path is specified, walk the godot_object using it to access the requested variable:
+		for (const [idx, path] of godot_id_with_path.path.entries()) {
+			const sub_val = sub_values.find((sv) => sv.name === path);
+			if (sub_val === undefined) {
+				throw new Error(
+					`Cannot retrieve path '${godot_id_with_path.toString()}'. Following subpath not found: '${godot_id_with_path.path.slice(0, idx + 1).join("/")}'.`,
+				);
+			}
+			sub_values = sub_val.sub_values;
+		}
+
+		const variables: DebugProtocol.Variable[] = [];
+		for (const va of sub_values) {
+			const godot_id_with_path_sub = va.id !== undefined ? new GodotIdWithPath(va.id, []) : undefined;
+			const vscode_id =
+				godot_id_with_path_sub !== undefined
+					? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(godot_id_with_path_sub)
+					: 0;
+			const variable: DebugProtocol.Variable = await this.parse_variable(
+				va,
+				vscode_id,
+				godot_id_with_path.godot_id,
+				godot_id_with_path.path,
+				this.godot_id_to_vscode_id_mapper,
+			);
+			variables.push(variable);
+		}
+
+		return variables;
+	}
+
+	public async get_vscode_variable_by_name(
+		variable_name: string,
+		stack_frame_id: number,
+	): Promise<DebugProtocol.Variable> {
+		let variable: GodotVariable;
+
+		const variable_names = variable_name.split(".");
+
+		for (var i = 0; i < variable_names.length; i++) {
+			if (i === 0) {
+				// find the first part of variable_name in scopes. Locals first, then Members, then Globals
+				const vscode_scope_ids = this.get_or_create_frame_scopes(stack_frame_id);
+				const vscode_ids = [vscode_scope_ids.Locals, vscode_scope_ids.Members, vscode_scope_ids.Globals];
+				const godot_ids = vscode_ids
+					.map((vscode_id) => this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id))
+					.map((godot_id_with_path) => godot_id_with_path.godot_id);
+				for (var godot_id of godot_ids) {
+					// check each scope for requested variable
+					const scope = await this.get_godot_object(godot_id);
+					variable = scope.sub_values.find((sv) => sv.name === variable_names[0]);
+					if (variable !== undefined) {
+						break;
+					}
+				}
+			} else {
+				// just look up the subpath using the current variable
+				if (variable.value instanceof ObjectId) {
+					const godot_object = await this.get_godot_object(variable.value.id);
+					variable = godot_object.sub_values.find((sv) => sv.name === variable_names[i]);
+				} else {
+					variable = variable.sub_values.find((sv) => sv.name === variable_names[i]);
+				}
+			}
+			if (variable === undefined) {
+				throw new Error(
+					`Cannot retrieve path '${variable_name}'. Following subpath not found: '${variable_names.slice(0, i + 1).join(".")}'`,
+				);
+			}
+		}
+
+		const parsed_variable = await this.parse_variable(
+			variable,
+			undefined,
+			godot_id,
+			[],
+			this.godot_id_to_vscode_id_mapper,
+		);
+		if (parsed_variable.variablesReference === undefined) {
+			const objectId = variable.value instanceof ObjectId ? variable.value : undefined;
+			const vscode_id =
+				objectId !== undefined
+					? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(objectId.id, []))
+					: 0;
+			parsed_variable.variablesReference = vscode_id;
+		}
+
+		return parsed_variable;
+	}
+
+	private async parse_variable(
+		va: GodotVariable,
+		vscode_id?: number,
+		parent_godot_id?: bigint,
+		relative_path?: string[],
+		mapper?: GodotIdToVscodeIdMapper,
+	): Promise<DebugProtocol.Variable> {
+		const value = va.value;
+		let rendered_value = "";
+		let reference = 0;
+
+		if (typeof value === "number") {
+			if (Number.isInteger(value)) {
+				rendered_value = `${value}`;
+			} else {
+				rendered_value = `${parseFloat(value.toFixed(5))}`;
+			}
+		} else if (typeof value === "bigint" || typeof value === "boolean" || typeof value === "string") {
+			rendered_value = `${value}`;
+		} else if (typeof value === "undefined") {
+			rendered_value = "null";
+		} else {
+			if (Array.isArray(value)) {
+				rendered_value = `(${value.length}) [${value.slice(0, 10).join(", ")}]`;
+				reference = mapper.get_or_create_vscode_id(
+					new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),
+				);
+			} else if (value instanceof Map) {
+				rendered_value = value["class_name"] ?? `Dictionary(${value.size})`;
+				reference = mapper.get_or_create_vscode_id(
+					new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),
+				);
+			} else if (value instanceof ObjectId) {
+				if (value.id === undefined) {
+					throw new Error("Invalid godot object: instanceof ObjectId but id is undefined");
+				}
+				// Godot returns only ID for the object.
+				// In order to retrieve the class name, we need to request the object
+				const godot_object = await this.get_godot_object(value.id);
+				rendered_value = `${godot_object.type}${value.stringify_value()}`;
+				// rendered_value = `${value.type_name()}${value.stringify_value()}`;
+				reference = vscode_id;
+			} else {
+				try {
+					rendered_value = `${value.type_name()}${value.stringify_value()}`;
+				} catch (e) {
+					rendered_value = `${value}`;
+				}
+				reference = mapper.get_or_create_vscode_id(
+					new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),
+				);
+				// reference = vsode_id ? vsode_id : 0;
+			}
+		}
+
+		const variable: DebugProtocol.Variable = {
+			name: va.name,
+			value: rendered_value,
+			variablesReference: reference,
+		};
+
+		return variable;
+	}
+
+	public resolve_variable(godot_id: bigint, className: string, sub_values: GodotVariable[]) {
+		const variable_promise = this.godot_object_promises.get(godot_id);
+		if (variable_promise === undefined) {
+			throw new Error(
+				`Received 'inspect_object' for godot_id ${godot_id} but no variable promise to resolve found`,
+			);
+		}
+
+		variable_promise.resolve({ godot_id: godot_id, type: className, sub_values: sub_values } as GodotObject);
+	}
+}