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.
This commit is contained in:
20
.vscode/launch.json
vendored
20
.vscode/launch.json
vendored
@@ -19,6 +19,21 @@
|
||||
"description": "Enable pretty-printing for gdb",
|
||||
"text": "-enable-pretty-printing",
|
||||
"ignoreFailures": true
|
||||
},
|
||||
{
|
||||
"description": "Don't truncate long arrays (needed for Debug Visualizer JSON)",
|
||||
"text": "-gdb-set print elements 0",
|
||||
"ignoreFailures": true
|
||||
},
|
||||
{
|
||||
"description": "Don't truncate long strings (needed for Debug Visualizer JSON)",
|
||||
"text": "-gdb-set print characters 0",
|
||||
"ignoreFailures": true
|
||||
},
|
||||
{
|
||||
"description": "Load the generic $debug_graph(...) Debug Visualizer helper",
|
||||
"text": "source ${workspaceFolder}/gdb/debug_graph.py",
|
||||
"ignoreFailures": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -50,6 +65,11 @@
|
||||
"description": "Don't truncate long strings (needed for Debug Visualizer JSON)",
|
||||
"text": "-gdb-set print characters 0",
|
||||
"ignoreFailures": true
|
||||
},
|
||||
{
|
||||
"description": "Load the generic $debug_graph(...) Debug Visualizer helper",
|
||||
"text": "source ${workspaceFolder}/gdb/debug_graph.py",
|
||||
"ignoreFailures": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"debugVisualizer.customVisualizerScriptPaths": [
|
||||
"${workspaceFolder}/vis-plugins/node-table-visualizer.js"
|
||||
]
|
||||
],
|
||||
"debugVisualizer.debugAdapterConfigurations": {
|
||||
"cppdbg": {
|
||||
"expressionTemplate": "$debug_graph(${expr})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
cpp/main.cpp
30
cpp/main.cpp
@@ -1,6 +1,5 @@
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
// A single node containing data and a raw pointer to the next node
|
||||
template <typename T>
|
||||
@@ -50,39 +49,10 @@ public:
|
||||
return head_ ? &head_->data : nullptr;
|
||||
}
|
||||
|
||||
// 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\":{\"nodeTable\":true},\"nodes\":[";
|
||||
for (Node<T>* n = head_; n; n = n->next) {
|
||||
if (n != head_) json << ",";
|
||||
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 << "\",\"label\":\"next\"}";
|
||||
}
|
||||
json << "]}";
|
||||
return json.str();
|
||||
}
|
||||
|
||||
private:
|
||||
Node<T>* head_;
|
||||
};
|
||||
|
||||
// Force instantiation of every member, including toDebugGraph(), which
|
||||
// nothing in this file calls directly -- without this, gdb has no symbol
|
||||
// to invoke from the debugger's watch expression.
|
||||
template class LinkedList<int>;
|
||||
|
||||
int main() {
|
||||
LinkedList<int> list;
|
||||
|
||||
|
||||
292
gdb/debug_graph.py
Normal file
292
gdb/debug_graph.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""Generic "pointer-memory structure" -> Debug Visualizer JSON walker.
|
||||
|
||||
Works on any struct/union reachable from an expression, purely by reading
|
||||
DWARF debug info through gdb's Python API -- no changes to the debuggee's
|
||||
source are required, and it works the same way for any language gdb (or
|
||||
rust-gdb) understands.
|
||||
|
||||
Rule: every member of a struct/union becomes one row in that node's field
|
||||
table. A member whose type is a pointer becomes an edge (and, if non-null,
|
||||
is recursed into); everything else is just stringified in place. A visited-
|
||||
address set makes this safe for cycles, so it handles lists, trees, and
|
||||
arbitrary (possibly cyclic) graphs uniformly.
|
||||
|
||||
Loaded via the "source" gdb command (see .vscode/launch.json), this defines
|
||||
a convenience function usable directly from the Debug Visualizer's Watch
|
||||
box:
|
||||
|
||||
$debug_graph(list)
|
||||
$debug_graph(list, "top", top) # extra name/pointer marker pairs
|
||||
"""
|
||||
|
||||
import gdb
|
||||
import json
|
||||
|
||||
# Marks the start of every payload this module returns (see json.dumps'
|
||||
# default separators below): used to recognize our own values in
|
||||
# _JsonResultPrettyPrinter without touching the user's own char* variables.
|
||||
_RESULT_MARKER = '{"kind": {"nodeTable"'
|
||||
|
||||
|
||||
def _addr_str(addr_int):
|
||||
return hex(addr_int)
|
||||
|
||||
|
||||
def _format_scalar(val):
|
||||
try:
|
||||
return str(val)
|
||||
except gdb.error as e:
|
||||
return "<error: %s>" % e
|
||||
|
||||
|
||||
def _is_variant_like(t):
|
||||
"""True for gdb's DWARF encoding of a Rust enum: a struct where one or
|
||||
more discriminant fields are marked "artificial" alongside the real
|
||||
variant payload fields. Deliberately loose (a C++ class with a vtable
|
||||
pointer can also have an artificial field) -- callers must tolerate
|
||||
_flatten_variant() finding no accessible variant and falling back
|
||||
gracefully."""
|
||||
if t.code != gdb.TYPE_CODE_STRUCT:
|
||||
return False
|
||||
try:
|
||||
fields = t.fields()
|
||||
except TypeError:
|
||||
return False
|
||||
has_artificial = any(getattr(f, "artificial", False) for f in fields)
|
||||
has_payload = any(not getattr(f, "artificial", False) for f in fields)
|
||||
return has_artificial and has_payload
|
||||
|
||||
|
||||
def _flatten_variant(val, t, label):
|
||||
"""For a live enum variant, yields (label, value) leaves to treat as if
|
||||
they were direct fields of the enclosing struct -- unwrapping the
|
||||
tuple-style __0 payload field so `next: Option<Box<Node<T>>>` reads
|
||||
exactly like a plain `next` pointer field. Yields nothing for a unit
|
||||
variant (e.g. None) or when no variant is accessible.
|
||||
|
||||
Which variant is live is determined by trying each non-artificial
|
||||
field and seeing which one gdb accepts -- accessing an inactive Rust
|
||||
enum variant raises gdb.error -- rather than parsing gdb's printed
|
||||
text, since that text's format depends on the debugger's current
|
||||
language setting."""
|
||||
for f in t.fields():
|
||||
if f.name is None or getattr(f, "artificial", False):
|
||||
continue
|
||||
try:
|
||||
variant_val = val[f.name]
|
||||
except gdb.error:
|
||||
continue
|
||||
variant_type = variant_val.type.strip_typedefs()
|
||||
if variant_type.code not in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
return # unit variant (e.g. None): no payload to flatten
|
||||
payload_fields = [pf for pf in variant_type.fields() if pf.name is not None]
|
||||
for pf in payload_fields:
|
||||
sub_label = (
|
||||
label if (pf.name == "__0" and len(payload_fields) == 1) else "%s.%s" % (label, pf.name)
|
||||
)
|
||||
yield sub_label, variant_val[pf.name]
|
||||
return
|
||||
|
||||
|
||||
def _process_member(label, member_val, node_id, fields_out, edges, nodes, visited):
|
||||
member_type = member_val.type.strip_typedefs()
|
||||
|
||||
if member_type.code == gdb.TYPE_CODE_PTR:
|
||||
try:
|
||||
ptr_int = int(member_val)
|
||||
except gdb.error:
|
||||
ptr_int = 0
|
||||
fields_out.append({"name": label, "value": _addr_str(ptr_int), "isPointer": True})
|
||||
if ptr_int:
|
||||
edges.append((node_id, _addr_str(ptr_int), label))
|
||||
_visit_pointer(member_val, ptr_int, nodes, edges, visited)
|
||||
return
|
||||
|
||||
if _is_variant_like(member_type):
|
||||
leaves = list(_flatten_variant(member_val, member_type, label))
|
||||
if leaves:
|
||||
for leaf_label, leaf_val in leaves:
|
||||
_process_member(leaf_label, leaf_val, node_id, fields_out, edges, nodes, visited)
|
||||
else:
|
||||
# Unit variant (e.g. None) or an active variant we couldn't
|
||||
# resolve -- fall back to gdb's own rendering of it.
|
||||
fields_out.append(
|
||||
{"name": label, "value": _format_scalar(member_val), "isPointer": False}
|
||||
)
|
||||
return
|
||||
|
||||
fields_out.append({"name": label, "value": _format_scalar(member_val), "isPointer": False})
|
||||
|
||||
|
||||
def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
"""val: a gdb.Value of struct/union type (already dereferenced)."""
|
||||
fields_out = []
|
||||
t = val.type.strip_typedefs()
|
||||
for f in t.fields():
|
||||
if f.name is None or getattr(f, "is_base_class", False):
|
||||
continue # anonymous member / base-class subobject
|
||||
try:
|
||||
member_val = val[f.name]
|
||||
except gdb.error:
|
||||
continue
|
||||
_process_member(f.name, member_val, node_id, fields_out, edges, nodes, visited)
|
||||
|
||||
nodes[node_id] = fields_out
|
||||
|
||||
|
||||
def _visit_pointer(ptr_val, ptr_int, nodes, edges, visited):
|
||||
node_id = _addr_str(ptr_int)
|
||||
if node_id in visited:
|
||||
return
|
||||
visited.add(node_id)
|
||||
|
||||
pointee_type = ptr_val.type.strip_typedefs().target().strip_typedefs()
|
||||
pointee = ptr_val.dereference()
|
||||
|
||||
if pointee_type.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
_describe_struct(pointee, node_id, nodes, edges, visited)
|
||||
else:
|
||||
# Pointer to a scalar (e.g. int*): still worth a leaf node.
|
||||
nodes[node_id] = [
|
||||
{"name": "value", "value": _format_scalar(pointee), "isPointer": False}
|
||||
]
|
||||
|
||||
|
||||
def build_graph_json(root_val, roots):
|
||||
nodes = {}
|
||||
edges = []
|
||||
visited = set()
|
||||
|
||||
t = root_val.type.strip_typedefs()
|
||||
if t.code == gdb.TYPE_CODE_PTR:
|
||||
ptr_int = int(root_val)
|
||||
if ptr_int:
|
||||
_visit_pointer(root_val, ptr_int, nodes, edges, visited)
|
||||
elif t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
node_id = _addr_str(int(root_val.address))
|
||||
visited.add(node_id)
|
||||
_describe_struct(root_val, node_id, nodes, edges, visited)
|
||||
else:
|
||||
nodes["root"] = [
|
||||
{"name": "value", "value": _format_scalar(root_val), "isPointer": False}
|
||||
]
|
||||
|
||||
nodes_json = [{"id": nid, "fields": fields} for nid, fields in nodes.items()]
|
||||
edges_json = [
|
||||
{"from": src, "to": dst, "label": label}
|
||||
for (src, dst, label) in edges
|
||||
if dst in nodes
|
||||
]
|
||||
roots_json = [{"name": name, "value": value} for (name, value) in roots]
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"kind": {"nodeTable": True},
|
||||
"nodes": nodes_json,
|
||||
"edges": edges_json,
|
||||
"roots": roots_json,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _as_debugger_string(text):
|
||||
"""Wrap `text` as a gdb.Value the Watch box will print as a proper
|
||||
quoted string. Letting Python's str->gdb.Value auto-conversion do this
|
||||
(i.e. just `return text`) produces a plain char array, which gdb's
|
||||
*Rust* value printer renders as a bracketed list of byte codes instead
|
||||
of text -- a display-time quirk based on the currently active
|
||||
language, not something fixable by constructing the value differently.
|
||||
Evaluating a string literal instead makes gdb's own (Rust- or
|
||||
C++-aware, whichever is active) expression parser build a proper
|
||||
string value, which prints correctly either way.
|
||||
|
||||
Under C++, VS Code's debug adapter (MIEngine) additionally gets this
|
||||
value through gdb's MI *variable-object* protocol (`-var-create`), not
|
||||
the plain CLI/`-data-evaluate-expression` path -- and for a raw
|
||||
`char[N]` array, that protocol reports the "value" as just a length
|
||||
placeholder like "[830]" with 830 one-character children, discarding
|
||||
the actual text entirely. `std::string` doesn't have this problem
|
||||
because libstdc++ registers a pretty-printer for it that overrides the
|
||||
default array display with a plain string and no children -- the
|
||||
_JsonResultPrettyPrinter registered below does the same thing for our
|
||||
values specifically, without needing std::string to exist in the
|
||||
debuggee (it wouldn't, since nothing in the target program uses it)."""
|
||||
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return gdb.parse_and_eval('"%s"' % escaped)
|
||||
|
||||
|
||||
class _JsonResultPrettyPrinter:
|
||||
"""Makes gdb's MI variable-object protocol (what VS Code's C++ debug
|
||||
adapter uses for Watch/evaluate) show our returned string in full, with
|
||||
no children -- exactly what libstdc++'s std::string pretty-printer
|
||||
does for MIEngine, which is why that approach worked when toDebugGraph
|
||||
was a real std::string-returning C++ method."""
|
||||
|
||||
def __init__(self, val):
|
||||
self._val = val
|
||||
|
||||
def to_string(self):
|
||||
try:
|
||||
return self._val.string()
|
||||
except (gdb.error, UnicodeDecodeError):
|
||||
return str(self._val)
|
||||
|
||||
def display_hint(self):
|
||||
return "string"
|
||||
|
||||
|
||||
def _pretty_print_lookup(val):
|
||||
t = val.type.strip_typedefs()
|
||||
if t.code == gdb.TYPE_CODE_PTR:
|
||||
elem_type = t.target().strip_typedefs()
|
||||
elif t.code == gdb.TYPE_CODE_ARRAY:
|
||||
elem_type = t.target().strip_typedefs()
|
||||
else:
|
||||
return None
|
||||
if elem_type.code not in (gdb.TYPE_CODE_INT, gdb.TYPE_CODE_CHAR) or elem_type.sizeof != 1:
|
||||
return None # not a char-like element type
|
||||
try:
|
||||
prefix = val.string(length=len(_RESULT_MARKER))
|
||||
except (gdb.error, UnicodeDecodeError):
|
||||
return None
|
||||
if prefix != _RESULT_MARKER:
|
||||
return None
|
||||
return _JsonResultPrettyPrinter(val)
|
||||
|
||||
|
||||
gdb.pretty_printers.append(_pretty_print_lookup)
|
||||
|
||||
|
||||
class DebugGraphFunction(gdb.Function):
|
||||
"""$debug_graph(EXPR[, name, ptr]...) -- render EXPR's pointer-linked
|
||||
structure as Debug Visualizer JSON. Extra name/pointer argument pairs
|
||||
are drawn as external marker pointers into the graph."""
|
||||
|
||||
def __init__(self):
|
||||
super(DebugGraphFunction, self).__init__("debug_graph")
|
||||
|
||||
def invoke(self, *args):
|
||||
if len(args) == 0:
|
||||
raise gdb.GdbError("usage: $debug_graph(expr, [name, ptr]...)")
|
||||
|
||||
root = args[0]
|
||||
extra = args[1:]
|
||||
roots = []
|
||||
i = 0
|
||||
while i + 1 < len(extra):
|
||||
name_val, ptr_val = extra[i], extra[i + 1]
|
||||
try:
|
||||
name = name_val.string()
|
||||
except (gdb.error, TypeError):
|
||||
name = str(name_val)
|
||||
try:
|
||||
ptr_int = int(ptr_val)
|
||||
except gdb.error:
|
||||
ptr_int = 0
|
||||
roots.append((name, _addr_str(ptr_int)))
|
||||
i += 2
|
||||
|
||||
return _as_debugger_string(build_graph_json(root, roots))
|
||||
|
||||
|
||||
DebugGraphFunction()
|
||||
114
scripts/dump_debug_graph.py
Executable file
114
scripts/dump_debug_graph.py
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drives both the C++ and Rust debug builds under gdb to their equivalent
|
||||
breakpoint (right after the 30/20/10 list is built), evaluates
|
||||
$debug_graph(list) exactly as VS Code's "Debug Visualizer" extension would
|
||||
(via -data-evaluate-expression), and replays the extension's own
|
||||
parseEvaluationResultFromGenericDebugAdapter algorithm on the raw result --
|
||||
so we can tell, without going through the VS Code UI, whether our JSON will
|
||||
actually be recognized or whether the extension will fall back to its
|
||||
generic byte-walk visualization.
|
||||
|
||||
The parsing port below mirrors:
|
||||
external/vscode-debug-visualizer/extension/src/VisualizationBackend/
|
||||
parseEvaluationResultFromGenericDebugAdapter.ts
|
||||
Keep it in sync if that file changes upstream.
|
||||
|
||||
Writes debug_cpp.json / debug_rust.json into the output directory (default
|
||||
~/lg, matching the style of the extension's own captured dumps): either the
|
||||
resolved data object on success, or a {"PARSE_ERROR": ..., "raw": ...}
|
||||
diagnostic on failure.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def run_gdb(gdb_bin, binary, break_file, break_line):
|
||||
commands = [
|
||||
"-gdb-set print elements 0",
|
||||
"-gdb-set print characters 0",
|
||||
"source %s" % (REPO / "gdb" / "debug_graph.py"),
|
||||
"-break-insert %s:%d" % (break_file, break_line),
|
||||
"-exec-run",
|
||||
'-data-evaluate-expression "$debug_graph(list)"',
|
||||
]
|
||||
proc = subprocess.run(
|
||||
[gdb_bin, "--interpreter=mi", binary],
|
||||
input="\n".join(commands).encode(),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=15,
|
||||
cwd=str(REPO),
|
||||
)
|
||||
out = proc.stdout.decode(errors="replace")
|
||||
matches = re.findall(r'\^done,value="(.*)"\s*$', out, re.M)
|
||||
errors = re.findall(r'\^error,msg="(.*)"\s*$', out, re.M)
|
||||
if not matches:
|
||||
raise RuntimeError("no ^done,value=... in gdb output; errors=%r\n---\n%s" % (errors, out))
|
||||
return matches[-1]
|
||||
|
||||
|
||||
def is_enclosed_with(s, ch):
|
||||
return s.startswith(ch) and s.endswith(ch) and len(s) >= 2
|
||||
|
||||
|
||||
def parse_like_extension(mi_escaped_value):
|
||||
"""Port of parseEvaluationResultFromGenericDebugAdapter, applied to the
|
||||
raw MI `value="..."` payload (itself one layer of MI's own
|
||||
string-escaping around whatever gdb's evaluate actually reported)."""
|
||||
result_text = json.loads('"' + mi_escaped_value + '"') # undo MI's escaping
|
||||
json_data = result_text.strip()
|
||||
|
||||
try:
|
||||
try:
|
||||
if is_enclosed_with(json_data, '"') or is_enclosed_with(json_data, "'"):
|
||||
json_data2 = json_data[1:-1]
|
||||
else:
|
||||
json_data2 = json_data
|
||||
result_obj = json.loads(json_data2)
|
||||
except Exception:
|
||||
# "in case of C++": the whole thing is itself a JSON string
|
||||
# literal; unwrap it once, then parse *that* as JSON.
|
||||
s = json.loads(json_data)
|
||||
result_obj = json.loads(s)
|
||||
return {"ok": True, "data": result_obj}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e), "raw": result_text}
|
||||
|
||||
|
||||
def dump(label, gdb_bin, binary, break_file, break_line, out_dir):
|
||||
try:
|
||||
raw_value = run_gdb(gdb_bin, binary, break_file, break_line)
|
||||
except Exception as e:
|
||||
result = {"PARSE_ERROR": "gdb invocation failed", "detail": str(e)}
|
||||
else:
|
||||
parsed = parse_like_extension(raw_value)
|
||||
if parsed["ok"]:
|
||||
result = parsed["data"]
|
||||
else:
|
||||
result = {
|
||||
"PARSE_ERROR": parsed["error"],
|
||||
"raw": parsed["raw"],
|
||||
"note": "This is what the Debug Visualizer extension would fail to parse, "
|
||||
"triggering its generic byte-walk fallback (constructGraphFromVariablesReference).",
|
||||
}
|
||||
|
||||
out_path = out_dir / ("debug_%s.json" % label)
|
||||
out_path.write_text(json.dumps(result, indent=4))
|
||||
print("Wrote %s (%s)" % (out_path, "OK" if "PARSE_ERROR" not in result else "PARSE_ERROR"))
|
||||
|
||||
|
||||
def main():
|
||||
out_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home() / "lg"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
dump("cpp", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 66, out_dir)
|
||||
dump("rust", "rust-gdb", str(REPO / "target" / "debug" / "list_example"), "src/main.rs", 51, out_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -10,6 +10,11 @@
|
||||
// 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;
|
||||
@@ -31,10 +36,16 @@ module.exports = function (register, lib) {
|
||||
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;
|
||||
@@ -44,6 +55,8 @@ module.exports = function (register, lib) {
|
||||
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) {
|
||||
@@ -146,6 +159,8 @@ module.exports = function (register, lib) {
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -164,12 +179,33 @@ module.exports = function (register, lib) {
|
||||
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);
|
||||
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",
|
||||
@@ -307,6 +343,48 @@ module.exports = function (register, lib) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user