Files
mini-debugger/adapter/debugAdapter.js
Raimon Zamora ecd24bd2e0 - [NEW] Expressions evaluables desde la consola de vscode
- [FIX] Ara els breakpoints que hi havia abans de llançar la aplicació ja es tenen en compte
- [FIX] Ja no fa falta apretar dos vegades el botó de parar en vscode
2026-03-31 09:22:38 +02:00

571 lines
16 KiB
JavaScript

const {
LoggingDebugSession,
InitializedEvent,
TerminatedEvent,
OutputEvent,
StoppedEvent,
ThreadEvent,
Thread,
Scope,
Variable
} = require('vscode-debugadapter');
const { spawn } = require('child_process');
class MyLuaDebugSession extends LoggingDebugSession {
constructor() {
super();
// Redirigir console.log a la Debug Console
console.log = (...args) => { this.sendEvent(new OutputEvent(args.join(" ") + "\n")); };
console.log("Debug Adapter iniciado");
this._stackFrames = [];
this.setDebuggerLinesStartAt1(true);
this.setDebuggerColumnsStartAt1(true);
this._pendingLocals = null;
this._pendingUpvalues = null;
this._pendingGlobals = null;
this._refTable = new Map(); // ref → { luaRef }
this._nextRef = 1000; // IDs únicos para tablas
this._pendingEval = null;
this.programStarted = false;
}
//initializeRequest(response, args) {
// this.sendEvent(new InitializedEvent());
//
// response.body = {
// supportsConfigurationDoneRequest: true
// };
//
// this.sendResponse(response);
//}
initializeRequest(response, args) {
console.log("[ADAPTER] initializeRequest");
this.sendEvent(new InitializedEvent());
response.body = {
supportsConfigurationDoneRequest: true,
supportsEvaluateForHovers: true,
supportsStepBack: false,
supportsSetVariable: false,
supportsRestartFrame: false,
supportsGotoTargetsRequest: false,
supportsStepInTargetsRequest: false,
supportsCompletionsRequest: false,
supportsRestartRequest: false,
supportsExceptionInfoRequest: false,
supportsDelayedStackTraceLoading: false,
supportsLoadedSourcesRequest: false,
supportsLogPoints: false,
supportsTerminateRequest: true,
supportsTerminateDebuggee: true,
supportsFunctionBreakpoints: false,
supportsConditionalBreakpoints: false,
supportsHitConditionalBreakpoints: false,
supportsConfigurationDoneRequest: true,
supportsSetExpression: false,
supportsClipboardContext: false,
supportsValueFormattingOptions: false,
supportsExceptionOptions: false,
supportsExceptionFilterOptions: false,
supportsSingleThreadExecutionRequests: true
};
this.sendResponse(response);
}
pendingStackTrace(frames) {
this._stackFrames = frames;
}
handleLine(line) {
console.log("[ADAPTER] RAW LINE:", JSON.stringify(line));
if (!line.startsWith("@@DEBUG@@")) {
console.log("[ADAPTER] Ignorando línea no-debug");
return;
}
const jsonStr = line.substring(9);
console.log("[ADAPTER] JSON A PARSEAR:", jsonStr);
let json;
try {
json = JSON.parse(jsonStr);
} catch (e) {
console.log("[ADAPTER] ERROR PARSEANDO JSON:", e.message);
return;
}
console.log("[ADAPTER] JSON OK:", json);
console.log("[ADAPTER] TYPE:", json.type);
if (json.type === "luaError") {
const e = new OutputEvent(json.message + "\n", "stderr");
e.body.source = { path: json.file };
e.body.line = json.line;
this.sendEvent(e);
return;
}
if (json.type === "break") {
this.currentFile = json.file;
this.currentLine = json.line;
this.sendEvent(new StoppedEvent("breakpoint", 1));
return;
}
if (json.type === "step") {
this.sendEvent(new StoppedEvent("step", 1));
return;
}
if (json.type === "pause") {
this.sendEvent(new StoppedEvent("pause", 1));
return;
}
if (json.type === "stackTrace") {
console.log("[ADAPTER] STACKTRACE PAYLOAD:", JSON.stringify(json.payload));
console.log("[ADAPTER] BEFORE HANDLING STACKTRACE");
this.pendingStackTrace(json.payload.frames);
console.log("[ADAPTER] AFTER HANDLING STACKTRACE");
return;
}
if (json.type === "variables") {
console.log("[ADAPTER] VARIABLES PAYLOAD:", JSON.stringify(json.payload));
// 1. Convertimos las variables del motor a formato DAP
const vars = json.payload.variables.map(v => {
if (v.type === "table" && v.ref > 0) {
const id = this._nextRef++;
this._refTable.set(id, v.ref);
return {
name: v.name,
value: v.value,
type: v.type,
variablesReference: id
};
}
return {
name: v.name,
value: v.value,
type: v.type,
variablesReference: 0
};
});
// 2. Seleccionamos el pendingResponse correcto según el kind
let response = null;
if (json.payload.kind === "locals") {
response = this._pendingLocals;
this._pendingLocals = null;
} else if (json.payload.kind === "upvalues") {
response = this._pendingUpvalues;
this._pendingUpvalues = null;
} else if (json.payload.kind === "globals") {
response = this._pendingGlobals;
this._pendingGlobals = null;
} else if (json.payload.kind === "expand") {
response = this._pendingExpand;
this._pendingExpand = null;
} else {
console.log("[ADAPTER] VARIABLES: kind desconocido:", json.payload.kind);
return;
}
if (!response) {
console.log("[ADAPTER] VARIABLES: no pending response para kind", json.payload.kind);
return;
}
// 3. Enviamos la respuesta a VSCode
response.body = { variables: vars };
this.sendResponse(response);
return;
}
if (json.type === "eval") {
const r = this._pendingEval;
this._pendingEval = null;
const v = json.payload.result;
let variablesReference = 0;
if (v.type === "table" && v.ref > 0) {
const id = this._nextRef++;
this._refTable.set(id, v.ref);
variablesReference = id;
}
r.body = {
result: v.value,
variablesReference
};
this.sendResponse(r);
return;
}
// salida normal
this.sendEvent(new OutputEvent(line + "\n", "stdout"));
}
launchRequest(response, args) {
console.log("[ADAPTER] launchRequest");
const program = args.program;
const cwd = args.cwd || process.cwd();
this.process = spawn(program, [], {
cwd: cwd,
stdio: ['pipe', 'pipe', 'pipe']
});
this.programStarted = true;
this.stdoutBuffer = "";
this.process.stdout.on('data', data => {
this.stdoutBuffer += data.toString();
let idx;
while ((idx = this.stdoutBuffer.indexOf("\n")) >= 0) {
const line = this.stdoutBuffer.slice(0, idx).trim();
this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
this.handleLine(line);
}
});
this.process.stderr.on('data', data => {
this.sendEvent(new OutputEvent(data.toString(), "stderr"));
});
this.process.on('exit', () => {
this.sendEvent(new TerminatedEvent());
});
// 3. Ahora que stdout ya está conectado, enviar breakpoints
if (this.breakpoints) {
for (const path in this.breakpoints) {
const lines = this.breakpoints[path];
this.sendDebugCommand({
cmd: "setBreakpoints",
file: path,
lines: lines
});
}
}
this.sendEvent(new ThreadEvent("started", 1));
this.sendResponse(response);
}
sendDebugCommand(obj) {
if (!this.process || !this.process.stdin) return;
const msg = "@@DEBUGCMD@@" + JSON.stringify(obj) + "\n";
console.log("ENVIANDO CMD:", msg);
this.process.stdin.write(msg);
}
nextRequest(response, args) {
this.sendDebugCommand({ cmd: "stepOver" });
this.sendResponse(response);
}
stepInRequest(response, args) {
this.sendDebugCommand({ cmd: "stepInto" });
this.sendResponse(response);
}
stepOutRequest(response, args) {
this.sendDebugCommand({ cmd: "stepOut" });
this.sendResponse(response);
}
setBreakPointsRequest(response, args) {
const path = args.source.path;
const clientBreakpoints = args.breakpoints || [];
const lines = clientBreakpoints.map(bp => bp.line);
if (!this.breakpoints)
this.breakpoints = {};
this.breakpoints[path] = lines;
// Si el programa YA está corriendo → enviar breakpoints al motor
if (this.programStarted) {
this.sendDebugCommand({
cmd: "setBreakpoints",
file: path,
lines: lines
});
}
response.body = {
breakpoints: lines.map(line => ({
verified: true,
line: line
}))
};
this.sendResponse(response);
}
/*setBreakPointsRequest(response, args) {
const path = args.source.path; // archivo donde se han puesto breakpoints
const clientBreakpoints = args.breakpoints || [];
// Extraemos solo las líneas
const lines = clientBreakpoints.map(bp => bp.line);
// Guardamos internamente los breakpoints
if (!this.breakpoints)
this.breakpoints = {};
this.breakpoints[path] = lines;
// Enviamos los breakpoints a tu aplicación por stdin
const msg = {
cmd: "setBreakpoints",
file: path,
lines: lines
};
this.sendDebugCommand(msg);
//this.process.stdin.write(
// "@@DEBUGCMD@@" + JSON.stringify(msg) + "\n"
//);
// VSCode necesita una respuesta con los breakpoints "verificados"
response.body = {
breakpoints: lines.map(line => ({
verified: true,
line: line
}))
};
this.sendResponse(response);
}*/
continueRequest(response, args) {
const msg = {
cmd: "continue"
};
this.sendDebugCommand(msg);
//this.process.stdin.write(
// "@@DEBUGCMD@@" + JSON.stringify(msg) + "\n"
//);
this.sendResponse(response);
}
pauseRequest(response, args) {
this.sendDebugCommand({ cmd: "pause" });
this.sendResponse(response);
}
stackTraceRequest(response, args) {
// Pedimos al motor el stack trace
this.sendDebugCommand({ cmd: "stackTrace" });
// IMPORTANTE: el motor responde async, así que esperamos
// a que llegue el mensaje @@DEBUG@@ con type=stackTrace
const check = () => {
if (this._stackFrames.length > 0) {
const frames = this._stackFrames;
this._stackFrames = [];
response.body = {
stackFrames: frames.map((f, i) => ({
id: i + 1,
name: f.name,
source: { path: f.file },
line: f.line,
column: 1
})),
totalFrames: frames.length
};
this.sendResponse(response);
} else {
setTimeout(check, 5);
}
};
check();
}
disconnectRequest(response, args) {
console.log("[ADAPTER] disconnectRequest");
if (this.process) {
try {
// 1. Cerrar stdin para desbloquear lecturas
this.process.stdin.end();
} catch (e) {}
try {
// 2. Intento de kill suave
this.process.kill();
} catch (e) {}
try {
// 3. Kill duro por si acaso
this.process.kill('SIGKILL');
} catch (e) {}
}
// 4. Notificar a VSCode
this.sendEvent(new TerminatedEvent());
this.sendResponse(response);
}
terminateRequest(response, args) {
console.log("[ADAPTER] terminateRequest");
if (this.process) {
try { this.process.stdin.end(); } catch (e) {}
try { this.process.kill(); } catch (e) {}
try { this.process.kill('SIGKILL'); } catch (e) {}
}
this.sendEvent(new TerminatedEvent());
this.sendResponse(response);
}
threadsRequest(response) {
// VSCode exige al menos un thread
response.body = {
threads: [
new Thread(1, "Main Thread")
]
};
this.sendResponse(response);
}
stackTraceRequest(response, args) {
console.log("[ADAPTER] stackTraceRequest");
// Pedimos el stack trace al motor
this.sendDebugCommand({ cmd: "stackTrace" });
// Esperamos la respuesta del motor
const check = () => {
if (this._stackFrames && this._stackFrames.length > 0) {
const frames = this._stackFrames;
this._stackFrames = [];
response.body = {
stackFrames: frames.map((f, i) => ({
id: i + 1,
name: f.name,
source: { path: f.file },
line: f.line,
column: 1
})),
totalFrames: frames.length
};
this.sendResponse(response);
} else {
setTimeout(check, 5);
}
};
check();
}
// Aún no tiene en cuenta el frame
scopesRequest(response, args) {
console.log("[ADAPTER] scopesRequest, frameId =", args.frameId);
response.body = {
scopes: [
new Scope("Locals", 1, false),
new Scope("Upvalues", 2, false),
new Scope("Globals", 3, true)
]
};
this.sendResponse(response);
}
variablesRequest(response, args) {
console.log("[ADAPTER] variablesRequest ref =", args.variablesReference);
if (args.variablesReference === 1) {
this._pendingLocals = response;
this.sendDebugCommand({ cmd: "locals" });
return;
}
if (args.variablesReference === 2) {
this._pendingUpvalues = response;
this.sendDebugCommand({ cmd: "upvalues" });
return;
}
if (args.variablesReference === 3) {
this._pendingGlobals = response;
this.sendDebugCommand({ cmd: "globals" });
return;
}
// Expansión de tablas
if (args.variablesReference >= 1000) {
const luaRef = this._refTable.get(args.variablesReference);
if (!luaRef) {
console.log("[ADAPTER] No existe luaRef para", args.variablesReference);
response.body = { variables: [] };
this.sendResponse(response);
return;
}
this._pendingExpand = response;
this.sendDebugCommand({ cmd: "expand", ref: luaRef });
return;
}
// de momento, resto vacío
response.body = { variables: [] };
this.sendResponse(response);
}
evaluateRequest(response, args) {
console.log("[ADAPTER] evaluateRequest:", args.expression);
this._pendingEval = response;
this.sendDebugCommand({
cmd: "eval",
expr: args.expression
});
}
configurationDoneRequest(response, args) {
console.log("[ADAPTER] configurationDoneRequest");
this.sendResponse(response);
}
}
new MyLuaDebugSession().start(process.stdin, process.stdout);