mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2025-12-31 13:48:24 +03:00
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:
@@ -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
156
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user