Compare commits
7 Commits
dependabot
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da0e350b8 | ||
|
|
f220fa8df7 | ||
|
|
13a28ae1bb | ||
|
|
f254c3a31c | ||
|
|
18acf1d7f4 | ||
|
|
96c26e5388 | ||
|
|
e0fbe007fa |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
target
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -87,4 +88,4 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
*.vsix
|
||||
*.vsix
|
||||
|
||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -53,7 +53,8 @@
|
||||
"HOT_RELOAD": "",
|
||||
"USE_DEV_UI": ""
|
||||
},
|
||||
"outFiles": ["${workspaceFolder}/extension/dist/**/*.js"]
|
||||
"outFiles": ["${workspaceFolder}/extension/dist/**/*.js"],
|
||||
"preLaunchTask": "npm: build - extension"
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (Dev UI)",
|
||||
|
||||
13
.vscode/tasks.json
vendored
13
.vscode/tasks.json
vendored
@@ -15,6 +15,19 @@
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"label": "npm: build - extension",
|
||||
// Deliberately the ROOT package.json's "build" script (data-extraction
|
||||
// -> webview -> extension, in that order), not extension/'s own build
|
||||
// script -- the extension's webpack just *copies* whatever is already
|
||||
// sitting in webview/dist (via the node_modules/debug-visualizer-webview
|
||||
// symlink) into its own bundle rather than rebuilding it. Scoping this
|
||||
// task to extension/ alone silently ships a stale webview bundle.
|
||||
"script": "build",
|
||||
"problemMatcher": [],
|
||||
"group": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
80
demos/lists/cpp/main.cpp
Normal file
80
demos/lists/cpp/main.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
|
||||
// A single node containing data and a raw pointer to the next node
|
||||
template <typename T>
|
||||
struct Node {
|
||||
T data;
|
||||
Node<T>* next;
|
||||
};
|
||||
|
||||
// The main wrapper for the linked list tracking the head node
|
||||
template <typename T>
|
||||
class LinkedList {
|
||||
public:
|
||||
LinkedList() : head_(nullptr) {}
|
||||
|
||||
~LinkedList() {
|
||||
Node<T>* current = head_;
|
||||
while (current) {
|
||||
Node<T>* next = current->next;
|
||||
delete current;
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Raw-pointer ownership can't be safely duplicated by a shallow copy
|
||||
LinkedList(const LinkedList&) = delete;
|
||||
LinkedList& operator=(const LinkedList&) = delete;
|
||||
|
||||
// Add a new element to the front of the list
|
||||
void push_front(T data) {
|
||||
head_ = new Node<T>{std::move(data), head_};
|
||||
}
|
||||
|
||||
// Remove and return the front element of the list
|
||||
std::optional<T> pop_front() {
|
||||
if (!head_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Node<T>* old_head = head_;
|
||||
T data = std::move(old_head->data);
|
||||
head_ = old_head->next;
|
||||
delete old_head;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Read the front element without removing it
|
||||
const T* peek_front() const {
|
||||
return head_ ? &head_->data : nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
Node<T>* head_;
|
||||
};
|
||||
|
||||
int main() {
|
||||
LinkedList<int> list;
|
||||
int count=0;
|
||||
char * message1 = "hello world";
|
||||
// Demonstrate pushing items
|
||||
list.push_front(10);
|
||||
count++;
|
||||
list.push_front(20);
|
||||
count++;
|
||||
list.push_front(30);
|
||||
count++;
|
||||
|
||||
// Demonstrate peeking at the top item
|
||||
if (const int* top = list.peek_front()) {
|
||||
std::cout << "Top element: " << *top << "\n"<<"count: "<<count<<"\n"; // Output: 30
|
||||
}
|
||||
std::cout<<message1<<"\n";
|
||||
|
||||
// Demonstrate popping items
|
||||
while (auto value = list.pop_front()) {
|
||||
std::cout << "Popped: " << *value << "\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
16
demos/lists/python/.vscode/launch.json
vendored
Normal file
16
demos/lists/python/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Python Debugger: Current File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
63
demos/lists/python/main.py
Normal file
63
demos/lists/python/main.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, Optional, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
# A single node containing data and a reference to the next node
|
||||
@dataclass
|
||||
class Node(Generic[T]):
|
||||
data: T
|
||||
next: Optional["Node[T]"]
|
||||
|
||||
|
||||
# The main wrapper for the linked list tracking the head node
|
||||
class LinkedList(Generic[T]):
|
||||
def __init__(self) -> None:
|
||||
self._head: Optional[Node[T]] = None
|
||||
|
||||
# Add a new element to the front of the list
|
||||
def push_front(self, data: T) -> None:
|
||||
self._head = Node(data, self._head)
|
||||
|
||||
# Remove and return the front element of the list
|
||||
def pop_front(self) -> Optional[T]:
|
||||
if self._head is None:
|
||||
return None
|
||||
data = self._head.data
|
||||
self._head = self._head.next
|
||||
return data
|
||||
|
||||
# Read the front element without removing it
|
||||
def peek_front(self) -> Optional[T]:
|
||||
return self._head.data if self._head else None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lst: LinkedList[int] = LinkedList()
|
||||
count = 0
|
||||
message1 = "hello world"
|
||||
|
||||
# Demonstrate pushing items
|
||||
lst.push_front(10)
|
||||
count += 1
|
||||
lst.push_front(20)
|
||||
count += 1
|
||||
lst.push_front(30)
|
||||
count += 1
|
||||
|
||||
# Demonstrate peeking at the top item
|
||||
top = lst.peek_front()
|
||||
if top is not None:
|
||||
print(f"Top element: {top}\ncount: {count}") # Output: 30
|
||||
print(message1)
|
||||
|
||||
# Demonstrate popping items
|
||||
while (value := lst.pop_front()) is not None:
|
||||
print(f"Popped: {value}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
21
demos/lists/rust/.vscode/launch.json
vendored
Normal file
21
demos/lists/rust/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"name": "Debug list_example",
|
||||
"program": "${workspaceFolder}/target/debug/list_example",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"preLaunchTask": "cargo build",
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "/usr/bin/rust-gdb",
|
||||
"stopAtEntry": false,
|
||||
"externalConsole": false
|
||||
}
|
||||
]
|
||||
}
|
||||
17
demos/lists/rust/.vscode/tasks.json
vendored
Normal file
17
demos/lists/rust/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "cargo",
|
||||
"command": "build",
|
||||
"problemMatcher": [
|
||||
"$rustc"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"label": "cargo build"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
demos/lists/rust/Cargo.lock
generated
Normal file
7
demos/lists/rust/Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "list_example"
|
||||
version = "0.1.0"
|
||||
6
demos/lists/rust/Cargo.toml
Normal file
6
demos/lists/rust/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "list_example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
65
demos/lists/rust/src/main.rs
Normal file
65
demos/lists/rust/src/main.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
// A single node containing data and an optional pointer to the next node
|
||||
struct Node<T> {
|
||||
data: T,
|
||||
next: Option<Box<Node<T>>>,
|
||||
}
|
||||
|
||||
// The main wrapper for the linked list tracking the head node
|
||||
pub struct LinkedList<T> {
|
||||
head: Option<Box<Node<T>>>,
|
||||
}
|
||||
|
||||
impl<T> LinkedList<T> {
|
||||
// Create an empty linked list
|
||||
pub fn new() -> Self {
|
||||
LinkedList { head: None }
|
||||
}
|
||||
|
||||
// Add a new element to the front of the list
|
||||
pub fn push_front(&mut self, data: T) {
|
||||
let new_node = Box::new(Node {
|
||||
data,
|
||||
// .take() temporarily leaves None in self.head and extracts the value
|
||||
next: self.head.take(),
|
||||
});
|
||||
self.head = Some(new_node);
|
||||
}
|
||||
|
||||
// Remove and return the front element of the list
|
||||
pub fn pop_front(&mut self) -> Option<T> {
|
||||
self.head.take().map(|node| {
|
||||
self.head = node.next;
|
||||
node.data
|
||||
})
|
||||
}
|
||||
|
||||
// Read the front element without removing it
|
||||
pub fn peek_front(&self) -> Option<&T> {
|
||||
self.head.as_ref().map(|node| &node.data)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut list = LinkedList::new();
|
||||
let mut count = 0;
|
||||
let message1 = "hello world";
|
||||
|
||||
// Demonstrate pushing items
|
||||
list.push_front(10);
|
||||
count += 1;
|
||||
list.push_front(20);
|
||||
count += 1;
|
||||
list.push_front(30);
|
||||
count += 1;
|
||||
|
||||
// Demonstrate peeking at the top item
|
||||
if let Some(top) = list.peek_front() {
|
||||
println!("Top element: {}\ncount: {}", top, count); // Output: 30
|
||||
}
|
||||
println!("{}", message1);
|
||||
|
||||
// Demonstrate popping items
|
||||
while let Some(value) = list.pop_front() {
|
||||
println!("Popped: {}", value);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@
|
||||
"displayName": "Debug Visualizer",
|
||||
"description": "A visual watch window that lets you visualize your data structures while debugging.",
|
||||
"icon": "docs/logo.drawio.png",
|
||||
"version": "2.6.0",
|
||||
"version": "3.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
},
|
||||
"publisher": "hediet",
|
||||
"publisher": "ai-coder",
|
||||
"keywords": [
|
||||
"debugger",
|
||||
"debugging",
|
||||
@@ -21,7 +21,7 @@
|
||||
"visualization"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/hediet/vscode-debug-visualizer/issues"
|
||||
"url": "https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer/issues"
|
||||
},
|
||||
"author": {
|
||||
"email": "henning.dieterichs@live.de",
|
||||
@@ -30,7 +30,7 @@
|
||||
"readme": "./README.md",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hediet/vscode-debug-visualizer.git"
|
||||
"url": "https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
@@ -103,6 +103,15 @@
|
||||
"repl"
|
||||
],
|
||||
"description": "The context to use for evaluating the expression."
|
||||
},
|
||||
"nodeTableGraph": {
|
||||
"type": "boolean",
|
||||
"description": "Use the built-in node-table graph visualization for this debug adapter type, walking the expression via the standard evaluate/variables/scopes requests instead of expressionTemplate. Enabled by default for every adapter type except ones with a more specific built-in engine (node/chrome/pwa-* JS adapters, rdbg); set to false to fall back to expressionTemplate for a given adapter type, or true to opt a JS/Ruby adapter back in."
|
||||
},
|
||||
"setupExpression": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Commands run once per debug session (context \"repl\") before the node-table graph walk -- e.g. gdb's pretty-printing/print-elements/print-characters options."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +121,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"pub": "vsce publish --packagePath ./dist/extension.vsix",
|
||||
"package": "vsce package --no-dependencies -o ./dist/extension.vsix --yarn --baseImagesUrl https://github.com/hediet/vscode-debug-visualizer/raw/master/extension --baseContentUrl https://github.com/hediet/vscode-debug-visualizer/raw/master/extension",
|
||||
"package": "vsce package --no-dependencies -o ./dist/extension.vsix --yarn --baseImagesUrl https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer/raw/master/extension --baseContentUrl https://git.fairfaxmedia.net/ai_coder/vscode-debug-visualizer/raw/master/extension",
|
||||
"vscode:prepublish": "",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "shx rm -rf dist && tsc -watch -p ./"
|
||||
|
||||
@@ -87,21 +87,65 @@ export class Config {
|
||||
);
|
||||
}
|
||||
|
||||
// Adapter types that already have a dedicated, richer visualization
|
||||
// engine (JsEvaluationEngine injects real typed data-extractors into the
|
||||
// debuggee; RbEvaluationEngine similarly) -- the generic node-table graph
|
||||
// defaults to *on* for every other adapter type (see getDebugAdapterConfig
|
||||
// below) so it works out of the box across languages with no workspace
|
||||
// config required, but shouldn't shadow those two's better-than-generic
|
||||
// support unless a user explicitly opts back in via
|
||||
// `debugVisualizer.debugAdapterConfigurations`.
|
||||
private static readonly nodeTableGraphExcludedAdapters = new Set([
|
||||
"node",
|
||||
"node2",
|
||||
"extensionHost",
|
||||
"chrome",
|
||||
"pwa-chrome",
|
||||
"pwa-node",
|
||||
"pwa-extensionHost",
|
||||
"node-terminal",
|
||||
"pwa-msedge",
|
||||
"rdbg",
|
||||
]);
|
||||
|
||||
// Built-in per-adapter extras beyond the nodeTableGraph on/off default
|
||||
// above -- currently just gdb's display-option setup commands, so the
|
||||
// node-table graph's pretty-printed/full-length output works with no
|
||||
// workspace config required. The user's own
|
||||
// `debugVisualizer.debugAdapterConfigurations` setting still wins
|
||||
// field-by-field (see getDebugAdapterConfig's merge below).
|
||||
private static readonly builtInDebugAdapterConfigs: DebugAdapterConfigs = {
|
||||
cppdbg: {
|
||||
setupExpression: [
|
||||
"-enable-pretty-printing",
|
||||
"-gdb-set print elements 0",
|
||||
"-gdb-set print characters 0",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
public getDebugAdapterConfig(
|
||||
debugAdapterType: string
|
||||
): DebugAdapterConfig | undefined {
|
||||
const c = this._debugAdapterConfigs.get()[debugAdapterType];
|
||||
if (!c) {
|
||||
const builtIn = Config.builtInDebugAdapterConfigs[debugAdapterType];
|
||||
const user = this._debugAdapterConfigs.get()[debugAdapterType];
|
||||
const defaultNodeTableGraph = !Config.nodeTableGraphExcludedAdapters.has(
|
||||
debugAdapterType
|
||||
);
|
||||
if (!builtIn && !user && !defaultNodeTableGraph) {
|
||||
return undefined;
|
||||
}
|
||||
const c = { nodeTableGraph: defaultNodeTableGraph, ...builtIn, ...user };
|
||||
|
||||
return {
|
||||
context: c.context || "watch",
|
||||
nodeTableGraph: c.nodeTableGraph,
|
||||
getFinalExpression: ({ expression, preferredExtractorId }) =>
|
||||
evaluateTemplate(c.expressionTemplate || "${expr}", {
|
||||
expr: expression,
|
||||
preferredDataExtractorId: preferredExtractorId || "",
|
||||
}),
|
||||
getSetupExpression: () => c.setupExpression,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -110,15 +154,19 @@ type DebugAdapterConfigs = {
|
||||
[debugAdapter: string]: {
|
||||
context?: "watch" | "repl";
|
||||
expressionTemplate?: "string";
|
||||
nodeTableGraph?: boolean;
|
||||
setupExpression?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export interface DebugAdapterConfig {
|
||||
context: "watch" | "repl";
|
||||
nodeTableGraph: boolean;
|
||||
getFinalExpression(vars: {
|
||||
expression: string;
|
||||
preferredExtractorId: DataExtractorId | undefined;
|
||||
}): string;
|
||||
getSetupExpression(): string[] | undefined;
|
||||
}
|
||||
|
||||
function evaluateTemplate(
|
||||
|
||||
@@ -131,4 +131,14 @@ export class DispatchingVisualizationBackend implements VisualizationBackend {
|
||||
)) || []
|
||||
);
|
||||
}
|
||||
|
||||
public async evaluateForEffect(
|
||||
expression: string
|
||||
): Promise<{ result: string } | { error: string }> {
|
||||
const backend = this.activeVisualizationBackend;
|
||||
if (!backend) {
|
||||
return { error: "No active debug session." };
|
||||
}
|
||||
return await backend.evaluateForEffect(expression);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,676 @@
|
||||
import {
|
||||
DataExtractionResult,
|
||||
VisualizationData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||
import { Config, DebugAdapterConfig } from "../Config";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||
import { FormattedMessage } from "../webviewContract";
|
||||
import {
|
||||
DebugSessionVisualizationSupport,
|
||||
GetVisualizationDataArgs,
|
||||
VisualizationBackend,
|
||||
VisualizationBackendBase,
|
||||
} from "./VisualizationBackend";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
// A generic, DAP-only replacement for the old gdb_debug_graph.py/
|
||||
// py_debug_graph.py debuggee-side scripts: walks whatever the debug adapter
|
||||
// already exposes via the standard "evaluate"/"variables"/"scopes" requests
|
||||
// and produces the same { kind: { nodeTable: true }, nodes, edges, roots }
|
||||
// shape the node-table-graph webview visualizer expects (see
|
||||
// webview/src/visualizers/nodeTableGraphVisualizer.ts) -- no script needs to
|
||||
// be sourced into gdb or imported into the debuggee's Python process.
|
||||
|
||||
interface NodeField {
|
||||
name: string;
|
||||
value: string;
|
||||
isPointer?: boolean;
|
||||
typeHint?: string;
|
||||
}
|
||||
|
||||
interface ArrayTableColumn {
|
||||
id: string;
|
||||
// Aligned by index with the owning ArrayTable's memberNames -- a member
|
||||
// this particular element doesn't have gets an empty-value placeholder
|
||||
// rather than shifting the columns out of alignment.
|
||||
values: NodeField[];
|
||||
}
|
||||
|
||||
interface ArrayTable {
|
||||
memberNames: string[];
|
||||
columns: ArrayTableColumn[];
|
||||
// The real element count and whether `columns` was capped at
|
||||
// MAX_ARRAY_ELEMENTS -- lets the webview show "N of M" instead of
|
||||
// silently hiding that the rest of a very large array wasn't fetched.
|
||||
totalCount: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
interface GraphNodeData {
|
||||
id: string;
|
||||
fields: NodeField[];
|
||||
type?: string;
|
||||
// Present instead of a meaningful `fields` (left []) when this node is an
|
||||
// array/slice/vector of struct-shaped elements -- rendered as a compact
|
||||
// table (one row per member, one column per element) instead of a chain
|
||||
// of per-element boxes. See looksLikeArrayOfStructs/buildArrayTableNode.
|
||||
arrayTable?: ArrayTable;
|
||||
}
|
||||
|
||||
interface GraphEdge {
|
||||
from: string;
|
||||
to: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface GraphRoot {
|
||||
name: string;
|
||||
value: string;
|
||||
kind?: "local" | "watched" | "global";
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface NodeTableVisualizationData extends VisualizationData {
|
||||
kind: { nodeTable: true };
|
||||
lang: string;
|
||||
nodes: GraphNodeData[];
|
||||
edges: GraphEdge[];
|
||||
roots: GraphRoot[];
|
||||
}
|
||||
|
||||
interface DapVariable {
|
||||
name: string;
|
||||
value: string;
|
||||
variablesReference: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
// A field only becomes its own graph node (and edge) when it looks like a
|
||||
// genuine pointer with an independent address -- everything else (by-value
|
||||
// nested structs, Option/std::optional wrappers, ...) is flattened inline as
|
||||
// dotted sub-fields instead, up to this depth. Once `-enable-pretty-printing`
|
||||
// is on (see Config.ts's built-in cppdbg setupExpression), a wrapper's DAP
|
||||
// children already reflect gdb's own pretty-printer output, so this stays a
|
||||
// shallow structural rule rather than needing to special-case any container
|
||||
// type by name.
|
||||
const INLINE_FLATTEN_MAX_DEPTH = 3;
|
||||
const ADDRESS_RE = /0x[0-9a-f]+/i;
|
||||
|
||||
// Fetching (and walking the members of) every element of a very large
|
||||
// array/slice/vector is both slow (one DAP round trip per element) and
|
||||
// produces an unusably wide table -- cap it and let the webview page
|
||||
// through what's fetched instead of hanging or rendering thousands of
|
||||
// columns at once.
|
||||
const MAX_ARRAY_ELEMENTS = 500;
|
||||
|
||||
// pydevd/debugpy (Python) injects these as synthetic pseudo-children into
|
||||
// *every* object's variable listing, grouping dunder attributes and bound
|
||||
// methods for the Variables view -- they aren't real fields. Left
|
||||
// unfiltered, the inline-flatten path in addField() (which has no notion of
|
||||
// "this isn't a real field, don't recurse") walks straight into them: each
|
||||
// one is itself an object with its own nested "special variables", so it
|
||||
// recurses into Python's dict/type/method-wrapper internals. Measured on a
|
||||
// 3-node linked list: ~126 DAP round trips and 400+ flattened junk fields,
|
||||
// several seconds, on every single refresh -- the "Debug Visualizer
|
||||
// freezes" symptom reported against a Python (debugpy) session. gdb/delve
|
||||
// have no equivalent synthetic grouping, so this is debugpy-specific.
|
||||
const PSEUDO_SCOPE_NAMES = new Set([
|
||||
"special variables",
|
||||
"function variables",
|
||||
"class variables",
|
||||
]);
|
||||
|
||||
@hotClass(module)
|
||||
export class NodeTableGraphEngine implements DebugSessionVisualizationSupport {
|
||||
constructor(
|
||||
private readonly debuggerView: DebuggerViewProxy,
|
||||
private readonly config: Config
|
||||
) {}
|
||||
|
||||
createBackend(
|
||||
session: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
const config = this.config.getDebugAdapterConfig(session.session.type);
|
||||
if (!config || !config.nodeTableGraph) {
|
||||
return undefined;
|
||||
}
|
||||
return new NodeTableGraphVisualizationBackend(
|
||||
session,
|
||||
this.debuggerView,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeTableGraphVisualizationBackend extends VisualizationBackendBase {
|
||||
public readonly expressionLanguageId = "text";
|
||||
|
||||
constructor(
|
||||
debugSession: DebugSessionProxy,
|
||||
debuggerView: DebuggerViewProxy,
|
||||
private readonly config: DebugAdapterConfig
|
||||
) {
|
||||
super(debugSession, debuggerView);
|
||||
}
|
||||
|
||||
private detectLang(): string {
|
||||
const type = this.debugSession.session.type;
|
||||
if (type === "debugpy" || type === "python") {
|
||||
return "python";
|
||||
}
|
||||
if (type === "go") {
|
||||
return "go";
|
||||
}
|
||||
const miDebuggerPath = String(
|
||||
(this.debugSession.session.configuration as { miDebuggerPath?: string })
|
||||
.miDebuggerPath || ""
|
||||
);
|
||||
if (miDebuggerPath.includes("rust-gdb")) {
|
||||
return "rust";
|
||||
}
|
||||
return "cpp";
|
||||
}
|
||||
|
||||
public async getVisualizationData({
|
||||
expression,
|
||||
sessionStore,
|
||||
}: GetVisualizationDataArgs): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
> {
|
||||
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||
this.debugSession
|
||||
);
|
||||
|
||||
try {
|
||||
await this.runSetupOnce(sessionStore, frameId);
|
||||
|
||||
const nodes = new Map<string, GraphNodeData>();
|
||||
const edges: GraphEdge[] = [];
|
||||
const visited = new Set<string>();
|
||||
const roots: GraphRoot[] = [];
|
||||
|
||||
const primary = await this.debugSession.evaluate({
|
||||
expression,
|
||||
frameId,
|
||||
context: this.config.context,
|
||||
});
|
||||
|
||||
let primaryId: string | undefined;
|
||||
if (primary.variablesReference > 0) {
|
||||
primaryId = await this.walkNodeOrArrayTable(
|
||||
primary.variablesReference,
|
||||
primary.result,
|
||||
nodes,
|
||||
edges,
|
||||
visited,
|
||||
undefined,
|
||||
primary.type
|
||||
);
|
||||
} else {
|
||||
nodes.set("root", {
|
||||
id: "root",
|
||||
fields: [{ name: "value", value: primary.result }],
|
||||
});
|
||||
}
|
||||
|
||||
await this.discoverLocals(frameId, primaryId, nodes, edges, visited, roots);
|
||||
|
||||
const data: NodeTableVisualizationData = {
|
||||
kind: { nodeTable: true },
|
||||
lang: this.detectLang(),
|
||||
nodes: [...nodes.values()],
|
||||
edges,
|
||||
roots,
|
||||
};
|
||||
|
||||
return {
|
||||
kind: "data",
|
||||
result: {
|
||||
availableExtractors: [],
|
||||
usedExtractor: {
|
||||
id: "node-table-graph" as any,
|
||||
name: "Node Table Graph",
|
||||
priority: 2000,
|
||||
},
|
||||
data,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: {
|
||||
kind: "list",
|
||||
items: [
|
||||
"An error occurred while building the node table graph:",
|
||||
error.message,
|
||||
`Used debug adapter: ${this.debugSession.session.configuration.type}`,
|
||||
{
|
||||
kind: "inlineList",
|
||||
items: [
|
||||
"Evaluated expression is",
|
||||
{ kind: "code", content: expression },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the adapter's setup commands (e.g. gdb's pretty-printing/
|
||||
// print-elements/print-characters options) once per debug session
|
||||
// instead of on every refresh -- sessionStore.data is a per-session slot
|
||||
// VisualizationWatchModel already keeps around for exactly this purpose.
|
||||
private async runSetupOnce(
|
||||
sessionStore: { data: unknown },
|
||||
frameId: number | undefined
|
||||
) {
|
||||
const store = sessionStore.data as
|
||||
| { nodeTableGraphSetupDone?: boolean }
|
||||
| undefined;
|
||||
if (store && store.nodeTableGraphSetupDone) {
|
||||
return;
|
||||
}
|
||||
const commands = this.config.getSetupExpression();
|
||||
if (commands) {
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
await this.debugSession.evaluate({
|
||||
expression: cmd,
|
||||
frameId,
|
||||
context: "repl",
|
||||
});
|
||||
} catch {
|
||||
// best-effort -- fall through, the real walk below will
|
||||
// still surface a clear error if something's actually wrong
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionStore.data = { ...(store || {}), nodeTableGraphSetupDone: true };
|
||||
}
|
||||
|
||||
private async discoverLocals(
|
||||
frameId: number | undefined,
|
||||
primaryId: string | undefined,
|
||||
nodes: Map<string, GraphNodeData>,
|
||||
edges: GraphEdge[],
|
||||
visited: Set<string>,
|
||||
roots: GraphRoot[]
|
||||
) {
|
||||
if (frameId === undefined) {
|
||||
return;
|
||||
}
|
||||
const scopes = await this.debugSession.getScopes({ frameId });
|
||||
const localsScope = scopes.find((s) => /local/i.test(s.name));
|
||||
if (!localsScope) {
|
||||
return;
|
||||
}
|
||||
const localVars = await this.debugSession.getVariables({
|
||||
variablesReference: localsScope.variablesReference,
|
||||
});
|
||||
for (const v of localVars as DapVariable[]) {
|
||||
const addr = this.extractAddress(v.value);
|
||||
if (primaryId !== undefined && addr === primaryId) {
|
||||
continue; // already covered by the primary root
|
||||
}
|
||||
if (v.variablesReference > 0) {
|
||||
const id = await this.walkNodeOrArrayTable(
|
||||
v.variablesReference,
|
||||
v.value,
|
||||
nodes,
|
||||
edges,
|
||||
visited,
|
||||
undefined,
|
||||
v.type
|
||||
);
|
||||
roots.push({ name: v.name, value: id, kind: "local", type: v.type });
|
||||
} else {
|
||||
roots.push({
|
||||
name: v.name,
|
||||
value: v.value,
|
||||
kind: "local",
|
||||
type: v.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds an address that plausibly belongs to `value` *itself*, not to
|
||||
// something nested inside its own summary text. Some adapters (delve,
|
||||
// with dlvLoadConfig's followPointers on) inline a pointer's entire
|
||||
// pointee summary -- including further nested pointers' addresses --
|
||||
// into the value string without ever printing the pointer's own
|
||||
// address anywhere; naively taking the first "0x..." match there would
|
||||
// silently misattribute some nested field's address as this value's own
|
||||
// identity. Rejecting a match that only appears after the value's own
|
||||
// summary has already opened a nested struct/array (a "{" or "[")
|
||||
// filters those out, while still matching the common bare-address
|
||||
// ("0x1234") and gdb-style ("0x1234 <Node>") and Python-repr
|
||||
// ("<Foo object at 0x1234>") shapes, none of which have that.
|
||||
private extractAddress(value: string): string | undefined {
|
||||
const m = ADDRESS_RE.exec(value);
|
||||
if (!m) {
|
||||
return undefined;
|
||||
}
|
||||
if (/[{[]/.test(value.slice(0, m.index))) {
|
||||
return undefined;
|
||||
}
|
||||
return m[0].toLowerCase();
|
||||
}
|
||||
|
||||
// Some debug adapters (delve/Go, at least) represent a pointer's
|
||||
// dereference as a single transparent child with an empty name, rather
|
||||
// than exposing the pointee's fields/elements directly -- unwrap through
|
||||
// it (recursively, in case of multiple indirections, e.g. `**T`) so
|
||||
// field names and array detection see the real members, not one
|
||||
// anonymous wrapper hop away from them.
|
||||
private async getResolvedChildren(
|
||||
variablesReference: number
|
||||
): Promise<DapVariable[]> {
|
||||
let children = await this.fetchRealChildren(variablesReference);
|
||||
while (
|
||||
children.length === 1 &&
|
||||
children[0].name === "" &&
|
||||
children[0].variablesReference > 0
|
||||
) {
|
||||
children = await this.fetchRealChildren(children[0].variablesReference);
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
// Filters out pydevd/debugpy's synthetic "special variables"/"function
|
||||
// variables" pseudo-children (see PSEUDO_SCOPE_NAMES) right at the
|
||||
// source, so nothing downstream -- array-of-structs detection, the
|
||||
// inline-flatten walk, array table columns -- ever has to know they
|
||||
// exist.
|
||||
private async fetchRealChildren(
|
||||
variablesReference: number
|
||||
): Promise<DapVariable[]> {
|
||||
const children = (await this.debugSession.getVariables({
|
||||
variablesReference,
|
||||
})) as DapVariable[];
|
||||
return children.filter((c) => !PSEUDO_SCOPE_NAMES.has(c.name));
|
||||
}
|
||||
|
||||
private looksLikePointer(v: { type?: string; value: string }): boolean {
|
||||
if (v.type) {
|
||||
// C/C++ (rust-gdb prints Rust pointers/references the same way
|
||||
// gdb's own C type-printer would): trailing sigil, e.g. "Node *".
|
||||
if (/\*\s*$/.test(v.type)) {
|
||||
return true;
|
||||
}
|
||||
// Go (delve) and Rust's own `*mut T`/`*const T`/`&T` syntax put
|
||||
// the sigil in front instead, e.g. "*main.Node", "&Node<i32>".
|
||||
if (/^\s*[*&]/.test(v.type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (/<.*object at 0x[0-9a-f]+>/i.test(v.value)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A value counts as "array of classes" when every child's name is a bare
|
||||
// or bracketed index (0,1,2,... or [0],[1],[2],...) -- the DAP naming
|
||||
// convention arrays/slices/vectors already use across gdb/MIEngine,
|
||||
// debugpy, and delve -- and the first element is itself struct-shaped
|
||||
// (has its own children), not a scalar leaf. Only the first element is
|
||||
// checked, on the assumption the array is homogeneous (true for every
|
||||
// language this targets); a scalar array (e.g. `int[5]`) intentionally
|
||||
// falls through to the ordinary inline-flatten path instead.
|
||||
private looksLikeArrayOfStructs(children: DapVariable[]): boolean {
|
||||
if (children.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (!children.every((c) => /^\[?\d+\]?$/.test(c.name))) {
|
||||
return false;
|
||||
}
|
||||
return children[0].variablesReference > 0;
|
||||
}
|
||||
|
||||
// Recursively walks a DAP variablesReference into `nodes`/`edges`,
|
||||
// returning the id of the node it produced -- a real address when one
|
||||
// is present in the variable's own printed value, else the DAP
|
||||
// variablesReference itself (verified against a real debugpy session,
|
||||
// including through a genuine self-referential cycle, to be stably
|
||||
// reused for the same underlying object -- not just an arbitrary
|
||||
// per-call counter, which would neither dedup re-visits of the same
|
||||
// addressless object nor terminate a true cycle). Mirrors the old
|
||||
// scripts' hex(ptr_int)/id(obj)-derived node ids, giving the same
|
||||
// cross-reference cycle dedup via `visited`. Routes to either a
|
||||
// regular struct node or an array-table node depending on shape, so an
|
||||
// array of structs gets the compact table rendering everywhere it
|
||||
// appears -- as the primary root, a struct's own field, a local, or a
|
||||
// pointer's target -- not just in one specific position.
|
||||
private async walkNodeOrArrayTable(
|
||||
variablesReference: number,
|
||||
ownValue: string,
|
||||
nodes: Map<string, GraphNodeData>,
|
||||
edges: GraphEdge[],
|
||||
visited: Set<string>,
|
||||
prefetchedChildren?: DapVariable[],
|
||||
ownType?: string
|
||||
): Promise<string> {
|
||||
const id = this.extractAddress(ownValue) || `vref:${variablesReference}`;
|
||||
if (visited.has(id)) {
|
||||
return id;
|
||||
}
|
||||
visited.add(id);
|
||||
|
||||
const children =
|
||||
prefetchedChildren ?? (await this.getResolvedChildren(variablesReference));
|
||||
|
||||
if (this.looksLikeArrayOfStructs(children)) {
|
||||
await this.buildArrayTableNode(id, children, nodes, edges, visited);
|
||||
} else {
|
||||
await this.buildStructFields(id, children, nodes, edges, visited, ownType);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private async buildStructFields(
|
||||
id: string,
|
||||
children: DapVariable[],
|
||||
nodes: Map<string, GraphNodeData>,
|
||||
edges: GraphEdge[],
|
||||
visited: Set<string>,
|
||||
ownType?: string
|
||||
) {
|
||||
const fields: NodeField[] = [];
|
||||
for (const child of children) {
|
||||
await this.addField(child, fields, edges, nodes, visited, id, 0, ownType);
|
||||
}
|
||||
nodes.set(id, { id, fields, type: ownType });
|
||||
}
|
||||
|
||||
// Builds a single node holding every element's members as columns
|
||||
// (memberNames is the union of member names across elements, in
|
||||
// first-seen order, so a ragged/heterogeneous array still lines up
|
||||
// sensibly). A pointer member is still a real edge out of this same
|
||||
// node for each element that has one, labeled "[i].member" -- pointers
|
||||
// stay navigable everywhere, they just fan out from one box instead of
|
||||
// each element getting its own.
|
||||
private async buildArrayTableNode(
|
||||
id: string,
|
||||
allElements: DapVariable[],
|
||||
nodes: Map<string, GraphNodeData>,
|
||||
edges: GraphEdge[],
|
||||
visited: Set<string>
|
||||
) {
|
||||
const totalCount = allElements.length;
|
||||
const truncated = totalCount > MAX_ARRAY_ELEMENTS;
|
||||
const elements = truncated
|
||||
? allElements.slice(0, MAX_ARRAY_ELEMENTS)
|
||||
: allElements;
|
||||
|
||||
const memberNames: string[] = [];
|
||||
const seenMembers = new Set<string>();
|
||||
const columnCells: Map<string, NodeField>[] = [];
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const cells = new Map<string, NodeField>();
|
||||
const el = elements[i];
|
||||
if (el.variablesReference > 0) {
|
||||
const members = await this.getResolvedChildren(el.variablesReference);
|
||||
for (const m of members) {
|
||||
if (!seenMembers.has(m.name)) {
|
||||
seenMembers.add(m.name);
|
||||
memberNames.push(m.name);
|
||||
}
|
||||
const addr = this.extractAddress(m.value);
|
||||
if (addr && this.looksLikePointer(m)) {
|
||||
if (m.variablesReference > 0) {
|
||||
const targetId = await this.walkNodeOrArrayTable(
|
||||
m.variablesReference,
|
||||
m.value,
|
||||
nodes,
|
||||
edges,
|
||||
visited,
|
||||
undefined,
|
||||
m.type
|
||||
);
|
||||
edges.push({
|
||||
from: id,
|
||||
to: targetId,
|
||||
label: `[${i}].${m.name}`,
|
||||
});
|
||||
}
|
||||
cells.set(m.name, {
|
||||
name: m.name,
|
||||
value: addr,
|
||||
isPointer: true,
|
||||
typeHint: m.type,
|
||||
});
|
||||
} else {
|
||||
cells.set(m.name, {
|
||||
name: m.name,
|
||||
value: m.value,
|
||||
typeHint: m.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
columnCells.push(cells);
|
||||
}
|
||||
|
||||
const columns: ArrayTableColumn[] = elements.map((el, i) => ({
|
||||
id: /^\[\d+\]$/.test(el.name) ? el.name : `[${el.name}]`,
|
||||
values: memberNames.map(
|
||||
(m) => columnCells[i].get(m) || { name: m, value: "" }
|
||||
),
|
||||
}));
|
||||
|
||||
nodes.set(id, {
|
||||
id,
|
||||
fields: [],
|
||||
arrayTable: { memberNames, columns, totalCount, truncated },
|
||||
});
|
||||
}
|
||||
|
||||
private async addField(
|
||||
child: DapVariable,
|
||||
fields: NodeField[],
|
||||
edges: GraphEdge[],
|
||||
nodes: Map<string, GraphNodeData>,
|
||||
visited: Set<string>,
|
||||
ownerId: string,
|
||||
depth: number,
|
||||
ownerType?: string
|
||||
) {
|
||||
const addr = this.extractAddress(child.value);
|
||||
// A field whose *static type* matches the type of the struct that
|
||||
// directly contains it (not necessarily the original top-level
|
||||
// owner -- see the recursive call below, which passes child.type
|
||||
// as the new ownerType) can only be sound if it's some kind of
|
||||
// reference/handle rather than a true by-value member: a value type
|
||||
// genuinely containing itself would have infinite size. This is
|
||||
// what catches Python's (and similarly Java/C#'s) recursive
|
||||
// dataclasses/objects, which never print an address in their repr
|
||||
// (looksLikePointer's <object at 0x...> check only fires for
|
||||
// classes that haven't overridden __repr__ -- dataclasses always
|
||||
// do) and so would otherwise fall through to the inline-flatten
|
||||
// path below and get capped/mangled at INLINE_FLATTEN_MAX_DEPTH
|
||||
// instead of forming a proper linked node graph.
|
||||
const isSelfReferential = !!ownerType && !!child.type && child.type === ownerType;
|
||||
if ((addr && this.looksLikePointer(child)) || isSelfReferential) {
|
||||
let targetId: string | undefined;
|
||||
if (child.variablesReference > 0) {
|
||||
targetId = await this.walkNodeOrArrayTable(
|
||||
child.variablesReference,
|
||||
child.value,
|
||||
nodes,
|
||||
edges,
|
||||
visited,
|
||||
undefined,
|
||||
child.type
|
||||
);
|
||||
edges.push({ from: ownerId, to: targetId, label: child.name });
|
||||
}
|
||||
fields.push({
|
||||
name: child.name,
|
||||
// Real pointers show their address; a self-referential
|
||||
// field with no address of its own shows the id of the
|
||||
// node it resolves to instead (still a stable, clickable
|
||||
// cross-reference) -- child.value as a last-resort fallback
|
||||
// covers a null/childless self-typed field (unusual, but
|
||||
// keeps this from rendering an empty value).
|
||||
value: addr || targetId || child.value,
|
||||
isPointer: true,
|
||||
typeHint: child.type,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (child.variablesReference > 0) {
|
||||
const grandchildren = await this.getResolvedChildren(
|
||||
child.variablesReference
|
||||
);
|
||||
|
||||
if (this.looksLikeArrayOfStructs(grandchildren)) {
|
||||
const targetId = await this.walkNodeOrArrayTable(
|
||||
child.variablesReference,
|
||||
child.value,
|
||||
nodes,
|
||||
edges,
|
||||
visited,
|
||||
grandchildren
|
||||
);
|
||||
edges.push({ from: ownerId, to: targetId, label: child.name });
|
||||
fields.push({
|
||||
name: child.name,
|
||||
value: targetId,
|
||||
isPointer: true,
|
||||
typeHint: child.type,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth < INLINE_FLATTEN_MAX_DEPTH) {
|
||||
if (grandchildren.length === 0) {
|
||||
fields.push({ name: child.name, value: child.value, typeHint: child.type });
|
||||
return;
|
||||
}
|
||||
for (const gc of grandchildren) {
|
||||
await this.addField(
|
||||
{ ...gc, name: `${child.name}.${gc.name}` },
|
||||
fields,
|
||||
edges,
|
||||
nodes,
|
||||
visited,
|
||||
ownerId,
|
||||
depth + 1,
|
||||
child.type
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({ name: child.name, value: child.value, typeHint: child.type });
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export class PyEvaluationEngine implements DebugSessionVisualizationSupport {
|
||||
createBackend(
|
||||
session: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
const supportedDebugAdapters = ["python"];
|
||||
const supportedDebugAdapters = ["python", "debugpy"];
|
||||
|
||||
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
|
||||
return new PyVisualizationBackend(
|
||||
|
||||
@@ -29,6 +29,14 @@ export interface VisualizationBackend extends Disposable {
|
||||
readonly expressionLanguageId: string | undefined;
|
||||
|
||||
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
|
||||
|
||||
/**
|
||||
* Evaluates `expression` in the current frame for its side effect
|
||||
* (e.g. an assignment) rather than to extract visualization data.
|
||||
*/
|
||||
evaluateForEffect(
|
||||
expression: string
|
||||
): Promise<{ result: string } | { error: string }>;
|
||||
}
|
||||
|
||||
export interface GetVisualizationDataArgs {
|
||||
@@ -80,4 +88,22 @@ export abstract class VisualizationBackendBase implements VisualizationBackend {
|
||||
column,
|
||||
});
|
||||
}
|
||||
|
||||
public async evaluateForEffect(
|
||||
expression: string
|
||||
): Promise<{ result: string } | { error: string }> {
|
||||
try {
|
||||
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||
this.debugSession
|
||||
);
|
||||
const reply = await this.debugSession.evaluate({
|
||||
expression,
|
||||
frameId,
|
||||
context: "repl",
|
||||
});
|
||||
return { result: reply.result };
|
||||
} catch (error: any) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export { PyEvaluationEngine } from "./PyVisualizationSupport";
|
||||
export { RbEvaluationEngine } from './RbVisualizationSupport';
|
||||
export { GenericVisualizationSupport } from "./GenericVisualizationSupport";
|
||||
export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport";
|
||||
export { NodeTableGraphEngine } from "./NodeTableGraphVisualizationSupport";
|
||||
|
||||
@@ -15,6 +15,14 @@ export interface VisualizationWatchModel {
|
||||
* This field is observable.
|
||||
*/
|
||||
readonly languageId: string | undefined;
|
||||
|
||||
/**
|
||||
* Evaluates `expression` in the current frame for its side effect
|
||||
* (e.g. an assignment) rather than to extract visualization data.
|
||||
*/
|
||||
evaluateForEffect(
|
||||
expression: string
|
||||
): Promise<{ result: string } | { error: string }>;
|
||||
}
|
||||
|
||||
export interface VisualizationWatchOptions {
|
||||
|
||||
@@ -44,6 +44,12 @@ export class VisualizationWatchModelImpl implements VisualizationWatchModel {
|
||||
public getCompletions(text: string, column: number): Promise<CompletionItem[]> {
|
||||
return this.visualizationBackend.getCompletions(text, column);
|
||||
}
|
||||
|
||||
public evaluateForEffect(
|
||||
expression: string
|
||||
): Promise<{ result: string } | { error: string }> {
|
||||
return this.visualizationBackend.evaluateForEffect(expression);
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableVisualizationWatch implements VisualizationWatch {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
RbEvaluationEngine,
|
||||
GenericVisualizationSupport,
|
||||
ConfigurableVisualizationSupport,
|
||||
NodeTableGraphEngine,
|
||||
} from "./VisualizationBackend";
|
||||
import { DispatchingVisualizationBackend } from "./VisualizationBackend/DispatchingVisualizationBackend";
|
||||
|
||||
@@ -48,6 +49,7 @@ export class Extension {
|
||||
public readonly dataSource = new VisualizationWatchModelImpl(
|
||||
new DispatchingVisualizationBackend(
|
||||
new ComposedVisualizationSupport([
|
||||
new NodeTableGraphEngine(this.debuggerView, this.config),
|
||||
new ConfigurableVisualizationSupport(
|
||||
this.config,
|
||||
this.debuggerView
|
||||
|
||||
@@ -85,7 +85,7 @@ export class DebugSessionProxy {
|
||||
expression: string;
|
||||
frameId: number | undefined;
|
||||
context: "watch" | "repl" | "copy";
|
||||
}): Promise<{ result: string; variablesReference: number }> {
|
||||
}): Promise<{ result: string; variablesReference: number; type?: string }> {
|
||||
const reply = await this.session.customRequest("evaluate", {
|
||||
expression: args.expression,
|
||||
frameId: args.frameId,
|
||||
@@ -94,6 +94,7 @@ export class DebugSessionProxy {
|
||||
return {
|
||||
result: reply.result,
|
||||
variablesReference: reply.variablesReference,
|
||||
type: reply.type,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -113,6 +114,7 @@ interface Variable {
|
||||
name: string;
|
||||
value: string;
|
||||
variablesReference: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface StackFrame {
|
||||
|
||||
@@ -103,6 +103,14 @@ export class WebviewConnection {
|
||||
completions,
|
||||
};
|
||||
},
|
||||
evaluateForEffect: async ({ expression }) => {
|
||||
throwIfNotAuthenticated();
|
||||
|
||||
const reply = await evaluationWatchService.evaluateForEffect(expression);
|
||||
return "error" in reply
|
||||
? { success: false, message: reply.error }
|
||||
: { success: true, message: reply.result };
|
||||
},
|
||||
});
|
||||
|
||||
this.client = client;
|
||||
|
||||
@@ -90,6 +90,23 @@ export const webviewContract = contract({
|
||||
completions: types.array(unchecked<CompletionItem>()),
|
||||
}),
|
||||
}),
|
||||
|
||||
// Evaluates `expression` for its side effect (e.g. an assignment
|
||||
// like `x = 5`) in the active debug session's current frame, using
|
||||
// the same DebugSessionProxy.evaluate() the read-only visualization
|
||||
// path already uses (context "repl", so debug adapters that only
|
||||
// truncate/sanitize "watch" context results still run this in full).
|
||||
// Used by custom visualizer scripts (see vis-plugins/node-table-
|
||||
// visualizer.js) to support editing values in place.
|
||||
evaluateForEffect: requestContract({
|
||||
params: types.type({
|
||||
expression: types.string,
|
||||
}),
|
||||
result: types.type({
|
||||
success: types.boolean,
|
||||
message: types.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"build": "yarn build-data-extraction && yarn build-ui && yarn build-extension",
|
||||
"build-data-extraction": "yarn workspace @hediet/debug-visualizer-data-extraction build",
|
||||
"build-ui": "yarn workspace debug-visualizer-webview build",
|
||||
"build-extension": "yarn workspace debug-visualizer build"
|
||||
"build-extension": "yarn workspace debug-visualizer build",
|
||||
"package": "yarn build && yarn workspace debug-visualizer package && node print-vsix-path.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.8.8"
|
||||
|
||||
13
print-vsix-path.js
Normal file
13
print-vsix-path.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Run after `yarn workspace debug-visualizer package` -- vsce's own stdout
|
||||
// ("DONE Packaged: ...") isn't meant to be parsed programmatically, so this
|
||||
// just checks the fixed -o path that extension/package.json's own "package"
|
||||
// script passes to vsce, and prints it (or fails loudly if it's missing).
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const vsixPath = path.resolve(__dirname, "extension", "dist", "extension.vsix");
|
||||
if (!fs.existsSync(vsixPath)) {
|
||||
console.error("Expected .vsix not found at " + vsixPath);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(vsixPath);
|
||||
@@ -4,6 +4,7 @@ import { EventTimer } from "@hediet/std/timer";
|
||||
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
|
||||
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
||||
import "@hediet/visualization-bundle";
|
||||
import "../visualizers/nodeTableGraphVisualizer";
|
||||
import {
|
||||
RegisterVisualizerFn,
|
||||
Visualization,
|
||||
@@ -161,6 +162,24 @@ export class Model {
|
||||
this.pollingTimer.onTick.sub(() => {
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
// Exposed globally (not passed into custom visualizer scripts'
|
||||
// closures) for the same reason setVisualizationModule is below:
|
||||
// setCustomVisualizerScript's `eval()` runs plugin code in this
|
||||
// same window/global scope, so a plugin (see vis-plugins/node-
|
||||
// table-visualizer.js) can reach these to support editing values
|
||||
// in place, without threading a new parameter through the whole
|
||||
// register()/getVisualization()/render() plugin contract.
|
||||
(globalThis as any).debugVisualizerSetValue = (expression: string) =>
|
||||
this.evaluateForEffect(expression);
|
||||
(globalThis as any).debugVisualizerRefresh = () => this.refresh();
|
||||
}
|
||||
|
||||
async evaluateForEffect(expression: string): Promise<{ success: boolean; message: string }> {
|
||||
if (!this.server) {
|
||||
return { success: false, message: "Not connected to the debug visualizer server." };
|
||||
}
|
||||
return this.server.evaluateForEffect({ expression });
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
1764
webview/src/visualizers/nodeTableGraphVisualizer.ts
Normal file
1764
webview/src/visualizers/nodeTableGraphVisualizer.ts
Normal file
File diff suppressed because it is too large
Load Diff
4634
webview/yarn.lock
4634
webview/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user