From 273d4dbf2b6c9196b95c65f68856dfbf3ed48979 Mon Sep 17 00:00:00 2001 From: mynameisdeleted Date: Thu, 9 Jul 2026 09:17:12 -0400 Subject: [PATCH] Enhance debug visualization for linked list with custom node table view --- .vscode/settings.json | 5 + cpp/main.cpp | 17 +- vis-plugins/node-table-visualizer.js | 332 +++++++++++++++++++++++++++ 3 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 vis-plugins/node-table-visualizer.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dc48258 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "debugVisualizer.customVisualizerScriptPaths": [ + "${workspaceFolder}/vis-plugins/node-table-visualizer.js" + ] +} diff --git a/cpp/main.cpp b/cpp/main.cpp index d88b680..396e052 100644 --- a/cpp/main.cpp +++ b/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* 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* 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(); diff --git a/vis-plugins/node-table-visualizer.js b/vis-plugins/node-table-visualizer.js new file mode 100644 index 0000000..8e1e7a5 --- /dev/null +++ b/vis-plugins/node-table-visualizer.js @@ -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::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() }; + }, + }; + }, + }); +};