Files
godot-vscode-plugin/src/debugger/godot4/variables/variables_manager.ts
David Kincaid 37bb1116fb Debugger Tool Improvements (#848)
A variety of debugger internal fixes + linter/style improvements
2025-07-31 15:17:33 -04:00

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