From 26306b68f8f12aacf9884045c20965afc43b73ef Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 2 Aug 2019 11:53:17 +0200 Subject: [PATCH] Implements some visualizers. --- .vscode/launch.json | 15 +++ data-extraction/src/CommonDataTypes.ts | 29 ++++ .../src/js/Extractors/AsIsDataExtractor.ts | 30 +++++ .../js/Extractors/TypeScriptDataExtractors.ts | 27 ++-- data-extraction/src/js/Extractors/index.ts | 9 +- example-project/src/index.ts | 124 ++---------------- extension/src/ConnectionHandler.ts | 10 +- extension/src/DataSource/DataSource.ts | 11 +- extension/src/DataSource/JsDebuggerSource.ts | 22 +++- extension/src/WebViews.ts | 3 +- extension/ui/src/Components/index.tsx | 66 ++++------ extension/ui/src/Model.ts | 31 ++++- .../ui/src/Visualizers/DotGraphVisualizer.tsx | 58 ++++++++ .../ui/src/Visualizers/DotVisualizer.tsx | 60 +++++++++ .../src/Visualizers/MonacoTextVisualizer.tsx | 72 ++++++++++ .../SvgVisualizer.tsx} | 97 +++++--------- .../ui/src/Visualizers/TextVisualizer.tsx | 32 +++++ .../Visualizers/TreeVisualizer/Visualizer.tsx | 61 +++++++++ .../Visualizers/TreeVisualizer/Visualizers.ts | 31 ----- extension/ui/src/Visualizers/Visualizer.ts | 76 +++++++++++ extension/ui/src/Visualizers/index.ts | 17 +++ extension/ui/src/Visualizers/style.scss | 16 +++ extension/ui/src/style.scss | 3 +- 23 files changed, 621 insertions(+), 279 deletions(-) create mode 100644 data-extraction/src/js/Extractors/AsIsDataExtractor.ts create mode 100644 extension/ui/src/Visualizers/DotGraphVisualizer.tsx create mode 100644 extension/ui/src/Visualizers/DotVisualizer.tsx create mode 100644 extension/ui/src/Visualizers/MonacoTextVisualizer.tsx rename extension/ui/src/{Components/GraphViz.tsx => Visualizers/SvgVisualizer.tsx} (51%) create mode 100644 extension/ui/src/Visualizers/TextVisualizer.tsx create mode 100644 extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx delete mode 100644 extension/ui/src/Visualizers/TreeVisualizer/Visualizers.ts create mode 100644 extension/ui/src/Visualizers/Visualizer.ts create mode 100644 extension/ui/src/Visualizers/index.ts create mode 100644 extension/ui/src/Visualizers/style.scss diff --git a/.vscode/launch.json b/.vscode/launch.json index 088199c..8d9132e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,21 @@ ], "outFiles": ["${workspaceFolder}/extension/out/**/*.js"], "preLaunchTask": "npm: dev - extension" + }, + { + "name": "Run Extension (Dev UI)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/extension", + "${workspaceFolder}\\example-project" + ], + "env": { + "USE_DEV_UI": "true" + }, + "outFiles": ["${workspaceFolder}/extension/out/**/*.js"], + "preLaunchTask": "npm: dev - extension" } ] } diff --git a/data-extraction/src/CommonDataTypes.ts b/data-extraction/src/CommonDataTypes.ts index cac1572..b753c88 100644 --- a/data-extraction/src/CommonDataTypes.ts +++ b/data-extraction/src/CommonDataTypes.ts @@ -1,5 +1,28 @@ import { ExtractedData } from "./DataExtractionResult"; +export function isCommonDataType( + data: ExtractedData, + kind: T +): data is Narrow { + for (const key of Object.keys(kind)) { + if (!(key in data.kind)) { + return false; + } + } + return true; +} + +type Narrow = T extends TKind ? T : never; + +export type CommonDataType = + | CommonDataTypes.Text + | CommonDataTypes.Svg + | CommonDataTypes.Html + | CommonDataTypes.DotGraph + | CommonDataTypes.TreeNodeData + | CommonDataTypes.AstData + | CommonDataTypes.GraphData; + export module CommonDataTypes { export interface Text extends ExtractedData { kind: { text: true }; @@ -8,6 +31,12 @@ export module CommonDataTypes { fileType?: string; } + export interface GraphData { + kind: { graph: true }; + nodes: { id: string; label: string }[]; + edges: { from: string; to: string; label: string }[]; + } + export interface Svg extends Text { kind: { text: true; svg: true }; text: string; diff --git a/data-extraction/src/js/Extractors/AsIsDataExtractor.ts b/data-extraction/src/js/Extractors/AsIsDataExtractor.ts new file mode 100644 index 0000000..61ee9cc --- /dev/null +++ b/data-extraction/src/js/Extractors/AsIsDataExtractor.ts @@ -0,0 +1,30 @@ +import { ExtractedData } from "../../DataExtractionResult"; +import { DataExtractor, ExtractionCollector } from ".."; + +export class AsIsDataExtractor implements DataExtractor { + readonly id = "as-is"; + getExtractions( + data: unknown, + 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: "as-is", + name: "As Is", + priority: 100, + extractData() { + return obj; + }, + }); + } + } + } + } +} diff --git a/data-extraction/src/js/Extractors/TypeScriptDataExtractors.ts b/data-extraction/src/js/Extractors/TypeScriptDataExtractors.ts index 6975d28..63705d1 100644 --- a/data-extraction/src/js/Extractors/TypeScriptDataExtractors.ts +++ b/data-extraction/src/js/Extractors/TypeScriptDataExtractors.ts @@ -11,7 +11,7 @@ export class TypeScriptAstDataExtractor collector: ExtractionCollector, evalFn: (expression: string) => TEval ): void { - if (typeof data !== "object" || data === undefined) { + if (!data) { return; } @@ -39,7 +39,7 @@ export class TypeScriptAstDataExtractor function toTreeNode( node: ts.Node, memberName: string, - marked: ts.Node + marked: Set ): CommonDataTypes.AstData["root"] { const name = tsApi.SyntaxKind[node.kind]; const children = node @@ -78,19 +78,28 @@ export class TypeScriptAstDataExtractor length: 0, position: 0, }, - isMarked: node === marked, + isMarked: marked.has(node), value, // startPos: node.pos, // endPos: node.end }; } - if ((tsApi as any).isNode(data)) { - let root = data as ts.Node; - const marked = root; - while (root.parent) { - root = root.parent; + function isNode(node: unknown): node is ts.Node { + return typeof node === "object" && (tsApi as any).isNode(node); + } + + if (isNode(data) || (Array.isArray(data) && data.every(isNode))) { + let root: ts.SourceFile; + let marked: Set; + if (Array.isArray(data)) { + root = (data[0] as ts.Node).getSourceFile(); + marked = new Set(data); + } else { + root = data.getSourceFile(); + marked = new Set([data]); } + collector.addExtraction({ id: "ts-ast", name: "TypeScript AST", @@ -99,7 +108,7 @@ export class TypeScriptAstDataExtractor return { kind: { text: true, tree: true, ast: true }, root: toTreeNode(root, "root", marked), - text: root.getSourceFile().text, + text: root.text, fileType: "ts", }; }, diff --git a/data-extraction/src/js/Extractors/index.ts b/data-extraction/src/js/Extractors/index.ts index 06d6f68..ada96b8 100644 --- a/data-extraction/src/js/Extractors/index.ts +++ b/data-extraction/src/js/Extractors/index.ts @@ -1,8 +1,15 @@ import { getDataExtractorApi } from "../DataExtractorApi"; import { TypeScriptAstDataExtractor } from "./TypeScriptDataExtractors"; +import { AsIsDataExtractor } from "./AsIsDataExtractor"; export * from "./TypeScriptDataExtractors"; export function registerAll() { - getDataExtractorApi().registerExtractor(new TypeScriptAstDataExtractor()); + const api = getDataExtractorApi(); + for (const item of [ + new TypeScriptAstDataExtractor(), + new AsIsDataExtractor(), + ]) { + api.registerExtractor(item); + } } diff --git a/example-project/src/index.ts b/example-project/src/index.ts index 4e269c3..aa2b9d0 100644 --- a/example-project/src/index.ts +++ b/example-project/src/index.ts @@ -5,8 +5,7 @@ import { hotClass, } from "@hediet/node-reload"; import { - getDataExtractorApi, - TypeScriptAstDataExtractor, + registerAll, CommonDataTypes, } from "@hediet/debug-visualizer-data-extraction"; enableHotReload(); @@ -15,7 +14,7 @@ import { liveLogId, liveLog } from "@hediet/live-debug"; registerUpdateReconciler(module); -getDataExtractorApi().registerExtractor(new TypeScriptAstDataExtractor()); +registerAll(); let i = 0; setInterval(() => { @@ -41,116 +40,6 @@ class Test { console.log("aaa"); const x = e; } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } -}class Test { - public test() { - console.log("aaa"); - const x = e; - } } `, ts.ScriptTarget.Latest, @@ -161,8 +50,13 @@ class Test { debugger; } - toString() { - return "hihi"; + test() { + // iue + return { + kind: { graph: true }, + edges: [{ from: "1", to: "2", label: "aa" }], + nodes: [{ id: "1", label: "uiae" }, { id: "2", label: "bar" }], + } as CommonDataTypes.GraphData; } } diff --git a/extension/src/ConnectionHandler.ts b/extension/src/ConnectionHandler.ts index d8e3ff1..b181fb2 100644 --- a/extension/src/ConnectionHandler.ts +++ b/extension/src/ConnectionHandler.ts @@ -27,11 +27,19 @@ export class ConnectionHandler { } }, setExpression: async ({ newExpression }) => { + let oldPreferredDataExtractor: EvaluationWatcher["preferredDataExtractor"]; if (this.watcher) { + oldPreferredDataExtractor = this.watcher + .preferredDataExtractor; this.dispose.untrack(this.watcher).dispose(); } this.watcher = this.dispose.track( - sources.jsSource.createEvaluationWatcher(newExpression) + sources.jsSource.createEvaluationWatcher( + newExpression, + { + preferredDataExtractor: oldPreferredDataExtractor, + } + ) ); }, openInBrowser: async ({}) => { diff --git a/extension/src/DataSource/DataSource.ts b/extension/src/DataSource/DataSource.ts index 1d33158..90f62b2 100644 --- a/extension/src/DataSource/DataSource.ts +++ b/extension/src/DataSource/DataSource.ts @@ -1,8 +1,15 @@ import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction"; import { DataExtractionState } from "@hediet/debug-visualizer-vscode-shared"; +export interface EvaluationWatcherOptions { + preferredDataExtractor?: DataExtractorId | undefined; +} + export interface DataSource { - createEvaluationWatcher(expression: string): EvaluationWatcher; + createEvaluationWatcher( + expression: string, + options: EvaluationWatcherOptions + ): EvaluationWatcher; } export interface EvaluationWatcher { @@ -11,7 +18,7 @@ export interface EvaluationWatcher { // is observable readonly preferredDataExtractor: DataExtractorId | undefined; - setPreferredDataExtractor(id: DataExtractorId): void; + setPreferredDataExtractor(id: DataExtractorId | undefined): void; refresh(): void; diff --git a/extension/src/DataSource/JsDebuggerSource.ts b/extension/src/DataSource/JsDebuggerSource.ts index 6eabc7e..89bdb86 100644 --- a/extension/src/DataSource/JsDebuggerSource.ts +++ b/extension/src/DataSource/JsDebuggerSource.ts @@ -1,4 +1,8 @@ -import { DataSource, EvaluationWatcher } from "./DataSource"; +import { + DataSource, + EvaluationWatcher, + EvaluationWatcherOptions, +} from "./DataSource"; import { observable } from "mobx"; import * as vscode from "vscode"; import { Disposable } from "@hediet/std/disposable"; @@ -46,8 +50,11 @@ class JsDebuggerSourceImplementation implements JsDataSource { registerDataExtractor(classExpression: JsCode): void {} - public createEvaluationWatcher(expression: string): EvaluationWatcher { - const w = new ObservableEvaluationWatcher(expression, this); + public createEvaluationWatcher( + expression: string, + options: EvaluationWatcherOptions + ): EvaluationWatcher { + const w = new ObservableEvaluationWatcher(expression, this, options); this.watchers.add(w); this.refresh(w); return w; @@ -132,8 +139,11 @@ class JsDebuggerSourceImplementation implements JsDataSource { class ObservableEvaluationWatcher implements EvaluationWatcher { constructor( public readonly expression: string, - private readonly source: JsDebuggerSourceImplementation - ) {} + private readonly source: JsDebuggerSourceImplementation, + options: EvaluationWatcherOptions + ) { + this._preferredDataExtractor = options.preferredDataExtractor; + } @observable _preferredDataExtractor: DataExtractorId | undefined = undefined; @@ -142,7 +152,7 @@ class ObservableEvaluationWatcher implements EvaluationWatcher { return this._preferredDataExtractor; } - public setPreferredDataExtractor(id: DataExtractorId): void { + public setPreferredDataExtractor(id: DataExtractorId | undefined): void { this._preferredDataExtractor = id; this.refresh(); } diff --git a/extension/src/WebViews.ts b/extension/src/WebViews.ts index 3df0193..ada7fb7 100644 --- a/extension/src/WebViews.ts +++ b/extension/src/WebViews.ts @@ -60,8 +60,7 @@ export class WebView { } export function getHtml(server: Server) { - const isDev = true; - // uiae + const isDev = !!process.env.USE_DEV_UI; return ` diff --git a/extension/ui/src/Components/index.tsx b/extension/ui/src/Components/index.tsx index 6bcd7bc..f407484 100644 --- a/extension/ui/src/Components/index.tsx +++ b/extension/ui/src/Components/index.tsx @@ -5,13 +5,8 @@ import { observable, action, computed } from "mobx"; import { Popover, Icon, Button, Spinner, InputGroup } from "@blueprintjs/core"; import classnames = require("classnames"); import Measure from "react-measure"; -import { DotGraphViewer } from "./GraphViz"; -import { TreeView } from "../Visualizers/TreeVisualizer/Views"; -import { createTreeViewModelFromTreeNodeData } from "../Visualizers/TreeVisualizer/Visualizers"; -import { - CommonDataTypes, - DataExtractorInfo, -} from "@hediet/debug-visualizer-data-extraction"; +import { DataExtractorInfo } from "@hediet/debug-visualizer-data-extraction"; +import { knownVisualizations } from "../Visualizers"; @observer export class GUI extends React.Component<{ model: Model }> { @observable text: string = "node"; @@ -89,6 +84,7 @@ export class ExpandedOptions extends React.Component<{ model: Model }> { name="Source" selected={0} options={[{ label: "js" }]} + onSelected={item => {}} />
@@ -108,7 +104,17 @@ export class ExpandedOptions extends React.Component<{ model: Model }> { { + m.setPreferredVisualizationId(item.id); + }} + options={ + m.visualizations + ? m.visualizations.allVisualizations.map(v => ({ + label: v.name, + id: v.id, + })) + : [] + } />
@@ -166,12 +172,6 @@ export class ExpressionInput extends React.Component<{ model: Model }> { }} /> ); - /* - - */ } } @@ -181,26 +181,6 @@ export class Visualizer extends React.Component<{ model: Model }> { return (
{this.renderContent()}
); - /* - - - - */ } renderContent(): JSX.Element { @@ -214,15 +194,12 @@ export class Visualizer extends React.Component<{ model: Model }> { } else if (s.kind === "noDebugSession") { return ; } else if (s.kind === "data") { - const data = s.result.data; - - if (data.kind.tree) { - const d = data as CommonDataTypes.TreeNodeData; - const m = createTreeViewModelFromTreeNodeData(d.root); - return ; - } else { - return
{JSON.stringify(s.result)}
; + const vis = this.props.model.visualizations; + if (!vis || !vis.visualization) { + return ; } + + return vis.visualization.render(); } else { const nvr: never = s; return
; @@ -316,7 +293,7 @@ export class Select extends React.Component<{ onSelected: (item: T) => void; options: T[]; }> { - @computed get selected(): T { + @computed get selected(): T | undefined { return this.props.options[this.props.selected]; } @@ -329,12 +306,13 @@ export class Select extends React.Component<{ usePortal={false} >
{this.props.options.map((o, idx) => (
this.props.onSelected(o)} > {o.label} diff --git a/extension/ui/src/Model.ts b/extension/ui/src/Model.ts index 033ec1e..ae53e1c 100644 --- a/extension/ui/src/Model.ts +++ b/extension/ui/src/Model.ts @@ -4,9 +4,11 @@ import { } from "@hediet/debug-visualizer-vscode-shared"; import { WebSocketStream } from "@hediet/typed-json-rpc-websocket"; import { ConsoleRpcLogger } from "@hediet/typed-json-rpc"; -import { observable, action } from "mobx"; +import { observable, action, computed } from "mobx"; import { Barrier } from "@hediet/std/synchronization"; import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction"; +import { Visualization, VisualizationId } from "./Visualizers/Visualizer"; +import { knownVisualizations } from "./Visualizers"; declare const window: Window & { serverPort?: number; @@ -67,6 +69,32 @@ export class Model { kind: "noExpression", }; + @observable private preferredVisualizationId: + | VisualizationId + | undefined = undefined; + + @action + public setPreferredVisualizationId(id: VisualizationId) { + this.preferredVisualizationId = id; + } + + @computed get visualizations(): + | { + visualization: Visualization | undefined; + allVisualizations: Visualization[]; + } + | undefined { + if (this.state.kind === "data") { + const vis = knownVisualizations.getBestVisualization( + this.state.result.data, + this.preferredVisualizationId + ); + return vis; + } else { + return undefined; + } + } + private server: | typeof debugVisualizerUIContract.TServerInterface | undefined = undefined; @@ -113,7 +141,6 @@ export class Model { } setPreferredExtractorId(id: DataExtractorId) { - console.log(id); if (this.server) { this.server.setPreferredDataExtractor({ dataExtractorId: id, diff --git a/extension/ui/src/Visualizers/DotGraphVisualizer.tsx b/extension/ui/src/Visualizers/DotGraphVisualizer.tsx new file mode 100644 index 0000000..db84559 --- /dev/null +++ b/extension/ui/src/Visualizers/DotGraphVisualizer.tsx @@ -0,0 +1,58 @@ +import { observer } from "mobx-react"; +import * as React from "react"; +import { DotViewer } from "./DotVisualizer"; +import { + VisualizationProvider, + VisualizationCollector, + asVisualizationId, +} from "./Visualizer"; +import { + ExtractedData, + isCommonDataType, +} from "@hediet/debug-visualizer-data-extraction"; + +@observer +export class DotGraphViewer extends React.Component<{ + nodes: { id: string; label: string }[]; + edges: { from: string; to: string; label: string }[]; +}> { + render() { + const { nodes, edges } = this.props; + const dotContent = ` + digraph MyGraph { + ${nodes + .map(n => `${n.id} [ label = ${JSON.stringify(n.label)} ];`) + .join("\n ")} + ${edges + .map( + e => + `${e.from} -> ${e.to} [ label = ${JSON.stringify( + e.label + )} ];` + ) + .join("\n")} + } + `; + 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 new file mode 100644 index 0000000..1c6a0ae --- /dev/null +++ b/extension/ui/src/Visualizers/DotVisualizer.tsx @@ -0,0 +1,60 @@ +import { observer, disposeOnUnmount } from "mobx-react"; +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 { + VisualizationProvider, + VisualizationCollector, + asVisualizationId, +} from "./Visualizer"; +import { + ExtractedData, + isCommonDataType, +} from "@hediet/debug-visualizer-data-extraction"; + +const viz: any = new Viz({ + Module: () => Module({ TOTAL_MEMORY: 1 << 30 }), + render, +}); + +@observer +export class DotViewer extends React.Component<{ + dotCode: string; + svgRef?: (element: SVGSVGElement | null) => void; +}> { + @observable private svg: string | null = null; + + @disposeOnUnmount + // @ts-ignore + private readonly _updateSvgAutorun = autorun(async () => { + const svg = await viz.renderString(this.props.dotCode); + runInAction("Update svg", () => (this.svg = svg)); + }); + + render() { + if (!this.svg) { + return
Loading...
; + } + 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/MonacoTextVisualizer.tsx b/extension/ui/src/Visualizers/MonacoTextVisualizer.tsx new file mode 100644 index 0000000..3eaa891 --- /dev/null +++ b/extension/ui/src/Visualizers/MonacoTextVisualizer.tsx @@ -0,0 +1,72 @@ +import { + VisualizationProvider, + VisualizationCollector, + asVisualizationId, +} from "./Visualizer"; +import { + ExtractedData, + isCommonDataType, +} from "@hediet/debug-visualizer-data-extraction"; +import * as monaco from "monaco-editor"; +import React = require("react"); +import { observer, disposeOnUnmount } from "mobx-react"; +import { autorun, observable } from "mobx"; + +@observer +export class MonacoEditor extends React.Component<{ text: string }> { + @observable private editor: monaco.editor.IStandaloneCodeEditor | undefined; + + componentWillUnmount() { + if (this.editor) { + this.editor.dispose(); + } + } + + @disposeOnUnmount + private _updateText = autorun(() => { + if (this.editor) { + this.editor.setValue(this.props.text); + } + }); + + private readonly setEditorDiv = (editorDiv: HTMLDivElement) => { + if (!editorDiv) { + return; + } + + this.editor = monaco.editor.create(editorDiv, { + value: this.props.text, + automaticLayout: true, + scrollBeyondLastLine: false, + minimap: { enabled: false }, + fixedOverflowWidgets: true, + readOnly: true, + }); + }; + + render() { + return ( +
+
+
+ ); + } +} + +export class MonacoTextVisualizer extends VisualizationProvider { + getVisualizations( + data: ExtractedData, + collector: VisualizationCollector + ): void { + if (isCommonDataType(data, { text: true })) { + collector.addVisualization({ + id: asVisualizationId("monaco-text"), + name: "Monaco Editor", + priority: 95, + render() { + return ; + }, + }); + } + } +} diff --git a/extension/ui/src/Components/GraphViz.tsx b/extension/ui/src/Visualizers/SvgVisualizer.tsx similarity index 51% rename from extension/ui/src/Components/GraphViz.tsx rename to extension/ui/src/Visualizers/SvgVisualizer.tsx index b1dc5cf..d2b8fae 100644 --- a/extension/ui/src/Components/GraphViz.tsx +++ b/extension/ui/src/Visualizers/SvgVisualizer.tsx @@ -1,68 +1,17 @@ -import { observer, disposeOnUnmount } from "mobx-react"; -import * as classNames from "classnames"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import { ReactSVGPanZoom, Tool, Value } from "react-svg-pan-zoom"; -import { computed, observable, autorun, runInAction, action } from "mobx"; import Measure, { ContentRect } from "react-measure"; -import Viz from "viz.js"; -import { Module, render } from "viz.js/full.render.js"; - -export interface GraphData { - kind: "GraphData"; - nodes: { id: string; label: string }[]; - edges: { from: string; to: string; label: string }[]; -} - -@observer -export class DotGraphViewer extends React.Component<{ data: GraphData }> { - render() { - const { nodes, edges } = this.props.data; - const dotContent = ` - digraph MyGraph { - ${nodes - .map(n => `${n.id} [ label = ${JSON.stringify(n.label)} ];`) - .join("\n ")} - ${edges - .map( - e => - `${e.from} -> ${e.to} [ label = ${JSON.stringify( - e.label - )} ];` - ) - .join("\n")} - } - `; - return ; - } -} - -const viz: any = new Viz({ - Module: () => Module({ TOTAL_MEMORY: 1 << 30 }), - render, -}); - -@observer -export class DotViewer extends React.Component<{ - dotCode: string; - svgRef?: (element: SVGSVGElement | null) => void; -}> { - @observable private svg: string | null = null; - - @disposeOnUnmount - // @ts-ignore - private readonly _updateSvgAutorun = autorun(async () => { - const svg = await viz.renderString(this.props.dotCode); - runInAction("Update svg", () => (this.svg = svg)); - }); - - render() { - if (!this.svg) { - return
Loading...
; - } - return ; - } -} +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action } from "mobx"; +import { Tool, ReactSVGPanZoom, Value } from "react-svg-pan-zoom"; +import { + VisualizationProvider, + VisualizationCollector, + asVisualizationId, +} from "./Visualizer"; +import { + ExtractedData, + isCommonDataType, +} from "@hediet/debug-visualizer-data-extraction"; function widthOrDefault(r: ContentRect): number { if (r.bounds && r.bounds.width) { @@ -128,7 +77,7 @@ export class SvgViewer extends React.Component<{ width={widthOrDefault(contentRect)} height={heightOrDefault(contentRect)} tool={tool} - value={val} + value={val as any} onChangeValue={v => (this.value = v)} onChangeTool={this.setTool} ref={e => this.setRef(e)} @@ -147,3 +96,21 @@ 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/TextVisualizer.tsx b/extension/ui/src/Visualizers/TextVisualizer.tsx new file mode 100644 index 0000000..31050b1 --- /dev/null +++ b/extension/ui/src/Visualizers/TextVisualizer.tsx @@ -0,0 +1,32 @@ +import { + VisualizationProvider, + VisualizationCollector, + asVisualizationId, +} from "./Visualizer"; +import { + ExtractedData, + isCommonDataType, +} from "@hediet/debug-visualizer-data-extraction"; +import React = require("react"); + +export class TextVisualizer extends VisualizationProvider { + getVisualizations( + data: ExtractedData, + collector: VisualizationCollector + ): void { + if (isCommonDataType(data, { text: true })) { + collector.addVisualization({ + id: asVisualizationId("text"), + name: "Plain Text", + priority: 90, + render() { + return ( +
+							{data.text}
+						
+ ); + }, + }); + } + } +} diff --git a/extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx b/extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx new file mode 100644 index 0000000..4ba900c --- /dev/null +++ b/extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx @@ -0,0 +1,61 @@ +import { TreeViewModel, TreeNodeViewModel, TreeView } from "./Views"; +import { + CommonDataTypes, + ExtractedData, + isCommonDataType, +} from "@hediet/debug-visualizer-data-extraction"; +import { + VisualizationProvider, + VisualizationCollector, + VisualizationId, + asVisualizationId, +} from "../Visualizer"; +import React = require("react"); + +export function createTreeViewModelFromTreeNodeData( + root: CommonDataTypes.TreeNodeData["root"] +): TreeViewModel { + const m = new TreeViewModel(); + m.root = recurse(root, m); + return m; + + function recurse( + node: CommonDataTypes.TreeNodeData["root"], + viewModel: TreeViewModel + ): TreeNodeViewModel { + const children: TreeNodeViewModel[] = node.children.map(c => + recurse(c, viewModel) + ); + const model = new TreeNodeViewModel( + viewModel, + node.id, + node.name, + node.value, + children + ); + model.isMarked = node.isMarked; + for (const c of children) { + c.parent = model; + } + return model; + } +} + +export class TreeVisualizer extends VisualizationProvider { + getVisualizations( + data: ExtractedData, + collector: VisualizationCollector + ): void { + if (isCommonDataType(data, { tree: true })) { + collector.addVisualization({ + id: asVisualizationId("tree"), + name: "Tree", + priority: 100, + render() { + const m = createTreeViewModelFromTreeNodeData(data.root); + return ; + }, + }); + } + } +} diff --git a/extension/ui/src/Visualizers/TreeVisualizer/Visualizers.ts b/extension/ui/src/Visualizers/TreeVisualizer/Visualizers.ts deleted file mode 100644 index 06e32b7..0000000 --- a/extension/ui/src/Visualizers/TreeVisualizer/Visualizers.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { TreeViewModel, TreeNodeViewModel } from "./Views"; -import { CommonDataTypes } from "@hediet/debug-visualizer-data-extraction"; - -export function createTreeViewModelFromTreeNodeData( - root: CommonDataTypes.TreeNodeData["root"] -): TreeViewModel { - const m = new TreeViewModel(); - m.root = recurse(root, m); - return m; - - function recurse( - node: CommonDataTypes.TreeNodeData["root"], - viewModel: TreeViewModel - ): TreeNodeViewModel { - const children: TreeNodeViewModel[] = node.children.map(c => - recurse(c, viewModel) - ); - const model = new TreeNodeViewModel( - viewModel, - node.id, - node.name, - node.value, - children - ); - model.isMarked = node.isMarked; - for (const c of children) { - c.parent = model; - } - return model; - } -} diff --git a/extension/ui/src/Visualizers/Visualizer.ts b/extension/ui/src/Visualizers/Visualizer.ts new file mode 100644 index 0000000..e3472d1 --- /dev/null +++ b/extension/ui/src/Visualizers/Visualizer.ts @@ -0,0 +1,76 @@ +import { ExtractedData } from "@hediet/debug-visualizer-data-extraction"; + +export interface VisualizationId extends String { + __brand: "VisualizationId"; +} + +export function asVisualizationId(id: string): VisualizationId { + return (id as unknown) as VisualizationId; +} + +export interface Visualization { + readonly name: string; + readonly id: VisualizationId; + readonly priority: number; + render(): React.ReactElement; +} + +export interface VisualizationCollector { + addVisualization(visualization: Visualization): void; +} + +export abstract class VisualizationProvider { + abstract getVisualizations( + data: ExtractedData, + collector: VisualizationCollector + ): void; + + getVisualizationsArray(data: ExtractedData): Visualization[] { + const result = new Array(); + this.getVisualizations(data, { + addVisualization(visualization) { + result.push(visualization); + }, + }); + return result; + } + + getBestVisualization( + data: ExtractedData, + preferredVisualization: VisualizationId | undefined + ): { + visualization: Visualization | undefined; + allVisualizations: Visualization[]; + } { + const allVisualizations = this.getVisualizationsArray(data); + allVisualizations.sort((a, b) => b.priority - a.priority); + + let visualization: Visualization | undefined = allVisualizations[0]; + if (preferredVisualization) { + const preferred = allVisualizations.find( + vis => vis.id === preferredVisualization + ); + if (preferred) { + visualization = preferred; + } + } + return { visualization, allVisualizations }; + } +} + +export class ComposedVisualizationProvider extends VisualizationProvider { + constructor( + private readonly composedProviders: ReadonlyArray + ) { + super(); + } + + getVisualizations( + data: ExtractedData, + collector: VisualizationCollector + ): void { + for (const c of this.composedProviders) { + c.getVisualizations(data, collector); + } + } +} diff --git a/extension/ui/src/Visualizers/index.ts b/extension/ui/src/Visualizers/index.ts new file mode 100644 index 0000000..a53380a --- /dev/null +++ b/extension/ui/src/Visualizers/index.ts @@ -0,0 +1,17 @@ +import "./style.scss"; +import { ComposedVisualizationProvider } from "./Visualizer"; +import { TreeVisualizer } from "./TreeVisualizer/Visualizer"; +import { DotGraphVisualizer } from "./DotGraphVisualizer"; +import { SvgVisualizer } from "./SvgVisualizer"; +import { DotVisualizer } from "./DotVisualizer"; +import { TextVisualizer } from "./TextVisualizer"; +import { MonacoTextVisualizer } from "./MonacoTextVisualizer"; + +export const knownVisualizations = new ComposedVisualizationProvider([ + new TreeVisualizer(), + new DotGraphVisualizer(), + new SvgVisualizer(), + new DotVisualizer(), + new TextVisualizer(), + new MonacoTextVisualizer(), +]); diff --git a/extension/ui/src/Visualizers/style.scss b/extension/ui/src/Visualizers/style.scss new file mode 100644 index 0000000..8beb580 --- /dev/null +++ b/extension/ui/src/Visualizers/style.scss @@ -0,0 +1,16 @@ +.component-monaco-editor { + min-height: 0; + min-width: 0; + + height: 100%; + box-sizing: border-box; + display: flex; + + // overflow: hidden; + + .part-editor { + min-width: 0; + min-height: 0; + flex: 1; + } +} diff --git a/extension/ui/src/style.scss b/extension/ui/src/style.scss index 23c934a..9de4cfe 100644 --- a/extension/ui/src/style.scss +++ b/extension/ui/src/style.scss @@ -180,11 +180,12 @@ body, .part-content { background-color: var(--vscode-sideBar-background); - div { + .part-option { color: black; padding: 12px 16px; text-decoration: none; display: block; + cursor: pointer; &:hover { background-color: #ddd;