This commit is contained in:
Henning Dieterichs
2020-02-10 12:13:07 +01:00
parent 51957d52eb
commit 8aae7f7b0d
45 changed files with 625 additions and 413 deletions

5
.vscode/launch.json vendored
View File

@@ -2,7 +2,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"name": "Run Extension (Build UI)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
@@ -11,7 +11,8 @@
"${workspaceFolder}\\example-project"
],
"env": {
"HOT_RELOAD": "true"
"HOT_RELOAD": "true",
"USE_DEV_UI": ""
},
"outFiles": ["${workspaceFolder}/extension/dist/**/*.js"],
"preLaunchTask": "npm: dev - extension"

View File

@@ -1,20 +1,29 @@
{
"name": "@hediet/debug-visualizer-data-extraction",
"private": true,
"version": "0.2.0",
"description": "A library that helps implementing data extractors for the Debug Visualizer VS Code extension.",
"version": "0.9.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": {
"name": "Henning Dieterichs",
"email": "henning.dieterichs@live.de"
},
"repository": {
"type": "git",
"url": "https://github.com/hediet/vscode-debug-visualizer.git"
},
"license": "MIT",
"files": [
"dist",
"src"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"dev": "tsc --watch"
"dev": "tsc --watch",
"build": "tsc"
},
"dependencies": {}
}

View File

