Implements some visualizers.

This commit is contained in:
Henning Dieterichs
2019-08-02 11:53:17 +02:00
parent 99a0f854a3
commit 26306b68f8
23 changed files with 621 additions and 279 deletions

15
.vscode/launch.json vendored
View File

@@ -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"
}
]
}

View File

@@ -1,5 +1,28 @@
import { ExtractedData } from "./DataExtractionResult";
export function isCommonDataType<T>(
data: ExtractedData,
kind: T
): data is Narrow<CommonDataType, { kind: T }> {
for (const key of Object.keys(kind)) {
if (!(key in data.kind)) {
return false;
}
}
return true;
}
type Narrow<T, TKind> = 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;

View File

@@ -0,0 +1,30 @@
import { ExtractedData } from "../../DataExtractionResult";
import { DataExtractor, ExtractionCollector } from "..";
export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
readonly id = "as-is";
getExtractions(
data: unknown,
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: "as-is",
name: "As Is",
priority: 100,
extractData() {
return obj;
},
});
}
}
}
}
}

View File

@@ -11,7 +11,7 @@ export class TypeScriptAstDataExtractor
collector: ExtractionCollector<CommonDataTypes.AstData>,
evalFn: <TEval>(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<ts.Node>
): 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<ts.Node>;
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",
};
},

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 ({}) => {

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -60,8 +60,7 @@ export class WebView {
}
export function getHtml(server: Server) {
const isDev = true;
// uiae
const isDev = !!process.env.USE_DEV_UI;
return `
<html>
<head>

View File

@@ -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 => {}}
/>
<div style={{ width: 10 }} />
@@ -108,7 +104,17 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
<NamedSelect
name="Visualizer"
selected={0}
options={[{ label: "literal" }]}
onSelected={item => {
m.setPreferredVisualizationId(item.id);
}}
options={
m.visualizations
? m.visualizations.allVisualizations.map(v => ({
label: v.name,
id: v.id,
}))
: []
}
/>
</div>
</>
@@ -166,12 +172,6 @@ export class ExpressionInput extends React.Component<{ model: Model }> {
}}
/>
);
/*
<input
className="part-expression-input"
/>
*/
}
}
@@ -181,26 +181,6 @@ export class Visualizer extends React.Component<{ model: Model }> {
return (
<div className="component-Visualizer">{this.renderContent()}</div>
);
/*
<DotGraphViewer
data={{
kind: "GraphData",
edges: [
{
from: "1",
to: "2",
label: "test",
},
],
nodes: [
{ id: "1", label: "foo" },
{ id: "2", label: "bar" },
],
}}
/>
*/
}
renderContent(): JSX.Element {
@@ -214,15 +194,12 @@ export class Visualizer extends React.Component<{ model: Model }> {
} else if (s.kind === "noDebugSession") {
return <NoData label="No Debug Session" />;
} 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 <TreeView model={m} />;
} else {
return <pre>{JSON.stringify(s.result)}</pre>;
const vis = this.props.model.visualizations;
if (!vis || !vis.visualization) {
return <NoData label="No Visualization" />;
}
return vis.visualization.render();
} else {
const nvr: never = s;
return <div />;
@@ -316,7 +293,7 @@ export class Select<T extends SelectableItem> 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<T extends SelectableItem> extends React.Component<{
usePortal={false}
>
<button className="part-button">
{this.selected.label}
{this.selected ? this.selected.label : "(none)"}
</button>
<div className={"part-content"}>
{this.props.options.map((o, idx) => (
<div
key={idx}
className="part-option"
onClick={() => this.props.onSelected(o)}
>
{o.label}

View File

@@ -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,

View File

@@ -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 <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

@@ -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 <div>Loading...</div>;
}
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

@@ -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 (
<div className="component-monaco-editor">
<div className="part-editor" ref={this.setEditorDiv} />
</div>
);
}
}
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 <MonacoEditor text={data.text} />;
},
});
}
}
}

View File

@@ -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 <DotViewer dotCode={dotContent} />;
}
}
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 <div>Loading...</div>;
}
return <SvgViewer svgRef={this.props.svgRef} svgContent={this.svg} />;
}
}
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 <SvgViewer svgContent={data.text} />;
},
});
}
}
}

View File

@@ -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 (
<pre style={{ marginLeft: 20, marginRight: 20 }}>
{data.text}
</pre>
);
},
});
}
}
}

View File

@@ -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 <TreeView model={m} />;
},
});
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<Visualization>();
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<VisualizationProvider>
) {
super();
}
getVisualizations(
data: ExtractedData,
collector: VisualizationCollector
): void {
for (const c of this.composedProviders) {
c.getVisualizations(data, collector);
}
}
}

View File

@@ -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(),
]);

View File

@@ -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;
}
}

View File

@@ -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;