Add automatic project formatting (#814)

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

* Align new debugger code with project style
This commit is contained in:
David Kincaid
2025-03-04 19:54:47 -05:00
committed by GitHub
parent 1dcbd651df
commit f4ae73c9a0
11 changed files with 1170 additions and 886 deletions

View File

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

156
package-lock.json generated
View File

@@ -25,6 +25,7 @@
"ya-bbcode": "^4.0.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/chai": "^4.3.11",
"@types/chai-as-promised": "^8.0.1",
"@types/chai-subset": "^1.3.5",
@@ -391,6 +392,161 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"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": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",

View File

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

View File

@@ -29,7 +29,7 @@ export class GodotDebugSession extends LoggingDebugSession {
public variables_manager: VariablesManager;
public constructor(projectVersion : string) {
public constructor(projectVersion: string) {
super();
this.setDebuggerLinesStartAt1(false);
@@ -233,14 +233,14 @@ export class GodotDebugSession extends LoggingDebugSession {
// TODO: create scopes dynamically for a given frame
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 = {
scopes: scopes_with_references
scopes: scopes_with_references,
// scopes: [
// { name: "Locals", variablesReference: 1, expensive: false },
// { name: "Members", variablesReference: 2, expensive: false },
@@ -252,7 +252,10 @@ export class GodotDebugSession extends LoggingDebugSession {
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);
try {
const variables = await this.variables_manager.get_vscode_object(args.variablesReference);
@@ -274,10 +277,13 @@ export class GodotDebugSession extends LoggingDebugSession {
log.info("evaluateRequest", args);
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 = {
result: parsed_variable.value,
variablesReference: parsed_variable.variablesReference
variablesReference: parsed_variable.variablesReference,
};
} catch (error) {
response.success = false;

View File

@@ -1,4 +1,4 @@
import { GodotVariable, } from "../debug_runtime";
import { GodotVariable } from "../debug_runtime";
import { SceneNode } from "../scene_tree_provider";
import { ObjectId } from "./variables/variants";
@@ -43,9 +43,12 @@ export function get_sub_values(value: any): GodotVariable[] {
} else if (value instanceof Map) {
subValues = [];
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;
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") {
subValues = value.sub_values()?.map((sva) => {
@@ -59,4 +62,4 @@ export function get_sub_values(value: any): GodotVariable[] {
}
return subValues;
}
}

View File

@@ -24,7 +24,7 @@ import { VariantDecoder } from "./variables/variant_decoder";
import { VariantEncoder } from "./variables/variant_encoder";
import { RawObject } from "./variables/variants";
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 socketLog = createLogger("debugger.socket");
@@ -42,7 +42,7 @@ class Command {
class GodotPartialStackVars {
Locals: GodotVariable[] = [];
Members: GodotVariable[] = [];
Globals: GodotVariable [] = [];
Globals: GodotVariable[] = [];
public remaining: number;
public stack_frame_id: number;
constructor(stack_frame_id: number) {
@@ -56,7 +56,7 @@ class GodotPartialStackVars {
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 scope = this[scopeName];
// 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 partialStackVars: GodotPartialStackVars;
private projectVersionMajor: number;
private projectVersionMinor : number;
private projectVersionPoint : number;
private projectVersionMinor: number;
private projectVersionPoint: number;
public constructor(public session: GodotDebugSession) {}
public setProjectVersion(projectVersion: string) {
const versionParts = projectVersion.split('.').map(Number);
const versionParts = projectVersion.split(".").map(Number);
this.projectVersionMajor = versionParts[0] || 0;
this.projectVersionMinor = versionParts[1] || 0;
this.projectVersionPoint = versionParts[2] || 0;
@@ -135,11 +135,12 @@ export class ServerController {
public request_stack_frame_vars(stack_frame_id: number) {
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.send_command("get_stack_frame_vars", [stack_frame_id]);
@@ -480,7 +481,9 @@ export class ServerController {
case "stack_frame_vars": {
/** first response to {@link request_stack_frame_vars} */
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];
// init this.partialStackVars, which will be filled with "stack_frame_var" responses data
@@ -493,15 +496,27 @@ export class ServerController {
return;
}
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;
}
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;
}
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;
}
var name: string = command.parameters[0];
@@ -517,13 +532,21 @@ export class ServerController {
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:
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(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;
}
@@ -532,8 +555,8 @@ export class ServerController {
this.didFirstOutput = true;
// 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;
}

View File

@@ -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);
});

View File

@@ -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);
});
});

View File

@@ -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");
});
});

View File

@@ -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);
}
}

View File

@@ -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);
}
}