Files
godot-vscode-plugin/src/debugger/godot4/debug_session.ts
MichaelXt 53f48ede63 DebugAdapter variables overhaul (#793)
- Redesigned the representation of godot objects to match internal structure of godot server
- Lazy evaluation for the godot objects
- Stack frames now can be switched with variables updated
2025-02-22 12:17:55 -05:00

296 lines
9.1 KiB
TypeScript

import {
Breakpoint,
InitializedEvent,
LoggingDebugSession,
Source,
TerminatedEvent,
Thread,
} from "@vscode/debugadapter";
import { DebugProtocol } from "@vscode/debugprotocol";
import { Subject } from "await-notify";
import * as fs from "node:fs";
import { createLogger } from "../../utils";
import { GodotDebugData } from "../debug_runtime";
import { AttachRequestArguments, LaunchRequestArguments } from "../debugger";
import { SceneTreeProvider } from "../scene_tree_provider";
import { ServerController } from "./server_controller";
import { VariablesManager } from "./variables/variables_manager";
const log = createLogger("debugger.session", { output: "Godot Debugger" });
export class GodotDebugSession extends LoggingDebugSession {
public controller = new ServerController(this);
public debug_data = new GodotDebugData(this);
public sceneTree: SceneTreeProvider;
private exception = false;
private configuration_done: Subject = new Subject();
private mode: "launch" | "attach" | "" = "";
public variables_manager: VariablesManager;
public constructor() {
super();
this.setDebuggerLinesStartAt1(false);
this.setDebuggerColumnsStartAt1(false);
}
public dispose() {
this.controller.stop();
}
protected initializeRequest(
response: DebugProtocol.InitializeResponse,
args: DebugProtocol.InitializeRequestArguments,
) {
log.info("initializeRequest", args);
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) {
log.info("launchRequest", args);
await this.configuration_done.wait(1000);
this.mode = "launch";
this.debug_data.projectPath = args.project;
this.exception = false;
await this.controller.launch(args);
this.sendResponse(response);
}
protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) {
log.info("attachRequest", args);
await this.configuration_done.wait(1000);
this.mode = "attach";
this.exception = false;
await this.controller.attach(args);
this.sendResponse(response);
}
public configurationDoneRequest(
response: DebugProtocol.ConfigurationDoneResponse,
args: DebugProtocol.ConfigurationDoneArguments,
) {
log.info("configurationDoneRequest", args);
this.configuration_done.notify();
this.sendResponse(response);
}
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) {
log.info("continueRequest", args);
if (!this.exception) {
response.body = { allThreadsContinued: true };
this.controller.continue();
this.sendResponse(response);
}
}
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) {
log.info("nextRequest", args);
if (!this.exception) {
this.controller.next();
this.sendResponse(response);
}
}
protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) {
log.info("pauseRequest", args);
if (!this.exception) {
this.controller.break();
this.sendResponse(response);
}
}
protected setBreakPointsRequest(
response: DebugProtocol.SetBreakpointsResponse,
args: DebugProtocol.SetBreakpointsArguments,
) {
log.info("setBreakPointsRequest", args);
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 stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) {
log.info("stepInRequest", args);
if (!this.exception) {
this.controller.step();
this.sendResponse(response);
}
}
protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) {
log.info("stepOutRequest", args);
if (!this.exception) {
this.controller.step_out();
this.sendResponse(response);
}
}
protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) {
log.info("terminateRequest", args);
if (this.mode === "launch") {
this.controller.stop();
this.sendEvent(new TerminatedEvent());
}
this.sendResponse(response);
}
protected threadsRequest(response: DebugProtocol.ThreadsResponse) {
log.info("threadsRequest");
response.body = { threads: [new Thread(0, "thread_1")] };
log.info("threadsRequest response", response);
this.sendResponse(response);
}
protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) {
log.info("stackTraceRequest", args);
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://", "")}`),
};
}),
};
}
log.info("stackTraceRequest response", response);
this.sendResponse(response);
}
protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) {
log.info("scopesRequest", args);
// this.variables_manager.variablesFrameId = args.frameId;
// TODO: create scopes dynamically for a given frame
const vscode_scope_ids = this.variables_manager.get_or_create_frame_scopes(args.frameId);
const scopes_with_references = [
{name: "Locals", variablesReference: vscode_scope_ids.Locals, expensive: false},
{name: "Members", variablesReference: vscode_scope_ids.Members, expensive: false},
{name: "Globals", variablesReference: vscode_scope_ids.Globals, expensive: false},
];
response.body = {
scopes: scopes_with_references
// scopes: [
// { name: "Locals", variablesReference: 1, expensive: false },
// { name: "Members", variablesReference: 2, expensive: false },
// { name: "Globals", variablesReference: 3, expensive: false },
// ],
};
log.info("scopesRequest response", response);
this.sendResponse(response);
}
protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments) {
log.info("variablesRequest", args);
try {
const variables = await this.variables_manager.get_vscode_object(args.variablesReference);
response.body = {
variables: variables,
};
} catch (error) {
log.error("variablesRequest", error);
response.success = false;
response.message = error.toString();
}
log.info("variablesRequest response", response);
this.sendResponse(response);
}
protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) {
log.info("evaluateRequest", args);
try {
const parsed_variable = await this.variables_manager.get_vscode_variable_by_name(args.expression, args.frameId);
response.body = {
result: parsed_variable.value,
variablesReference: parsed_variable.variablesReference
};
} catch (error) {
response.success = false;
response.message = error.toString();
response.body = {
result: "null",
variablesReference: 0,
};
}
log.info("evaluateRequest response", response);
this.sendResponse(response);
}
public set_exception(exception: boolean) {
this.exception = true;
}
}