Enhance debug visualization for linked list with custom node table view
This commit is contained in:
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"debugVisualizer.customVisualizerScriptPaths": [
|
||||
"${workspaceFolder}/vis-plugins/node-table-visualizer.js"
|
||||
]
|
||||
}
|
||||
17
cpp/main.cpp
17
cpp/main.cpp
@@ -50,20 +50,25 @@ public:
|
||||
return head_ ? &head_->data : nullptr;
|
||||
}
|
||||
|
||||
// Build a JSON string describing the list as a graph, for the VS Code
|
||||
// "Debug Visualizer" extension: watch `list.toDebugGraph()` while paused.
|
||||
// Schema: https://hediet.github.io/visualization/docs/visualization-data-schema.json
|
||||
// Build a JSON string describing the list for the VS Code "Debug
|
||||
// Visualizer" extension's custom "node table" view (see
|
||||
// vis-plugins/node-table-visualizer.js): watch `list.toDebugGraph()`
|
||||
// while paused. Each node is rendered as its own address-labeled box
|
||||
// with a field table -- data as a plain value, next as a hex pointer.
|
||||
std::string toDebugGraph() const {
|
||||
std::ostringstream json;
|
||||
json << "{\"kind\":{\"graph\":true},\"nodes\":[";
|
||||
json << "{\"kind\":{\"nodeTable\":true},\"nodes\":[";
|
||||
for (Node<T>* n = head_; n; n = n->next) {
|
||||
if (n != head_) json << ",";
|
||||
json << "{\"id\":\"" << n << "\",\"label\":\"" << n->data << "\"}";
|
||||
json << "{\"id\":\"" << n << "\",\"fields\":["
|
||||
<< "{\"name\":\"data\",\"value\":\"" << n->data << "\"},"
|
||||
<< "{\"name\":\"next\",\"value\":\"" << n->next << "\",\"isPointer\":true}"
|
||||
<< "]}";
|
||||
}
|
||||
json << "],\"edges\":[";
|
||||
for (Node<T>* n = head_; n && n->next; n = n->next) {
|
||||
if (n != head_) json << ",";
|
||||
json << "{\"from\":\"" << n << "\",\"to\":\"" << n->next << "\"}";
|
||||
json << "{\"from\":\"" << n << "\",\"to\":\"" << n->next << "\",\"label\":\"next\"}";
|
||||
}
|
||||
json << "]}";
|
||||
return json.str();
|
||||
|
||||
332
vis-plugins/node-table-visualizer.js
Normal file
332
vis-plugins/node-table-visualizer.js
Normal file
@@ -0,0 +1,332 @@
|
||||
// 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? }]
|
||||
|
||||
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 sData = sj.sOpenObject({
|
||||
kind: sj.sOpenObject({ nodeTable: sj.sLiteral(true) }),
|
||||
nodes: sj.sArrayOf(sNode),
|
||||
edges: sj.sArrayOf(sEdge),
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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 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 = 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]);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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() };
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user