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 } //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; } // 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.sendEvent(new ThreadEvent("started", 1)); 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()); }); 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; // 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) { const msg = { cmd: "continue" }; // por si estaba pausado this.sendDebugCommand(msg); //this.process.stdin.write("@@DEBUGCMD@@" + JSON.stringify(msg) + "\n"); this.process.kill(); 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); } configurationDoneRequest(response, args) { console.log("[ADAPTER] configurationDoneRequest"); this.sendResponse(response); } } new MyLuaDebugSession().start(process.stdin, process.stdout);