{% extends "workspace_layout.html" %}
{% block content %}
<div class="h-full flex flex-col gap-4" id="debugger-workspace" up-keep>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="bg-primary/10 text-primary p-2 rounded-box">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
</div>
<h2 class="text-2xl font-bold">Debugging: {{ procedure_name }}</h2>
</div>
<div id="debug-toolbar" class="flex gap-2">
<button id="btn-save" class="btn btn-sm btn-outline">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg>
Save
</button>
<button id="btn-run" class="btn btn-sm btn-success">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path></svg>
Run / Debug
</button>
<div class="divider divider-horizontal mx-1"></div>
<button id="btn-continue" class="btn btn-sm btn-primary" disabled>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path></svg>
Continue
</button>
<button id="btn-step-over" class="btn btn-sm btn-secondary" disabled>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"></path></svg>
Step Over
</button>
<button id="btn-stop" class="btn btn-sm btn-error" disabled>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
Stop
</button>
</div>
</div>
<div class="card bg-base-100 shadow-sm border border-base-200 flex-1 flex flex-col min-h-[250px]">
<div id="cm-editor" class="flex-1 w-full h-full relative" style="overflow: auto;"></div>
<script type="module">
import {EditorView, basicSetup} from "https://esm.sh/codemirror@6.0.1";
import {StateField, StateEffect, RangeSet} from "https://esm.sh/@codemirror/state@6.0.1";
import {gutter, GutterMarker, Decoration, lineNumbers} from "https://esm.sh/@codemirror/view@6.0.1";
import {sql} from "https://esm.sh/@codemirror/lang-sql@6";
import {python} from "https://esm.sh/@codemirror/lang-python@6";
import {javascript} from "https://esm.sh/@codemirror/lang-javascript@6";
import { DAPClient } from "/workspace/static/js/dap-client.js";
const procedureName = "{{ procedure_name }}";
const searchParams = new URLSearchParams(window.location.search);
const procType = searchParams.get("type") || "procedures";
let code = "";
let procData = null;
try {
const response = await fetch(`/api/data/system.${procType}?name=eq.${procedureName}`);
const data = await response.json();
if (data && data.length > 0 && data[0].code) {
code = data[0].code;
procData = data[0];
const bodyMatch = code.match(/AS\s+'([\s\S]*)'/i);
if (bodyMatch && bodyMatch[1]) {
let extracted = bodyMatch[1];
if (extracted.endsWith("';")) {
extracted = extracted.substring(0, extracted.length - 2);
} else if (extracted.endsWith("'")) {
extracted = extracted.substring(0, extracted.length - 1);
}
code = extracted.replace(/''/g, "'");
}
} else {
code = `-- Procedure ${procedureName} not found or has no code`;
}
} catch(e) {
console.error("Failed to fetch procedure code", e);
code = `-- Error loading code`;
}
let langExtension = javascript(); if (procData) {
const lang = procData.LANGUAGE.toLowerCase();
if (lang === 'plsql' || lang === 'sql') {
langExtension = sql();
} else if (lang === 'python') {
langExtension = python();
}
}
window.cmEditor = new EditorView({
doc: code,
extensions: [
basicSetup,
langExtension,
EditorView.theme({
".cm-content": {fontFamily: "monospace"},
".cm-activeLine": {backgroundColor: "rgba(255, 255, 0, 0.1)"},
".cm-gutterElement": {cursor: "pointer", transition: "color 0.1s"}
})
],
parent: document.getElementById("cm-editor")
});
document.getElementById("cm-editor").addEventListener('mousedown', function(event) {
if (event.target.classList.contains("cm-gutterElement") && event.target.innerText.trim() !== "") {
const lineText = event.target.innerText.trim();
if (!isNaN(lineText)) {
const lineNum = parseInt(lineText);
if (event.target.style.color === "red") {
event.target.style.color = "";
event.target.style.fontWeight = "";
} else {
event.target.style.color = "red";
event.target.style.fontWeight = "bold";
}
const newBps = [];
document.querySelectorAll(".cm-gutterElement").forEach(el => {
if (el.style.color === "red") {
const num = parseInt(el.innerText.trim());
if (!isNaN(num)) newBps.push({ line: num });
}
});
if (window.dapClient) {
window.dapClient.sendRequest('setBreakpoints', {
source: { path: procedureName },
breakpoints: newBps
}).catch(e => console.error("Failed to set breakpoints", e));
}
}
}
});
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/workspace/dap-ws`;
window.dapClient = new DAPClient(wsUrl);
window.dapClient.on('initialized', () => {
console.log('Debugger initialized');
});
window.dapClient.on('stopped', async (e) => {
console.log('Debugger stopped', e);
document.getElementById('btn-continue').disabled = false;
document.getElementById('btn-step-over').disabled = false;
document.getElementById('btn-stop').disabled = false;
const threadId = e.body.threadId || 1;
try {
const threads = await window.dapClient.sendRequest('threads');
const stackTrace = await window.dapClient.sendRequest('stackTrace', { threadId });
if (stackTrace.body.stackFrames && stackTrace.body.stackFrames.length > 0) {
const frameId = stackTrace.body.stackFrames[0].id;
const scopes = await window.dapClient.sendRequest('scopes', { frameId });
if (scopes.body.scopes && scopes.body.scopes.length > 0) {
const varRef = scopes.body.scopes[0].variablesReference;
const vars = await window.dapClient.sendRequest('variables', { variablesReference: varRef });
const container = document.getElementById('debug-variables-container');
if (container) {
container.innerHTML = '';
vars.body.variables.forEach(v => {
const el = document.createElement('div');
el.innerHTML = `<strong>${v.name}</strong>: <span>${v.value}</span>`;
container.appendChild(el);
});
}
}
}
} catch (err) {
console.error('Error fetching debug state', err);
}
});
window.dapClient.on('continued', () => {
document.getElementById('btn-continue').disabled = true;
document.getElementById('btn-step-over').disabled = true;
});
document.getElementById('btn-continue').addEventListener('click', () => {
window.dapClient.sendRequest('continue', { threadId: 1 });
});
document.getElementById('btn-step-over').addEventListener('click', () => {
window.dapClient.sendRequest('next', { threadId: 1 });
});
document.getElementById('btn-stop').addEventListener('click', () => {
window.dapClient.sendRequest('disconnect', {});
});
document.getElementById('btn-save').addEventListener('click', async () => {
if (!procData) return;
const newCode = window.cmEditor.state.doc.toString();
const btn = document.getElementById('btn-save');
btn.disabled = true;
btn.innerText = "Saving...";
const isProcedure = procType === 'procedures';
const kind = isProcedure ? 'PROCEDURE' : 'FUNCTION';
let paramsArray = [];
try {
if (procData.parameters) {
paramsArray = JSON.parse(procData.parameters);
}
} catch(e) {}
let paramStr = paramsArray.map(p => `${p.name} ${p.data_type}`).join(", ");
const escapedCode = newCode.replace(/'/g, "''");
const ddl = `DROP ${kind} IF EXISTS ${procedureName};\nCREATE ${kind} ${procedureName}(${paramStr}) LANGUAGE ${procData.LANGUAGE} AS '${escapedCode}';`;
try {
const response = await fetch('/api/sql', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query: ddl})
});
const res = await response.json();
if (res.error) {
alert("Error saving: " + res.error);
} else {
console.log("Saved successfully!");
}
} catch(err) {
alert("Network error: " + err);
} finally {
btn.disabled = false;
btn.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path></svg> Save`;
}
});
document.getElementById('btn-run').addEventListener('click', async () => {
let body = {};
if (procData && procData.parameters && procData.parameters !== "[]") {
try {
body = await up.layer.ask({
url: `/workspace/run_modal?procedure_name=${procedureName}`,
mode: 'modal'
});
const params = JSON.parse(procData.parameters);
for (const param of params) {
if (body[param.name] !== undefined) {
let val = body[param.name];
if (param.data_type === 'INTEGER') {
val = parseInt(val, 10);
if (isNaN(val)) val = 0;
} else if (param.data_type === 'FLOAT') {
val = parseFloat(val);
if (isNaN(val)) val = 0.0;
} else if (param.data_type === 'BOOLEAN') {
val = (String(val).toLowerCase() === 'true' || val === '1');
}
body[param.name] = val;
}
}
} catch (e) {
return; }
}
const btn = document.getElementById('btn-run');
btn.disabled = true;
fetch(`/api/rpc/${procedureName}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
}).then(r => r.json()).then(res => {
if (res.error) {
alert("Run error: " + res.error);
} else {
console.log("Run finished successfully:", res);
const container = document.getElementById('debug-variables-container');
if (res.result !== undefined) {
if (container) {
container.innerHTML = `<div class="mb-2 font-bold text-success">Execution finished.</div><div><strong>Result</strong>: <pre class="mt-2 bg-base-300 p-2 rounded overflow-auto max-h-48">${typeof res.result === 'object' ? JSON.stringify(res.result, null, 2) : res.result}</pre></div>`;
}
} else if (res.status === "success") {
if (container) {
container.innerHTML = `<div class="mb-2 font-bold text-success">Execution finished successfully. (No return value)</div>`;
}
}
}
}).catch(err => console.error("Run network error:", err))
.finally(() => { btn.disabled = false; });
});
window.dapClient.connect();
</script>
<div class="mt-4 px-4 pb-4">
<h3 class="text-xs font-semibold uppercase opacity-50 mb-2">Debugger State</h3>
<div id="debug-variables-container" class="text-sm font-mono p-4 bg-base-200/50 rounded-box min-h-[150px] border border-base-200">
<span class="opacity-40 italic">Not paused...</span>
</div>
</div>
</div>
</div>
{% endblock %}