mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2026-01-04 10:09:58 +03:00
286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
import { DebugProtocol } from "@vscode/debugprotocol";
|
|
import { GodotVariable } from "../../debug_runtime";
|
|
import { ServerController } from "../server_controller";
|
|
import { GodotIdToVscodeIdMapper, GodotIdWithPath } from "./godot_id_to_vscode_id_mapper";
|
|
import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
|
|
import { ObjectId } from "./variants";
|
|
|
|
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 {
|
|
let 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let 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(".");
|
|
let parent_id: bigint;
|
|
|
|
for (let 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 (const 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) {
|
|
parent_id = godot_id;
|
|
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,
|
|
parent_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 = `${Number.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) {
|
|
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
|
|
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);
|
|
}
|
|
}
|