mirror of
https://github.com/godotengine/godot-vscode-plugin.git
synced 2026-01-04 10:09:58 +03:00
525 lines
14 KiB
TypeScript
525 lines
14 KiB
TypeScript
import * as fs from "node:fs";
|
|
import {
|
|
Breakpoint,
|
|
InitializedEvent,
|
|
LoggingDebugSession,
|
|
Source,
|
|
TerminatedEvent,
|
|
Thread,
|
|
} from "@vscode/debugadapter";
|
|
import { DebugProtocol } from "@vscode/debugprotocol";
|
|
import { Subject } from "await-notify";
|
|
import { debug } from "vscode";
|
|
import { createLogger } from "../../utils";
|
|
import { GodotDebugData, GodotStackVars, GodotVariable } from "../debug_runtime";
|
|
import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
|
|
import { InspectorProvider } from "../inspector_provider";
|
|
import { SceneTreeProvider } from "../scene_tree_provider";
|
|
import { is_variable_built_in_type, parse_variable } from "./helpers";
|
|
import { ServerController } from "./server_controller";
|
|
import { ObjectId } from "./variables/variants";
|
|
|
|
const log = createLogger("debugger.session", { output: "Godot Debugger" });
|
|
|
|
interface Variable {
|
|
variable: GodotVariable;
|
|
index: number;
|
|
object_id: number;
|
|
}
|
|
|
|
export class GodotDebugSession extends LoggingDebugSession {
|
|
private all_scopes: GodotVariable[];
|
|
public controller = new ServerController(this);
|
|
public debug_data = new GodotDebugData(this);
|
|
public sceneTree: SceneTreeProvider;
|
|
public inspector: InspectorProvider;
|
|
private got_scope: Subject = new Subject();
|
|
private ongoing_inspections: bigint[] = [];
|
|
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 constructor() {
|
|
super();
|
|
|
|
this.setDebuggerLinesStartAt1(false);
|
|
this.setDebuggerColumnsStartAt1(false);
|
|
}
|
|
|
|
public dispose() {
|
|
this.controller.stop();
|
|
}
|
|
|
|
protected initializeRequest(
|
|
response: DebugProtocol.InitializeResponse,
|
|
args: DebugProtocol.InitializeRequestArguments,
|
|
) {
|
|
response.body = response.body || {};
|
|
|
|
response.body.supportsConfigurationDoneRequest = true;
|
|
response.body.supportsTerminateRequest = true;
|
|
response.body.supportsEvaluateForHovers = false;
|
|
response.body.supportsStepBack = false;
|
|
response.body.supportsGotoTargetsRequest = false;
|
|
response.body.supportsCancelRequest = false;
|
|
response.body.supportsCompletionsRequest = false;
|
|
response.body.supportsFunctionBreakpoints = false;
|
|
response.body.supportsDataBreakpoints = false;
|
|
response.body.supportsBreakpointLocationsRequest = false;
|
|
response.body.supportsConditionalBreakpoints = false;
|
|
response.body.supportsHitConditionalBreakpoints = false;
|
|
response.body.supportsLogPoints = false;
|
|
response.body.supportsModulesRequest = false;
|
|
response.body.supportsReadMemoryRequest = false;
|
|
response.body.supportsRestartFrame = false;
|
|
response.body.supportsRestartRequest = false;
|
|
response.body.supportsSetExpression = false;
|
|
response.body.supportsStepInTargetsRequest = false;
|
|
response.body.supportsTerminateThreadsRequest = false;
|
|
|
|
this.sendResponse(response);
|
|
this.sendEvent(new InitializedEvent());
|
|
}
|
|
|
|
protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) {
|
|
await this.configuration_done.wait(1000);
|
|
|
|
this.mode = "launch";
|
|
|
|
this.debug_data.projectPath = args.project;
|
|
await this.controller.launch(args);
|
|
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) {
|
|
await this.configuration_done.wait(1000);
|
|
|
|
this.mode = "attach";
|
|
|
|
await this.controller.attach(args);
|
|
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
public configurationDoneRequest(
|
|
response: DebugProtocol.ConfigurationDoneResponse,
|
|
args: DebugProtocol.ConfigurationDoneArguments,
|
|
) {
|
|
this.configuration_done.notify();
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
|
|
response.body = { allThreadsContinued: true };
|
|
this.controller.continue();
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
|
|
await debug.activeDebugSession.customRequest("scopes", { frameId: 0 });
|
|
|
|
if (this.all_scopes) {
|
|
try {
|
|
const variable = this.get_variable(args.expression, null, null, null);
|
|
const parsed_variable = parse_variable(variable.variable);
|
|
response.body = {
|
|
result: parsed_variable.value,
|
|
variablesReference: !is_variable_built_in_type(variable.variable) ? variable.index : 0,
|
|
};
|
|
} catch (error) {
|
|
response.success = false;
|
|
response.message = error.toString();
|
|
}
|
|
}
|
|
|
|
if (!response.body) {
|
|
response.body = {
|
|
result: "null",
|
|
variablesReference: 0,
|
|
};
|
|
}
|
|
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
|
|
this.controller.next();
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
|
|
this.controller.break();
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
|
|
this.controller.request_stack_frame_vars(args.frameId);
|
|
await this.got_scope.wait(2000);
|
|
|
|
response.body = {
|
|
scopes: [
|
|
{ name: "Locals", variablesReference: 1, expensive: false },
|
|
{ name: "Members", variablesReference: 2, expensive: false },
|
|
{ name: "Globals", variablesReference: 3, expensive: false },
|
|
],
|
|
};
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected setBreakPointsRequest(
|
|
response: DebugProtocol.SetBreakpointsResponse,
|
|
args: DebugProtocol.SetBreakpointsArguments,
|
|
) {
|
|
const path = (args.source.path as string).replace(/\\/g, "/");
|
|
const client_lines = args.lines || [];
|
|
|
|
if (fs.existsSync(path)) {
|
|
let bps = this.debug_data.get_breakpoints(path);
|
|
const bp_lines = bps.map((bp) => bp.line);
|
|
|
|
for (const bp of bps) {
|
|
if (client_lines.indexOf(bp.line) === -1) {
|
|
this.debug_data.remove_breakpoint(path, bp.line);
|
|
}
|
|
}
|
|
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);
|
|
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.
|
|
bps.sort((a, b) => (a.line < b.line ? -1 : 1));
|
|
|
|
response.body = {
|
|
breakpoints: bps.map((bp) => {
|
|
return new Breakpoint(true, bp.line, 1, new Source(bp.file.split("/").reverse()[0], bp.file));
|
|
}),
|
|
};
|
|
|
|
this.sendResponse(response);
|
|
}
|
|
}
|
|
|
|
protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
|
|
if (this.debug_data.last_frame) {
|
|
response.body = {
|
|
totalFrames: this.debug_data.last_frames.length,
|
|
stackFrames: this.debug_data.last_frames.map((sf) => {
|
|
return {
|
|
id: sf.id,
|
|
name: sf.function,
|
|
line: sf.line,
|
|
column: 1,
|
|
source: new Source(sf.file, `${this.debug_data.projectPath}/${sf.file.replace("res://", "")}`),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
|
|
this.controller.step();
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
|
|
this.controller.step_out();
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
|
|
if (this.mode === "launch") {
|
|
this.controller.stop();
|
|
this.sendEvent(new TerminatedEvent());
|
|
}
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
|
|
response.body = { threads: [new Thread(0, "thread_1")] };
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
protected async variablesRequest(
|
|
response: DebugProtocol.VariablesResponse,
|
|
args: DebugProtocol.VariablesArguments,
|
|
) {
|
|
if (!this.all_scopes) {
|
|
response.body = {
|
|
variables: [],
|
|
};
|
|
this.sendResponse(response);
|
|
return;
|
|
}
|
|
|
|
const reference = this.all_scopes[args.variablesReference];
|
|
let variables: DebugProtocol.Variable[];
|
|
|
|
if (!reference.sub_values) {
|
|
variables = [];
|
|
} 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,
|
|
);
|
|
if (sva) {
|
|
return parse_variable(
|
|
sva,
|
|
this.all_scopes.findIndex(
|
|
(va_idx) =>
|
|
va_idx &&
|
|
va_idx.scope_path === `${reference.scope_path}.${reference.name}` &&
|
|
va_idx.name === va.name,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
response.body = {
|
|
variables: variables,
|
|
};
|
|
|
|
this.sendResponse(response);
|
|
}
|
|
|
|
public set_scopes(stackVars: GodotStackVars) {
|
|
this.all_scopes = [
|
|
undefined,
|
|
{
|
|
name: "local",
|
|
value: undefined,
|
|
sub_values: stackVars.locals,
|
|
scope_path: "@",
|
|
},
|
|
{
|
|
name: "member",
|
|
value: undefined,
|
|
sub_values: stackVars.members,
|
|
scope_path: "@",
|
|
},
|
|
{
|
|
name: "global",
|
|
value: undefined,
|
|
sub_values: stackVars.globals,
|
|
scope_path: "@",
|
|
},
|
|
];
|
|
|
|
for (const va of stackVars.locals) {
|
|
va.scope_path = "@.local";
|
|
this.append_variable(va);
|
|
}
|
|
|
|
for (const va of stackVars.members) {
|
|
va.scope_path = "@.member";
|
|
this.append_variable(va);
|
|
}
|
|
|
|
for (const va of stackVars.globals) {
|
|
va.scope_path = "@.global";
|
|
this.append_variable(va);
|
|
}
|
|
|
|
this.add_to_inspections();
|
|
|
|
if (this.ongoing_inspections.length === 0) {
|
|
this.previous_inspections = [];
|
|
this.got_scope.notify();
|
|
}
|
|
}
|
|
|
|
public set_inspection(id: bigint, replacement: GodotVariable) {
|
|
const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id);
|
|
|
|
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,
|
|
);
|
|
|
|
this.previous_inspections.push(id);
|
|
|
|
// this.add_to_inspections();
|
|
|
|
if (this.ongoing_inspections.length === 0) {
|
|
this.previous_inspections = [];
|
|
this.got_scope.notify();
|
|
}
|
|
}
|
|
|
|
private add_to_inspections() {
|
|
for (const va of this.all_scopes) {
|
|
if (va && va.value instanceof ObjectId) {
|
|
if (
|
|
!this.ongoing_inspections.includes(va.value.id) &&
|
|
!this.previous_inspections.includes(va.value.id)
|
|
) {
|
|
this.controller.request_inspect_object(va.value.id);
|
|
this.ongoing_inspections.push(va.value.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
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
|
|
let key = (propertyName.match(/(?<=\[).*(?=\])/) || [null])[0];
|
|
if (key) {
|
|
key = key.replace(/['"]+/g, "");
|
|
propertyName = propertyName
|
|
.split(/(?<=\[).*(?=\])/)
|
|
.join("")
|
|
.split("[]")
|
|
.join("");
|
|
if (path) path += ".";
|
|
path += propertyName;
|
|
propertyName = key;
|
|
}
|
|
|
|
function sanitizeName(name: string) {
|
|
return name.split("Members/").join("").split("Locals/").join("");
|
|
}
|
|
|
|
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("");
|
|
}
|
|
|
|
const sanitized_all_scopes = this.all_scopes
|
|
.filter((x) => x)
|
|
.map((x) => ({
|
|
sanitized: {
|
|
name: sanitizeName(x.name),
|
|
scope_path: sanitizeScopePath(x.scope_path),
|
|
},
|
|
real: x,
|
|
}));
|
|
|
|
result.variable = sanitized_all_scopes.find(
|
|
(x) => x.sanitized.name === propertyName && x.sanitized.scope_path === path,
|
|
)?.real;
|
|
if (!result.variable) {
|
|
throw new Error(`Could not find: ${propertyName}`);
|
|
}
|
|
|
|
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;
|
|
} else if (key) {
|
|
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 item = Array.from(root.value.entries()).find(
|
|
(x) => x && x[0].split("Members/").join("").split("Locals/").join("") === propertyName,
|
|
);
|
|
result.object_id = item?.[1].id;
|
|
}
|
|
}
|
|
|
|
if (!result.object_id) {
|
|
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,
|
|
);
|
|
|
|
if (items.length > 2 && index < items.length - 2) {
|
|
result = this.get_variable(items.join("."), result.variable, index + 1, result.object_id);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private append_variable(variable: GodotVariable, index?: number) {
|
|
if (index) {
|
|
this.all_scopes.splice(index, 0, variable);
|
|
} else {
|
|
this.all_scopes.push(variable);
|
|
}
|
|
const base_path = `${variable.scope_path}.${variable.name}`;
|
|
if (variable.sub_values) {
|
|
variable.sub_values.forEach((va, i) => {
|
|
va.scope_path = base_path;
|
|
this.append_variable(va, index ? index + i + 1 : undefined);
|
|
});
|
|
}
|
|
}
|
|
}
|