variables_manager.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { DebugProtocol } from "@vscode/debugprotocol";
  2. import { ServerController } from "../server_controller";
  3. import { GodotObject, GodotObjectPromise } from "./godot_object_promise";
  4. import { GodotVariable } from "../../debug_runtime";
  5. import { ObjectId } from "./variants";
  6. import { GodotIdToVscodeIdMapper, GodotIdWithPath } from "./godot_id_to_vscode_id_mapper";
  7. export interface VsCodeScopeIDs {
  8. Locals: number;
  9. Members: number;
  10. Globals: number;
  11. }
  12. export class VariablesManager {
  13. constructor(public controller: ServerController) {}
  14. public godot_object_promises: Map<bigint, GodotObjectPromise> = new Map();
  15. public godot_id_to_vscode_id_mapper = new GodotIdToVscodeIdMapper();
  16. // variablesFrameId: number;
  17. private frame_id_to_scopes_map: Map<number, VsCodeScopeIDs> = new Map();
  18. /**
  19. * Returns Locals, Members, and Globals vscode_ids
  20. * @param stack_frame_id the id of the stack frame
  21. * @returns an object with Locals, Members, and Globals vscode_ids
  22. */
  23. public get_or_create_frame_scopes(stack_frame_id: number): VsCodeScopeIDs {
  24. var scopes = this.frame_id_to_scopes_map.get(stack_frame_id);
  25. if (scopes === undefined) {
  26. const frame_id = BigInt(stack_frame_id);
  27. scopes = {} as VsCodeScopeIDs;
  28. scopes.Locals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(
  29. new GodotIdWithPath(-frame_id * 3n - 1n, []),
  30. );
  31. scopes.Members = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(
  32. new GodotIdWithPath(-frame_id * 3n - 2n, []),
  33. );
  34. scopes.Globals = this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(
  35. new GodotIdWithPath(-frame_id * 3n - 3n, []),
  36. );
  37. this.frame_id_to_scopes_map.set(stack_frame_id, scopes);
  38. }
  39. return scopes;
  40. }
  41. /**
  42. * Retrieves a Godot object from the cache or godot debug server
  43. * @param godot_id the id of the object
  44. * @returns a promise that resolves to the requested object
  45. */
  46. public async get_godot_object(godot_id: bigint, force_refresh = false) {
  47. if (force_refresh) {
  48. // delete the object
  49. this.godot_object_promises.delete(godot_id);
  50. // check if member scopes also need to be refreshed:
  51. for (const [stack_frame_id, scopes] of this.frame_id_to_scopes_map) {
  52. const members_godot_id = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(scopes.Members);
  53. const scopes_object = await this.get_godot_object(members_godot_id.godot_id);
  54. const self = scopes_object.sub_values.find((sv) => sv.name === "self");
  55. if (self !== undefined && self.value instanceof ObjectId) {
  56. if (self.value.id === godot_id) {
  57. this.godot_object_promises.delete(members_godot_id.godot_id); // force refresh the member scope
  58. }
  59. }
  60. }
  61. }
  62. var variable_promise = this.godot_object_promises.get(godot_id);
  63. if (variable_promise === undefined) {
  64. // variable not found, request one
  65. if (godot_id < 0) {
  66. // special case for scopes, which have godot_id below 0. see @this.get_or_create_frame_scopes
  67. // all 3 scopes for current stackFrameId are retrieved at the same time, aka [-1,-2-,3], [-4,-5,-6], etc..
  68. // init corresponding promises
  69. const requested_stack_frame_id = (-godot_id - 1n) / 3n;
  70. // this.variablesFrameId will be undefined when the debugger just stopped at breakpoint:
  71. // evaluateRequest is called before scopesRequest
  72. const local_scopes_godot_id = -requested_stack_frame_id * 3n - 1n;
  73. const member_scopes_godot_id = -requested_stack_frame_id * 3n - 2n;
  74. const global_scopes_godot_id = -requested_stack_frame_id * 3n - 3n;
  75. this.godot_object_promises.set(local_scopes_godot_id, new GodotObjectPromise());
  76. this.godot_object_promises.set(member_scopes_godot_id, new GodotObjectPromise());
  77. this.godot_object_promises.set(global_scopes_godot_id, new GodotObjectPromise());
  78. variable_promise = this.godot_object_promises.get(godot_id);
  79. // request stack vars from godot server, which will resolve variable promises 1,2 & 3
  80. // see file://../server_controller.ts 'case "stack_frame_vars":'
  81. this.controller.request_stack_frame_vars(Number(requested_stack_frame_id));
  82. } else {
  83. this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(godot_id, []));
  84. variable_promise = new GodotObjectPromise();
  85. this.godot_object_promises.set(godot_id, variable_promise);
  86. // request the object from godot server. Once godot server responds, the controller will resolve the variable_promise
  87. this.controller.request_inspect_object(godot_id);
  88. }
  89. }
  90. const godot_object = await variable_promise.promise;
  91. return godot_object;
  92. }
  93. public async get_vscode_object(vscode_id: number): Promise<DebugProtocol.Variable[]> {
  94. const godot_id_with_path = this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id);
  95. if (godot_id_with_path === undefined) {
  96. throw new Error(`Unknown variablesReference ${vscode_id}`);
  97. }
  98. const godot_object = await this.get_godot_object(godot_id_with_path.godot_id);
  99. if (godot_object === undefined) {
  100. throw new Error(
  101. `Cannot retrieve path '${godot_id_with_path.toString()}'. Godot object with id ${godot_id_with_path.godot_id} not found.`,
  102. );
  103. }
  104. let sub_values: GodotVariable[] = godot_object.sub_values;
  105. // if the path is specified, walk the godot_object using it to access the requested variable:
  106. for (const [idx, path] of godot_id_with_path.path.entries()) {
  107. const sub_val = sub_values.find((sv) => sv.name === path);
  108. if (sub_val === undefined) {
  109. throw new Error(
  110. `Cannot retrieve path '${godot_id_with_path.toString()}'. Following subpath not found: '${godot_id_with_path.path.slice(0, idx + 1).join("/")}'.`,
  111. );
  112. }
  113. sub_values = sub_val.sub_values;
  114. }
  115. const variables: DebugProtocol.Variable[] = [];
  116. for (const va of sub_values) {
  117. const godot_id_with_path_sub = va.id !== undefined ? new GodotIdWithPath(va.id, []) : undefined;
  118. const vscode_id =
  119. godot_id_with_path_sub !== undefined
  120. ? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(godot_id_with_path_sub)
  121. : 0;
  122. const variable: DebugProtocol.Variable = await this.parse_variable(
  123. va,
  124. vscode_id,
  125. godot_id_with_path.godot_id,
  126. godot_id_with_path.path,
  127. this.godot_id_to_vscode_id_mapper,
  128. );
  129. variables.push(variable);
  130. }
  131. return variables;
  132. }
  133. public async get_vscode_variable_by_name(
  134. variable_name: string,
  135. stack_frame_id: number,
  136. ): Promise<DebugProtocol.Variable> {
  137. let variable: GodotVariable;
  138. const variable_names = variable_name.split(".");
  139. for (var i = 0; i < variable_names.length; i++) {
  140. if (i === 0) {
  141. // find the first part of variable_name in scopes. Locals first, then Members, then Globals
  142. const vscode_scope_ids = this.get_or_create_frame_scopes(stack_frame_id);
  143. const vscode_ids = [vscode_scope_ids.Locals, vscode_scope_ids.Members, vscode_scope_ids.Globals];
  144. const godot_ids = vscode_ids
  145. .map((vscode_id) => this.godot_id_to_vscode_id_mapper.get_godot_id_with_path(vscode_id))
  146. .map((godot_id_with_path) => godot_id_with_path.godot_id);
  147. for (var godot_id of godot_ids) {
  148. // check each scope for requested variable
  149. const scope = await this.get_godot_object(godot_id);
  150. variable = scope.sub_values.find((sv) => sv.name === variable_names[0]);
  151. if (variable !== undefined) {
  152. break;
  153. }
  154. }
  155. } else {
  156. // just look up the subpath using the current variable
  157. if (variable.value instanceof ObjectId) {
  158. const godot_object = await this.get_godot_object(variable.value.id);
  159. variable = godot_object.sub_values.find((sv) => sv.name === variable_names[i]);
  160. } else {
  161. variable = variable.sub_values.find((sv) => sv.name === variable_names[i]);
  162. }
  163. }
  164. if (variable === undefined) {
  165. throw new Error(
  166. `Cannot retrieve path '${variable_name}'. Following subpath not found: '${variable_names.slice(0, i + 1).join(".")}'`,
  167. );
  168. }
  169. }
  170. const parsed_variable = await this.parse_variable(
  171. variable,
  172. undefined,
  173. godot_id,
  174. [],
  175. this.godot_id_to_vscode_id_mapper,
  176. );
  177. if (parsed_variable.variablesReference === undefined) {
  178. const objectId = variable.value instanceof ObjectId ? variable.value : undefined;
  179. const vscode_id =
  180. objectId !== undefined
  181. ? this.godot_id_to_vscode_id_mapper.get_or_create_vscode_id(new GodotIdWithPath(objectId.id, []))
  182. : 0;
  183. parsed_variable.variablesReference = vscode_id;
  184. }
  185. return parsed_variable;
  186. }
  187. private async parse_variable(
  188. va: GodotVariable,
  189. vscode_id?: number,
  190. parent_godot_id?: bigint,
  191. relative_path?: string[],
  192. mapper?: GodotIdToVscodeIdMapper,
  193. ): Promise<DebugProtocol.Variable> {
  194. const value = va.value;
  195. let rendered_value = "";
  196. let reference = 0;
  197. if (typeof value === "number") {
  198. if (Number.isInteger(value)) {
  199. rendered_value = `${value}`;
  200. } else {
  201. rendered_value = `${parseFloat(value.toFixed(5))}`;
  202. }
  203. } else if (typeof value === "bigint" || typeof value === "boolean" || typeof value === "string") {
  204. rendered_value = `${value}`;
  205. } else if (typeof value === "undefined") {
  206. rendered_value = "null";
  207. } else {
  208. if (Array.isArray(value)) {
  209. rendered_value = `(${value.length}) [${value.slice(0, 10).join(", ")}]`;
  210. reference = mapper.get_or_create_vscode_id(
  211. new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),
  212. );
  213. } else if (value instanceof Map) {
  214. rendered_value = value["class_name"] ?? `Dictionary(${value.size})`;
  215. reference = mapper.get_or_create_vscode_id(
  216. new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),
  217. );
  218. } else if (value instanceof ObjectId) {
  219. if (value.id === undefined) {
  220. throw new Error("Invalid godot object: instanceof ObjectId but id is undefined");
  221. }
  222. // Godot returns only ID for the object.
  223. // In order to retrieve the class name, we need to request the object
  224. const godot_object = await this.get_godot_object(value.id);
  225. rendered_value = `${godot_object.type}${value.stringify_value()}`;
  226. // rendered_value = `${value.type_name()}${value.stringify_value()}`;
  227. reference = vscode_id;
  228. } else {
  229. try {
  230. rendered_value = `${value.type_name()}${value.stringify_value()}`;
  231. } catch (e) {
  232. rendered_value = `${value}`;
  233. }
  234. reference = mapper.get_or_create_vscode_id(
  235. new GodotIdWithPath(parent_godot_id, [...relative_path, va.name]),
  236. );
  237. // reference = vsode_id ? vsode_id : 0;
  238. }
  239. }
  240. const variable: DebugProtocol.Variable = {
  241. name: va.name,
  242. value: rendered_value,
  243. variablesReference: reference,
  244. };
  245. return variable;
  246. }
  247. public resolve_variable(godot_id: bigint, className: string, sub_values: GodotVariable[]) {
  248. const variable_promise = this.godot_object_promises.get(godot_id);
  249. if (variable_promise === undefined) {
  250. throw new Error(
  251. `Received 'inspect_object' for godot_id ${godot_id} but no variable promise to resolve found`,
  252. );
  253. }
  254. variable_promise.resolve({ godot_id: godot_id, type: className, sub_values: sub_values } as GodotObject);
  255. }
  256. }