Implements some visualizers.
This commit is contained in:
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
30
data-extraction/src/js/Extractors/AsIsDataExtractor.ts
Normal file
30
data-extraction/src/js/Extractors/AsIsDataExtractor.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ({}) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
58
extension/ui/src/Visualizers/DotGraphVisualizer.tsx
Normal file
58
extension/ui/src/Visualizers/DotGraphVisualizer.tsx
Normal 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} />
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
60
extension/ui/src/Visualizers/DotVisualizer.tsx
Normal file
60
extension/ui/src/Visualizers/DotVisualizer.tsx
Normal 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} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
72
extension/ui/src/Visualizers/MonacoTextVisualizer.tsx
Normal file
72
extension/ui/src/Visualizers/MonacoTextVisualizer.tsx
Normal 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} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
32
extension/ui/src/Visualizers/TextVisualizer.tsx
Normal file
32
extension/ui/src/Visualizers/TextVisualizer.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
61
extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx
Normal file
61
extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx
Normal 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} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
76
extension/ui/src/Visualizers/Visualizer.ts
Normal file
76
extension/ui/src/Visualizers/Visualizer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
extension/ui/src/Visualizers/index.ts
Normal file
17
extension/ui/src/Visualizers/index.ts
Normal 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(),
|
||||
]);
|
||||
16
extension/ui/src/Visualizers/style.scss
Normal file
16
extension/ui/src/Visualizers/style.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user