Various visualizer improvements, uses registerDebugAdapterTrackerFactory now.

This commit is contained in:
Henning Dieterichs
2020-02-09 18:32:35 +01:00
parent 58ee8e768a
commit 0d9e40b115
26 changed files with 678 additions and 235 deletions

4
.vscode/launch.json vendored
View File

@@ -10,6 +10,9 @@
"--extensionDevelopmentPath=${workspaceFolder}/extension",
"${workspaceFolder}\\example-project"
],
"env": {
"HOT_RELOAD": "true"
},
"outFiles": ["${workspaceFolder}/extension/dist/**/*.js"],
"preLaunchTask": "npm: dev - extension"
},
@@ -23,6 +26,7 @@
"${workspaceFolder}\\example-project"
],
"env": {
"HOT_RELOAD": "true",
"USE_DEV_UI": "true"
},
"outFiles": ["${workspaceFolder}/extension/dist/**/*.js"],

View File

@@ -1,7 +1,7 @@
{
"name": "@hediet/debug-visualizer-data-extraction",
"private": true,
"version": "0.1.0",
"version": "0.2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": {

View File

@@ -4,7 +4,7 @@ export type DataExtractionResult = {
availableExtractors: DataExtractorInfo[];
};
// is json
// Instances must be valid json values.
export type ExtractedData = {
kind: Record<string, true>;
};

View File

@@ -8,23 +8,27 @@ export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
extractionCollector: ExtractionCollector<ExtractedData>,
evalFn: <TEval>(expression: string) => TEval
): void {
if (typeof data === "object" && data && "kind" in data) {
const obj = data as any;
if (typeof obj.kind === "object" && obj.kind) {
const areAllTrue = Object.values(obj.kind).every(
val => val === true
);
if (areAllTrue) {
extractionCollector.addExtraction({
id: this.id,
name: "As Is",
priority: 500,
extractData() {
return obj;
},
});
}
}
if (typeof data !== "object" || !data || !("kind" in data)) {
return;
}
const obj = data as any;
if (typeof obj.kind !== "object" || !obj.kind) {
return;
}
const areAllTrue = Object.values(obj.kind).every(val => val === true);
if (!areAllTrue) {
return;
}
extractionCollector.addExtraction({
id: this.id,
name: "As Is",
priority: 500,
extractData() {
return obj;
},
});
}
}

View File

@@ -10,18 +10,20 @@ export class GetDebugVisualizationDataExtractor
evalFn: <TEval>(expression: string) => TEval
): void {
if (
typeof data === "object" &&
data &&
"getDebugVisualization" in data
typeof data !== "object" ||
!data ||
!("getDebugVisualization" in data)
) {
extractionCollector.addExtraction({
id: this.id,
name: "getDebugVisualization",
priority: 500,
extractData() {
return (data as any).getDebugVisualization();
},
});
return;
}
extractionCollector.addExtraction({
id: this.id,
name: "Use Method 'getDebugVisualization'",
priority: 500,
extractData() {
return (data as any).getDebugVisualization();
},
});
}
}

View File

@@ -0,0 +1,80 @@
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,3 +1,4 @@
export * from "./Extractors";
export * from "./DataExtractor";
export * from "./DataExtractorApi";
export * from "./getClosure";

BIN
docs/linked-list-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -5,12 +5,20 @@
"version": "0.2.0",
"configurations": [
{
"type": "node2-with-pause-event",
"type": "node",
"request": "launch",
"name": "Launch Program",
"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"
}
]
}

View File

@@ -0,0 +1,72 @@
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