@@ -19,9 +19,9 @@ export type CommonDataType =
| CommonDataTypes.Svg
| CommonDataTypes.Html
| CommonDataTypes.DotGraph
| CommonDataTypes.TreeNodeData
| CommonDataTypes.AstData
| CommonDataTypes.GraphData;
| CommonDataTypes.Tree
| CommonDataTypes.Ast
| CommonDataTypes.Graph;
export module CommonDataTypes {
export interface Text {
@@ -31,34 +31,31 @@ export module CommonDataTypes {
fileName?: string;
}
export interface GraphData {
export interface Graph {
kind: { graph: true };
nodes: { id: string; label: string }[];
edges: { from: string; to: string; label: string }[];
nodes: NodeGraphData[];
edges: EdgeGraphData[];
}
export interface Svg extends Text {
kind: { text: true; svg: true };
text: string;
}
export interface Html extends Text {
kind: { text: true; html: true };
text: string;
}
export interface DotGraph extends Text {
kind: { text: true; dotGraph: true };
text: string;
}
export interface TreeNodeData<TData = unknown> {
export interface Tree<TData = unknown> {
kind: { tree: true };
root: TreeNode<TData>;
}
export interface AstData
extends TreeNodeData<{
export interface Ast
extends Tree<{
position: number;
length: number;
}>,
@@ -68,11 +65,26 @@ export module CommonDataTypes {
}
export interface TreeNode<TExtraData> {
id: string | undefined;
name: string;
value: string | undefined;
emphasizedValue: string | undefined;
children: TreeNode<TExtraData>[];
data: TExtraData;
isMarked: boolean;
id?: string;
value?: string;
emphasizedValue?: string;
isMarked?: boolean;
}
export interface NodeGraphData {
id: string;
label: string;
color?: string;
}
export interface EdgeGraphData {
from: string;
to: string;
label: string;
id?: string;
color?: string;
weight?: number;
}

View File

@@ -37,7 +37,7 @@ declare const window: any;
export function selfContainedInitDataExtractorApi(): boolean {
const obj = typeof window === "object" ? (window as any) : (global as any);
const key = "@hediet/data-extractor";
const key = "@hediet/data-extractor/v1";
const prefix = key + "::";
let api: DataExtractorApi | undefined = obj[key];
@@ -135,7 +135,7 @@ export const ApiHasNotBeenInitializedCode = "EgH0cybXij1jYUozyakO" as const;
export function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi {
const obj = typeof window === "object" ? (window as any) : (global as any);
const key = "@hediet/data-extractor";
const key = "@hediet/data-extractor/v1";
let api: DataExtractorApi | undefined = obj[key];
if (!api) {
const code: typeof ApiHasNotBeenInitializedCode =

View File

@@ -4,12 +4,12 @@ import { CommonDataTypes } from "../../CommonDataTypes";
// This class is self contained and can be injected into both nodejs and browser environments.
export class TypeScriptAstDataExtractor
implements DataExtractor<CommonDataTypes.AstData> {
implements DataExtractor<CommonDataTypes.Ast> {
readonly id = "TypeScriptAst";
getExtractions(
data: unknown,
collector: ExtractionCollector<CommonDataTypes.AstData>,
collector: ExtractionCollector<CommonDataTypes.Ast>,
evalFn: <TEval>(expression: string) => TEval
): void {
if (!data) {
@@ -63,7 +63,7 @@ export class TypeScriptAstDataExtractor
memberName: string,
marked: Set<ts.Node>,
emphasizedValueFn: (node: ts.Node) => string | undefined
): CommonDataTypes.AstData["root"] {
): CommonDataTypes.Ast["root"] {
const name = tsApi.SyntaxKind[node.kind];
const children = getChildren(node)
.map((childNode, idx) => {

View File

@@ -0,0 +1,105 @@
import {
CommonDataTypes,
EdgeGraphData,
NodeGraphData,
} from "../CommonDataTypes";
export function createGraphFromPointers<T>(
roots: Record<string, T | undefined | null>,
infoSelector: (
item: T
) => {
id?: string | number;
edges: ({ to: T } & Omit<EdgeGraphData, "from" | "to">)[];
} & Omit<NodeGraphData, "id">
): CommonDataTypes.Graph {
const marker = {};
interface Pointer {
marker: {};
name: string;
value: T | null | undefined;
}
const items = Object.entries(roots).map<Pointer>(([name, value]) => ({
marker,
name,
value,
}));
const includeLabelInName = false;
return createGraph<T | Pointer>(items, item => {
if ("marker" in item && item["marker"] === marker) {
return {
id: "label____" + item.name,
color: "orange",
label: item.name,
edges: [{ to: item.value!, color: "orange", label: "" }].filter(
t => !!t.to
),
};
} else {
return infoSelector(item as T);
}
});
}
export function createGraph<T>(
roots: T[],
infoSelector: (
item: T
) => {
id?: string | number;
edges: ({ to: T } & Omit<EdgeGraphData, "from" | "to">)[];
} & Omit<NodeGraphData, "id">
): CommonDataTypes.Graph {
const r: CommonDataTypes.Graph = {
kind: {
graph: true,
},
nodes: [],
edges: [],
};
let idCounter = 1;
const ids = new Map<T, string>();
function getId(item: T): string {
const _id = infoSelector(item).id;
if (_id !== undefined) {
return "" + _id;
}
let id = ids.get(item);
if (!id) {
id = (idCounter++).toString();
ids.set(item, id);
}
return id;
}
const queue = new Array<T>(...roots);
const processed = new Set<T>();
while (queue.length > 0) {
const item = queue.shift()!;
if (processed.has(item)) {
continue;
}
processed.add(item);
const nodeInfo = infoSelector(item);
const fromId = getId(item);
r.nodes.push({ ...nodeInfo, id: fromId, ["edges" as any]: undefined });
for (const e of nodeInfo.edges) {
const toId = getId(e.to);
r.edges.push({
...e,
from: fromId,
to: toId,
});
if (!processed.has(e.to)) {
queue.push(e.to);
}
}
}
return r;
}

View File

@@ -1,80 +0,0 @@
import { CommonDataTypes } from "../CommonDataTypes";
export function getClosureObj<T>(
roots: Record<string, T | undefined | null>,
edgeSelector: (item: T) => { item: T; edgeLabel: string }[],
labelSelector: (item: T) => string,
idSelector?: (item: T) => string | number
): CommonDataTypes.GraphData {
const items = Object.entries(roots).map(([k, v]) => v);
return getClosure<T>(
items.filter(i => !!i) as T[],
edgeSelector,
item => {
const names = new Array<string>();
for (const [key, val] of Object.entries(roots)) {
if (val === item) {
names.push(key);
}
}
const base = labelSelector(item);
return names.length === 0 ? base : `${names.join(", ")}: ${base}`;
},
idSelector
);
}
export function getClosure<T>(
roots: T[],
edgeSelector: (item: T) => { item: T; edgeLabel: string }[],
labelSelector: (item: T) => string,
idSelector?: (item: T) => string | number
): CommonDataTypes.GraphData {
const r: CommonDataTypes.GraphData = {
kind: {
graph: true,
},
nodes: [],
edges: [],
};
let idCounter = 1;
const ids = new Map<T, string>();
function getId(item: T): string {
if (idSelector) {
return "" + idSelector(item);
}
let id = ids.get(item);
if (!id) {
id = (idCounter++).toString();
ids.set(item, id);
}
return id;
}
const queue = new Array<T>(...roots);
const processed = new Set<T>();
while (queue.length > 0) {
const item = queue.shift()!;
if (processed.has(item)) {
continue;
}
processed.add(item);
const edges = edgeSelector(item);
const fromId = getId(item);
r.nodes.push({ id: fromId, label: labelSelector(item) });
for (const e of edges) {
const toId = getId(e.item);
r.edges.push({
from: fromId,
to: toId,
label: e.edgeLabel,
});
if (!processed.has(e.item)) {
queue.push(e.item);
}
}
}
return r;
}

View File

@@ -1,4 +1,4 @@
export * from "./Extractors";
export * from "./DataExtractor";
export * from "./DataExtractorApi";
export * from "./getClosure";
export * from "./createGraph";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

BIN
docs/visualization-ast.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

BIN
docs/visualization-ast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/visualization-tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -3,22 +3,5 @@
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Index",
"program": "${workspaceFolder}\\dist\\index.js",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "npm: dev"
},
{
"type": "node",
"request": "launch",
"name": "Launch Double Linked List Demo",
"program": "${workspaceFolder}\\dist\\DoubleLinkedList.js",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "npm: dev"
}
]
"configurations": []
}

4
example-project/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"debugVisualizer.useChromeKioskMode": false,
"editor.minimap.enabled": false
}

View File

@@ -1,72 +0,0 @@
import {
enableHotReload,
registerUpdateReconciler,
getReloadCount,
hotClass,
} from "@hediet/node-reload";
enableHotReload();
registerUpdateReconciler(module);
import {
registerAll,
CommonDataTypes,
getClosureObj,
} from "@hediet/debug-visualizer-data-extraction";
registerAll();
setTimeout(() => {
new Main().run();
}, 0);
@hotClass(module)
class Main {
run() {
let myValue: DoubleLinkedList | undefined = new DoubleLinkedList("1");
const first = myValue;
myValue.setNext(new DoubleLinkedList("2"));
myValue.next!.setNext(new DoubleLinkedList("3"));
myValue.next!.next!.setNext(new DoubleLinkedList("4"));
const dbgLast = myValue.next!.next!.next!;
let last: DoubleLinkedList | undefined = undefined;
while (myValue) {
myValue.prev = myValue.next;
myValue.next = last;
last = myValue;
myValue = myValue.prev;
}
myValue = first;
}
getDebugVisualization(
items: Record<string, DoubleLinkedList | undefined>
): CommonDataTypes.GraphData {
return getClosureObj(
items,
r =>
[
{ item: r.next!, edgeLabel: "next" },
{ item: r.prev!, edgeLabel: "prev" },
].filter(r => !!r.item),
item => item.name,
item => item.id
);
}
}
let id = 0;
class DoubleLinkedList {
public readonly id = (id++).toString();
constructor(public name: string) {}
next: DoubleLinkedList | undefined;
prev: DoubleLinkedList | undefined;
public setNext(val: DoubleLinkedList): void {
val.prev = this;
this.next = val;
}
}

View File

@@ -0,0 +1,59 @@
import {
registerAll,
CommonDataTypes,
createGraphFromPointers,
} from "@hediet/debug-visualizer-data-extraction";
registerAll();
setTimeout(() => {
new Main().run();
}, 0);
class Main {
run() {
const list = new DoublyLinkedList("1");
list.setNext(new DoublyLinkedList("2"));
list.next!.setNext(new DoublyLinkedList("3"));
list.next!.next!.setNext(new DoublyLinkedList("4"));
const visualize = () =>
createGraphFromPointers({ list, last, cur }, i => ({
id: i.id,
label: i.name,
color: finished.has(i) ? "lime" : undefined,
edges: [
{ to: i.next!, label: "next" },
{ to: i.prev!, label: "prev", color: "lightgray" },
].filter(r => !!r.to),
}));
const finished = new Set();
var cur: DoublyLinkedList | undefined = list;
// Reverses `list`. Finished nodes have correct pointers,
// their next node is also finished.
var last: DoublyLinkedList | undefined = undefined;
while (cur) {
cur.prev = cur.next;
cur.next = last;
finished.add(cur);
last = cur;
cur = cur.prev;
}
console.log("finished");
}
}
let id = 0;
class DoublyLinkedList {
public readonly id = (id++).toString();
constructor(public name: string) {}
next: DoublyLinkedList | undefined;
prev: DoublyLinkedList | undefined;
public setNext(val: DoublyLinkedList): void {
val.prev = this;
this.next = val;
}
}

View File

@@ -1,24 +1,13 @@
import * as ts from "typescript";
import {
enableHotReload,
registerUpdateReconciler,
getReloadCount,
hotClass,
} from "@hediet/node-reload";
enableHotReload();
import {
registerAll,
CommonDataTypes,
getClosure,
} from "@hediet/debug-visualizer-data-extraction";
import { registerAll } from "@hediet/debug-visualizer-data-extraction";
import { MockLanguageServiceHost } from "./MockLanguageServiceHost";
registerAll();
registerUpdateReconciler(module);
setTimeout(() => {
new Main().run();
}, 0);
@hotClass(module)
class Main {
run() {
const mainFile = {
@@ -49,10 +38,12 @@ class Test1 {
const c = prog.getTypeChecker();
let myValue = undefined;
const sf = prog.getSourceFiles()[0];
myValue = sf.getText();
const sourceFileAst = prog.getSourceFiles()[0];
myValue = sourceFileAst.getText();
console.log("myValue is the source code of the AST");
myValue = {
sf,
sf: sourceFileAst,
fn: (n: ts.Node) => {
try {
const t = c.getTypeAtLocation(n);
@@ -62,8 +53,10 @@ class Test1 {
}
},
};
console.log("myValue is AST, annotated with type information");
myValue = {
sf,
sf: sourceFileAst,
fn: (n: ts.Node) => {
try {
const t = c.getSymbolAtLocation(n);
@@ -73,12 +66,16 @@ class Test1 {
}
},
};
console.log("myValue is AST, annotated with symbol information");
for (const ident of identifiers) {
const s = c.getSymbolAtLocation(ident);
myValue = ident;
console.log("myValue is an identifier");
}
}
}
/*
myValue = {
kind: { text: true, svg: true },
text: `
@@ -89,10 +86,4 @@ class Test1 {
/>
</svg>
`,
};
}
}
if (getReloadCount(module) === 0) {
new Main().run();
}
};*/

5
extension/CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
# Change Log
## 0.9.0
- Initial release

View File

@@ -1,62 +1,171 @@
# Debug Visualizer (Preview)
# Debug Visualizer
[![](https://img.shields.io/twitter/follow/hediet_dev.svg?style=social)](https://twitter.com/intent/follow?screen_name=hediet_dev)
Provides infrastructure to visualize values while debugging.
A VS Code extension for visualizing data structures during debugging.
This extension is still in development.
![](docs/demo.gif)
![](../docs/doubly-linked-list-reverse-demo.gif)
## Usage
After installing this extension, use the command `Open a new Debug Visualizer View` to open a new visualizer view.
There you can enter an expression that is visualized when debugging your application.
In this view you can enter an expression that is visualized when debugging your application, e.g.
```ts
{ kind: { graph: true }, nodes: [ { id: "1", label: "1" }, { id: "2", label: "2" } ], edges: [{ from: "1", to: "2", label: "edge" }]}
```
## Integrated Visualizers
Visualizers present preprocessed data.
They are mostly React-Components and live in the webview.
Visualizers present data extracted by a _Data Extractor_.
Visualizers are (mostly) React components and live in the webview of the extension.
These visualizers are integrated:
### Tree Visualization
- Tree Viewer
![](./docs/tree-viewer.png)
The tree visualizer renders data that matches the `Tree` interface.
- Svg Viewer
- Dot Graph Viewer
- VisJs Graph Viewer
![](./docs/linked-list-demo.png)
```ts
interface Tree<TData = unknown> {
kind: { tree: true };
root: TreeNode<TData>;
}
interface TreeNode<TExtraData> {
id: string | undefined;
name: string;
value: string | undefined;
emphasizedValue: string | undefined;
children: TreeNode<TExtraData>[];
data: TExtraData;
isMarked: boolean;
}
```
- Graph Viewer
![](./docs/graph-viewer.png)
![](../docs/visualization-tree.png)
- Text Viewer
- Plain Text
- Monaco
### AST Visualization
The AST visualizer renders data that matches the `Ast` interface.
```ts
interface Ast
extends Tree<{
position: number;
length: number;
}>,
Text {
kind: { text: true; tree: true; ast: true };
}
```
In addition to the tree view, the text representation is shown.
![](../docs/visualization-ast.png)
### Text Visualization
The text visualizer renders data that matches the `Text` interface.
```ts
interface Text {
kind: { text: true };
text: string;
mimeType?: string;
fileName?: string;
}
```
The `mimeType` and the file extension of `fileName` are used for syntax highlighting.
### SVG Visualization
The SVG visualizer renders data that matches the `Svg` interface.
The actual SVG data must be stored in `text`.
```ts
interface Svg extends Text {
kind: { text: true; svg: true };
}
```
### Dot Graph Visualization
The Graphviz Dot visualizer renders data that matches the `DotGraph` interface.
```ts
interface DotGraph extends Text {
kind: { text: true; dotGraph: true };
}
```
`Viz.js` (Graphviz) is used for rendering.
### Graph Visualization
The Graphviz and vis.js visualizers render data that matches the `Graph` interface.
```ts
interface Graph {
kind: { graph: true };
nodes: NodeGraphData[];
edges: EdgeGraphData[];
}
interface NodeGraphData {
id: string;
label: string;
color?: string;
}
interface EdgeGraphData {
from: string;
to: string;
label: string;
id?: string;
color?: string;
weight?: number;
}
```
The graphviz visualizer uses the SVG viewer to render the SVG created by `viz.js`.
![](../docs/visualization-graphviz.png)
![](../docs/visualization-visjs.png)
## Integrated Data Extractors
Data extractors convert arbitrary values into visualizable data.
They live in the debugee.
They live in the debugee. The following data extractors are injected automatically by this extension.
Custom data extractors can be registered too.
These visualizers are integrated:
### ToString
- ToString Text Extractor
- TypeScript Ast to Tree Data
- Direct Visualization of `ts.Node`s
- Visualization of `Record<string, ts.Node>` and `[ts.Node]`. If the record contains a key `fn`, its value is displayed for each node.
Just calls `.toString()` on values and treats the result as text.
![](./docs/ast-viewer.png)
### TypeScript AST
- As Is Data Extractor. Allows to return data directly.
- Direct Visualization of `ts.Node`s
- Visualization of `Record<string, ts.Node>` and `[ts.Node]`. If the record contains a key `fn`, its value is displayed for each node.
### As Is Data Extractor
Treats the data as direct input to the visualizer.
### Use Method 'getDebugVisualization'
Calls `.getDebugVisualization()` on values and treats the result as direct input to the visualizer.
## Limitations
Currently, only JavaScript (and thus TypeScript) values can be visualized and only a few visualizations are supported.
The architecture is solid enough to support other languages in the future.
# See Also
This extension works very well together with my library [`@hediet/node-reload`](https://github.com/hediet/node-reload):
This extension works very well together with my library [`@hediet/node-reload`](https://github.com/hediet/node-reload).
Together, they provide an interactive typescript playground.
![](./docs/demo-hot.gif)
![](../docs/demo-hot.gif)
```
```

BIN
extension/docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -2,7 +2,8 @@
"name": "debug-visualizer",
"displayName": "Debug Visualizer",
"description": "An extension that visualizes values while debugging.",
"version": "0.2.1",
"icon": "docs/logo.png",
"version": "0.9.0",
"license": "MIT",
"engines": {
"vscode": "^1.35.0"
@@ -20,7 +21,6 @@
"categories": [
"Other"
],
"preview": true,
"activationEvents": [
"onCommand:vscode-debug-visualizer.new-visualizer",
"onDebug",
@@ -46,6 +46,7 @@
}
},
"scripts": {
"pub": "vsce publish --baseImagesUrl https://github.com/hediet/vscode-debug-visualizer/raw/master/extension",
"vscode:prepublish": "yarn build",
"build": "webpack --mode production",
"dev": "tsc -watch -p ./"
@@ -59,7 +60,8 @@
"express": "^4.17.1",
"open": "^6.4.0",
"serve-static": "^1.14.1",
"ws": "^7.1.1"
"ws": "^7.1.1",
"crypto-random-string": "^3.1.0"
},
"devDependencies": {
"@types/express": "^4.17.0",

View File

@@ -1,6 +1,6 @@
import { Disposable } from "@hediet/std/disposable";
import { debugVisualizerUIContract } from "@hediet/debug-visualizer-vscode-shared";
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
import { ConsoleRpcLogger, RequestHandlingError } from "@hediet/typed-json-rpc";
import { EvaluationWatcher } from "./DataSource/DataSource";
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import { observable, autorun } from "mobx";
@@ -19,8 +19,17 @@ export class ConnectionHandler {
sources: Sources,
stream: WebSocketStream,
server: Server,
config: Config
config: Config,
serverSecret: string
) {
let authenticated = false;
function throwIfNotAuthenticated() {
if (!authenticated) {
throw new RequestHandlingError("Not authenticated");
}
}
const {
client,
channel,
@@ -28,12 +37,23 @@ export class ConnectionHandler {
stream,
new ConsoleRpcLogger(),
{
authenticate: async ({ secret }, { newErr }) => {
if (secret !== serverSecret) {
return newErr({ errorMessage: "Invalid Secret" });
} else {
authenticated = true;
}
},
refresh: async () => {
throwIfNotAuthenticated();
if (this.watcher) {
this.watcher.refresh();
}
},
setExpression: async ({ newExpression }) => {
throwIfNotAuthenticated();
let oldPreferredDataExtractor: EvaluationWatcher["preferredDataExtractor"];
if (this.watcher) {
oldPreferredDataExtractor = this.watcher
@@ -50,6 +70,8 @@ export class ConnectionHandler {
);
},
openInBrowser: async ({}) => {
throwIfNotAuthenticated();
let opened = false;
if (config.useChromeKioskMode()) {
opened = await launchChrome(server.indexUrl);
@@ -59,11 +81,15 @@ export class ConnectionHandler {
}
},
setPreferredDataExtractor: async ({ dataExtractorId }) => {
throwIfNotAuthenticated();
if (this.watcher) {
this.watcher.setPreferredDataExtractor(dataExtractorId);
}
},
getCompletions: async ({ text, column }) => {
throwIfNotAuthenticated();
const completions = await sources.jsSource.getCompletions(
text,
column

View File

@@ -8,9 +8,11 @@ import * as express from "express";
import * as http from "http";
import * as serveStatic from "serve-static";
import { Config } from "./Config";
import cryptoRandomString = require("crypto-random-string");
export class Server {
private server: http.Server;
public readonly secret = cryptoRandomString({ length: 20 });
constructor(sources: Sources, config: Config) {
const app = express();
@@ -22,13 +24,13 @@ export class Server {
const wss = new WebSocket.Server({ server: this.server });
wss.on("connection", ws => {
const stream = new WebSocketStream(ws);
new ConnectionHandler(sources, stream, this, config);
new ConnectionHandler(sources, stream, this, config, this.secret);
});
}
public get indexUrl(): string {
const port = process.env.USE_DEV_UI ? 8080 : this.port;
return `http://localhost:${port}/index.html?serverPort=${this.port}`;
return `http://localhost:${port}/index.html?serverPort=${this.port}&serverSecret=${this.secret}`;
}
public get mainBundleUrl(): string {

View File

@@ -72,6 +72,7 @@ export function getHtml(server: Server) {
<body>
<script>
window.serverPort = ${server.port};
window.serverSecret = ${JSON.stringify(server.secret)};
const api = window.VsCodeApi = acquireVsCodeApi();
window.addEventListener('message', event => {
if (event.source === window.frames[0]) {

View File

@@ -1,90 +1,42 @@
import React = require("react");
import { Model } from "../Model/Model";
import { observer } from "mobx-react";
import { observable, computed } from "mobx";
import { Popover, Button, Spinner } from "@blueprintjs/core";
import classnames = require("classnames");
import { Popover } from "@blueprintjs/core";
import { DataExtractorInfo } from "@hediet/debug-visualizer-data-extraction";
import { Visualizer } from "./Visualizer";
import { ExpressionInput } from "./ExpressionInput";
@observer
export class GUI extends React.Component<{ model: Model }> {
@observable text: string = "node";
@observable expanded = false;
render() {
const m = this.props.model;
return (
<div className="component-GUI" tabIndex={0}>
<div className="part-header">
<ExpandButton
expanded={this.expanded}
setExpanded={v => (this.expanded = v)}
/>
<div className="part-header-content">
<div className="part-header-main">
<div className="part-expression-input ">
<ExpressionInput model={m} />
</div>
<div style={{ width: 4 }} />
{m.loading ? (
<div style={{ padding: "0 4px" }}>
<Spinner size={Spinner.SIZE_SMALL} />
</div>
) : (
<Button
minimal
small
className="part-icon"
icon="refresh"
onClick={() => m.refresh()}
/>
)}
<Button
minimal
small
className="part-icon"
icon="log-in"
onClick={() => m.openBrowser()}
/>
<Button
minimal
small
className="part-icon"
icon="locate"
onClick={() => m.useSelectionAsExpression()}
/>
</div>
{this.expanded && <ExpandedOptions model={m} />}
</div>
</div>
<div className="part-visualizer">
<Visualizer model={m} />
</div>
</div>
);
}
renderValue(value: string) {
return <div>{`${value.length}`}</div>;
}
}
import { computed } from "mobx";
import { observer } from "mobx-react";
import { Model } from "../Model/Model";
import { VisualizationId } from "../Visualizers/Visualizer";
@observer
export class ExpandedOptions extends React.Component<{ model: Model }> {
render() {
const m = this.props.model;
let availableExtractors = new Array<DataExtractorInfo>();
let selected = -1;
let selectedExtractor = -1;
if (m.state.kind === "data") {
const result = m.state.result;
availableExtractors = result.availableExtractors;
selected = result.availableExtractors.findIndex(
selectedExtractor = result.availableExtractors.findIndex(
i => i.id === result.usedExtractor.id
);
}
let visualizations = new Array<{
label: string;
id: VisualizationId;
}>();
let selectedVisualization = -1;
if (m.visualizations) {
visualizations = m.visualizations.allVisualizations.map(v => ({
label: v.name,
id: v.id,
}));
if (m.visualizations.visualization) {
selectedVisualization = visualizations.findIndex(
v => v.id === m.visualizations!.visualization!.id
);
}
}
return (
<>
<div style={{ height: 6 }} />
@@ -99,7 +51,7 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
<NamedSelect
name="Extractor"
selected={selected}
selected={selectedExtractor}
options={availableExtractors.map(e => ({
label: e.name,
id: e.id,
@@ -112,7 +64,7 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
<NamedSelect
name="Visualizer"
selected={0}
selected={selectedVisualization}
onSelected={item => {
m.setPreferredVisualizationId(item.id);
}}
@@ -131,23 +83,6 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
}
}
export class ExpandButton extends React.Component<{
expanded: boolean;
setExpanded: (val: boolean) => void;
}> {
render() {
return (
<div
onClick={() => this.props.setExpanded(!this.props.expanded)}
className={classnames(
"component-ExpandButton",
this.props.expanded && "expanded"
)}
/>
);
}
}
function NamedSelect<T extends SelectableItem>(props: {
name: string;
selected: number;
@@ -169,7 +104,7 @@ interface SelectableItem {
}
@observer
export class Select<T extends SelectableItem> extends React.Component<{
class Select<T extends SelectableItem> extends React.Component<{
selected: number;
onSelected: (item: T) => void;
options: T[];

View File

@@ -0,0 +1,93 @@
import React = require("react");
import { Button, Spinner } from "@blueprintjs/core";
import { observable } from "mobx";
import { observer } from "mobx-react";
import { Model } from "../Model/Model";
import { ExpressionInput } from "./ExpressionInput";
import { Visualizer } from "./Visualizer";
import classnames = require("classnames");
import { ExpandedOptions } from "./ExpandedOptions";
@observer
export class GUI extends React.Component<{ model: Model }> {
@observable text: string = "node";
@observable expanded = false;
render() {
const m = this.props.model;
return (
<div className="component-GUI" tabIndex={0}>
<div className="part-header">
<ExpandButton
expanded={this.expanded}
setExpanded={v => (this.expanded = v)}
/>
<div className="part-header-content">
<div className="part-header-main">
<div className="part-expression-input ">
<ExpressionInput model={m} />
</div>
<div style={{ width: 4 }} />
{m.loading ? (
<div style={{ padding: "0 4px" }}>
<Spinner size={Spinner.SIZE_SMALL} />
</div>
) : (
<Button
minimal
small
className="part-icon"
icon="refresh"
onClick={() => m.refresh()}
/>
)}
<Button
minimal
small
className="part-icon"
icon="log-in"
onClick={() => m.openBrowser()}
/>
{/*
TODO
<Button
minimal
small
className="part-icon"
icon="locate"
onClick={() => m.useSelectionAsExpression()}
/>
*/}
</div>
{this.expanded && <ExpandedOptions model={m} />}
</div>
</div>
<div className="part-visualizer">
<Visualizer model={m} />
</div>
</div>
);
}
renderValue(value: string) {
return <div>{`${value.length}`}</div>;
}
}
export class ExpandButton extends React.Component<{
expanded: boolean;
setExpanded: (val: boolean) => void;
}> {
render() {
return (
<div
onClick={() => this.props.setExpanded(!this.props.expanded)}
className={classnames(
"component-ExpandButton",
this.props.expanded && "expanded"
)}
/>
);
}
}

View File

@@ -13,10 +13,12 @@ import { MonacoBridge } from "./MonacoBridge";
declare const window: Window & {
serverPort?: number;
serverSecret?: string;
};
export class Model {
port: number;
private port: number;
private serverSecret: string;
@observable expression: string = "";
@observable state: DataExtractionState | { kind: "noExpression" } = {
@@ -67,6 +69,7 @@ export class Model {
constructor() {
if (window.serverPort) {
this.port = window.serverPort;
this.serverSecret = window.serverSecret!;
} else {
const url = new URL(window.location.href);
const portStr = url.searchParams.get("serverPort");
@@ -79,6 +82,12 @@ export class Model {
if (expr) {
this.setExpression(expr);
}
const secret = url.searchParams.get("serverSecret");
if (!secret) {
throw new Error("Server secret not set.");
}
this.serverSecret = secret;
}
this.stayConnected();
@@ -141,6 +150,11 @@ export class Model {
},
}
);
try {
await server.authenticate({ secret: this.serverSecret });
} catch (e) {
console.error(e);
}
this.server = server;
await stream.onClosed;

View File

@@ -9,16 +9,12 @@ import {
CommonDataTypes,
} from "@hediet/debug-visualizer-data-extraction";
import { createTreeViewModelFromTreeNodeData } from "./TreeVisualizer/Visualizer";
import {
TreeWithPathView,
TreeViewModel,
TreeView,
} from "./TreeVisualizer/Views";
import { TreeWithPathView, TreeViewModel } from "./TreeVisualizer/Views";
import React = require("react");
import { observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun, trace } from "mobx";
import * as monaco from "monaco-editor";
import { getLanguageId } from "./MonacoTextVisualizer";
import { getLanguageId } from "./TextVisualizer/MonacoTextVisualizer";
import LineColumn = require("line-column");
export class AstVisualizer extends VisualizationProvider {
@@ -70,7 +66,7 @@ interface NodeInfo {
@observer
export class AstTree extends React.Component<{
model: TreeViewModel<NodeInfo>;
data: CommonDataTypes.AstData;
data: CommonDataTypes.Ast;
nodeInfoToRange: (info: NodeInfo) => monaco.IRange;
}> {
render() {

View File

@@ -3,12 +3,12 @@ import * as React from "react";
import { observable, autorun, runInAction } from "mobx";
import Viz from "viz.js";
import { Module, render } from "viz.js/full.render.js";
import { SvgViewer } from "./SvgVisualizer";
import { SvgViewer } from "../SvgVisualizer";
import {
VisualizationProvider,
VisualizationCollector,
asVisualizationId,
} from "./Visualizer";
} from "../Visualizer";
import {
ExtractedData,
isCommonDataType,
@@ -19,18 +19,18 @@ const viz: any = new Viz({
render,
});
export class DotVisualizer extends VisualizationProvider {
export class GraphvizDotVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { dotGraph: true })) {
collector.addVisualization({
id: asVisualizationId("dot-graph"),
name: "Dot Graph",
id: asVisualizationId("graphviz-dot"),
name: "Graphviz (Dot Data)",
priority: 100,
render() {
return <DotViewer dotCode={data.text} />;
return <GraphvizDotViewer dotCode={data.text} />;
},
});
}
@@ -38,7 +38,7 @@ export class DotVisualizer extends VisualizationProvider {
}
@observer
export class DotViewer extends React.Component<{
export class GraphvizDotViewer extends React.Component<{
dotCode: string;
svgRef?: (element: SVGSVGElement | null) => void;
}> {

View File

@@ -1,29 +1,32 @@
import { observer } from "mobx-react";
import * as React from "react";
import { DotViewer } from "./DotVisualizer";
import { GraphvizDotViewer } from "./GraphvizDotVisualizer";
import {
VisualizationProvider,
VisualizationCollector,
asVisualizationId,
} from "./Visualizer";
} from "../Visualizer";
import {
ExtractedData,
isCommonDataType,
} from "@hediet/debug-visualizer-data-extraction";
export class DotGraphVisualizer extends VisualizationProvider {
export class GraphvizGraphVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { graph: true })) {
collector.addVisualization({
id: asVisualizationId("dot-graph"),
name: "Dot Graph",
id: asVisualizationId("graphviz-graph"),
name: "Graphviz",
priority: 100,
render() {
return (
<DotGraphViewer edges={data.edges} nodes={data.nodes} />
<GraphvizGraphViewer
edges={data.edges}
nodes={data.nodes}
/>
);
},
});
@@ -32,7 +35,7 @@ export class DotGraphVisualizer extends VisualizationProvider {
}
@observer
export class DotGraphViewer extends React.Component<{
export class GraphvizGraphViewer extends React.Component<{
nodes: { id: string; label: string }[];
edges: { from: string; to: string; label: string }[];
}> {
@@ -53,6 +56,6 @@ export class DotGraphViewer extends React.Component<{
.join("\n")}
}
`;
return <DotViewer dotCode={dotContent} />;
return <GraphvizDotViewer dotCode={dotContent} />;
}
}

View File

@@ -4,10 +4,12 @@ import {
VisualizationProvider,
VisualizationCollector,
asVisualizationId,
} from "./Visualizer";
} from "../Visualizer";
import {
ExtractedData,
isCommonDataType,
NodeGraphData,
EdgeGraphData,
} from "@hediet/debug-visualizer-data-extraction";
import { DataSet, Network } from "vis-network";
@@ -19,7 +21,7 @@ export class VisJsGraphVisualizer extends VisualizationProvider {
if (isCommonDataType(data, { graph: true })) {
collector.addVisualization({
id: asVisualizationId("vis-js-graph"),
name: "VisJs Graph",
name: "vis.js",
priority: 1001,
render() {
return (
@@ -34,34 +36,26 @@ export class VisJsGraphVisualizer extends VisualizationProvider {
}
}
interface GraphNode {
id: string;
label: string;
}
interface GraphEdge {
id?: string;
label: string;
from: string;
to: string;
}
@observer
export class VisJsGraphViewer extends React.Component<{
nodes: GraphNode[];
edges: GraphEdge[];
nodes: NodeGraphData[];
edges: EdgeGraphData[];
}> {
private readonly divRef = React.createRef<HTMLDivElement>();
private readonly nodes = new DataSet<{ id: string; label: string }>();
private readonly nodes = new DataSet<{
id: string;
label: string;
color?: string;
}>();
private readonly edges = new DataSet<{
id: string;
label: string;
from: string;
to: string;
color?: string;
}>();
render() {
//const { nodes, edges } = this.props;
return <div style={{ height: "100%" }} ref={this.divRef} />;
}
@@ -69,7 +63,11 @@ export class VisJsGraphViewer extends React.Component<{
const newNodes = new Set<string>();
for (const n of this.props.nodes) {
newNodes.add(n.id);
this.nodes.update({ id: n.id, label: n.label });
this.nodes.update({
id: n.id,
label: n.label,
color: n.color,
});
}
this.nodes.forEach(item => {
if (!newNodes.has(item.id)) {
@@ -77,11 +75,11 @@ export class VisJsGraphViewer extends React.Component<{
}
});
function getIdOfEdge(e: GraphEdge): string {
function getIdOfEdge(e: EdgeGraphData): string {
if (e.id) {
return e.id;
}
return e.from + "####" + e.to;
return e.from + "####" + e.to + "|" + e.label;
}
const newEdges = new Set<string>();
@@ -93,6 +91,7 @@ export class VisJsGraphViewer extends React.Component<{
label: n.label,
from: n.from,
to: n.to,
color: n.color,
});
}
this.edges.forEach(item => {
@@ -121,6 +120,5 @@ export class VisJsGraphViewer extends React.Component<{
},
};
const network = new Network(this.divRef.current!, data, options);
//network.stabilize();
}
}

View File

@@ -2,7 +2,7 @@ import {
VisualizationProvider,
VisualizationCollector,
asVisualizationId,
} from "./Visualizer";
} from "../Visualizer";
import {
ExtractedData,
isCommonDataType,

View File

@@ -2,7 +2,7 @@ import {
VisualizationProvider,
VisualizationCollector,
asVisualizationId,
} from "./Visualizer";
} from "../Visualizer";
import {
ExtractedData,
isCommonDataType,

View File

@@ -5,7 +5,6 @@ import {
TreeWithPathView,
} from "./Views";
import {
CommonDataTypes,
ExtractedData,
isCommonDataType,
TreeNode,
@@ -40,7 +39,7 @@ export function createTreeViewModelFromTreeNodeData<TData>(
children,
node.data
);
model.isMarked = node.isMarked;
model.isMarked = !!node.isMarked;
for (const c of children) {
c.parent = model;
}

View File

@@ -1,20 +1,20 @@
import "./style.scss";
import { ComposedVisualizationProvider } from "./Visualizer";
import { TreeVisualizer } from "./TreeVisualizer/Visualizer";
import { DotGraphVisualizer } from "./DotGraphVisualizer";
import { GraphvizGraphVisualizer } from "./GraphVisualizer/GraphvizGraphVisualizer";
import { SvgVisualizer } from "./SvgVisualizer";
import { DotVisualizer } from "./DotVisualizer";
import { TextVisualizer } from "./TextVisualizer";
import { MonacoTextVisualizer } from "./MonacoTextVisualizer";
import { GraphvizDotVisualizer } from "./GraphVisualizer/GraphvizDotVisualizer";
import { TextVisualizer } from "./TextVisualizer/TextVisualizer";
import { MonacoTextVisualizer } from "./TextVisualizer/MonacoTextVisualizer";
import { AstVisualizer } from "./AstViewer";
import { VisJsGraphVisualizer } from "./VisJsGraphVisualizer";
import { VisJsGraphVisualizer } from "./GraphVisualizer/VisJsGraphVisualizer";
export const knownVisualizations = new ComposedVisualizationProvider([
new TreeVisualizer(),
new DotGraphVisualizer(),
new GraphvizGraphVisualizer(),
new VisJsGraphVisualizer(),
new SvgVisualizer(),
new DotVisualizer(),
new GraphvizDotVisualizer(),
new TextVisualizer(),
new MonacoTextVisualizer(),
new AstVisualizer(),

View File

@@ -2,12 +2,12 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import "./style.scss";
import { Model } from "./Model/Model";
import Components = require("./Components");
import Components = require("./Components/GUI");
const model = new Model();
function render(target: HTMLDivElement) {
const c = require("./Components") as typeof Components;
const c = require("./Components/GUI") as typeof Components;
ReactDOM.render(<c.GUI model={model} />, target);
}
@@ -23,7 +23,7 @@ declare var module: {
declare var require: (name: string) => any;
if (module.hot) {
module.hot.accept("./Components", () => {
module.hot.accept("./Components/GUI", () => {
render(target);
});
}

View File

@@ -87,6 +87,11 @@ export const debugVisualizerUIContract = contract({
}),
},
server: {
authenticate: requestContract({
params: types.type({
secret: types.string,
}),
}),
setPreferredDataExtractor: notificationContract({
params: types.type({
dataExtractorId: unchecked<DataExtractorId>(),

View File

@@ -1512,6 +1512,13 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
crypto-random-string@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-3.1.0.tgz#0368382de82e914179ad2ca9c7a788e260184bae"
integrity sha512-Tip3yGB+bA7B0W8E4K4mNf2rZhu5r2G5Tb89/utEl5tP1QuLjTF/S9a1b8ifDrR4ORc9Utf6tscpSEtBY3YcPQ==
dependencies:
type-fest "^0.8.1"
css-loader@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.1.0.tgz#6f008b993b8ce812e6bab57f3cbfdc7a7cf28685"
@@ -5727,6 +5734,11 @@ type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5:
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
type-fest@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"