diff --git a/.vscode/launch.json b/.vscode/launch.json index 65b81695a..25c121774 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,10 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ], + "skipFiles": [ + "**/extensionHostProcess.js", + "/**/*.js" + ], "preLaunchTask": "npm: watch", "env": { "VSCODE_DEBUG_MODE": true @@ -34,6 +38,10 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ], + "skipFiles": [ + "**/extensionHostProcess.js", + "/**/*.js" + ], "preLaunchTask": "npm: watch", "env": { "VSCODE_DEBUG_MODE": true diff --git a/biome.json b/biome.json index f1c363e6d..5d9f08706 100644 --- a/biome.json +++ b/biome.json @@ -15,7 +15,8 @@ "linter": { "rules": { "style": { - "noUselessElse": "off" + "noUselessElse": "off", + "useImportType": "off" } } } diff --git a/src/debugger/debug_runtime.ts b/src/debugger/debug_runtime.ts index 6a70aa385..7ac94fdd4 100644 --- a/src/debugger/debug_runtime.ts +++ b/src/debugger/debug_runtime.ts @@ -1,6 +1,7 @@ -import { SceneTreeProvider } from "./scene_tree_provider"; -import path = require("path"); +import * as path from "node:path"; + import { createLogger } from "../utils"; +import { SceneTreeProvider } from "./scene_tree_provider"; const log = createLogger("debugger.runtime"); @@ -24,9 +25,9 @@ export class GodotStackVars { public locals: GodotVariable[] = [], public members: GodotVariable[] = [], public globals: GodotVariable[] = [], - ) { } + ) {} - public reset(count: number = 0) { + public reset(count = 0) { this.locals = []; this.members = []; this.globals = []; @@ -62,7 +63,7 @@ export class RawObject extends Map { } export class ObjectId implements GDObject { - constructor(public id: bigint) { } + constructor(public id: bigint) {} public stringify_value(): string { return `<${this.id}>`; @@ -85,7 +86,7 @@ export class GodotDebugData { public last_frames: GodotStackFrame[] = []; public projectPath: string; public scene_tree?: SceneTreeProvider; - public stack_count: number = 0; + public stack_count = 0; public stack_files: string[] = []; public session; @@ -126,19 +127,16 @@ export class GodotDebugData { bps.splice(index, 1); this.breakpoints.set(pathTo, bps); const file = `res://${path.relative(this.projectPath, bp.file)}`; - this.session?.controller.remove_breakpoint( - file.replace(/\\/g, "/"), - bp.line, - ); + this.session?.controller.remove_breakpoint(file.replace(/\\/g, "/"), bp.line); } } } public get_all_breakpoints(): GodotBreakpoint[] { const output: GodotBreakpoint[] = []; - Array.from(this.breakpoints.values()).forEach((bp_array) => { + for (const bp_array of Array.from(this.breakpoints.values())) { output.push(...bp_array); - }); + } return output; } @@ -150,14 +148,14 @@ export class GodotDebugData { const breakpoints = this.get_all_breakpoints(); let output = ""; if (breakpoints.length > 0) { - output += " --breakpoints \""; + output += ' --breakpoints "'; breakpoints.forEach((bp, i) => { output += `${this.get_breakpoint_path(bp.file)}:${bp.line}`; if (i < breakpoints.length - 1) { output += ","; } }); - output += "\""; + output += '"'; } return output; } diff --git a/src/debugger/debugger.ts b/src/debugger/debugger.ts index 34ec7adae..9c779576e 100644 --- a/src/debugger/debugger.ts +++ b/src/debugger/debugger.ts @@ -309,13 +309,13 @@ export class GodotDebugger implements DebugAdapterDescriptorFactory, DebugConfig break; case "number": if (is_float) { - new_parsed_value = parseFloat(value); - if (isNaN(new_parsed_value)) { + new_parsed_value = Number.parseFloat(value); + if (Number.isNaN(new_parsed_value)) { return; } } else { - new_parsed_value = parseInt(value); - if (isNaN(new_parsed_value)) { + new_parsed_value = Number.parseInt(value); + if (Number.isNaN(new_parsed_value)) { return; } } diff --git a/src/debugger/godot3/debug_session.ts b/src/debugger/godot3/debug_session.ts index 620bc8e22..8a840988f 100644 --- a/src/debugger/godot3/debug_session.ts +++ b/src/debugger/godot3/debug_session.ts @@ -1,26 +1,33 @@ -import * as fs from "fs"; import { - LoggingDebugSession, + Breakpoint, InitializedEvent, - Thread, + LoggingDebugSession, Source, - Breakpoint, - StoppedEvent, TerminatedEvent, + Thread, } from "@vscode/debugadapter"; import { DebugProtocol } from "@vscode/debugprotocol"; -import { debug } from "vscode"; import { Subject } from "await-notify"; -import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime"; -import { LaunchRequestArguments, AttachRequestArguments } from "../debugger"; +import * as fs from "node:fs"; +import { debug } from "vscode"; + +import { createLogger } from "../../utils"; +import { GodotDebugData, GodotStackVars, GodotVariable } from "../debug_runtime"; +import { AttachRequestArguments, LaunchRequestArguments } from "../debugger"; import { SceneTreeProvider } from "../scene_tree_provider"; -import { ObjectId } from "./variables/variants"; -import { parse_variable, is_variable_built_in_type } from "./helpers"; +import { is_variable_built_in_type, parse_variable } from "./helpers"; import { ServerController } from "./server_controller"; -import { createLogger } from "../../utils"; +import { ObjectId } from "./variables/variants"; const log = createLogger("debugger.session", { output: "Godot Debugger" }); +interface Variable { + variable: GodotVariable; + index: number; + object_id: number; + error: string; +} + export class GodotDebugSession extends LoggingDebugSession { private all_scopes: GodotVariable[]; public controller = new ServerController(this); @@ -32,10 +39,7 @@ export class GodotDebugSession extends LoggingDebugSession { private previous_inspections: bigint[] = []; private configuration_done: Subject = new Subject(); private mode: "launch" | "attach" | "" = ""; - public inspect_callbacks: Map< - bigint, - (class_name: string, variable: GodotVariable) => void - > = new Map(); + public inspect_callbacks: Map void> = new Map(); public constructor() { super(); @@ -50,7 +54,7 @@ export class GodotDebugSession extends LoggingDebugSession { protected initializeRequest( response: DebugProtocol.InitializeResponse, - args: DebugProtocol.InitializeRequestArguments + args: DebugProtocol.InitializeRequestArguments, ) { response.body = response.body || {}; @@ -79,10 +83,7 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendEvent(new InitializedEvent()); } - protected async launchRequest( - response: DebugProtocol.LaunchResponse, - args: LaunchRequestArguments - ) { + protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) { await this.configuration_done.wait(1000); this.mode = "launch"; @@ -94,10 +95,7 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendResponse(response); } - protected async attachRequest( - response: DebugProtocol.AttachResponse, - args: AttachRequestArguments - ) { + protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { await this.configuration_done.wait(1000); this.mode = "attach"; @@ -110,16 +108,13 @@ export class GodotDebugSession extends LoggingDebugSession { public configurationDoneRequest( response: DebugProtocol.ConfigurationDoneResponse, - args: DebugProtocol.ConfigurationDoneArguments + args: DebugProtocol.ConfigurationDoneArguments, ) { this.configuration_done.notify(); this.sendResponse(response); } - protected continueRequest( - response: DebugProtocol.ContinueResponse, - args: DebugProtocol.ContinueArguments - ) { + protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) { if (!this.exception) { response.body = { allThreadsContinued: true }; this.controller.continue(); @@ -127,20 +122,17 @@ export class GodotDebugSession extends LoggingDebugSession { } } - protected async evaluateRequest( - response: DebugProtocol.EvaluateResponse, - args: DebugProtocol.EvaluateArguments - ) { + protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) { await debug.activeDebugSession.customRequest("scopes", { frameId: 0 }); if (this.all_scopes) { - var variable = this.get_variable(args.expression, null, null, null); + const variable = this.get_variable(args.expression, null, null, null); if (variable.error == null) { - var parsed_variable = parse_variable(variable.variable); + const parsed_variable = parse_variable(variable.variable); response.body = { result: parsed_variable.value, - variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0 + variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0, }; } else { response.success = false; @@ -158,30 +150,21 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendResponse(response); } - protected nextRequest( - response: DebugProtocol.NextResponse, - args: DebugProtocol.NextArguments - ) { + protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) { if (!this.exception) { this.controller.next(); this.sendResponse(response); } } - protected pauseRequest( - response: DebugProtocol.PauseResponse, - args: DebugProtocol.PauseArguments - ) { + protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) { if (!this.exception) { this.controller.break(); this.sendResponse(response); } } - protected async scopesRequest( - response: DebugProtocol.ScopesResponse, - args: DebugProtocol.ScopesArguments - ) { + protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) { this.controller.request_stack_frame_vars(args.frameId); await this.got_scope.wait(2000); @@ -197,7 +180,7 @@ export class GodotDebugSession extends LoggingDebugSession { protected setBreakPointsRequest( response: DebugProtocol.SetBreakpointsResponse, - args: DebugProtocol.SetBreakpointsArguments + args: DebugProtocol.SetBreakpointsArguments, ) { const path = (args.source.path as string).replace(/\\/g, "/"); const client_lines = args.lines || []; @@ -206,19 +189,19 @@ export class GodotDebugSession extends LoggingDebugSession { let bps = this.debug_data.get_breakpoints(path); const bp_lines = bps.map((bp) => bp.line); - bps.forEach((bp) => { + for (const bp of bps) { if (client_lines.indexOf(bp.line) === -1) { this.debug_data.remove_breakpoint(path, bp.line); } - }); - client_lines.forEach((l) => { + } + for (const l of client_lines) { if (bp_lines.indexOf(l) === -1) { - const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l)); + const bp = args.breakpoints.find((bp_at_line) => bp_at_line.line === l); if (!bp.condition) { this.debug_data.set_breakpoint(path, l); } } - }); + } bps = this.debug_data.get_breakpoints(path); // Sort to ensure breakpoints aren't out-of-order, which would confuse VS Code. @@ -226,12 +209,7 @@ export class GodotDebugSession extends LoggingDebugSession { response.body = { breakpoints: bps.map((bp) => { - return new Breakpoint( - true, - bp.line, - 1, - new Source(bp.file.split("/").reverse()[0], bp.file) - ); + return new Breakpoint(true, bp.line, 1, new Source(bp.file.split("/").reverse()[0], bp.file)); }), }; @@ -239,10 +217,7 @@ export class GodotDebugSession extends LoggingDebugSession { } } - protected stackTraceRequest( - response: DebugProtocol.StackTraceResponse, - args: DebugProtocol.StackTraceArguments - ) { + protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) { if (this.debug_data.last_frame) { response.body = { totalFrames: this.debug_data.last_frames.length, @@ -252,10 +227,7 @@ export class GodotDebugSession extends LoggingDebugSession { name: sf.function, line: sf.line, column: 1, - source: new Source( - sf.file, - `${this.debug_data.projectPath}/${sf.file.replace("res://", "")}` - ), + source: new Source(sf.file, `${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`), }; }), }; @@ -263,30 +235,21 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendResponse(response); } - protected stepInRequest( - response: DebugProtocol.StepInResponse, - args: DebugProtocol.StepInArguments - ) { + protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) { if (!this.exception) { this.controller.step(); this.sendResponse(response); } } - protected stepOutRequest( - response: DebugProtocol.StepOutResponse, - args: DebugProtocol.StepOutArguments - ) { + protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) { if (!this.exception) { this.controller.step_out(); this.sendResponse(response); } } - protected terminateRequest( - response: DebugProtocol.TerminateResponse, - args: DebugProtocol.TerminateArguments - ) { + protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) { if (this.mode === "launch") { this.controller.stop(); this.sendEvent(new TerminatedEvent()); @@ -301,11 +264,11 @@ export class GodotDebugSession extends LoggingDebugSession { protected async variablesRequest( response: DebugProtocol.VariablesResponse, - args: DebugProtocol.VariablesArguments + args: DebugProtocol.VariablesArguments, ) { if (!this.all_scopes) { response.body = { - variables: [] + variables: [], }; this.sendResponse(response); return; @@ -319,8 +282,7 @@ export class GodotDebugSession extends LoggingDebugSession { } else { variables = reference.sub_values.map((va) => { const sva = this.all_scopes.find( - (sva) => - sva && sva.scope_path === va.scope_path && sva.name === va.name + (sva) => sva && sva.scope_path === va.scope_path && sva.name === va.name, ); if (sva) { return parse_variable( @@ -329,8 +291,8 @@ export class GodotDebugSession extends LoggingDebugSession { (va_idx) => va_idx && va_idx.scope_path === `${reference.scope_path}.${reference.name}` && - va_idx.name === va.name - ) + va_idx.name === va.name, + ), ); } }); @@ -354,7 +316,7 @@ export class GodotDebugSession extends LoggingDebugSession { name: "local", value: undefined, sub_values: stackVars.locals, - scope_path: "@" + scope_path: "@", }, { name: "member", @@ -370,20 +332,20 @@ export class GodotDebugSession extends LoggingDebugSession { }, ]; - stackVars.locals.forEach((va) => { + for (const va of stackVars.locals) { va.scope_path = "@.local"; this.append_variable(va); - }); + } - stackVars.members.forEach((va) => { + for (const va of stackVars.members) { va.scope_path = "@.member"; this.append_variable(va); - }); + } - stackVars.globals.forEach((va) => { + for (const va of stackVars.globals) { va.scope_path = "@.global"; this.append_variable(va); - }); + } this.add_to_inspections(); @@ -394,21 +356,19 @@ export class GodotDebugSession extends LoggingDebugSession { } public set_inspection(id: bigint, replacement: GodotVariable) { - const variables = this.all_scopes.filter( - (va) => va && va.value instanceof ObjectId && va.value.id === id - ); + const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id); - variables.forEach((va) => { + for (const va of variables) { const index = this.all_scopes.findIndex((va_id) => va_id === va); const old = this.all_scopes.splice(index, 1); replacement.name = old[0].name; replacement.scope_path = old[0].scope_path; this.append_variable(replacement, index); - }); + } this.ongoing_inspections.splice( this.ongoing_inspections.findIndex((va_id) => va_id === id), - 1 + 1, ); this.previous_inspections.push(id); @@ -422,7 +382,7 @@ export class GodotDebugSession extends LoggingDebugSession { } private add_to_inspections() { - this.all_scopes.forEach((va) => { + for (const va of this.all_scopes) { if (va && va.value instanceof ObjectId) { if ( !this.ongoing_inspections.includes(va.value.id) && @@ -432,38 +392,58 @@ export class GodotDebugSession extends LoggingDebugSession { this.ongoing_inspections.push(va.value.id); } } - }); + } } - protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } { - var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: null }; + protected get_variable( + expression: string, + root: GodotVariable = null, + index = 0, + object_id: number = null, + ): Variable { + let result: Variable = { + variable: null, + index: null, + object_id: null, + error: null, + }; if (!root) { if (!expression.includes("self")) { expression = "self." + expression; } - root = this.all_scopes.find(x => x && x.name == "self"); - object_id = this.all_scopes.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value; + root = this.all_scopes.find((x) => x && x.name === "self"); + object_id = this.all_scopes.find((x) => x && x.name === "id" && x.scope_path === "@.member.self").value; } - var items = expression.split("."); - var propertyName = items[index + 1]; - var path = items.slice(0, index + 1).join(".") - .split("self.").join("") - .split("self").join("") - .split("[").join(".") - .split("]").join(""); - - if (items.length == 1 && items[0] == "self") { + const items = expression.split("."); + let propertyName = items[index + 1]; + let path = items + .slice(0, index + 1) + .join(".") + .split("self.") + .join("") + .split("self") + .join("") + .split("[") + .join(".") + .split("]") + .join(""); + + if (items.length === 1 && items[0] === "self") { propertyName = "self"; } // Detect index/key - var key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0]; + let key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0]; if (key) { key = key.replace(/['"]+/g, ""); - propertyName = propertyName.split(/(?<=\[).*(?=\])/).join("").split("\[\]").join(""); + propertyName = propertyName + .split(/(?<=\[).*(?=\])/) + .join("") + .split("[]") + .join(""); if (path) path += "."; path += propertyName; propertyName = key; @@ -474,49 +454,61 @@ export class GodotDebugSession extends LoggingDebugSession { } function sanitizeScopePath(scope_path: string) { - return scope_path.split("@.member.self.").join("") - .split("@.member.self").join("") - .split("@.member.").join("") - .split("@.member").join("") - .split("@.local.").join("") - .split("@.local").join("") - .split("Locals/").join("") - .split("Members/").join("") - .split("@").join(""); + return scope_path + .split("@.member.self.") + .join("") + .split("@.member.self") + .join("") + .split("@.member.") + .join("") + .split("@.member") + .join("") + .split("@.local.") + .join("") + .split("@.local") + .join("") + .split("Locals/") + .join("") + .split("Members/") + .join("") + .split("@") + .join(""); } - var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) { - return { + const sanitized_all_scopes = this.all_scopes + .filter((x) => x) + .map((x) => ({ sanitized: { name: sanitizeName(x.name), - scope_path: sanitizeScopePath(x.scope_path) + scope_path: sanitizeScopePath(x.scope_path), }, - real: x - }; - }); + real: x, + })); - result.variable = sanitized_all_scopes - .find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path) - ?.real; + result.variable = sanitized_all_scopes.find( + (x) => x.sanitized.name === propertyName && x.sanitized.scope_path === path, + )?.real; if (!result.variable) { result.error = `Could not find: ${propertyName}`; return result; } if (root.value.entries) { - if (result.variable.name == "self") { - result.object_id = this.all_scopes - .find(x => x && x.name == "id" && x.scope_path == "@.member.self").value; + if (result.variable.name === "self") { + result.object_id = this.all_scopes.find( + (x) => x && x.name === "id" && x.scope_path === "@.member.self", + ).value; } else if (key) { - var collection = path.split(".")[path.split(".").length - 1]; - var collection_items = Array.from(root.value.entries()) - .find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == collection)[1]; - result.object_id = collection_items.get - ? collection_items.get(key)?.id - : collection_items[key]?.id; + const collection = path.split(".")[path.split(".").length - 1]; + const collection_items = Array.from(root.value.entries()).find( + (x) => x && x[0].split("Members/").join("").split("Locals/").join("") === collection, + )[1]; + result.object_id = collection_items.get ? collection_items.get(key)?.id : collection_items[key]?.id; } else { - result.object_id = Array.from(root.value.entries()) - .find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName)[1].id; + const item = Array.from(root.value.entries()).find( + (x) => x && x[0].split("Members/").join("").split("Locals/").join("") === propertyName, + ); + result.object_id = item?.[1].id; } } @@ -524,7 +516,9 @@ export class GodotDebugSession extends LoggingDebugSession { result.object_id = object_id; } - result.index = this.all_scopes.findIndex(x => x && x.name == result.variable.name && x.scope_path == result.variable.scope_path); + result.index = this.all_scopes.findIndex( + (x) => x && x.name === result.variable.name && x.scope_path === result.variable.scope_path, + ); if (items.length > 2 && index < items.length - 2) { result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id); diff --git a/src/debugger/godot3/server_controller.ts b/src/debugger/godot3/server_controller.ts index 4dda105ef..eb6bd4b91 100644 --- a/src/debugger/godot3/server_controller.ts +++ b/src/debugger/godot3/server_controller.ts @@ -1,17 +1,28 @@ -import * as fs from "fs"; -import net = require("net"); -import { debug, window } from "vscode"; import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter"; -import { VariantEncoder } from "./variables/variant_encoder"; -import { VariantDecoder } from "./variables/variant_decoder"; -import { RawObject } from "./variables/variants"; +import { DebugProtocol } from "@vscode/debugprotocol"; +import * as fs from "node:fs"; +import * as net from "node:net"; +import { debug, window } from "vscode"; + +import { + ansi, + convert_resource_path_to_uri, + createLogger, + get_configuration, + get_free_port, + get_project_version, + verify_godot_version, + VERIFY_RESULT, +} from "../../utils"; +import { prompt_for_godot_executable } from "../../utils/prompts"; +import { killSubProcesses, subProcess } from "../../utils/subspawn"; import { GodotStackFrame, GodotStackVars } from "../debug_runtime"; +import { AttachRequestArguments, LaunchRequestArguments, pinnedScene } from "../debugger"; import { GodotDebugSession } from "./debug_session"; -import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers"; -import { get_configuration, get_free_port, createLogger, verify_godot_version, get_project_version } from "../../utils"; -import { prompt_for_godot_executable } from "../../utils/prompts"; -import { subProcess, killSubProcesses } from "../../utils/subspawn"; -import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger"; +import { build_sub_values, parse_next_scene_node, split_buffers } from "./helpers"; +import { VariantDecoder } from "./variables/variant_decoder"; +import { VariantEncoder } from "./variables/variant_encoder"; +import { RawObject } from "./variables/variants"; const log = createLogger("debugger.controller", { output: "Godot Debugger" }); const socketLog = createLogger("debugger.socket"); @@ -37,9 +48,7 @@ export class ServerController { private didFirstOutput: boolean = false; private connectedVersion = ""; - public constructor( - public session: GodotDebugSession - ) { } + public constructor(public session: GodotDebugSession) {} public break() { this.send_command("break"); @@ -87,12 +96,8 @@ export class ServerController { this.send_command("get_stack_frame_vars", [frame_id]); } - public set_object_property(objectId: bigint, label: string, newParsedValue: any) { - this.send_command("set_object_property", [ - objectId, - label, - newParsedValue, - ]); + public set_object_property(objectId: bigint, label: string, newParsedValue) { + this.send_command("set_object_property", [objectId, label, newParsedValue]); } public set_exception(exception: string) { @@ -103,7 +108,7 @@ export class ServerController { log.info("Starting game process"); let godotPath: string; - let result; + let result: VERIFY_RESULT; if (args.editor_path) { log.info("Using 'editor_path' variable from launch.json"); @@ -168,12 +173,12 @@ export class ServerController { const address = args.address.replace("tcp://", ""); command += ` --remote-debug "${address}:${args.port}"`; - if (args.profiling) { command += " --profiling"; } - if (args.debug_collisions) { command += " --debug-collisions"; } - if (args.debug_paths) { command += " --debug-paths"; } - if (args.frame_delay) { command += ` --frame-delay ${args.frame_delay}`; } - if (args.time_scale) { command += ` --time-scale ${args.time_scale}`; } - if (args.fixed_fps) { command += ` --fixed-fps ${args.fixed_fps}`; } + if (args.profiling) command += " --profiling"; + if (args.debug_collisions) command += " --debug-collisions"; + if (args.debug_paths) command += " --debug-paths"; + if (args.frame_delay) command += ` --frame-delay ${args.frame_delay}`; + if (args.time_scale) command += ` --time-scale ${args.time_scale}`; + if (args.fixed_fps) command += ` --fixed-fps ${args.fixed_fps}`; if (args.scene && args.scene !== "main") { log.info(`Custom scene argument provided: ${args.scene}`); @@ -219,15 +224,15 @@ export class ServerController { command += this.session.debug_data.get_breakpoint_string(); if (args.additional_options) { - command += " " + args.additional_options; + command += ` ${args.additional_options}`; } log.info(`Launching game process using command: '${command}'`); const debugProcess = subProcess("debug", command, { shell: true, detached: true }); - debugProcess.stdout.on("data", (data) => { }); - debugProcess.stderr.on("data", (data) => { }); - debugProcess.on("close", (code) => { }); + debugProcess.stdout.on("data", (data) => {}); + debugProcess.stderr.on("data", (data) => {}); + debugProcess.on("close", (code) => {}); } private stash: Buffer; @@ -351,7 +356,7 @@ export class ServerController { } } - private handle_command(command: Command) { + private async handle_command(command: Command) { switch (command.command) { case "debug_enter": { const reason: string = command.parameters[1]; @@ -379,7 +384,7 @@ export class ServerController { case "message:inspect_object": { let id = BigInt(command.parameters[0]); const className: string = command.parameters[1]; - const properties: any[] = command.parameters[2]; + const properties: string[] = command.parameters[2]; // message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer, // thus we need to convert it to its equivalent unsigned value here. @@ -388,16 +393,13 @@ export class ServerController { } const rawObject = new RawObject(className); - properties.forEach((prop) => { + for (const prop of properties) { rawObject.set(prop[0], prop[5]); - }); + } const inspectedVariable = { name: "", value: rawObject }; build_sub_values(inspectedVariable); if (this.session.inspect_callbacks.has(BigInt(id))) { - this.session.inspect_callbacks.get(BigInt(id))( - inspectedVariable.name, - inspectedVariable - ); + this.session.inspect_callbacks.get(BigInt(id))(inspectedVariable.name, inspectedVariable); this.session.inspect_callbacks.delete(BigInt(id)); } this.session.set_inspection(id, inspectedVariable); @@ -425,15 +427,100 @@ export class ServerController { this.didFirstOutput = true; // this.request_scene_tree(); } - - command.parameters.forEach((line) => { - debug.activeDebugConsole.appendLine(line[0]); - }); + const lines = command.parameters; + for (const line of lines) { + debug.activeDebugConsole.appendLine(ansi.bright.blue + line[0]); + } + break; + } + case "error": { + if (!this.didFirstOutput) { + this.didFirstOutput = true; + } + this.handle_error(command); break; } } } + async handle_error(command: Command) { + const params = command.parameters[0]; + const e = { + hr: params[0], + min: params[1], + sec: params[2], + msec: params[3], + func: params[4] as string, + file: params[5] as string, + line: params[6], + cond: params[7] as string, + msg: params[8] as string, + warning: params[9] as boolean, + stack: [], + }; + const stackCount = command.parameters[1]; + for (let i = 0; i < stackCount; i += 3) { + const file = command.parameters[i + 2]; + const func = command.parameters[i + 3]; + const line = command.parameters[i + 4]; + const msg = `${file}:${line} @ ${func}()`; + const extras = { + source: { name: (await convert_resource_path_to_uri(file)).toString() }, + line: line, + }; + e.stack.push({ msg: msg, extras: extras }); + } + + const time = `${e.hr}:${e.min}:${e.sec}.${e.msec}`; + const location = `${e.file}:${e.line} @ ${e.func}()`; + const color = e.warning ? "yellow" : "red"; + const lang = e.file.startsWith("res://") ? "GDScript" : "C++"; + + const extras = { + source: { name: (await convert_resource_path_to_uri(e.file)).toString() }, + line: e.line, + group: "startCollapsed", + }; + if (e.msg) { + this.stderr(`${ansi[color]}${time} | ${e.func}: ${e.msg}`, extras); + this.stderr(`${ansi.dim.white}<${lang} Error> ${ansi.white}${e.cond}`); + } else { + this.stderr(`${ansi[color]}${time} | ${e.func}: ${e.cond}`, extras); + } + this.stderr(`${ansi.dim.white}<${lang} Source> ${ansi.white}${location}`); + + if (stackCount !== 0) { + this.stderr(`${ansi.dim.white}`, { group: "start" }); + for (const frame of e.stack) { + this.stderr(`${ansi.white}${frame.msg}`, frame.extras); + } + this.stderr("", { group: "end" }); + } + this.stderr("", { group: "end" }); + } + + stdout(output = "", extra = {}) { + this.session.sendEvent({ + event: "output", + body: { + category: "stdout", + output: output + ansi.reset, + ...extra, + }, + } as DebugProtocol.OutputEvent); + } + + stderr(output = "", extra = {}) { + this.session.sendEvent({ + event: "output", + body: { + category: "stderr", + output: output + ansi.reset, + ...extra, + }, + } as DebugProtocol.OutputEvent); + } + public abort() { log.info("Aborting debug controller"); this.session.sendEvent(new TerminatedEvent()); @@ -468,19 +555,14 @@ export class ServerController { const line = stackFrames[0].line; if (this.steppingOut) { - const breakpoint = this.session.debug_data - .get_breakpoints(file) - .find((bp) => bp.line === line); + const breakpoint = this.session.debug_data.get_breakpoints(file).find((bp) => bp.line === line); if (!breakpoint) { if (this.session.debug_data.stack_count > 1) { continueStepping = this.session.debug_data.stack_count === stackCount; } else { - const fileSame = - stackFrames[0].file === this.session.debug_data.last_frame.file; - const funcSame = - stackFrames[0].function === this.session.debug_data.last_frame.function; - const lineGreater = - stackFrames[0].line >= this.session.debug_data.last_frame.line; + const fileSame = stackFrames[0].file === this.session.debug_data.last_frame.file; + const funcSame = stackFrames[0].function === this.session.debug_data.last_frame.function; + const lineGreater = stackFrames[0].line >= this.session.debug_data.last_frame.line; continueStepping = fileSame && funcSame && lineGreater; } @@ -506,9 +588,7 @@ export class ServerController { this.session.sendEvent(new StoppedEvent("breakpoint", 0)); } else { this.session.set_exception(true); - this.session.sendEvent( - new StoppedEvent("exception", 0, this.exception) - ); + this.session.sendEvent(new StoppedEvent("exception", 0, this.exception)); } } @@ -535,8 +615,8 @@ export class ServerController { const stackVars = new GodotStackVars(); let localsRemaining = parameters[0]; - let membersRemaining = parameters[1 + (localsRemaining * 2)]; - let globalsRemaining = parameters[2 + ((localsRemaining + membersRemaining) * 2)]; + let membersRemaining = parameters[1 + localsRemaining * 2]; + let globalsRemaining = parameters[2 + (localsRemaining + membersRemaining) * 2]; let i = 1; while (localsRemaining--) { @@ -551,7 +631,7 @@ export class ServerController { stackVars.globals.push({ name: parameters[i++], value: parameters[i++] }); } - stackVars.forEach(item => build_sub_values(item)); + stackVars.forEach((item) => build_sub_values(item)); this.session.set_scopes(stackVars); } diff --git a/src/debugger/godot4/debug_session.ts b/src/debugger/godot4/debug_session.ts index f36159d4c..8a840988f 100644 --- a/src/debugger/godot4/debug_session.ts +++ b/src/debugger/godot4/debug_session.ts @@ -1,26 +1,33 @@ -import * as fs from "fs"; import { - LoggingDebugSession, + Breakpoint, InitializedEvent, - Thread, + LoggingDebugSession, Source, - Breakpoint, - StoppedEvent, TerminatedEvent, + Thread, } from "@vscode/debugadapter"; import { DebugProtocol } from "@vscode/debugprotocol"; -import { debug } from "vscode"; import { Subject } from "await-notify"; -import { GodotDebugData, GodotVariable, GodotStackVars } from "../debug_runtime"; -import { LaunchRequestArguments, AttachRequestArguments } from "../debugger"; +import * as fs from "node:fs"; +import { debug } from "vscode"; + +import { createLogger } from "../../utils"; +import { GodotDebugData, GodotStackVars, GodotVariable } from "../debug_runtime"; +import { AttachRequestArguments, LaunchRequestArguments } from "../debugger"; import { SceneTreeProvider } from "../scene_tree_provider"; -import { ObjectId } from "./variables/variants"; -import { parse_variable, is_variable_built_in_type } from "./helpers"; +import { is_variable_built_in_type, parse_variable } from "./helpers"; import { ServerController } from "./server_controller"; -import { createLogger } from "../../utils"; +import { ObjectId } from "./variables/variants"; const log = createLogger("debugger.session", { output: "Godot Debugger" }); +interface Variable { + variable: GodotVariable; + index: number; + object_id: number; + error: string; +} + export class GodotDebugSession extends LoggingDebugSession { private all_scopes: GodotVariable[]; public controller = new ServerController(this); @@ -32,10 +39,7 @@ export class GodotDebugSession extends LoggingDebugSession { private previous_inspections: bigint[] = []; private configuration_done: Subject = new Subject(); private mode: "launch" | "attach" | "" = ""; - public inspect_callbacks: Map< - bigint, - (class_name: string, variable: GodotVariable) => void - > = new Map(); + public inspect_callbacks: Map void> = new Map(); public constructor() { super(); @@ -50,7 +54,7 @@ export class GodotDebugSession extends LoggingDebugSession { protected initializeRequest( response: DebugProtocol.InitializeResponse, - args: DebugProtocol.InitializeRequestArguments + args: DebugProtocol.InitializeRequestArguments, ) { response.body = response.body || {}; @@ -79,10 +83,7 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendEvent(new InitializedEvent()); } - protected async launchRequest( - response: DebugProtocol.LaunchResponse, - args: LaunchRequestArguments - ) { + protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) { await this.configuration_done.wait(1000); this.mode = "launch"; @@ -94,10 +95,7 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendResponse(response); } - protected async attachRequest( - response: DebugProtocol.AttachResponse, - args: AttachRequestArguments - ) { + protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { await this.configuration_done.wait(1000); this.mode = "attach"; @@ -110,16 +108,13 @@ export class GodotDebugSession extends LoggingDebugSession { public configurationDoneRequest( response: DebugProtocol.ConfigurationDoneResponse, - args: DebugProtocol.ConfigurationDoneArguments + args: DebugProtocol.ConfigurationDoneArguments, ) { this.configuration_done.notify(); this.sendResponse(response); } - protected continueRequest( - response: DebugProtocol.ContinueResponse, - args: DebugProtocol.ContinueArguments - ) { + protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) { if (!this.exception) { response.body = { allThreadsContinued: true }; this.controller.continue(); @@ -127,20 +122,17 @@ export class GodotDebugSession extends LoggingDebugSession { } } - protected async evaluateRequest( - response: DebugProtocol.EvaluateResponse, - args: DebugProtocol.EvaluateArguments - ) { + protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) { await debug.activeDebugSession.customRequest("scopes", { frameId: 0 }); if (this.all_scopes) { - var variable = this.get_variable(args.expression, null, null, null); + const variable = this.get_variable(args.expression, null, null, null); if (variable.error == null) { - var parsed_variable = parse_variable(variable.variable); + const parsed_variable = parse_variable(variable.variable); response.body = { result: parsed_variable.value, - variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0 + variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0, }; } else { response.success = false; @@ -158,30 +150,21 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendResponse(response); } - protected nextRequest( - response: DebugProtocol.NextResponse, - args: DebugProtocol.NextArguments - ) { + protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) { if (!this.exception) { this.controller.next(); this.sendResponse(response); } } - protected pauseRequest( - response: DebugProtocol.PauseResponse, - args: DebugProtocol.PauseArguments - ) { + protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) { if (!this.exception) { this.controller.break(); this.sendResponse(response); } } - protected async scopesRequest( - response: DebugProtocol.ScopesResponse, - args: DebugProtocol.ScopesArguments - ) { + protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) { this.controller.request_stack_frame_vars(args.frameId); await this.got_scope.wait(2000); @@ -197,7 +180,7 @@ export class GodotDebugSession extends LoggingDebugSession { protected setBreakPointsRequest( response: DebugProtocol.SetBreakpointsResponse, - args: DebugProtocol.SetBreakpointsArguments + args: DebugProtocol.SetBreakpointsArguments, ) { const path = (args.source.path as string).replace(/\\/g, "/"); const client_lines = args.lines || []; @@ -206,19 +189,19 @@ export class GodotDebugSession extends LoggingDebugSession { let bps = this.debug_data.get_breakpoints(path); const bp_lines = bps.map((bp) => bp.line); - bps.forEach((bp) => { + for (const bp of bps) { if (client_lines.indexOf(bp.line) === -1) { this.debug_data.remove_breakpoint(path, bp.line); } - }); - client_lines.forEach((l) => { + } + for (const l of client_lines) { if (bp_lines.indexOf(l) === -1) { - const bp = args.breakpoints.find((bp_at_line) => (bp_at_line.line == l)); + const bp = args.breakpoints.find((bp_at_line) => bp_at_line.line === l); if (!bp.condition) { this.debug_data.set_breakpoint(path, l); } } - }); + } bps = this.debug_data.get_breakpoints(path); // Sort to ensure breakpoints aren't out-of-order, which would confuse VS Code. @@ -226,12 +209,7 @@ export class GodotDebugSession extends LoggingDebugSession { response.body = { breakpoints: bps.map((bp) => { - return new Breakpoint( - true, - bp.line, - 1, - new Source(bp.file.split("/").reverse()[0], bp.file) - ); + return new Breakpoint(true, bp.line, 1, new Source(bp.file.split("/").reverse()[0], bp.file)); }), }; @@ -239,10 +217,7 @@ export class GodotDebugSession extends LoggingDebugSession { } } - protected stackTraceRequest( - response: DebugProtocol.StackTraceResponse, - args: DebugProtocol.StackTraceArguments - ) { + protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) { if (this.debug_data.last_frame) { response.body = { totalFrames: this.debug_data.last_frames.length, @@ -252,10 +227,7 @@ export class GodotDebugSession extends LoggingDebugSession { name: sf.function, line: sf.line, column: 1, - source: new Source( - sf.file, - `${this.debug_data.projectPath}/${sf.file.replace("res://", "")}` - ), + source: new Source(sf.file, `${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`), }; }), }; @@ -263,30 +235,21 @@ export class GodotDebugSession extends LoggingDebugSession { this.sendResponse(response); } - protected stepInRequest( - response: DebugProtocol.StepInResponse, - args: DebugProtocol.StepInArguments - ) { + protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) { if (!this.exception) { this.controller.step(); this.sendResponse(response); } } - protected stepOutRequest( - response: DebugProtocol.StepOutResponse, - args: DebugProtocol.StepOutArguments - ) { + protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) { if (!this.exception) { this.controller.step_out(); this.sendResponse(response); } } - protected terminateRequest( - response: DebugProtocol.TerminateResponse, - args: DebugProtocol.TerminateArguments - ) { + protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) { if (this.mode === "launch") { this.controller.stop(); this.sendEvent(new TerminatedEvent()); @@ -301,11 +264,11 @@ export class GodotDebugSession extends LoggingDebugSession { protected async variablesRequest( response: DebugProtocol.VariablesResponse, - args: DebugProtocol.VariablesArguments + args: DebugProtocol.VariablesArguments, ) { if (!this.all_scopes) { response.body = { - variables: [] + variables: [], }; this.sendResponse(response); return; @@ -319,8 +282,7 @@ export class GodotDebugSession extends LoggingDebugSession { } else { variables = reference.sub_values.map((va) => { const sva = this.all_scopes.find( - (sva) => - sva && sva.scope_path === va.scope_path && sva.name === va.name + (sva) => sva && sva.scope_path === va.scope_path && sva.name === va.name, ); if (sva) { return parse_variable( @@ -329,8 +291,8 @@ export class GodotDebugSession extends LoggingDebugSession { (va_idx) => va_idx && va_idx.scope_path === `${reference.scope_path}.${reference.name}` && - va_idx.name === va.name - ) + va_idx.name === va.name, + ), ); } }); @@ -354,7 +316,7 @@ export class GodotDebugSession extends LoggingDebugSession { name: "local", value: undefined, sub_values: stackVars.locals, - scope_path: "@" + scope_path: "@", }, { name: "member", @@ -370,20 +332,20 @@ export class GodotDebugSession extends LoggingDebugSession { }, ]; - stackVars.locals.forEach((va) => { + for (const va of stackVars.locals) { va.scope_path = "@.local"; this.append_variable(va); - }); + } - stackVars.members.forEach((va) => { + for (const va of stackVars.members) { va.scope_path = "@.member"; this.append_variable(va); - }); + } - stackVars.globals.forEach((va) => { + for (const va of stackVars.globals) { va.scope_path = "@.global"; this.append_variable(va); - }); + } this.add_to_inspections(); @@ -394,21 +356,19 @@ export class GodotDebugSession extends LoggingDebugSession { } public set_inspection(id: bigint, replacement: GodotVariable) { - const variables = this.all_scopes.filter( - (va) => va && va.value instanceof ObjectId && va.value.id === id - ); + const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id); - variables.forEach((va) => { + for (const va of variables) { const index = this.all_scopes.findIndex((va_id) => va_id === va); const old = this.all_scopes.splice(index, 1); replacement.name = old[0].name; replacement.scope_path = old[0].scope_path; this.append_variable(replacement, index); - }); + } this.ongoing_inspections.splice( this.ongoing_inspections.findIndex((va_id) => va_id === id), - 1 + 1, ); this.previous_inspections.push(id); @@ -422,7 +382,7 @@ export class GodotDebugSession extends LoggingDebugSession { } private add_to_inspections() { - this.all_scopes.forEach((va) => { + for (const va of this.all_scopes) { if (va && va.value instanceof ObjectId) { if ( !this.ongoing_inspections.includes(va.value.id) && @@ -432,38 +392,58 @@ export class GodotDebugSession extends LoggingDebugSession { this.ongoing_inspections.push(va.value.id); } } - }); + } } - protected get_variable(expression: string, root: GodotVariable = null, index: number = 0, object_id: number = null): { variable: GodotVariable, index: number, object_id: number, error: string } { - var result: { variable: GodotVariable, index: number, object_id: number, error: string } = { variable: null, index: null, object_id: null, error: null }; + protected get_variable( + expression: string, + root: GodotVariable = null, + index = 0, + object_id: number = null, + ): Variable { + let result: Variable = { + variable: null, + index: null, + object_id: null, + error: null, + }; if (!root) { if (!expression.includes("self")) { expression = "self." + expression; } - root = this.all_scopes.find(x => x && x.name == "self"); - object_id = this.all_scopes.find(x => x && x.name == "id" && x.scope_path == "@.member.self").value; + root = this.all_scopes.find((x) => x && x.name === "self"); + object_id = this.all_scopes.find((x) => x && x.name === "id" && x.scope_path === "@.member.self").value; } - var items = expression.split("."); - var propertyName = items[index + 1]; - var path = items.slice(0, index + 1).join(".") - .split("self.").join("") - .split("self").join("") - .split("[").join(".") - .split("]").join(""); - - if (items.length == 1 && items[0] == "self") { + const items = expression.split("."); + let propertyName = items[index + 1]; + let path = items + .slice(0, index + 1) + .join(".") + .split("self.") + .join("") + .split("self") + .join("") + .split("[") + .join(".") + .split("]") + .join(""); + + if (items.length === 1 && items[0] === "self") { propertyName = "self"; } // Detect index/key - var key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0]; + let key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0]; if (key) { key = key.replace(/['"]+/g, ""); - propertyName = propertyName.split(/(?<=\[).*(?=\])/).join("").split("\[\]").join(""); + propertyName = propertyName + .split(/(?<=\[).*(?=\])/) + .join("") + .split("[]") + .join(""); if (path) path += "."; path += propertyName; propertyName = key; @@ -474,49 +454,60 @@ export class GodotDebugSession extends LoggingDebugSession { } function sanitizeScopePath(scope_path: string) { - return scope_path.split("@.member.self.").join("") - .split("@.member.self").join("") - .split("@.member.").join("") - .split("@.member").join("") - .split("@.local.").join("") - .split("@.local").join("") - .split("Locals/").join("") - .split("Members/").join("") - .split("@").join(""); + return scope_path + .split("@.member.self.") + .join("") + .split("@.member.self") + .join("") + .split("@.member.") + .join("") + .split("@.member") + .join("") + .split("@.local.") + .join("") + .split("@.local") + .join("") + .split("Locals/") + .join("") + .split("Members/") + .join("") + .split("@") + .join(""); } - var sanitized_all_scopes = this.all_scopes.filter(x => x).map(function (x) { - return { + const sanitized_all_scopes = this.all_scopes + .filter((x) => x) + .map((x) => ({ sanitized: { name: sanitizeName(x.name), - scope_path: sanitizeScopePath(x.scope_path) + scope_path: sanitizeScopePath(x.scope_path), }, - real: x - }; - }); + real: x, + })); - result.variable = sanitized_all_scopes - .find(x => x.sanitized.name == propertyName && x.sanitized.scope_path == path) - ?.real; + result.variable = sanitized_all_scopes.find( + (x) => x.sanitized.name === propertyName && x.sanitized.scope_path === path, + )?.real; if (!result.variable) { result.error = `Could not find: ${propertyName}`; return result; } if (root.value.entries) { - if (result.variable.name == "self") { - result.object_id = this.all_scopes - .find(x => x && x.name == "id" && x.scope_path == "@.member.self").value; + if (result.variable.name === "self") { + result.object_id = this.all_scopes.find( + (x) => x && x.name === "id" && x.scope_path === "@.member.self", + ).value; } else if (key) { - var collection = path.split(".")[path.split(".").length - 1]; - var collection_items = Array.from(root.value.entries()) - .find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == collection)[1]; - result.object_id = collection_items.get - ? collection_items.get(key)?.id - : collection_items[key]?.id; + const collection = path.split(".")[path.split(".").length - 1]; + const collection_items = Array.from(root.value.entries()).find( + (x) => x && x[0].split("Members/").join("").split("Locals/").join("") === collection, + )[1]; + result.object_id = collection_items.get ? collection_items.get(key)?.id : collection_items[key]?.id; } else { - const entries = Array.from(root.value.entries()); - const item = entries.find(x => x && x[0].split("Members/").join("").split("Locals/").join("") == propertyName); + const item = Array.from(root.value.entries()).find( + (x) => x && x[0].split("Members/").join("").split("Locals/").join("") === propertyName, + ); result.object_id = item?.[1].id; } } @@ -525,7 +516,9 @@ export class GodotDebugSession extends LoggingDebugSession { result.object_id = object_id; } - result.index = this.all_scopes.findIndex(x => x && x.name == result.variable.name && x.scope_path == result.variable.scope_path); + result.index = this.all_scopes.findIndex( + (x) => x && x.name === result.variable.name && x.scope_path === result.variable.scope_path, + ); if (items.length > 2 && index < items.length - 2) { result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id); diff --git a/src/debugger/godot4/server_controller.ts b/src/debugger/godot4/server_controller.ts index 724e11e97..4eff0fa88 100644 --- a/src/debugger/godot4/server_controller.ts +++ b/src/debugger/godot4/server_controller.ts @@ -1,17 +1,28 @@ -import * as fs from "fs"; -import net = require("net"); -import { debug, window } from "vscode"; import { StoppedEvent, TerminatedEvent } from "@vscode/debugadapter"; -import { VariantEncoder } from "./variables/variant_encoder"; +import { DebugProtocol } from "@vscode/debugprotocol"; +import * as fs from "node:fs"; +import * as net from "node:net"; +import { debug, window } from "vscode"; + +import { + ansi, + convert_resource_path_to_uri, + createLogger, + get_configuration, + get_free_port, + get_project_version, + verify_godot_version, + VERIFY_RESULT, +} from "../../utils"; +import { prompt_for_godot_executable } from "../../utils/prompts"; +import { killSubProcesses, subProcess } from "../../utils/subspawn"; +import { GodotStackFrame, GodotStackVars, GodotVariable } from "../debug_runtime"; +import { AttachRequestArguments, LaunchRequestArguments, pinnedScene } from "../debugger"; +import { GodotDebugSession } from "./debug_session"; +import { build_sub_values, parse_next_scene_node, split_buffers } from "./helpers"; import { VariantDecoder } from "./variables/variant_decoder"; +import { VariantEncoder } from "./variables/variant_encoder"; import { RawObject } from "./variables/variants"; -import { GodotStackFrame, GodotVariable, GodotStackVars } from "../debug_runtime"; -import { GodotDebugSession } from "./debug_session"; -import { parse_next_scene_node, split_buffers, build_sub_values } from "./helpers"; -import { get_configuration, get_free_port, createLogger, verify_godot_version, get_project_version } from "../../utils"; -import { prompt_for_godot_executable } from "../../utils/prompts"; -import { subProcess, killSubProcesses } from "../../utils/subspawn"; -import { LaunchRequestArguments, AttachRequestArguments, pinnedScene } from "../debugger"; const log = createLogger("debugger.controller", { output: "Godot Debugger" }); const socketLog = createLogger("debugger.socket"); @@ -34,13 +45,11 @@ export class ServerController { private server?: net.Server; private socket?: net.Socket; private steppingOut = false; - private didFirstOutput: boolean = false; + private didFirstOutput = false; private partialStackVars = new GodotStackVars(); private connectedVersion = ""; - public constructor( - public session: GodotDebugSession - ) { } + public constructor(public session: GodotDebugSession) {} public break() { this.send_command("break"); @@ -88,12 +97,8 @@ export class ServerController { this.send_command("get_stack_frame_vars", [frame_id]); } - public set_object_property(objectId: bigint, label: string, newParsedValue: any) { - this.send_command("scene:set_object_property", [ - objectId, - label, - newParsedValue, - ]); + public set_object_property(objectId: bigint, label: string, newParsedValue) { + this.send_command("scene:set_object_property", [objectId, label, newParsedValue]); } public set_exception(exception: string) { @@ -104,7 +109,7 @@ export class ServerController { log.info("Starting game process"); let godotPath: string; - let result; + let result: VERIFY_RESULT; if (args.editor_path) { log.info("Using 'editor_path' variable from launch.json"); @@ -169,17 +174,17 @@ export class ServerController { const address = args.address.replace("tcp://", ""); command += ` --remote-debug "tcp://${address}:${args.port}"`; - if (args.profiling) { command += " --profiling"; } - if (args.single_threaded_scene) { command += " --single-threaded-scene"; } - if (args.debug_collisions) { command += " --debug-collisions"; } - if (args.debug_paths) { command += " --debug-paths"; } - if (args.debug_navigation) { command += " --debug-navigation"; } - if (args.debug_avoidance) { command += " --debug-avoidance"; } - if (args.debug_stringnames) { command += " --debug-stringnames"; } - if (args.frame_delay) { command += ` --frame-delay ${args.frame_delay}`; } - if (args.time_scale) { command += ` --time-scale ${args.time_scale}`; } - if (args.disable_vsync) { command += " --disable-vsync"; } - if (args.fixed_fps) { command += ` --fixed-fps ${args.fixed_fps}`; } + if (args.profiling) command += " --profiling"; + if (args.single_threaded_scene) command += " --single-threaded-scene"; + if (args.debug_collisions) command += " --debug-collisions"; + if (args.debug_paths) command += " --debug-paths"; + if (args.debug_navigation) command += " --debug-navigation"; + if (args.debug_avoidance) command += " --debug-avoidance"; + if (args.debug_stringnames) command += " --debug-stringnames"; + if (args.frame_delay) command += ` --frame-delay ${args.frame_delay}`; + if (args.time_scale) command += ` --time-scale ${args.time_scale}`; + if (args.disable_vsync) command += " --disable-vsync"; + if (args.fixed_fps) command += ` --fixed-fps ${args.fixed_fps}`; if (args.scene && args.scene !== "main") { log.info(`Custom scene argument provided: ${args.scene}`); @@ -225,15 +230,15 @@ export class ServerController { command += this.session.debug_data.get_breakpoint_string(); if (args.additional_options) { - command += " " + args.additional_options; + command += ` ${args.additional_options}`; } log.info(`Launching game process using command: '${command}'`); const debugProcess = subProcess("debug", command, { shell: true, detached: true }); - debugProcess.stdout.on("data", (data) => { }); - debugProcess.stderr.on("data", (data) => { }); - debugProcess.on("close", (code) => { }); + debugProcess.stdout.on("data", (data) => {}); + debugProcess.stderr.on("data", (data) => {}); + debugProcess.on("close", (code) => {}); } private stash: Buffer; @@ -336,7 +341,7 @@ export class ServerController { this.server.listen(args.port, args.address); } - private parse_message(dataset: any[]) { + private parse_message(dataset: []) { const command = new Command(); let i = 0; command.command = dataset[i++]; @@ -347,7 +352,7 @@ export class ServerController { return command; } - private handle_command(command: Command) { + private async handle_command(command: Command) { switch (command.command) { case "debug_enter": { const reason: string = command.parameters[1]; @@ -378,7 +383,7 @@ export class ServerController { case "scene:inspect_object": { let id = BigInt(command.parameters[0]); const className: string = command.parameters[1]; - const properties: any[] = command.parameters[2]; + const properties: string[] = command.parameters[2]; // message:inspect_object returns the id as an unsigned 64 bit integer, but it is decoded as a signed 64 bit integer, // thus we need to convert it to its equivalent unsigned value here. @@ -387,16 +392,13 @@ export class ServerController { } const rawObject = new RawObject(className); - properties.forEach((prop) => { + for (const prop of properties) { rawObject.set(prop[0], prop[5]); - }); + } const inspectedVariable = { name: "", value: rawObject }; build_sub_values(inspectedVariable); if (this.session.inspect_callbacks.has(BigInt(id))) { - this.session.inspect_callbacks.get(BigInt(id))( - inspectedVariable.name, - inspectedVariable - ); + this.session.inspect_callbacks.get(BigInt(id))(inspectedVariable.name, inspectedVariable); this.session.inspect_callbacks.delete(BigInt(id)); } this.session.set_inspection(id, inspectedVariable); @@ -439,13 +441,102 @@ export class ServerController { } const lines = command.parameters[0]; for (const line of lines) { - debug.activeDebugConsole.appendLine(line); + debug.activeDebugConsole.appendLine(ansi.bright.blue + line); + } + break; + } + case "error": { + if (!this.didFirstOutput) { + this.didFirstOutput = true; } + this.handle_error(command); break; } } } + async handle_error(command: Command) { + const params = command.parameters; + const e = { + hr: params[0], + min: params[1], + sec: params[2], + msec: params[3], + file: params[4] as string, + func: params[5] as string, + line: params[6], + error: params[7] as string, + desc: params[8] as string, + warning: params[9] as boolean, + stack: [], + }; + const stackCount = params[10] ?? 0; + for (let i = 0; i < stackCount; i += 3) { + const file = params[11 + i]; + const func = params[12 + i]; + const line = params[13 + i]; + const msg = `${file.slice("res://".length)}:${line} @ ${func}()`; + const extras = { + source: { name: (await convert_resource_path_to_uri(file)).toString() }, + line: line, + }; + e.stack.push({ msg: msg, extras: extras }); + } + + const time = `${e.hr}:${e.min}:${e.sec}.${e.msec}`; + let file = e.file; + if (file.startsWith("res://")) { + file = file.slice("res://".length); + } + const location = `${file}:${e.line}`; + const color = e.warning ? "yellow" : "red"; + const lang = e.file.startsWith("res://") ? "GDScript" : "C++"; + + const extras = { + source: { name: (await convert_resource_path_to_uri(e.file)).toString() }, + line: e.line, + group: "startCollapsed", + }; + if (e.desc) { + this.stderr(`${ansi[color]}${time} | ${e.desc}`, extras); + this.stderr(`${ansi.dim.white}<${lang} Error> ${ansi.white}${e.error}`); + } else { + this.stderr(`${ansi[color]}${time} | ${e.error}`, extras); + } + this.stderr(`${ansi.dim.white}<${lang} Source> ${ansi.white}${location}`); + + if (stackCount !== 0) { + this.stderr(`${ansi.dim.white}`, { group: "start" }); + for (const frame of e.stack) { + this.stderr(`${ansi.white}${frame.msg}`, frame.extras); + } + this.stderr("", { group: "end" }); + } + this.stderr("", { group: "end" }); + } + + stdout(output = "", extra = {}) { + this.session.sendEvent({ + event: "output", + body: { + category: "stdout", + output: output + ansi.reset, + ...extra, + }, + } as DebugProtocol.OutputEvent); + } + + stderr(output = "", extra = {}) { + this.session.sendEvent({ + event: "output", + body: { + category: "stderr", + output: output + ansi.reset, + ...extra, + }, + } as DebugProtocol.OutputEvent); + } + public abort() { log.info("Aborting debug controller"); this.session.sendEvent(new TerminatedEvent()); @@ -480,19 +571,14 @@ export class ServerController { const line = stackFrames[0].line; if (this.steppingOut) { - const breakpoint = this.session.debug_data - .get_breakpoints(file) - .find((bp) => bp.line === line); + const breakpoint = this.session.debug_data.get_breakpoints(file).find((bp) => bp.line === line); if (!breakpoint) { if (this.session.debug_data.stack_count > 1) { continueStepping = this.session.debug_data.stack_count === stackCount; } else { - const fileSame = - stackFrames[0].file === this.session.debug_data.last_frame.file; - const funcSame = - stackFrames[0].function === this.session.debug_data.last_frame.function; - const lineGreater = - stackFrames[0].line >= this.session.debug_data.last_frame.line; + const fileSame = stackFrames[0].file === this.session.debug_data.last_frame.file; + const funcSame = stackFrames[0].function === this.session.debug_data.last_frame.function; + const lineGreater = stackFrames[0].line >= this.session.debug_data.last_frame.line; continueStepping = fileSame && funcSame && lineGreater; } @@ -518,15 +604,12 @@ export class ServerController { this.session.sendEvent(new StoppedEvent("breakpoint", 0)); } else { this.session.set_exception(true); - this.session.sendEvent( - new StoppedEvent("exception", 0, this.exception) - ); + this.session.sendEvent(new StoppedEvent("exception", 0, this.exception)); } } private send_command(command: string, parameters?: any[]) { const commandArray: any[] = [command]; - // log.debug("send_command", this.connectedVersion); if (this.connectedVersion[2] >= "2") { commandArray.push(this.threadId); } diff --git a/src/utils/index.ts b/src/utils/index.ts index 409a31983..a880989a4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,7 @@ -import * as vscode from "vscode"; -import * as path from "path"; -import * as fs from "fs"; import { AddressInfo, createServer } from "net"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as vscode from "vscode"; export * from "./logger"; export * from "./project_utils"; @@ -21,12 +21,12 @@ export async function find_file(file: string): Promise { if (results.length === 1) { return results[0]; } - + return null; } export async function get_free_port(): Promise { - return new Promise(res => { + return new Promise((res) => { const srv = createServer(); srv.listen(0, () => { const port = (srv.address() as AddressInfo).port; @@ -45,7 +45,7 @@ export function make_docs_uri(path: string, fragment?: string) { /** * Can be used to convert a conventional node name to a snake_case variable name. - * + * * @example * ```ts * nodeNameToVar("MyNode") // my_node @@ -54,10 +54,39 @@ export function make_docs_uri(path: string, fragment?: string) { * ``` */ export function node_name_to_snake(name: string): string { - const snakeCase: string = name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").toLowerCase(); - - if (snakeCase.startsWith("_")) { - return snakeCase.substring(1); - } - return snakeCase; + const snakeCase: string = name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").toLowerCase(); + + if (snakeCase.startsWith("_")) { + return snakeCase.substring(1); + } + return snakeCase; } + +export const ansi = { + reset: "\u001b[0;37m", + red: "\u001b[0;31m", + green: "\u001b[0;32m", + yellow: "\u001b[0;33m", + blue: "\u001b[0;34m", + purple: "\u001b[0;35m", + cyan: "\u001b[0;36m", + white: "\u001b[0;37m", + bright: { + red: "\u001b[1;31m", + green: "\u001b[1;32m", + yellow: "\u001b[1;33m", + blue: "\u001b[1;34m", + purple: "\u001b[1;35m", + cyan: "\u001b[1;36m", + white: "\u001b[1;37m", + }, + dim: { + red: "\u001b[1;2;31m", + green: "\u001b[1;2;32m", + yellow: "\u001b[1;2;33m", + blue: "\u001b[1;2;34m", + purple: "\u001b[1;2;35m", + cyan: "\u001b[1;2;36m", + white: "\u001b[1;2;37m", + }, +} as const; diff --git a/src/utils/project_utils.ts b/src/utils/project_utils.ts index 344ebbde2..03955a0fb 100644 --- a/src/utils/project_utils.ts +++ b/src/utils/project_utils.ts @@ -105,8 +105,8 @@ export async function convert_resource_path_to_uri(resPath: string): Promise