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; this._nextVarRef = 100; this._varRefMap = new Map(); this._exceptionFilters = new Set(["uncaught"]); } initializeRequest(response, args) { console.log("[ADAPTER] initializeRequest"); this.sendEvent(new InitializedEvent()); response.body = { supportsConfigurationDoneRequest: true, supportsEvaluateForHovers: true, supportsStepBack: false, supportsSetVariable: true, supportsRestartFrame: false, supportsGotoTargetsRequest: false, supportsStepInTargetsRequest: false, supportsCompletionsRequest: false, supportsRestartRequest: false, supportsExceptionInfoRequest: false, supportsDelayedStackTraceLoading: false, supportsLoadedSourcesRequest: false, supportsLogPoints: true, supportsTerminateRequest: true, supportsTerminateDebuggee: true, supportsFunctionBreakpoints: false, supportsConditionalBreakpoints: true, supportsHitConditionalBreakpoints: true, supportsConfigurationDoneRequest: true, supportsSetExpression: false, supportsClipboardContext: false, supportsValueFormattingOptions: false, supportsExceptionOptions: false, supportsExceptionFilterOptions: true, supportsSingleThreadExecutionRequests: true }; response.body.exceptionBreakpointFilters = [ { filter: "all", label: "Break on all exceptions", default: false }, { filter: "uncaught", label: "Break on uncaught exceptions", default: 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") { this._lastStackFrames = json.payload.stackFrames; this._lastTotalFrames = json.payload.totalFrames; if (this._pendingStackTraceResponse) { const resp = this._pendingStackTraceResponse; this._pendingStackTraceResponse = null; resp.body = { stackFrames: this._lastStackFrames, totalFrames: this._lastTotalFrames }; this.sendResponse(resp); } 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; } if (json.type === "setVariable") { if (this._pendingSetVariable) { //const newValue = String(msg.payload.value); this._pendingSetVariable.body = { value: json.payload.value }; this.sendResponse(this._pendingSetVariable); this._pendingSetVariable = null; } } if (json.type === "log") { this.sendEvent(new OutputEvent(json.payload.text + "\n")); return; } if (json.type === "stopped" && json.payload.reason === "exception") { this.sendEvent(new StoppedEvent("exception", 1, json.payload.text)); 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, breakpoints: this.breakpoints[path] //lines }); } } this.sendDebugCommand({ cmd: "setExceptionFilters", filters: [...this._exceptionFilters] }); 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; this.breakpoints[path] = clientBreakpoints.map(bp => ({ line: bp.line, condition: bp.condition || null, logMessage: bp.logMessage || null, hitCondition: bp.hitCondition || null })); // Si el programa YA está corriendo → enviar breakpoints al motor if (this.programStarted) { this.sendDebugCommand({ cmd: "setBreakpoints", file: path, breakpoints: this.breakpoints[path] }); } 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); } 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"); if (!this._lastStackFrames) { // Pedimos el stack trace al motor this.sendDebugCommand({ cmd: "stackTrace" }); // Guardar el response para enviarlo más tarde this._pendingStackTraceResponse = response; return; } // Si ya tenemos frames, responder inmediatamente response.body = { stackFrames: this._lastStackFrames, totalFrames: this._lastTotalFrames }; this.sendResponse(response); // 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, name: f.name, source: { path: f.file }, line: f.line, column: 1 })), totalFrames: frames.length }; this._lastFrames = frames; this.sendResponse(response); } else { setTimeout(check, 5); } }; check();*/ } makeVarRef(type, frameId) { const ref = this._nextVarRef++; this._varRefMap.set(ref, { type, frameId }); return ref; } // Aún no tiene en cuenta el frame scopesRequest(response, args) { console.log("[ADAPTER] scopesRequest, frameId =", args.frameId); const frameId = args.frameId; response.body = { scopes: [ new Scope("Locals", this.makeVarRef("locals", frameId), false), new Scope("Upvalues", this.makeVarRef("upvalues", frameId), false), new Scope("Globals", this.makeVarRef("globals", frameId), true) ] }; this.sendResponse(response); } variablesRequest(response, args) { console.log("[ADAPTER] variablesRequest ref =", args.variablesReference); const info = this._varRefMap.get(args.variablesReference); if (info) { const { type, frameId } = info; if (type === "locals") { this._pendingLocals = response; this.sendDebugCommand({ cmd: "locals", frame: frameId }); return; } if (type === "upvalues") { this._pendingUpvalues = response; this.sendDebugCommand({ cmd: "upvalues", frame: frameId }); return; } if (type === "globals") { 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) { const expr = args.expression; const frameId = args.frameId ?? 0; // Detectar asignación: contiene "=" y no empieza por "==" const isAssign = expr.includes("=") && !expr.includes("=="); this._pendingEval = response; if (isAssign) { this.sendDebugCommand({ cmd: "evalAssign", frame: frameId, expr: expr }); } else { this.sendDebugCommand({ cmd: "eval", expr: expr, frame: frameId }); } } setVariableRequest(response, args) { const { variablesReference, name, value } = args; // Caso 1: locals/upvalues/globals const info = this._varRefMap.get(variablesReference); if (info) { this._pendingSetVariable = response; this.sendDebugCommand({ cmd: "setVariable", frame: info.frameId, scope: info.type, name: name, value: value }); return; } // Caso 2: miembros de tabla if (variablesReference >= 1000) { const luaRef = this._refTable.get(variablesReference); this._pendingSetVariable = response; this.sendDebugCommand({ cmd: "setTableField", ref: luaRef, key: name, value: value }); return; } // fallback response.body = { value: value }; this.sendResponse(response); } setExceptionBreakPointsRequest(response, args) { // Si VSCode envía filtros explícitos, los usamos if (args.filters && args.filters.length > 0) { this._exceptionFilters = new Set(args.filters); } // Si VSCode envía [], NO tocamos los filtros por defecto response.body = {}; this.sendResponse(response); } configurationDoneRequest(response, args) { console.log("[ADAPTER] configurationDoneRequest"); this.sendResponse(response); } } new MyLuaDebugSession().start(process.stdin, process.stdout);