@@ -10,6 +10,7 @@ enableHotReload();
import {
registerAll,
CommonDataTypes,
getClosure,
} from "@hediet/debug-visualizer-data-extraction";
import { MockLanguageServiceHost } from "./MockLanguageServiceHost";
@@ -17,82 +18,6 @@ registerAll();
registerUpdateReconciler(module);
function getClosure<T>(
root: T,
edgeSelector: (item: T) => { item: T; edgeLabel: string }[],
labelSelector: (item: T) => string
): 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 {
let id = ids.get(item);
if (!id) {
id = (idCounter++).toString();
ids.set(item, id);
}
return id;
}
const queue = new Array<T>(root);
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;
}
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;
}
toData(): CommonDataTypes.GraphData {
return getClosure<DoubleLinkedList>(
this,
r =>
[
{ item: r.next!, edgeLabel: "next" },
{ item: r.prev!, edgeLabel: "prev" },
].filter(r => !!r.item),
item => item.name
);
}
}
@hotClass(module)
class Main {
run() {
@@ -149,26 +74,11 @@ class Test1 {
},
};
debugger;
for (const ident of identifiers) {
const s = c.getSymbolAtLocation(ident);
myValue = ident;
}
const first = (myValue = new DoubleLinkedList("1"));
myValue.setNext(new DoubleLinkedList("2"));
myValue.next!.setNext(new DoubleLinkedList("3"));
myValue.next!.next!.setNext(new DoubleLinkedList("4"));
let last: DoubleLinkedList | undefined = undefined;
while (myValue) {
myValue.prev = myValue.next;
myValue.next = last;
last = myValue;
myValue = myValue.prev;
}
myValue = first;
myValue = {
kind: { text: true, svg: true },
text: `

View File

@@ -2,7 +2,7 @@
"name": "debug-visualizer",
"displayName": "Debug Visualizer",
"description": "An extension that visualizes values while debugging.",
"version": "0.1.2",
"version": "0.2.0",
"license": "MIT",
"engines": {
"vscode": "^1.35.0"

View File

@@ -3,7 +3,7 @@ import {
EvaluationWatcher,
EvaluationWatcherOptions,
} from "./DataSource";
import { observable } from "mobx";
import { observable, autorun } from "mobx";
import * as vscode from "vscode";
import { Disposable } from "@hediet/std/disposable";
import {
@@ -14,15 +14,19 @@ import {
selfContainedInitDataExtractorApi,
TypeScriptAstDataExtractor,
AsIsDataExtractor,
GetDebugVisualizationDataExtractor,
} from "@hediet/debug-visualizer-data-extraction";
import {
DataExtractionState,
CompletionItem,
} from "@hediet/debug-visualizer-vscode-shared";
import { hotClass } from "@hediet/node-reload";
import { VsCodeDebuggerView, VsCodeDebugSession } from "../VsCodeDebugger";
export function createJsDebuggerSource(): JsDataSource {
return new JsDebuggerSourceImplementation();
export function createJsDebuggerSource(args: {
vsCodeDebuggerView: VsCodeDebuggerView;
}): JsDataSource {
return new JsDebuggerSourceImplementation(args.vsCodeDebuggerView);
}
export interface JsCode extends String {
@@ -35,48 +39,41 @@ export interface JsDataSource extends DataSource {
@hotClass(module)
export class JsDebuggerSourceImplementation implements JsDataSource {
private lastFrameId: number | null = null;
public readonly dispose = Disposable.fn();
private readonly watchers = new Set<ObservableEvaluationWatcher>();
public readonly dispose = Disposable.fn();
constructor() {
this.dispose.track(
vscode.debug.onDidReceiveDebugSessionCustomEvent(e => {
if (e.event === "paused") {
const frameId = e.body.currentFrameId;
this.lastFrameId = frameId;
constructor(private readonly vsCodeDebuggerView: VsCodeDebuggerView) {
this.dispose.track({
dispose: autorun(() => {
if (
vsCodeDebuggerView.activeDebugSession &&
vsCodeDebuggerView.activeFrameId !== undefined
) {
for (const w of this.watchers) {
w.refresh();
}
}
})
);
}),
});
}
registerDataExtractor(classExpression: JsCode): void {}
registerDataExtractor(classExpression: JsCode): void {
// TODO implement
}
async getCompletions(
public async getCompletions(
text: string,
column: number
): Promise<CompletionItem[]> {
const session = vscode.debug.activeDebugSession;
const session = this.vsCodeDebuggerView.activeDebugSession;
if (!session) {
return [];
}
try {
const reply = await session.customRequest("completions", {
text,
frameId: this.lastFrameId,
column,
});
return reply.targets;
} catch (error) {
console.error(error);
return [];
}
return await session.getCompletions({
text,
frameId: this.vsCodeDebuggerView.activeFrameId,
column,
});
}
public createEvaluationWatcher(
@@ -94,12 +91,14 @@ export class JsDebuggerSourceImplementation implements JsDataSource {
}
public async refresh(w: ObservableEvaluationWatcher): Promise<void> {
const session = vscode.debug.activeDebugSession;
const session = this.vsCodeDebuggerView.activeDebugSession;
if (!session) {
w._state = { kind: "noDebugSession" };
return;
}
const frameId = this.vsCodeDebuggerView.activeFrameId;
try {
w._state = { kind: "loading" };
const fnSrc = selfContainedGetInitializedDataExtractorApi.toString();
@@ -111,12 +110,11 @@ export class JsDebuggerSourceImplementation implements JsDataSource {
const body = `(${fnSrc})().getData(${w.expression}, expr => eval(expr), ${preferredExtractor})`;
const expression = `(() => { try { return ${body}; } catch (e) { return JSON.stringify({ kind: "Error", message: e.message, stack: e.stack }); } })()`;
const reply = await session.customRequest("evaluate", {
const reply = await session.evaluate({
expression,
frameId: this.lastFrameId,
context: "watch",
frameId,
});
const resultStr = reply.result as string;
const resultStr = reply.result;
const jsonData = resultStr.substr(1, resultStr.length - 2);
const result = JSON.parse(jsonData) as DataResult;
if (result.kind === "NoExtractors") {
@@ -132,7 +130,7 @@ export class JsDebuggerSourceImplementation implements JsDataSource {
} catch (error) {
const msg = error.message as string | undefined;
if (msg && msg.includes(ApiHasNotBeenInitializedCode)) {
if (await this.initializeApi(session)) {
if (await this.initializeApi(session, frameId)) {
await this.refresh(w);
return;
}
@@ -146,21 +144,23 @@ export class JsDebuggerSourceImplementation implements JsDataSource {
}
private async initializeApi(
session: vscode.DebugSession
session: VsCodeDebugSession,
frameId: number | undefined
): Promise<boolean> {
try {
let expression = `(${selfContainedInitDataExtractorApi.toString()})();`;
const es = [TypeScriptAstDataExtractor, AsIsDataExtractor].map(
e => `new (${e.toString()})()`
);
const es = [
TypeScriptAstDataExtractor,
AsIsDataExtractor,
GetDebugVisualizationDataExtractor,
].map(e => `new (${e.toString()})()`);
expression += `(${selfContainedGetInitializedDataExtractorApi.toString()})()`;
expression += `.registerExtractors([${es.join(",")}])`;
const reply = await session.customRequest("evaluate", {
const reply = await session.evaluate({
expression,
frameId: this.lastFrameId,
context: "watch",
frameId,
});
if (reply.result === "true") {
// register extractors

View File

@@ -0,0 +1,15 @@
import { Disposable } from "@hediet/std/disposable";
import { VsCodeDebugger, VsCodeDebuggerView } from "../VsCodeDebugger";
import { createJsDebuggerSource } from "./JsDebuggerSource";
export class Sources {
public readonly dispose = Disposable.fn();
private readonly debugger = this.dispose.track(new VsCodeDebugger());
private readonly debuggerView = this.dispose.track(
new VsCodeDebuggerView(this.debugger)
);
public readonly jsSource = createJsDebuggerSource({
vsCodeDebuggerView: this.debuggerView,
});
}

View File

@@ -1,5 +1 @@
import { createJsDebuggerSource } from "./JsDebuggerSource";
export class Sources {
public readonly jsSource = createJsDebuggerSource();
}
export * from "./Sources";

View File

@@ -0,0 +1,192 @@
import { Disposable } from "@hediet/std/disposable";
import { debug, DebugSession } from "vscode";
import { EventEmitter } from "@hediet/std/events";
import { CompletionItem } from "@hediet/debug-visualizer-vscode-shared";
import { observable, runInAction } from "mobx";
export class VsCodeDebugger {
public readonly dispose = Disposable.fn();
private readonly sessions = new Map<DebugSession, VsCodeDebugSession>();
private readonly _onDidStartDebugSession = new EventEmitter<{
session: VsCodeDebugSession;
}>();
public readonly onDidStartDebugSession = this._onDidStartDebugSession.asEvent();
public getDebugSession(session: DebugSession): VsCodeDebugSession {
return this.sessions.get(session)!;
}
constructor() {
this.dispose.track([
debug.onDidStartDebugSession(session => {
const e = this.sessions.get(session)!;
this._onDidStartDebugSession.emit({ session: e });
}),
debug.onDidTerminateDebugSession(session => {
const e = this.sessions.get(session)!;
// TODO add proper event
this.sessions.delete(session);
}),
debug.registerDebugAdapterTrackerFactory("*", {
createDebugAdapterTracker: session => {
const extendedSession = new VsCodeDebugSession(session);
this.sessions.set(session, extendedSession);
return {
onDidSendMessage: async msg => {
type Message =
| StoppedEvent
| ThreadsResponse
| ContinueResponse
| NextResponse;
interface ContinueResponse {
type: "response";
command: "continue";
}
interface NextResponse {
type: "response";
command: "next";
}
interface StoppedEvent {
type: "event";
event: "stopped";
body: {
threadId: number;
};
}
interface ThreadsResponse {
type: "response";
command: "threads";
success: boolean;
body: {
threads: ThreadInfo[];
};
}
interface ThreadInfo {
id: number;
name: string;
}
const m = msg as Message;
if (m.type === "event") {
if (m.event === "stopped") {
const threadId = m.body.threadId;
const r = await extendedSession[
"getStackTrace"
]({
threadId,
levels: 1,
});
extendedSession["activeStackFrame"] =
r.stackFrames.length > 0
? r.stackFrames[0].id
: undefined;
}
} else if (m.type === "response") {
if (
m.command === "continue" ||
m.command === "next"
) {
extendedSession[
"activeStackFrame"
] = undefined;
}
}
},
};
},
}),
]);
}
}
export class VsCodeDebugSession {
@observable protected activeStackFrame: number | undefined;
constructor(public readonly session: DebugSession) {}
protected async getStackTrace(args: { threadId: number; levels?: number }) {
interface StackFrame {
id: number;
name: string;
}
const reply = (await this.session.customRequest("stackTrace", {
threadId: args.threadId,
levels: args.levels,
})) as { totalFrames?: number; stackFrames: StackFrame[] };
return reply;
}
public async getCompletions(args: {
text: string;
column: number;
frameId: number | undefined;
}): Promise<CompletionItem[]> {
try {
const reply = await this.session.customRequest("completions", {
text: args.text,
frameId: args.frameId,
column: args.column,
});
return reply.targets;
} catch (error) {
console.error(error);
return [];
}
}
public async evaluate(args: {
expression: string;
frameId: number | undefined;
}): Promise<{ result: string }> {
const reply = await this.session.customRequest("evaluate", {
expression: args.expression,
frameId: args.frameId,
context: "watch",
});
return { result: reply.result };
}
}
export class VsCodeDebuggerView {
public readonly dispose = Disposable.fn();
@observable private _activeDebugSession: VsCodeDebugSession | undefined;
public get activeDebugSession(): VsCodeDebugSession | undefined {
return this._activeDebugSession;
}
public get activeFrameId(): number | undefined {
if (!this._activeDebugSession) {
return undefined;
} else {
return this._activeDebugSession["activeStackFrame"];
}
}
constructor(private vsCodeDebugger: VsCodeDebugger) {
this.dispose.track(
debug.onDidChangeActiveDebugSession(activeSession => {
runInAction("Update active debug session", () => {
if (!activeSession) {
this._activeDebugSession = undefined;
} else {
const s = this.vsCodeDebugger.getDebugSession(
activeSession
);
this._activeDebugSession = s;
}
});
})
);
}
}

View File

@@ -1,4 +1,4 @@
import { window, ExtensionContext, WebviewPanel, commands } from "vscode";
import { window, ExtensionContext, commands } from "vscode";
import { Disposable } from "@hediet/std/disposable";
import {
enableHotReload,
@@ -7,22 +7,21 @@ import {
getReloadCount,
} from "@hediet/node-reload";
if (process.env.USE_DEV_UI) {
if (process.env.HOT_RELOAD) {
enableHotReload({ entryModule: module, loggingEnabled: true });
}
registerUpdateReconciler(module);
import { WebViews } from "./WebViews";
import { Server } from "./Server";
import { Config } from "./Config";
import { Sources } from "./DataSource";
registerUpdateReconciler(module);
export class Extension {
public dispose = Disposable.fn();
private readonly config = new Config();
private readonly sources = new Sources();
private readonly sources = this.dispose.track(new Sources());
private readonly server = new Server(this.sources, this.config);
private readonly views = this.dispose.track(new WebViews(this.server));

View File

@@ -1,5 +1,6 @@
declare namespace NodeJS {
declare interface ProcessEnv {
HOT_RELOAD?: "true";
USE_DEV_UI?: "true";
}
}

View File

@@ -23,12 +23,15 @@
"react-measure": "^2.3.0",
"react-svg-pan-zoom": "^3.1.0",
"viz.js": "^2.1.2",
"line-column": "^1.0.2"
"line-column": "^1.0.2",
"vis-network": "^7.3.4",
"vis-data": "^6.3.5"
},
"devDependencies": {
"@types/classnames": "^2.2.8",
"@types/react": "^16.8.21",
"@types/react-dom": "^16.8.4",
"@types/line-column": "^1.0.0",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.0.0",
"file-loader": "^4.0.0",

View File

@@ -19,7 +19,7 @@ import { observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun, trace } from "mobx";
import * as monaco from "monaco-editor";
import { getLanguageId } from "./MonacoTextVisualizer";
import * as LineColumn from "line-column";
import LineColumn = require("line-column");
export class AstVisualizer extends VisualizationProvider {
getVisualizations(

View File

@@ -11,6 +11,26 @@ import {
isCommonDataType,
} from "@hediet/debug-visualizer-data-extraction";
export class DotGraphVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { graph: true })) {
collector.addVisualization({
id: asVisualizationId("dot-graph"),
name: "Dot Graph",
priority: 100,
render() {
return (
<DotGraphViewer edges={data.edges} nodes={data.nodes} />
);
},
});
}
}
}
@observer
export class DotGraphViewer extends React.Component<{
nodes: { id: string; label: string }[];
@@ -36,23 +56,3 @@ export class DotGraphViewer extends React.Component<{
return <DotViewer dotCode={dotContent} />;
}
}
export class DotGraphVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { graph: true })) {
collector.addVisualization({
id: asVisualizationId("dot-graph"),
name: "Dot Graph",
priority: 100,
render() {
return (
<DotGraphViewer edges={data.edges} nodes={data.nodes} />
);
},
});
}
}
}

View File

@@ -19,6 +19,24 @@ const viz: any = new Viz({
render,
});
export class DotVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { dotGraph: true })) {
collector.addVisualization({
id: asVisualizationId("dot-graph"),
name: "Dot Graph",
priority: 100,
render() {
return <DotViewer dotCode={data.text} />;
},
});
}
}
}
@observer
export class DotViewer extends React.Component<{
dotCode: string;
@@ -40,21 +58,3 @@ export class DotViewer extends React.Component<{
return <SvgViewer svgRef={this.props.svgRef} svgContent={this.svg} />;
}
}
export class DotVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { dotGraph: true })) {
collector.addVisualization({
id: asVisualizationId("dot-graph"),
name: "Dot Graph",
priority: 100,
render() {
return <DotViewer dotCode={data.text} />;
},
});
}
}
}

View File

@@ -13,6 +13,24 @@ import {
isCommonDataType,
} from "@hediet/debug-visualizer-data-extraction";
export class SvgVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { svg: true })) {
collector.addVisualization({
id: asVisualizationId("svg"),
name: "Svg",
priority: 100,
render() {
return <SvgViewer svgContent={data.text} />;
},
});
}
}
}
function widthOrDefault(r: ContentRect): number {
if (r.bounds && r.bounds.width) {
return r.bounds.width;
@@ -96,21 +114,3 @@ export class SvgViewer extends React.Component<{
);
}
}
export class SvgVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { svg: true })) {
collector.addVisualization({
id: asVisualizationId("svg"),
name: "Svg",
priority: 100,
render() {
return <SvgViewer svgContent={data.text} />;
},
});
}
}
}

View File

@@ -0,0 +1,126 @@
import { observer } from "mobx-react";
import * as React from "react";
import {
VisualizationProvider,
VisualizationCollector,
asVisualizationId,
} from "./Visualizer";
import {
ExtractedData,
isCommonDataType,
} from "@hediet/debug-visualizer-data-extraction";
import { DataSet, Network } from "vis-network";
export class VisJsGraphVisualizer extends VisualizationProvider {
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
if (isCommonDataType(data, { graph: true })) {
collector.addVisualization({
id: asVisualizationId("vis-js-graph"),
name: "VisJs Graph",
priority: 1001,
render() {
return (
<VisJsGraphViewer
edges={data.edges}
nodes={data.nodes}
/>
);
},
});
}
}
}
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[];
}> {
private readonly divRef = React.createRef<HTMLDivElement>();
private readonly nodes = new DataSet<{ id: string; label: string }>();
private readonly edges = new DataSet<{
id: string;
label: string;
from: string;
to: string;
}>();
render() {
//const { nodes, edges } = this.props;
return <div style={{ height: "100%" }} ref={this.divRef} />;
}
synchronizeData() {
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.forEach(item => {
if (!newNodes.has(item.id)) {
this.nodes.remove(item);
}
});
function getIdOfEdge(e: GraphEdge): string {
if (e.id) {
return e.id;
}
return e.from + "####" + e.to;
}
const newEdges = new Set<string>();
for (const n of this.props.edges) {
const id = getIdOfEdge(n);
newEdges.add(id);
this.edges.update({
id: id,
label: n.label,
from: n.from,
to: n.to,
});
}
this.edges.forEach(item => {
if (!newEdges.has(item.id)) {
this.edges.remove(item);
}
});
}
componentDidUpdate() {
this.synchronizeData();
}
componentDidMount() {
this.synchronizeData();
const data = {
nodes: this.nodes,
edges: this.edges,
};
const options = {
edges: {
arrows: {
to: { enabled: true, scaleFactor: 1, type: "arrow" },
},
},
};
const network = new Network(this.divRef.current!, data, options);
//network.stabilize();
}
}

View File

@@ -7,10 +7,12 @@ import { DotVisualizer } from "./DotVisualizer";
import { TextVisualizer } from "./TextVisualizer";
import { MonacoTextVisualizer } from "./MonacoTextVisualizer";
import { AstVisualizer } from "./AstViewer";
import { VisJsGraphVisualizer } from "./VisJsGraphVisualizer";
export const knownVisualizations = new ComposedVisualizationProvider([
new TreeVisualizer(),
new DotGraphVisualizer(),
new VisJsGraphVisualizer(),
new SvgVisualizer(),
new DotVisualizer(),
new TextVisualizer(),

View File

@@ -233,6 +233,11 @@
"@types/tapable" "*"
"@types/webpack" "*"
"@types/line-column@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/line-column/-/line-column-1.0.0.tgz#fa5a59c21e885fef3739a273b43dacf55b63437f"
integrity sha512-wbw+IDRw/xY/RGy+BL6f4Eey4jsUgHQrMuA4Qj0CSG3x/7C2Oc57pmRoM2z3M4DkylWRz+G1pfX06sCXQm0J+w==
"@types/mime@*":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
@@ -5912,6 +5917,29 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vis-data@^6.3.5:
version "6.3.5"
resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-6.3.5.tgz#db53cb0828601aa0908f86eca1cfe80f69f17873"
integrity sha512-H8lD1NTmC5HimYeqTouxhbUya5qrYQGjpJhIHkxsm5R34EEdcm/D1cLTMLFFyIoZC6SQkADQj+dlFKf5DOnSww==
optionalDependencies:
vis-util "2.0.1"
vis-uuid "1.1.3"
vis-network@^7.3.4:
version "7.3.4"
resolved "https://registry.yarnpkg.com/vis-network/-/vis-network-7.3.4.tgz#818ccf7d82afffae7059e6b2cb520cd28ea01070"
integrity sha512-4hfB1DDnzclUUdniBh2WN2agcCmrL2ERmWsRB8wv8XtiiQ+Gg6HNzx5v7xySRWK+yH88LVnrrzaM+mDb5jcQtg==
vis-util@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/vis-util/-/vis-util-2.0.1.tgz#4538aee119795bde1f7996a3ee8e0148baf71124"
integrity sha512-VBQ6zCT+Iaum70uzLbjk0nLMOjp9NhPzU42j0gaM6q6tg0eTcgIWNXzAqamhQtmm1kDj0jMLzKvkFOPENYv34w==
vis-uuid@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/vis-uuid/-/vis-uuid-1.1.3.tgz#2f53ff35e9e026b0ec93bc433ce685c40c2f784c"
integrity sha512-2B6XdY1bkzbUh+TugmnAaFa61KO9R5pzBzIuFIm8a9FrkbxIdSmQXV+FbfkL8QunkQV/bT0JDLQ2puqCS2+0Og==
viz.js@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/viz.js/-/viz.js-2.1.2.tgz#6f09cd4e10af28754a6d50b055bd2e4a7693983a"