diff --git a/.vscode/launch.json b/.vscode/launch.json index 4ece952..ed285e7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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"], diff --git a/data-extraction/package.json b/data-extraction/package.json index 3f9e7f6..1455d95 100644 --- a/data-extraction/package.json +++ b/data-extraction/package.json @@ -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": { diff --git a/data-extraction/src/DataExtractionResult.ts b/data-extraction/src/DataExtractionResult.ts index cce98ba..c9346b4 100644 --- a/data-extraction/src/DataExtractionResult.ts +++ b/data-extraction/src/DataExtractionResult.ts @@ -4,7 +4,7 @@ export type DataExtractionResult = { availableExtractors: DataExtractorInfo[]; }; -// is json +// Instances must be valid json values. export type ExtractedData = { kind: Record; }; diff --git a/data-extraction/src/js/Extractors/AsIsDataExtractor.ts b/data-extraction/src/js/Extractors/AsIsDataExtractor.ts index c1f012a..eb2b0dd 100644 --- a/data-extraction/src/js/Extractors/AsIsDataExtractor.ts +++ b/data-extraction/src/js/Extractors/AsIsDataExtractor.ts @@ -8,23 +8,27 @@ export class AsIsDataExtractor implements DataExtractor { extractionCollector: ExtractionCollector, evalFn: (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; + }, + }); } } diff --git a/data-extraction/src/js/Extractors/GetDebugVisualizationDataExtractor.ts b/data-extraction/src/js/Extractors/GetDebugVisualizationDataExtractor.ts index 0ba68b9..9f93553 100644 --- a/data-extraction/src/js/Extractors/GetDebugVisualizationDataExtractor.ts +++ b/data-extraction/src/js/Extractors/GetDebugVisualizationDataExtractor.ts @@ -10,18 +10,20 @@ export class GetDebugVisualizationDataExtractor evalFn: (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(); + }, + }); } } diff --git a/data-extraction/src/js/getClosure.ts b/data-extraction/src/js/getClosure.ts new file mode 100644 index 0000000..c0bb322 --- /dev/null +++ b/data-extraction/src/js/getClosure.ts @@ -0,0 +1,80 @@ +import { CommonDataTypes } from "../CommonDataTypes"; + +export function getClosureObj( + roots: Record, + 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( + items.filter(i => !!i) as T[], + edgeSelector, + item => { + const names = new Array(); + 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( + 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(); + 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(...roots); + const processed = new Set(); + + 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; +} diff --git a/data-extraction/src/js/index.ts b/data-extraction/src/js/index.ts index bfcc9eb..1d011bd 100644 --- a/data-extraction/src/js/index.ts +++ b/data-extraction/src/js/index.ts @@ -1,3 +1,4 @@ export * from "./Extractors"; export * from "./DataExtractor"; export * from "./DataExtractorApi"; +export * from "./getClosure"; diff --git a/docs/linked-list-demo.gif b/docs/linked-list-demo.gif new file mode 100644 index 0000000..da35b6e Binary files /dev/null and b/docs/linked-list-demo.gif differ diff --git a/example-project/.vscode/launch.json b/example-project/.vscode/launch.json index 75473e8..de328a3 100644 --- a/example-project/.vscode/launch.json +++ b/example-project/.vscode/launch.json @@ -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" } ] } diff --git a/example-project/src/DoubleLinkedList.ts b/example-project/src/DoubleLinkedList.ts new file mode 100644 index 0000000..ce240b3 --- /dev/null +++ b/example-project/src/DoubleLinkedList.ts @@ -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 + ): 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; + } +} diff --git a/example-project/src/index.ts b/example-project/src/index.ts index cfb42be..8524f9c 100644 --- a/example-project/src/index.ts +++ b/example-project/src/index.ts @@ -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( - 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(); - 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(root); - const processed = new Set(); - - 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( - 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: ` diff --git a/extension/package.json b/extension/package.json index 3f3cb0e..8cea79b 100644 --- a/extension/package.json +++ b/extension/package.json @@ -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" diff --git a/extension/src/DataSource/JsDebuggerSource.ts b/extension/src/DataSource/JsDebuggerSource.ts index 820507e..bcd536e 100644 --- a/extension/src/DataSource/JsDebuggerSource.ts +++ b/extension/src/DataSource/JsDebuggerSource.ts @@ -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(); - 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 { - 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 { - 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 { 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 diff --git a/extension/src/DataSource/Sources.ts b/extension/src/DataSource/Sources.ts new file mode 100644 index 0000000..a571dc9 --- /dev/null +++ b/extension/src/DataSource/Sources.ts @@ -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, + }); +} diff --git a/extension/src/DataSource/index.ts b/extension/src/DataSource/index.ts index a2a0cf2..fe0b90f 100644 --- a/extension/src/DataSource/index.ts +++ b/extension/src/DataSource/index.ts @@ -1,5 +1 @@ -import { createJsDebuggerSource } from "./JsDebuggerSource"; - -export class Sources { - public readonly jsSource = createJsDebuggerSource(); -} +export * from "./Sources"; diff --git a/extension/src/VsCodeDebugger.ts b/extension/src/VsCodeDebugger.ts new file mode 100644 index 0000000..95dec6a --- /dev/null +++ b/extension/src/VsCodeDebugger.ts @@ -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(); + + 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 { + 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; + } + }); + }) + ); + } +} diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 7591564..7dd23b1 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -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)); diff --git a/extension/src/types.d.ts b/extension/src/types.d.ts index f45acbb..33a08b8 100644 --- a/extension/src/types.d.ts +++ b/extension/src/types.d.ts @@ -1,5 +1,6 @@ declare namespace NodeJS { declare interface ProcessEnv { + HOT_RELOAD?: "true"; USE_DEV_UI?: "true"; } } diff --git a/extension/ui/package.json b/extension/ui/package.json index 59475ed..a22c909 100644 --- a/extension/ui/package.json +++ b/extension/ui/package.json @@ -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", diff --git a/extension/ui/src/Visualizers/AstViewer.tsx b/extension/ui/src/Visualizers/AstViewer.tsx index bf8f82a..bce37b9 100644 --- a/extension/ui/src/Visualizers/AstViewer.tsx +++ b/extension/ui/src/Visualizers/AstViewer.tsx @@ -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( diff --git a/extension/ui/src/Visualizers/DotGraphVisualizer.tsx b/extension/ui/src/Visualizers/DotGraphVisualizer.tsx index db84559..f0f56e2 100644 --- a/extension/ui/src/Visualizers/DotGraphVisualizer.tsx +++ b/extension/ui/src/Visualizers/DotGraphVisualizer.tsx @@ -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 ( + + ); + }, + }); + } + } +} + @observer export class DotGraphViewer extends React.Component<{ nodes: { id: string; label: string }[]; @@ -36,23 +56,3 @@ export class DotGraphViewer extends React.Component<{ return ; } } - -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 ( - - ); - }, - }); - } - } -} diff --git a/extension/ui/src/Visualizers/DotVisualizer.tsx b/extension/ui/src/Visualizers/DotVisualizer.tsx index 1c6a0ae..a8ef6f8 100644 --- a/extension/ui/src/Visualizers/DotVisualizer.tsx +++ b/extension/ui/src/Visualizers/DotVisualizer.tsx @@ -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 ; + }, + }); + } + } +} + @observer export class DotViewer extends React.Component<{ dotCode: string; @@ -40,21 +58,3 @@ export class DotViewer extends React.Component<{ return ; } } - -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 ; - }, - }); - } - } -} diff --git a/extension/ui/src/Visualizers/SvgVisualizer.tsx b/extension/ui/src/Visualizers/SvgVisualizer.tsx index d2b8fae..c78c5a4 100644 --- a/extension/ui/src/Visualizers/SvgVisualizer.tsx +++ b/extension/ui/src/Visualizers/SvgVisualizer.tsx @@ -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 ; + }, + }); + } + } +} + 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 ; - }, - }); - } - } -} diff --git a/extension/ui/src/Visualizers/VisJsGraphVisualizer.tsx b/extension/ui/src/Visualizers/VisJsGraphVisualizer.tsx new file mode 100644 index 0000000..b490c03 --- /dev/null +++ b/extension/ui/src/Visualizers/VisJsGraphVisualizer.tsx @@ -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 ( + + ); + }, + }); + } + } +} + +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(); + 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
; + } + + synchronizeData() { + const newNodes = new Set(); + 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(); + 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(); + } +} diff --git a/extension/ui/src/Visualizers/index.ts b/extension/ui/src/Visualizers/index.ts index eb125e4..0bbac03 100644 --- a/extension/ui/src/Visualizers/index.ts +++ b/extension/ui/src/Visualizers/index.ts @@ -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(), diff --git a/yarn.lock b/yarn.lock index 7743094..b442d3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"