Files
visual_debugger/vis-plugins/node-table-visualizer.js
mynameisdeleted a8d105005e feat(debug): add GDB-based debug visualizer for linked lists
Replace the custom toDebugGraph() method that generated JSON directly in C++ with a generic GDB Python script (debug_graph.py). The script traverses pointers and structures to build a graph representation for the VS Code Debug Visualizer, enabling external pointer display and more extensible visualization.

Update launch configurations to load the script and to disable array/string truncation (needed for the visualizer JSON). Add a debugAdapterConfigurations entry in settings.json to use the expression template $debug_graph(${expr}) for the cppdbg adapter.
2026-07-09 12:14:27 -04:00

411 lines
11 KiB
JavaScript

// Custom "Debug Visualizer" (hediet.debug-visualizer) visualization.
// Renders a { kind: { nodeTable: true }, nodes: [...], edges: [...] } payload
// as a row of boxes -- one per struct/node -- where each box is a table of
// that node's member variables (address in the header, fields below,
// pointer values in hex), connected by arrows for pointer fields.
//
// Registered via the "debugVisualizer.customVisualizerScriptPaths" setting;
// the extension hot-reloads this file on save, no debug session restart needed.
//
// Data shape produced by cpp/main.cpp's LinkedList<T>::toDebugGraph():
// nodes: [{ id: "0x...", fields: [{ name, value, isPointer? }, ...] }]
// edges: [{ from: "0x...", to: "0x...", label? }]
// roots: [{ name: "top", value: "0x..." }] -- external pointer variables
// (e.g. a local `top`/`end` in the caller's stack frame); drawn as a
// small pill above the graph with an arrow to whichever node's id
// matches its value, since a node's own address equals its first
// member's address.
module.exports = function (register, lib) {
var sj = lib.semanticJson;
var sField = sj.sOpenObject({
name: sj.sString(),
value: sj.sString(),
isPointer: sj.sOptionalProp(sj.sBoolean(), {}),
});
var sNode = sj.sOpenObject({
id: sj.sString(),
fields: sj.sArrayOf(sField),
});
var sEdge = sj.sOpenObject({
from: sj.sString(),
to: sj.sString(),
label: sj.sOptionalProp(sj.sString(), {}),
});
var sRoot = sj.sOpenObject({
name: sj.sString(),
value: sj.sString(),
});
var sData = sj.sOpenObject({
kind: sj.sOpenObject({ nodeTable: sj.sLiteral(true) }),
nodes: sj.sArrayOf(sNode),
edges: sj.sArrayOf(sEdge),
roots: sj.sOptionalProp(sj.sArrayOf(sRoot), {}),
});
var HEADER_HEIGHT = 24;
var ROW_HEIGHT = 20;
var MAX_VISIBLE_ROWS = 10;
var NODE_WIDTH = 220;
var H_GAP = 70;
var V_GAP = 24;
var PADDING = 20;
var ROOT_ROW_HEIGHT = 50;
var ROOT_BADGE_HEIGHT = 22;
function layout(nodes, edges) {
var ids = nodes.map(function (n) {
return n.id;
});
var adjacency = {};
var incoming = {};
ids.forEach(function (id) {
adjacency[id] = [];
incoming[id] = 0;
});
edges.forEach(function (e) {
if (adjacency[e.from]) adjacency[e.from].push(e.to);
if (incoming[e.to] !== undefined) incoming[e.to]++;
});
var level = {};
var queue = [];
ids.forEach(function (id) {
if (incoming[id] === 0) {
level[id] = 0;
queue.push(id);
}
});
if (queue.length === 0 && ids.length > 0) {
level[ids[0]] = 0;
queue.push(ids[0]);
}
var head = 0;
var maxLevel = 0;
while (head < queue.length) {
var cur = queue[head++];
var curLevel = level[cur];
maxLevel = Math.max(maxLevel, curLevel);
(adjacency[cur] || []).forEach(function (next) {
if (level[next] === undefined) {
level[next] = curLevel + 1;
queue.push(next);
}
});
}
var colCount = {};
var positions = {};
ids.forEach(function (id) {
var lvl = level[id];
if (lvl === undefined) {
maxLevel++;
lvl = maxLevel;
}
var col = colCount[lvl] || 0;
colCount[lvl] = col + 1;
positions[id] = { level: lvl, col: col };
});
return positions;
}
function el(tag, style, attrs) {
var e = document.createElement(tag);
if (style) Object.assign(e.style, style);
if (attrs) Object.keys(attrs).forEach(function (k) {
e.setAttribute(k, attrs[k]);
});
return e;
}
function render(data, target, theme) {
target.innerHTML = "";
Object.assign(target.style, {
position: "relative",
overflow: "auto",
width: "100%",
height: "100%",
boxSizing: "border-box",
padding: PADDING + "px",
});
var dark = theme === "dark";
var colors = dark
? {
border: "#5a5a5a",
headerBg: "#333333",
headerFg: "#e8e8e8",
bodyBg: "#252526",
fieldName: "#9cdcfe",
fieldValue: "#d4d4d4",
pointerValue: "#ce9178",
edge: "#8a8a8a",
}
: {
border: "#b8b8b8",
headerBg: "#eeeeee",
headerFg: "#1a1a1a",
bodyBg: "#ffffff",
fieldName: "#0451a5",
fieldValue: "#1a1a1a",
pointerValue: "#a31515",
edge: "#6b6b6b",
};
var positions = layout(data.nodes, data.edges);
var roots = data.roots || [];
var topOffset = roots.length > 0 ? ROOT_ROW_HEIGHT : 0;
var byId = {};
data.nodes.forEach(function (n) {
byId[n.id] = n;
});
var boxHeights = {};
data.nodes.forEach(function (n) {
var visibleRows = Math.min(n.fields.length, MAX_VISIBLE_ROWS);
boxHeights[n.id] = HEADER_HEIGHT + Math.max(visibleRows, 1) * ROW_HEIGHT;
});
var pixelPos = {};
var maxX = 0;
var maxY = 0;
Object.keys(positions).forEach(function (id) {
var p = positions[id];
var x = p.level * (NODE_WIDTH + H_GAP);
var y = topOffset + p.col * (Math.max(boxHeights[id], HEADER_HEIGHT + ROW_HEIGHT) + V_GAP);
pixelPos[id] = { x: x, y: y };
maxX = Math.max(maxX, x + NODE_WIDTH);
maxY = Math.max(maxY, y + boxHeights[id]);
});
// Spread root badges above the nodes they point to; unmatched roots
// (e.g. a pointer into the middle of a struct, or a stale/null value)
// queue up along the top-left instead.
var rootPos = {};
var nextUnmatchedX = 0;
var badgesPerTarget = {};
roots.forEach(function (r) {
var target = pixelPos[r.value];
var x;
if (target) {
var stackIndex = badgesPerTarget[r.value] || 0;
badgesPerTarget[r.value] = stackIndex + 1;
x = target.x + stackIndex * 90;
} else {
x = nextUnmatchedX;
nextUnmatchedX += NODE_WIDTH / 2 + 20;
}
rootPos[r.name] = { x: x, y: 0, matched: !!target };
maxX = Math.max(maxX, x + NODE_WIDTH);
});
var canvas = el("div", {
position: "relative",
width: maxX + "px",
height: maxY + "px",
});
var svgNs = "http://www.w3.org/2000/svg";
var svg = document.createElementNS(svgNs, "svg");
svg.setAttribute("width", String(maxX));
svg.setAttribute("height", String(maxY));
Object.assign(svg.style, {
position: "absolute",
left: "0",
top: "0",
overflow: "visible",
pointerEvents: "none",
});
var marker = document.createElementNS(svgNs, "marker");
marker.setAttribute("id", "dv-arrow");
marker.setAttribute("markerWidth", "8");
marker.setAttribute("markerHeight", "8");
marker.setAttribute("refX", "7");
marker.setAttribute("refY", "4");
marker.setAttribute("orient", "auto");
var arrowPath = document.createElementNS(svgNs, "path");
arrowPath.setAttribute("d", "M0,0 L8,4 L0,8 Z");
arrowPath.setAttribute("fill", colors.edge);
marker.appendChild(arrowPath);
var defs = document.createElementNS(svgNs, "defs");
defs.appendChild(marker);
svg.appendChild(defs);
data.edges.forEach(function (e) {
var from = pixelPos[e.from];
var to = pixelPos[e.to];
if (!from || !to) return;
var x1 = from.x + NODE_WIDTH;
var y1 = from.y + boxHeights[e.from] / 2;
var x2 = to.x;
var y2 = to.y + boxHeights[e.to] / 2;
var line = document.createElementNS(svgNs, "path");
var midX = (x1 + x2) / 2;
line.setAttribute(
"d",
"M" + x1 + "," + y1 + " C " + midX + "," + y1 + " " + midX + "," + y2 + " " + x2 + "," + y2
);
line.setAttribute("fill", "none");
line.setAttribute("stroke", colors.edge);
line.setAttribute("stroke-width", "1.5");
line.setAttribute("marker-end", "url(#dv-arrow)");
svg.appendChild(line);
if (e.label) {
var text = document.createElementNS(svgNs, "text");
text.setAttribute("x", String(midX));
text.setAttribute("y", String((y1 + y2) / 2 - 4));
text.setAttribute("fill", colors.edge);
text.setAttribute("font-size", "11");
text.setAttribute("font-family", "monospace");
text.setAttribute("text-anchor", "middle");
text.textContent = e.label;
svg.appendChild(text);
}
});
canvas.appendChild(svg);
data.nodes.forEach(function (n) {
var pos = pixelPos[n.id];
var box = el("div", {
position: "absolute",
left: pos.x + "px",
top: pos.y + "px",
width: NODE_WIDTH + "px",
border: "1px solid " + colors.border,
borderRadius: "6px",
background: colors.bodyBg,
boxShadow: "0 1px 3px rgba(0,0,0,0.3)",
fontFamily: "var(--vscode-editor-font-family, monospace)",
fontSize: "12px",
overflow: "hidden",
});
var header = el("div", {
padding: "4px 8px",
background: colors.headerBg,
color: colors.headerFg,
fontWeight: "bold",
borderBottom: "1px solid " + colors.border,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
});
header.textContent = n.id;
box.appendChild(header);
var body = el("div", {
maxHeight: MAX_VISIBLE_ROWS * ROW_HEIGHT + "px",
overflowY: "auto",
});
var table = el("table", {
borderCollapse: "collapse",
width: "100%",
});
n.fields.forEach(function (f) {
var row = el("tr");
var nameCell = el("td", {
padding: "2px 8px",
color: colors.fieldName,
whiteSpace: "nowrap",
height: ROW_HEIGHT - 4 + "px",
});
nameCell.textContent = f.name;
var valueCell = el("td", {
padding: "2px 8px",
color: f.isPointer ? colors.pointerValue : colors.fieldValue,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "0",
width: "100%",
});
valueCell.textContent = f.value;
row.appendChild(nameCell);
row.appendChild(valueCell);
table.appendChild(row);
});
body.appendChild(table);
box.appendChild(body);
canvas.appendChild(box);
});
roots.forEach(function (r) {
var pos = rootPos[r.name];
if (pos.matched) {
var targetPos = pixelPos[r.value];
var x1 = pos.x + NODE_WIDTH / 2;
var y1 = ROOT_BADGE_HEIGHT;
var x2 = targetPos.x + NODE_WIDTH / 2;
var y2 = targetPos.y;
var line = document.createElementNS(svgNs, "path");
line.setAttribute("d", "M" + x1 + "," + y1 + " L " + x2 + "," + y2);
line.setAttribute("fill", "none");
line.setAttribute("stroke", colors.edge);
line.setAttribute("stroke-width", "1.5");
line.setAttribute("stroke-dasharray", "3,3");
line.setAttribute("marker-end", "url(#dv-arrow)");
svg.appendChild(line);
}
var badge = el("div", {
position: "absolute",
left: pos.x + "px",
top: "0px",
width: NODE_WIDTH / 2 + "px",
height: ROOT_BADGE_HEIGHT + "px",
lineHeight: ROOT_BADGE_HEIGHT + "px",
textAlign: "center",
borderRadius: "11px",
border: "1px solid " + colors.border,
background: pos.matched ? colors.headerBg : "transparent",
color: pos.matched ? colors.headerFg : colors.pointerValue,
fontFamily: "var(--vscode-editor-font-family, monospace)",
fontSize: "12px",
fontWeight: "bold",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
});
badge.title = r.name + " = " + r.value;
badge.textContent = r.name + (pos.matched ? "" : " (" + r.value + ")");
canvas.appendChild(badge);
});
target.appendChild(canvas);
}
register({
id: "node-table-graph",
name: "Node Table Graph",
serializer: sData,
getVisualization: function (data, self) {
return {
id: "node-table-graph-instance",
name: "Node Table Graph",
priority: 2000,
preload: function () {
return Promise.resolve();
},
render: function (target, options) {
render(data, target, options.theme.kind);
return { renderState: undefined, ready: Promise.resolve() };
},
};
},
});
};