Moves visualizers into their own package.
This commit is contained in:
30
visualization/src/LazyLoadable.tsx
Normal file
30
visualization/src/LazyLoadable.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react";
|
||||
import { observable, runInAction } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export function makeLazyLoadable<TProps>(
|
||||
selector: () => Promise<React.ComponentClass<TProps>>
|
||||
): React.ComponentClass<TProps> {
|
||||
@observer
|
||||
class MyComponent extends React.Component<TProps> {
|
||||
@observable
|
||||
component: React.ComponentClass<TProps> | undefined;
|
||||
|
||||
async componentDidMount() {
|
||||
const result = await selector();
|
||||
runInAction("Set Lazily Loaded Component", () => {
|
||||
this.component = result;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.component) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
const C = this.component;
|
||||
return <C {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
return MyComponent;
|
||||
}
|
||||
71
visualization/src/Visualizer.ts
Normal file
71
visualization/src/Visualizer.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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 Visualizer {
|
||||
abstract visualize(
|
||||
data: ExtractedData,
|
||||
collector: VisualizationCollector
|
||||
): void;
|
||||
|
||||
getVisualizationsArray(data: ExtractedData): Visualization[] {
|
||||
const result = new Array<Visualization>();
|
||||
this.visualize(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 ComposedVisualizer extends Visualizer {
|
||||
constructor(private readonly composedProviders: ReadonlyArray<Visualizer>) {
|
||||
super();
|
||||
}
|
||||
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
for (const c of this.composedProviders) {
|
||||
c.visualize(data, collector);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
visualization/src/VisualizerView.tsx
Normal file
7
visualization/src/VisualizerView.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
export class VisualizerView extends React.Component {
|
||||
render(): React.ReactNode {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
3
visualization/src/index.ts
Normal file
3
visualization/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./visualizers";
|
||||
export * from "./Visualizer";
|
||||
export * from "./VisualizerView";
|
||||
22
visualization/src/types.d.ts
vendored
Normal file
22
visualization/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
declare module "viz.js" {
|
||||
const x: any;
|
||||
export default x;
|
||||
}
|
||||
|
||||
declare module "viz.js/full.render.js" {
|
||||
export const render: any;
|
||||
export const Module: any;
|
||||
}
|
||||
|
||||
declare module "line-column" {
|
||||
export = function(
|
||||
text: string
|
||||
): {
|
||||
fromIndex(
|
||||
idx: number
|
||||
): {
|
||||
line: number;
|
||||
col: number;
|
||||
};
|
||||
} {};
|
||||
}
|
||||
256
visualization/src/utils/Point.ts
Normal file
256
visualization/src/utils/Point.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
function sqr(a: number) {
|
||||
return a * a;
|
||||
}
|
||||
|
||||
export type PointLike =
|
||||
| Point
|
||||
| { x?: number; y: number }
|
||||
| { x: number; y?: number };
|
||||
|
||||
export function point(data: PointLike) {
|
||||
if (data instanceof Point) {
|
||||
return data;
|
||||
}
|
||||
return new Point(data.x || 0, data.y || 0);
|
||||
}
|
||||
|
||||
export class Point {
|
||||
public static readonly Zero = new Point(0, 0);
|
||||
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
|
||||
public mapXY(fn: (arg: number) => number): Point {
|
||||
return new Point(fn(this.x), fn(this.y));
|
||||
}
|
||||
|
||||
public distance(other: PointLike = Point.Zero): number {
|
||||
const d = this.sub(other);
|
||||
return Math.sqrt(sqr(d.x) + sqr(d.y));
|
||||
}
|
||||
|
||||
public sub(other: PointLike): Point {
|
||||
const o = point(other);
|
||||
return new Point(this.x - o.x, this.y - o.y);
|
||||
}
|
||||
|
||||
public add(other: PointLike): Point {
|
||||
const o = point(other);
|
||||
return new Point(this.x + o.x, this.y + o.y);
|
||||
}
|
||||
|
||||
public mul(scalar: number): Point {
|
||||
return new Point(this.x * scalar, this.y * scalar);
|
||||
}
|
||||
|
||||
public div(scalar: number): Point {
|
||||
return new Point(this.x / scalar, this.y / scalar);
|
||||
}
|
||||
|
||||
public equals(other: PointLike) {
|
||||
const o = point(other);
|
||||
return this.x === o.x && this.y === o.y;
|
||||
}
|
||||
|
||||
public angle(): number {
|
||||
const angle = Math.atan2(this.x, this.y);
|
||||
return angle;
|
||||
}
|
||||
|
||||
public getPointCloserTo(dest: PointLike, dist: number): Point {
|
||||
if (this.equals(dest)) return this;
|
||||
|
||||
const angle = point(dest)
|
||||
.sub(this)
|
||||
.angle();
|
||||
|
||||
const result = new Point(
|
||||
this.x + Math.sin(angle) * dist,
|
||||
this.y + Math.cos(angle) * dist
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
public toJson(): { x: number; y: number } {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
}
|
||||
|
||||
function turn(p1: Point, p2: Point, p3: Point): number {
|
||||
const A = (p3.y - p1.y) * (p2.x - p1.x);
|
||||
const B = (p2.y - p1.y) * (p3.x - p1.x);
|
||||
return A > B + Number.MIN_VALUE ? 1 : A + Number.MIN_VALUE < B ? -1 : 0;
|
||||
}
|
||||
|
||||
export function isIntersect(
|
||||
aStart: Point,
|
||||
aEnd: Point,
|
||||
bStart: Point,
|
||||
bEnd: Point
|
||||
): boolean {
|
||||
return (
|
||||
turn(aStart, bStart, bEnd) != turn(aEnd, bStart, bEnd) &&
|
||||
turn(aStart, aEnd, bStart) != turn(aStart, aEnd, bEnd)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liang-Barsky function by Daniel White
|
||||
*
|
||||
* @link http://www.skytopia.com/project/articles/compsci/clipping.html
|
||||
*
|
||||
* @param {number} x0
|
||||
* @param {number} y0
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {array<number>} bbox
|
||||
* @return {array<array<number>>|null}
|
||||
*/
|
||||
function liangBarsky(
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
bbox: [number, number, number, number]
|
||||
): [[number, number], [number, number]] | null {
|
||||
let [xmin, xmax, ymin, ymax] = bbox;
|
||||
let t0 = 0,
|
||||
t1 = 1;
|
||||
let dx = x1 - x0,
|
||||
dy = y1 - y0;
|
||||
let p = 0,
|
||||
q = 0,
|
||||
r = 0;
|
||||
|
||||
for (let edge: 0 | 1 | 2 | 3 = 0; edge < 4; edge++) {
|
||||
// Traverse through left, right, bottom, top edges.
|
||||
if (edge === 0) {
|
||||
p = -dx;
|
||||
q = -(xmin - x0);
|
||||
} else if (edge === 1) {
|
||||
p = dx;
|
||||
q = xmax - x0;
|
||||
} else if (edge === 2) {
|
||||
p = -dy;
|
||||
q = -(ymin - y0);
|
||||
} else if (edge === 3) {
|
||||
p = dy;
|
||||
q = ymax - y0;
|
||||
}
|
||||
|
||||
r = q / p;
|
||||
|
||||
if (p === 0 && q < 0) return null; // Don't draw line at all. (parallel line outside)
|
||||
|
||||
if (p < 0) {
|
||||
if (r > t1) return null;
|
||||
// Don't draw line at all.
|
||||
else if (r > t0) t0 = r; // Line is clipped!
|
||||
} else if (p > 0) {
|
||||
if (r < t0) return null;
|
||||
// Don't draw line at all.
|
||||
else if (r < t1) t1 = r; // Line is clipped!
|
||||
}
|
||||
}
|
||||
|
||||
return [[x0 + t0 * dx, y0 + t0 * dy], [x0 + t1 * dx, y0 + t1 * dy]];
|
||||
}
|
||||
|
||||
export function intersectRectWithLine(
|
||||
start: Point,
|
||||
end: Point,
|
||||
rect: Rectangle
|
||||
): { first: Point; second: Point } | undefined {
|
||||
const r = liangBarsky(start.x, start.y, end.x, end.y, [
|
||||
rect.topLeft.x,
|
||||
rect.bottomRight.x,
|
||||
rect.topLeft.y,
|
||||
rect.bottomRight.y,
|
||||
]);
|
||||
if (!r) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
first: new Point(r[0][0], r[0][1]),
|
||||
second: new Point(r[1][0], r[1][1]),
|
||||
};
|
||||
}
|
||||
|
||||
// first zoom then translate
|
||||
export function scale(
|
||||
clientOffset: Point,
|
||||
clientSize: Point,
|
||||
viewSize: Point
|
||||
): { clientZoom: number; clientOffset: Point } {
|
||||
const clientRatio = clientSize.x / clientSize.y;
|
||||
const viewRatio = viewSize.x / viewSize.y;
|
||||
|
||||
let zoom = 1;
|
||||
|
||||
if (clientRatio < viewRatio) zoom = viewSize.y / clientSize.y;
|
||||
else zoom = viewSize.x / clientSize.x;
|
||||
|
||||
const clientMid = clientOffset.mul(zoom).add(clientSize.mul(zoom / 2));
|
||||
const viewMid = viewSize.div(2);
|
||||
|
||||
const clientOffset2 = viewMid.sub(clientMid);
|
||||
|
||||
return { clientOffset: clientOffset2, clientZoom: zoom };
|
||||
}
|
||||
|
||||
export class Rectangle {
|
||||
public static ofSize(position: PointLike, size: PointLike): Rectangle {
|
||||
const pos = point(position);
|
||||
return new Rectangle(pos, pos.add(size));
|
||||
}
|
||||
|
||||
public static spanning(first: Point, ...rest: Point[]): Rectangle {
|
||||
let min = first.toJson();
|
||||
let max = first.toJson();
|
||||
for (const p of rest) {
|
||||
min.x = Math.min(min.x, p.x);
|
||||
min.y = Math.min(min.y, p.y);
|
||||
max.x = Math.max(max.x, p.x);
|
||||
max.y = Math.max(max.y, p.y);
|
||||
}
|
||||
|
||||
return new Rectangle(point(min), point(max));
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly topLeft: Point,
|
||||
public readonly bottomRight: Point
|
||||
) {}
|
||||
|
||||
get center(): Point {
|
||||
return this.bottomRight.add(this.topLeft).div(2);
|
||||
}
|
||||
|
||||
get size(): Point {
|
||||
return this.bottomRight.sub(this.topLeft);
|
||||
}
|
||||
|
||||
get topRight(): Point {
|
||||
return new Point(this.bottomRight.x, this.topLeft.y);
|
||||
}
|
||||
|
||||
get bottomLeft(): Point {
|
||||
return new Point(this.topLeft.x, this.bottomRight.y);
|
||||
}
|
||||
|
||||
public intersects(selectionRect: Rectangle): boolean {
|
||||
return !(
|
||||
selectionRect.topLeft.x > this.bottomRight.x ||
|
||||
selectionRect.bottomRight.x < this.topLeft.x ||
|
||||
selectionRect.topLeft.y > this.bottomRight.y ||
|
||||
selectionRect.bottomRight.y < this.topLeft.y
|
||||
);
|
||||
}
|
||||
|
||||
equals(other: Rectangle) {
|
||||
return (
|
||||
this.topLeft.equals(other.topLeft) &&
|
||||
this.bottomRight.equals(other.bottomRight)
|
||||
);
|
||||
}
|
||||
}
|
||||
90
visualization/src/utils/SvgElements.tsx
Normal file
90
visualization/src/utils/SvgElements.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Point, Rectangle } from "./Point";
|
||||
import * as React from "react";
|
||||
import { Properties } from "csstype";
|
||||
|
||||
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
||||
const newObj: Omit<T, K> = {} as any;
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (!keys.includes(key as K)) {
|
||||
(newObj as any)[key] = val;
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
export interface SvgAttributes<T> extends React.DOMAttributes<T> {
|
||||
stroke?: string;
|
||||
className?: string;
|
||||
style?: Properties;
|
||||
pointerEvents?: "none" | "auto" | "initial";
|
||||
}
|
||||
|
||||
export function SvgText(
|
||||
props: {
|
||||
position: Point;
|
||||
children: any;
|
||||
childRef?: React.Ref<SVGTextElement>;
|
||||
textAnchor?: "middle" | "end" | "start";
|
||||
dominantBaseline?: "central" | "middle";
|
||||
} & SvgAttributes<SVGTextElement>
|
||||
) {
|
||||
return (
|
||||
<text
|
||||
x={props.position.x}
|
||||
y={props.position.y}
|
||||
ref={props.childRef}
|
||||
{...omit(props, ["position", "childRef"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgCircle(
|
||||
props: {
|
||||
center: Point;
|
||||
radius: number;
|
||||
childRef?: React.Ref<SVGCircleElement>;
|
||||
} & SvgAttributes<SVGCircleElement>
|
||||
) {
|
||||
return (
|
||||
<circle
|
||||
cx={props.center.x}
|
||||
cy={props.center.y}
|
||||
r={props.radius}
|
||||
ref={props.childRef}
|
||||
{...omit(props, ["center", "radius", "childRef"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgLine(
|
||||
props: { start: Point; end: Point } & SvgAttributes<SVGLineElement>
|
||||
) {
|
||||
return (
|
||||
<line
|
||||
x1={props.start.x}
|
||||
y1={props.start.y}
|
||||
x2={props.end.x}
|
||||
y2={props.end.y}
|
||||
{...omit(props, ["end", "start"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgRect(
|
||||
props: { rectangle: Rectangle; fill?: string } & SvgAttributes<
|
||||
SVGRectElement
|
||||
>
|
||||
) {
|
||||
const r = props.rectangle;
|
||||
return (
|
||||
<rect
|
||||
x={r.topLeft.x}
|
||||
y={r.topLeft.y}
|
||||
width={r.size.x}
|
||||
height={r.size.y}
|
||||
{...omit(props, ["rectangle"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
219
visualization/src/visualizers/AstVisualizer.tsx
Normal file
219
visualization/src/visualizers/AstVisualizer.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
CommonDataTypes,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { createTreeViewModelFromTreeNodeData } from "./TreeVisualizer/TreeVisualizer";
|
||||
import { TreeWithPathView, TreeViewModel } from "./TreeVisualizer/Views";
|
||||
import * as React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun, trace } from "mobx";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { getLanguageId } from "./text-visualizers/MonacoTextVisualizer";
|
||||
import * as LineColumn from "line-column";
|
||||
|
||||
export class AstVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { ast: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("ast"),
|
||||
name: "AST",
|
||||
priority: 110,
|
||||
render() {
|
||||
const m = createTreeViewModelFromTreeNodeData(data.root);
|
||||
const l = LineColumn(data.text);
|
||||
function translatePosition(pos: number): monaco.IPosition {
|
||||
const r = l.fromIndex(pos);
|
||||
return {
|
||||
column: r.col,
|
||||
lineNumber: r.line,
|
||||
};
|
||||
}
|
||||
const nodeInfoToRange = (info: NodeInfo): monaco.IRange => {
|
||||
const start = translatePosition(info.position);
|
||||
const end = translatePosition(
|
||||
info.position + info.length
|
||||
);
|
||||
const range = monaco.Range.fromPositions(start, end);
|
||||
return range;
|
||||
};
|
||||
return (
|
||||
<AstTree
|
||||
model={m}
|
||||
data={data}
|
||||
nodeInfoToRange={nodeInfoToRange}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface NodeInfo {
|
||||
position: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AstTree extends React.Component<{
|
||||
model: TreeViewModel<NodeInfo>;
|
||||
data: CommonDataTypes.Ast;
|
||||
nodeInfoToRange: (info: NodeInfo) => monaco.IRange;
|
||||
}> {
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
const data = this.props.data;
|
||||
let languageId = "text";
|
||||
if (data.fileName) {
|
||||
languageId = getLanguageId(data.fileName);
|
||||
}
|
||||
return (
|
||||
<div className="component-AstTree">
|
||||
<div className="part-tree2">
|
||||
<TreeWithPathView model={model} />
|
||||
</div>
|
||||
<div className="part-editor">
|
||||
<MonacoEditor
|
||||
nodeInfoToRange={this.props.nodeInfoToRange}
|
||||
model={model}
|
||||
languageId={languageId}
|
||||
text={data.text}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MonacoEditor extends React.Component<{
|
||||
text: string;
|
||||
languageId: string;
|
||||
model: TreeViewModel<NodeInfo>;
|
||||
nodeInfoToRange: (info: NodeInfo) => monaco.IRange;
|
||||
}> {
|
||||
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
|
||||
private modelNotObservable:
|
||||
| monaco.editor.ITextModel
|
||||
| undefined = undefined;
|
||||
|
||||
@observable private model: monaco.editor.ITextModel | undefined = undefined;
|
||||
|
||||
private markedDecorations: string[] = [];
|
||||
private selectedDecorations: string[] = [];
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateMarkedDecorations = autorun(
|
||||
() => {
|
||||
if (this.editor && this.model) {
|
||||
const editorModel = this.model;
|
||||
const ranges = this.props.model.marked.map(s =>
|
||||
this.props.nodeInfoToRange(s.data)
|
||||
);
|
||||
this.markedDecorations = this.editor.deltaDecorations(
|
||||
this.markedDecorations,
|
||||
ranges.map(range => ({
|
||||
range,
|
||||
options: { className: "marked" },
|
||||
}))
|
||||
);
|
||||
if (ranges.length > 0) {
|
||||
this.editor.revealRange(
|
||||
ranges[0],
|
||||
monaco.editor.ScrollType.Smooth
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ name: "updateMarkedDecorations" }
|
||||
);
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateSelectedDecoration = autorun(
|
||||
() => {
|
||||
if (this.editor && this.model) {
|
||||
const selected = this.props.model.selected;
|
||||
if (selected) {
|
||||
const range = this.props.nodeInfoToRange(selected.data);
|
||||
this.selectedDecorations = this.editor.deltaDecorations(
|
||||
this.selectedDecorations,
|
||||
[
|
||||
{
|
||||
range,
|
||||
options: { className: "selected" },
|
||||
},
|
||||
]
|
||||
);
|
||||
this.editor.revealRange(
|
||||
range,
|
||||
monaco.editor.ScrollType.Smooth
|
||||
);
|
||||
} else {
|
||||
this.selectedDecorations = this.editor.deltaDecorations(
|
||||
this.selectedDecorations,
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ name: "updateDecorations" }
|
||||
);
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateText = autorun(() => {
|
||||
if (this.editor) {
|
||||
const model = monaco.editor.createModel(
|
||||
this.props.text,
|
||||
this.props.languageId,
|
||||
undefined
|
||||
);
|
||||
|
||||
this.editor.setModel(model);
|
||||
|
||||
if (this.modelNotObservable) {
|
||||
this.modelNotObservable.dispose();
|
||||
}
|
||||
this.model = model;
|
||||
}
|
||||
});
|
||||
|
||||
private readonly setEditorDiv = (editorDiv: HTMLDivElement) => {
|
||||
if (!editorDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor = monaco.editor.create(editorDiv, {
|
||||
model: null,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: { enabled: false },
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: true,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 4,
|
||||
glyphMargin: false,
|
||||
smoothScrolling: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="component-monaco-editor">
|
||||
<div className="part-editor" ref={this.setEditorDiv} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
399
visualization/src/visualizers/GridVisualizer.tsx
Normal file
399
visualization/src/visualizers/GridVisualizer.tsx
Normal file
@@ -0,0 +1,399 @@
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
CommonDataTypes,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed, action, observable } from "mobx";
|
||||
import { Icon } from "@blueprintjs/core";
|
||||
|
||||
export class GridVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { array: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("grid"),
|
||||
name: "Grid",
|
||||
priority: 90,
|
||||
render: () => <DecoratedGridComponent data={data} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DecoratedGridComponent extends React.Component<{
|
||||
data: CommonDataTypes.Grid;
|
||||
}> {
|
||||
render() {
|
||||
const d = this.props.data;
|
||||
|
||||
const map = new Set<string>();
|
||||
function getUniqueId(tag: string): string {
|
||||
let n = 0;
|
||||
while (true) {
|
||||
const id = tag + (n === 0 ? "" : `-${n}`);
|
||||
if (!map.has(id)) {
|
||||
map.add(id);
|
||||
return id;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
const rows: GridComponent["props"]["rows"] = [];
|
||||
rows.push();
|
||||
let rowIdx = 0;
|
||||
let columnCount = 0;
|
||||
for (const row of d.rows) {
|
||||
columnCount = Math.max(columnCount, row.columns.length);
|
||||
rows.push({
|
||||
columns: row.columns.map((c, colIdx) => ({
|
||||
content: (
|
||||
<Cell>
|
||||
{c.content !== undefined ? c.content : c.tag || ""}
|
||||
</Cell>
|
||||
),
|
||||
id:
|
||||
c.tag !== undefined
|
||||
? getUniqueId(c.tag)
|
||||
: `${rowIdx}-${colIdx}`,
|
||||
color: c.color,
|
||||
kind: "data",
|
||||
})),
|
||||
});
|
||||
rowIdx++;
|
||||
}
|
||||
|
||||
rows.unshift({
|
||||
columns: [...new Array(columnCount)].map((v, idx) => ({
|
||||
id: `column-header-${idx}`,
|
||||
content: <Cell>{idx}</Cell>,
|
||||
kind: "header",
|
||||
})),
|
||||
});
|
||||
|
||||
if (d.markers) {
|
||||
for (const m of d.markers) {
|
||||
rows.push({
|
||||
columns: [],
|
||||
});
|
||||
const r = rows[rows.length - 1];
|
||||
for (let i = 0; i < m.column; i++) {
|
||||
r.columns.push({
|
||||
content: "",
|
||||
kind: "empty",
|
||||
id: `marker-spacer-${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
r.columns.push({
|
||||
id: m.id,
|
||||
kind: "empty",
|
||||
content: (
|
||||
<Cell>
|
||||
<Icon
|
||||
icon="arrow-up"
|
||||
color="var(--vscode-editor-foreground)"
|
||||
/>
|
||||
{m.label || m.id}
|
||||
</Cell>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 10,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<GridComponent rows={rows} markers={[]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Cell(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CellData {
|
||||
content: React.ReactNode;
|
||||
id: string;
|
||||
color?: string;
|
||||
kind: "data" | "header" | "empty";
|
||||
}
|
||||
|
||||
@observer
|
||||
class GridComponent extends React.Component<{
|
||||
rows: {
|
||||
columns: CellData[];
|
||||
}[];
|
||||
markers: {
|
||||
id: string;
|
||||
|
||||
row: number;
|
||||
column: number;
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
|
||||
label?: string;
|
||||
color?: string;
|
||||
}[];
|
||||
}> {
|
||||
private readonly cells = new Map<string, CellInfo>();
|
||||
|
||||
@computed get cellsGrid(): { maxWidth: number; grid: CellInfo[][] } {
|
||||
const unusedIds = new Set(this.cells.keys());
|
||||
|
||||
const rows = this.props.rows;
|
||||
|
||||
const grid = new Array<Array<CellInfo>>();
|
||||
|
||||
let maxWidth = 0;
|
||||
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
||||
const row = rows[rowIdx];
|
||||
maxWidth = Math.max(maxWidth, row.columns.length);
|
||||
const cellRow = new Array<CellInfo>();
|
||||
grid.push(cellRow);
|
||||
|
||||
for (let colIdx = 0; colIdx < row.columns.length; colIdx++) {
|
||||
const col = row.columns[colIdx];
|
||||
const key = col.id;
|
||||
unusedIds.delete(key);
|
||||
let c = this.cells.get(key);
|
||||
if (!c) {
|
||||
// New cell
|
||||
c = new CellInfo(col);
|
||||
this.cells.set(key, c);
|
||||
} else {
|
||||
c.updateCellData(col);
|
||||
}
|
||||
cellRow.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in unusedIds.values()) {
|
||||
this.cells.delete(id);
|
||||
}
|
||||
|
||||
return { maxWidth, grid };
|
||||
}
|
||||
|
||||
@computed
|
||||
private get layout(): {
|
||||
cells: {
|
||||
cell: CellInfo;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
const { maxWidth, grid } = this.cellsGrid;
|
||||
|
||||
const maxWidths = new Array<number>();
|
||||
for (let colIdx = 0; colIdx < maxWidth; colIdx++) {
|
||||
let maxWidth = 30;
|
||||
for (const row of grid) {
|
||||
const col = row[colIdx];
|
||||
if (col) {
|
||||
maxWidth = Math.max(maxWidth, col.contentWidth || 0);
|
||||
}
|
||||
}
|
||||
maxWidths.push(maxWidth);
|
||||
}
|
||||
|
||||
const result = new Array<{
|
||||
cell: CellInfo;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}>();
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
for (const row of grid) {
|
||||
let maxHeight = 0;
|
||||
for (const col of row) {
|
||||
maxHeight = Math.max(maxHeight, col.contentHeight || 0);
|
||||
}
|
||||
|
||||
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
||||
const maxWidth = maxWidths[colIdx];
|
||||
const col = row[colIdx];
|
||||
result.push({
|
||||
cell: col,
|
||||
top,
|
||||
left,
|
||||
height: maxHeight,
|
||||
width: maxWidth,
|
||||
});
|
||||
height = Math.max(height, top + maxHeight);
|
||||
width = Math.max(width, left + maxWidth);
|
||||
col.lastTop = col.top;
|
||||
col.lastLeft = col.left;
|
||||
col.top = top;
|
||||
col.left = left;
|
||||
|
||||
col.lastContentArea = col.contentArea;
|
||||
col.contentArea = maxHeight * maxWidth;
|
||||
left += maxWidth;
|
||||
}
|
||||
|
||||
left = 0;
|
||||
top += maxHeight;
|
||||
}
|
||||
|
||||
result.sort((a, b) => a.cell.id.localeCompare(b.cell.id));
|
||||
return { cells: result, height, width };
|
||||
}
|
||||
|
||||
render() {
|
||||
const l = this.layout;
|
||||
return (
|
||||
<div
|
||||
className="component-Grid"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: l.height,
|
||||
width: l.width,
|
||||
}}
|
||||
>
|
||||
{l.cells.map(i => (
|
||||
<div
|
||||
key={i.cell.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: i.top,
|
||||
left: i.left,
|
||||
height: i.height,
|
||||
width: i.width,
|
||||
zIndex: i.cell.distance,
|
||||
transition:
|
||||
i.cell.lastContentArea !== 0 ? "all 1s" : "",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`part-${i.cell.kind}`}
|
||||
style={
|
||||
{
|
||||
empty: {},
|
||||
data: {
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
justifyContent: "center",
|
||||
margin: 2,
|
||||
},
|
||||
header: {
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: 2,
|
||||
},
|
||||
}[i.cell.kind]
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={i.cell.handleRef}
|
||||
style={{
|
||||
width: "fit-content",
|
||||
height: "fit-content",
|
||||
}}
|
||||
>
|
||||
{i.cell.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateContentSize();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateContentSize();
|
||||
}
|
||||
|
||||
@action
|
||||
updateContentSize() {
|
||||
for (const c of this.cells.values()) {
|
||||
if (c.ref) {
|
||||
const r = c.ref.getBoundingClientRect();
|
||||
c.contentHeight = r.height + 6;
|
||||
c.contentWidth = r.width + 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CellInfo {
|
||||
@observable public contentWidth: number | undefined = undefined;
|
||||
@observable public contentHeight: number | undefined = undefined;
|
||||
|
||||
public contentArea = 0;
|
||||
public lastContentArea = 0;
|
||||
|
||||
public lastTop = 0;
|
||||
public top = 0;
|
||||
public lastLeft = 0;
|
||||
public left = 0;
|
||||
|
||||
public get distance(): number {
|
||||
return Math.floor(
|
||||
Math.pow(this.top - this.lastTop, 2) +
|
||||
Math.pow(this.left - this.lastLeft, 2)
|
||||
);
|
||||
}
|
||||
|
||||
content!: React.ReactNode;
|
||||
|
||||
ref: HTMLDivElement | null = null;
|
||||
|
||||
public readonly id: string;
|
||||
public readonly kind: CellData["kind"];
|
||||
|
||||
constructor(cellData: CellData) {
|
||||
this.id = cellData.id;
|
||||
this.kind = cellData.kind;
|
||||
this.updateCellData(cellData);
|
||||
}
|
||||
|
||||
public updateCellData(cellData: CellData) {
|
||||
this.content = cellData.content;
|
||||
}
|
||||
|
||||
public readonly handleRef = (ref: HTMLDivElement | null) => {
|
||||
this.ref = ref;
|
||||
};
|
||||
}
|
||||
113
visualization/src/visualizers/SvgVisualizer.tsx
Normal file
113
visualization/src/visualizers/SvgVisualizer.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import Measure, { ContentRect } from "react-measure";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { observable, action } from "mobx";
|
||||
import { Tool, ReactSVGPanZoom, Value } from "react-svg-pan-zoom";
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
|
||||
export class SvgVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { svg: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("svg"),
|
||||
name: "Svg",
|
||||
priority: 100,
|
||||
render() {
|
||||
return <SvgViewer svgContent={data.text} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function widthOrDefault(r: ContentRect): number {
|
||||
if (r.bounds && r.bounds.width) {
|
||||
return r.bounds.width;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function heightOrDefault(r: ContentRect): number {
|
||||
console.log(r);
|
||||
if (r.bounds && r.bounds.height) {
|
||||
return r.bounds.height;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class SvgViewer extends React.Component<{
|
||||
svgContent: string;
|
||||
svgRef?: (element: SVGSVGElement | null) => void;
|
||||
}> {
|
||||
@observable tool: Tool = "pan";
|
||||
|
||||
@action.bound
|
||||
private setTool(tool: Tool): void {
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
private setRef(element: ReactSVGPanZoom | null) {
|
||||
if (this.props.svgRef) {
|
||||
if (!element) {
|
||||
this.props.svgRef(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = (element as any).ViewerDOM as SVGSVGElement;
|
||||
this.props.svgRef(svg);
|
||||
}
|
||||
}
|
||||
|
||||
@observable value: Value | {} = {};
|
||||
|
||||
render() {
|
||||
let { svgContent } = this.props;
|
||||
let width: number = 0;
|
||||
let height: number = 0;
|
||||
svgContent = svgContent.replace(
|
||||
/viewBox="[0-9\.]+ [0-9\.]+ ([0-9\.]+) ([0-9\.]+)"/,
|
||||
(r, w, h) => {
|
||||
width = parseFloat(w);
|
||||
height = parseFloat(h);
|
||||
return "";
|
||||
}
|
||||
);
|
||||
const tool = this.tool;
|
||||
const val = this.value;
|
||||
|
||||
return (
|
||||
<Measure bounds>
|
||||
{({ measureRef, contentRect }) => (
|
||||
<div ref={measureRef} className="svgViewer">
|
||||
<ReactSVGPanZoom
|
||||
width={widthOrDefault(contentRect)}
|
||||
height={heightOrDefault(contentRect)}
|
||||
tool={tool}
|
||||
value={val as any}
|
||||
onChangeValue={v => (this.value = v)}
|
||||
onChangeTool={this.setTool}
|
||||
ref={e => this.setRef(e)}
|
||||
>
|
||||
<svg width={width} height={height}>
|
||||
<g
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: svgContent,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</ReactSVGPanZoom>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
TreeViewModel,
|
||||
TreeNodeViewModel,
|
||||
TreeView,
|
||||
TreeWithPathView,
|
||||
} from "./Views";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
TreeNode,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../../Visualizer";
|
||||
import * as React from "react";
|
||||
|
||||
export class TreeVisualizer extends Visualizer {
|
||||
visualize(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 <TreeWithPathView model={m} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createTreeViewModelFromTreeNodeData<TData>(
|
||||
root: TreeNode<TData>
|
||||
): TreeViewModel<TData> {
|
||||
const m = new TreeViewModel<TData>();
|
||||
m.root = recurse(root, m);
|
||||
return m;
|
||||
|
||||
function recurse(
|
||||
node: TreeNode<TData>,
|
||||
viewModel: TreeViewModel<TData>
|
||||
): TreeNodeViewModel<TData> {
|
||||
const children: TreeNodeViewModel<TData>[] = node.children.map(c =>
|
||||
recurse(c, viewModel)
|
||||
);
|
||||
const model = new TreeNodeViewModel<TData>(
|
||||
viewModel,
|
||||
node.id,
|
||||
node.name,
|
||||
node.value,
|
||||
node.emphasizedValue,
|
||||
children,
|
||||
node.data
|
||||
);
|
||||
model.isMarked = !!node.isMarked;
|
||||
for (const c of children) {
|
||||
c.parent = model;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
530
visualization/src/visualizers/TreeVisualizer/Views.tsx
Normal file
530
visualization/src/visualizers/TreeVisualizer/Views.tsx
Normal file
@@ -0,0 +1,530 @@
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun, computed, action, runInAction } from "mobx";
|
||||
import * as React from "react";
|
||||
import * as classNames from "classnames";
|
||||
import { Rectangle, Point } from "../../utils/Point";
|
||||
import { SvgLine } from "../../utils/SvgElements";
|
||||
// import "./style.scss";
|
||||
|
||||
export class TreeViewModel<TData = unknown> {
|
||||
private started = true;
|
||||
start() {
|
||||
this.started = true;
|
||||
this.updateOffsets();
|
||||
}
|
||||
stop() {
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
private version = 0;
|
||||
private lastUpdateVersion = -1;
|
||||
|
||||
@observable public root: TreeNodeViewModel<TData> | undefined = undefined;
|
||||
|
||||
public invalidateOffsets() {
|
||||
this.version++;
|
||||
}
|
||||
|
||||
@action
|
||||
public updateOffsets() {
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
if (!this.root) {
|
||||
return;
|
||||
}
|
||||
if (this.lastUpdateVersion === this.version) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdateVersion = this.version;
|
||||
console.log("update");
|
||||
this.root.updateOffsets();
|
||||
}
|
||||
|
||||
public get marked(): TreeNodeViewModel<TData>[] {
|
||||
const result = new Array<TreeNodeViewModel<TData>>();
|
||||
function recurse(node: TreeNodeViewModel<TData>) {
|
||||
if (node.isMarked) {
|
||||
result.push(node);
|
||||
}
|
||||
for (const c of node.children) {
|
||||
recurse(c);
|
||||
}
|
||||
}
|
||||
if (this.root) {
|
||||
recurse(this.root);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@observable selected: TreeNodeViewModel<TData> | undefined = undefined;
|
||||
|
||||
@action public select(current: TreeNodeViewModel<TData> | undefined) {
|
||||
if (this.selected === current) {
|
||||
return;
|
||||
}
|
||||
if (this.selected) {
|
||||
this.selected.isSelected = false;
|
||||
}
|
||||
if (current) {
|
||||
current.isSelected = true;
|
||||
}
|
||||
this.selected = current;
|
||||
}
|
||||
|
||||
@action public toggleSelect(current: TreeNodeViewModel<TData>) {
|
||||
if (this.selected === current) {
|
||||
this.select(undefined);
|
||||
} else {
|
||||
this.select(current);
|
||||
}
|
||||
}
|
||||
|
||||
private selectOnHoverEnabled = false;
|
||||
private selectedBeforeHover:
|
||||
| TreeNodeViewModel<TData>
|
||||
| undefined = undefined;
|
||||
|
||||
@action public handleHoverEvent(
|
||||
current: TreeNodeViewModel<TData> | undefined
|
||||
) {
|
||||
if (this.selectOnHoverEnabled) {
|
||||
if (current) {
|
||||
this.select(current);
|
||||
} else {
|
||||
this.select(this.selectedBeforeHover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectOnHover(enable: boolean) {
|
||||
if (this.selectOnHoverEnabled === enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
this.selectedBeforeHover = this.selected;
|
||||
} else if (!this.selected) {
|
||||
this.select(this.selectedBeforeHover);
|
||||
}
|
||||
|
||||
this.selectOnHoverEnabled = enable;
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
||||
export class TreeNodeViewModel<TData = unknown> {
|
||||
public parent: TreeNodeViewModel<TData> | null = null;
|
||||
|
||||
constructor(
|
||||
public readonly treeViewModel: TreeViewModel<TData>,
|
||||
public readonly id: string | undefined,
|
||||
public readonly name: string,
|
||||
public readonly value: string | undefined,
|
||||
public readonly emphasizedValue: string | undefined,
|
||||
public readonly children: TreeNodeViewModel<TData>[],
|
||||
public readonly data: TData
|
||||
) {}
|
||||
|
||||
@observable expanderOffsetRect: Rectangle | undefined = undefined;
|
||||
private expanderDiv: HTMLDivElement | null = null;
|
||||
|
||||
@action.bound
|
||||
setExpanderDiv(div: HTMLDivElement | null) {
|
||||
this.expanderDiv = div;
|
||||
this.treeViewModel.invalidateOffsets();
|
||||
}
|
||||
|
||||
@observable public expanded: boolean = false;
|
||||
@observable public isMarked: boolean = false;
|
||||
@observable public isSelected: boolean = false;
|
||||
|
||||
@action
|
||||
public toggleExpanded() {
|
||||
this.expanded = !this.expanded;
|
||||
this.treeViewModel.invalidateOffsets();
|
||||
}
|
||||
|
||||
@computed public get isChildOrThisMarked(): boolean {
|
||||
if (this.isMarked) return true;
|
||||
return this.children.some(c => c.isChildOrThisMarked);
|
||||
}
|
||||
|
||||
@computed public get isChildOrThisSelected(): boolean {
|
||||
if (this.isSelected) return true;
|
||||
return this.children.some(c => c.isChildOrThisSelected);
|
||||
}
|
||||
|
||||
@computed public get isParentOrThisSelected(): boolean {
|
||||
if (this.isSelected) return true;
|
||||
if (this.parent) return this.parent.isParentOrThisSelected;
|
||||
return false;
|
||||
}
|
||||
|
||||
get path(): string[] {
|
||||
const result = new Array<string>();
|
||||
let c: TreeNodeViewModel = this;
|
||||
while (c.parent) {
|
||||
result.unshift(c.id || c.name);
|
||||
c = c.parent;
|
||||
}
|
||||
if (result.length === 0) {
|
||||
result.push("root");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
updateOffsets() {
|
||||
for (const c of this.children) {
|
||||
c.updateOffsets();
|
||||
}
|
||||
|
||||
if (!this.expanderDiv) {
|
||||
return;
|
||||
}
|
||||
const newRect = Rectangle.ofSize(
|
||||
new Point(this.expanderDiv.offsetLeft, this.expanderDiv.offsetTop),
|
||||
new Point(
|
||||
this.expanderDiv.offsetWidth,
|
||||
this.expanderDiv.offsetHeight
|
||||
)
|
||||
);
|
||||
if (
|
||||
!this.expanderOffsetRect ||
|
||||
!newRect.equals(this.expanderOffsetRect)
|
||||
) {
|
||||
this.expanderOffsetRect = newRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isValidFunctionName = (function() {
|
||||
var validName = /^[$A-Z_][0-9A-Z_$]*$/i;
|
||||
var reserved = {
|
||||
abstract: true,
|
||||
boolean: true,
|
||||
// ...
|
||||
with: true,
|
||||
} as any;
|
||||
return function(s: string) {
|
||||
// Ensure a valid name and not reserved.
|
||||
return validName.test(s) && !reserved[s];
|
||||
};
|
||||
})();
|
||||
|
||||
@observer
|
||||
export class TreeWithPathView extends React.Component<{
|
||||
model: TreeViewModel;
|
||||
}> {
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
return (
|
||||
<div className="component-TreeWithPathView">
|
||||
<div className="part-path">
|
||||
<span>
|
||||
{model.selected ? (
|
||||
<span>
|
||||
{model.selected.path.reduce((acc, v) => {
|
||||
acc = acc.slice();
|
||||
function add() {
|
||||
acc.push(
|
||||
<span
|
||||
className="part-path-item"
|
||||
key={acc.length}
|
||||
>
|
||||
{v}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isValidFunctionName(v)) {
|
||||
if (acc.length > 0) {
|
||||
acc.push(
|
||||
<span key={acc.length}>.</span>
|
||||
);
|
||||
}
|
||||
add();
|
||||
} else {
|
||||
acc.push(
|
||||
<span key={acc.length}>[</span>
|
||||
);
|
||||
add();
|
||||
acc.push(
|
||||
<span key={acc.length}>]</span>
|
||||
);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Array<React.ReactElement>())}
|
||||
</span>
|
||||
) : (
|
||||
"(Nothing Selected)"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="part-tree">
|
||||
<TreeView model={model} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TreeView extends React.Component<{ model: TreeViewModel }> {
|
||||
componentWillUpdate(newProps: this["props"]) {
|
||||
newProps.model.stop();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.model.stop();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.model.start();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.model.start();
|
||||
}
|
||||
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="component-TreeView"
|
||||
onKeyDown={e => {
|
||||
if (e.keyCode === 18) {
|
||||
// alt
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
model.setSelectOnHover(true);
|
||||
}
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 18) {
|
||||
// alt
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
model.setSelectOnHover(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{model.root && (
|
||||
<>
|
||||
<svg className="part-svg">
|
||||
<TreeNodeSvgView model={model.root} />
|
||||
</svg>
|
||||
<div className="part-node">
|
||||
<TreeNodeView model={model.root} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TreeNodeSvgView extends React.Component<{
|
||||
model: TreeNodeViewModel;
|
||||
}> {
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
if (
|
||||
!model.expanderOffsetRect ||
|
||||
model.children.length === 0 ||
|
||||
model.expanded
|
||||
) {
|
||||
return <g />;
|
||||
}
|
||||
|
||||
function findLastExpanderOffset(
|
||||
predicate: (v: TreeNodeViewModel) => boolean
|
||||
): Rectangle | undefined {
|
||||
const all = model.children
|
||||
.filter(predicate)
|
||||
.map(c => c.expanderOffsetRect);
|
||||
const last = all[all.length - 1];
|
||||
return last;
|
||||
}
|
||||
|
||||
const lastOffset = findLastExpanderOffset(() => true);
|
||||
const selectedOffset = findLastExpanderOffset(
|
||||
v => v.isChildOrThisSelected
|
||||
);
|
||||
const markedOffset = findLastExpanderOffset(v => v.isChildOrThisMarked);
|
||||
|
||||
if (!lastOffset) {
|
||||
return <g />;
|
||||
}
|
||||
|
||||
const start = model.expanderOffsetRect.center.add({ y: 10 });
|
||||
|
||||
return (
|
||||
<g className="component-TreeNodeSvgView">
|
||||
{markedOffset && (
|
||||
<SvgLine
|
||||
className="path childOrThisMarked"
|
||||
start={start}
|
||||
end={new Point(start.x, markedOffset.center.y)}
|
||||
/>
|
||||
)}
|
||||
{selectedOffset && (
|
||||
<SvgLine
|
||||
className="path childOrThisSelected"
|
||||
start={start}
|
||||
end={new Point(start.x, selectedOffset.center.y)}
|
||||
/>
|
||||
)}
|
||||
<SvgLine
|
||||
className="path"
|
||||
start={start}
|
||||
end={new Point(start.x, lastOffset.center.y)}
|
||||
/>
|
||||
{model.children.map((c, idx) => {
|
||||
const end = c.expanderOffsetRect!.center.sub({ x: 5 });
|
||||
|
||||
return (
|
||||
<g key={idx}>
|
||||
{c.isChildOrThisMarked && (
|
||||
<SvgLine
|
||||
className={classNames(
|
||||
"path",
|
||||
"childOrThisMarked"
|
||||
)}
|
||||
start={new Point(start.x, end.y)}
|
||||
end={end}
|
||||
/>
|
||||
)}
|
||||
<SvgLine
|
||||
className={classNames(
|
||||
"path",
|
||||
c.isChildOrThisSelected &&
|
||||
"childOrThisSelected"
|
||||
)}
|
||||
start={new Point(start.x, end.y)}
|
||||
end={end}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{model.children.map((c, idx) => (
|
||||
<TreeNodeSvgView key={idx} model={c} />
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TreeNodeView extends React.Component<{
|
||||
model: TreeNodeViewModel;
|
||||
}> {
|
||||
@action.bound
|
||||
private clickHandler() {
|
||||
this.props.model.treeViewModel.toggleSelect(this.props.model);
|
||||
}
|
||||
|
||||
private handleMouseEnterOrLeave(enter: boolean) {
|
||||
this.props.model.treeViewModel.handleHoverEvent(
|
||||
enter ? this.props.model : undefined
|
||||
);
|
||||
}
|
||||
|
||||
@observable
|
||||
private rootDiv: HTMLElement | null = null;
|
||||
@action.bound
|
||||
private setRootDiv(div: HTMLElement | null) {
|
||||
this.rootDiv = div;
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private readonly _autorun = autorun(() => {
|
||||
if (this.props.model.isMarked && this.rootDiv) {
|
||||
this.rootDiv.scrollIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.model.treeViewModel.updateOffsets();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.props.model.treeViewModel.updateOffsets();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const model = this.props.model;
|
||||
const collapsed = model.expanded;
|
||||
console.log("render");
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"component-TreeNodeView",
|
||||
model.expanded && "collapsed",
|
||||
model.isMarked && "selected",
|
||||
model.isSelected && "hovered",
|
||||
model.isChildOrThisSelected &&
|
||||
!model.isSelected &&
|
||||
"childHovered",
|
||||
model.isParentOrThisSelected &&
|
||||
!model.isSelected &&
|
||||
"parentHovered"
|
||||
)}
|
||||
ref={this.setRootDiv}
|
||||
>
|
||||
<div
|
||||
className="part-header"
|
||||
onMouseOver={e => {
|
||||
this.handleMouseEnterOrLeave(true);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onMouseOut={e => {
|
||||
this.handleMouseEnterOrLeave(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={model.setExpanderDiv}
|
||||
onClick={() => model.toggleExpanded()}
|
||||
className={classNames(
|
||||
"part-collapser",
|
||||
model.children.length > 0 &&
|
||||
"decorator-collapse-icon",
|
||||
!collapsed && "expanded"
|
||||
)}
|
||||
/>
|
||||
|
||||
<span className="part-text" onClick={this.clickHandler}>
|
||||
{model.id && (
|
||||
<>
|
||||
<span className="part-id">{model.id}</span>
|
||||
<span>: </span>
|
||||
</>
|
||||
)}
|
||||
<span className="part-name">{model.name}</span>
|
||||
{model.value && (
|
||||
<span className="part-value">{model.value}</span>
|
||||
)}
|
||||
{model.emphasizedValue && (
|
||||
<span className="part-emphasized-value">
|
||||
{model.emphasizedValue}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{model.children.length > 0 && !model.expanded && (
|
||||
<div className="part-children">
|
||||
{model.children.map((c, idx) => (
|
||||
<TreeNodeView model={c} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
141
visualization/src/visualizers/TreeVisualizer/style.scss
Normal file
141
visualization/src/visualizers/TreeVisualizer/style.scss
Normal file
@@ -0,0 +1,141 @@
|
||||
.component-TreeView {
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
||||
& > .part-svg,
|
||||
& > .part-node {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.component-TreeNodeSvgView {
|
||||
.path {
|
||||
stroke: gray;
|
||||
stroke-dasharray: 1;
|
||||
|
||||
&.childOrThisMarked {
|
||||
stroke: orange;
|
||||
stroke-width: 5;
|
||||
stroke-dasharray: 0;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
&.childOrThisSelected {
|
||||
stroke: black;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component-TreeNodeView {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI,
|
||||
HelveticaNeue-Light, Ubuntu, Droid Sans, sans-serif;
|
||||
|
||||
.part-header {
|
||||
padding: 2px;
|
||||
padding-left: 5px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.part-collapser {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.part-text {
|
||||
cursor: pointer;
|
||||
.part-id {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.part-value {
|
||||
color: darkgreen;
|
||||
text-decoration: underline;
|
||||
|
||||
/*&::before,
|
||||
&::after {
|
||||
content: '"';
|
||||
}*/
|
||||
|
||||
padding: 1px 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.part-emphasized-value {
|
||||
color: darkred;
|
||||
border: black solid 1px;
|
||||
padding: 1px 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.part-children {
|
||||
margin-left: 0px;
|
||||
margin-top: 0px;
|
||||
|
||||
padding: 5px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
& > .part-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
&.hovered {
|
||||
& > .part-header > span {
|
||||
font-weight: bold;
|
||||
}
|
||||
& > .part-children {
|
||||
background: transparentize(gray, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component-TreeWithPathView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.part-path {
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
padding: 5px 10px;
|
||||
border-bottom: solid gray 1px;
|
||||
|
||||
color: lightgray;
|
||||
|
||||
.part-path-item {
|
||||
color: black;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
.part-tree {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.decorator-collapse-icon {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23646465' d='M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z'/%3E%3C/svg%3E");
|
||||
|
||||
background-position: 50%;
|
||||
background-position-x: 50%;
|
||||
background-position-y: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&.expanded {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23646465' d='M11 10.07H5.344L11 4.414v5.656z'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { observable, autorun, runInAction } from "mobx";
|
||||
import { SvgViewer } from "../SvgVisualizer";
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
|
||||
export class GraphvizDotVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { dotGraph: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("graphviz-dot"),
|
||||
name: "Graphviz (Dot Data)",
|
||||
priority: 100,
|
||||
render() {
|
||||
return <GraphvizDotViewer dotCode={data.text} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VisLoader {
|
||||
private result: any | undefined;
|
||||
|
||||
async getViz(): Promise<any> {
|
||||
if (!this.result) {
|
||||
const Viz = await import("viz.js");
|
||||
const { Module, render } = await import("viz.js/full.render.js");
|
||||
|
||||
const viz = new Viz.default({
|
||||
Module: () => Module({ TOTAL_MEMORY: 1 << 30 }),
|
||||
render,
|
||||
});
|
||||
|
||||
this.result = viz;
|
||||
}
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
const vizLoader = new VisLoader();
|
||||
|
||||
@observer
|
||||
export class GraphvizDotViewer 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 viz = await vizLoader.getViz();
|
||||
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} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { GraphvizDotViewer } from "./GraphvizDotVisualizer";
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
|
||||
export class GraphvizGraphVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { graph: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("graphviz-graph"),
|
||||
name: "Graphviz",
|
||||
priority: 100,
|
||||
render() {
|
||||
return (
|
||||
<GraphvizGraphViewer
|
||||
edges={data.edges}
|
||||
nodes={data.nodes}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class GraphvizGraphViewer 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 !== undefined ? n.label : n.id
|
||||
)} ];`
|
||||
)
|
||||
.join("\n ")}
|
||||
${edges
|
||||
.map(
|
||||
e =>
|
||||
`"${e.from}" -> "${
|
||||
e.to
|
||||
}" [ label = ${JSON.stringify(
|
||||
e.label !== undefined ? e.label : ""
|
||||
)} ];`
|
||||
)
|
||||
.join("\n")}
|
||||
}
|
||||
`;
|
||||
return <GraphvizDotViewer dotCode={dotContent} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { DataSet, Network, Options } from "vis-network";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
NodeGraphData,
|
||||
EdgeGraphData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
|
||||
@observer
|
||||
export class VisJsGraphViewer extends React.Component<{
|
||||
nodes: NodeGraphData[];
|
||||
edges: EdgeGraphData[];
|
||||
}> {
|
||||
private readonly divRef = React.createRef<HTMLDivElement>();
|
||||
private readonly nodes = new DataSet<{
|
||||
id: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
shape?: string;
|
||||
}>();
|
||||
private readonly edges = new DataSet<{
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
dashes?: boolean;
|
||||
shape?: boolean;
|
||||
}>();
|
||||
|
||||
render() {
|
||||
return <div style={{ height: "100%" }} ref={this.divRef} />;
|
||||
}
|
||||
|
||||
synchronizeData() {
|
||||
const newNodes = new Set<string>();
|
||||
for (const n of this.props.nodes) {
|
||||
newNodes.add(n.id);
|
||||
this.nodes.update({
|
||||
id: n.id,
|
||||
label: n.label !== undefined ? n.label : n.id,
|
||||
color: n.color,
|
||||
shape: n.shape,
|
||||
});
|
||||
}
|
||||
this.nodes.forEach(item => {
|
||||
if (!newNodes.has(item.id)) {
|
||||
this.nodes.remove(item);
|
||||
}
|
||||
});
|
||||
|
||||
function getIdOfEdge(e: EdgeGraphData): string {
|
||||
if (e.id) {
|
||||
return e.id;
|
||||
}
|
||||
return e.from + "####" + e.to + "|" + e.label;
|
||||
}
|
||||
|
||||
const newEdges = new Set<string>();
|
||||
for (const n of this.props.edges) {
|
||||
const id = getIdOfEdge(n);
|
||||
newEdges.add(id);
|
||||
this.edges.update({
|
||||
id: id,
|
||||
label: n.label !== undefined ? n.label : "",
|
||||
from: n.from,
|
||||
to: n.to,
|
||||
color: n.color,
|
||||
dashes: n.dashes,
|
||||
});
|
||||
}
|
||||
this.edges.forEach(item => {
|
||||
if (!newEdges.has(item.id)) {
|
||||
this.edges.remove(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.synchronizeData();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.synchronizeData();
|
||||
|
||||
const data = {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges,
|
||||
};
|
||||
const options: Options = {
|
||||
edges: {
|
||||
arrows: {
|
||||
to: { enabled: true, scaleFactor: 1, type: "arrow" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const network = new Network(this.divRef.current!, data, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { makeLazyLoadable } from "../../LazyLoadable";
|
||||
// import { DataSet, Network, Options } from "vis-network";
|
||||
|
||||
const VisJsGraphViewerLazyLoadable = makeLazyLoadable(
|
||||
async () => (await import("./VisJsGraphViewer")).VisJsGraphViewer
|
||||
);
|
||||
|
||||
export class VisJsGraphVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { graph: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("vis-js-graph"),
|
||||
name: "vis.js",
|
||||
priority: 1001,
|
||||
render() {
|
||||
return (
|
||||
<VisJsGraphViewerLazyLoadable
|
||||
edges={data.edges}
|
||||
nodes={data.nodes}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
25
visualization/src/visualizers/index.ts
Normal file
25
visualization/src/visualizers/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// import "./style.scss";
|
||||
import { ComposedVisualizer } from "../Visualizer";
|
||||
import { TreeVisualizer } from "./TreeVisualizer/TreeVisualizer";
|
||||
import { GraphvizGraphVisualizer } from "./graph-visualizers/GraphvizGraphVisualizer";
|
||||
import { SvgVisualizer } from "./SvgVisualizer";
|
||||
import { GraphvizDotVisualizer } from "./graph-visualizers/GraphvizDotVisualizer";
|
||||
import { TextVisualizer } from "./text-visualizers/TextVisualizer";
|
||||
import { MonacoTextVisualizer } from "./text-visualizers/MonacoTextVisualizer";
|
||||
import { AstVisualizer } from "./AstVisualizer";
|
||||
import { VisJsGraphVisualizer } from "./graph-visualizers/VisJsGraphVisualizer";
|
||||
import { PlotlyVisualizer } from "./plotly";
|
||||
import { GridVisualizer } from "./GridVisualizer";
|
||||
|
||||
export const knownVisualizations = new ComposedVisualizer([
|
||||
new TreeVisualizer(),
|
||||
new GraphvizGraphVisualizer(),
|
||||
new VisJsGraphVisualizer(),
|
||||
new SvgVisualizer(),
|
||||
new GraphvizDotVisualizer(),
|
||||
new TextVisualizer(),
|
||||
new MonacoTextVisualizer(),
|
||||
new AstVisualizer(),
|
||||
new PlotlyVisualizer(),
|
||||
new GridVisualizer(),
|
||||
]);
|
||||
19
visualization/src/visualizers/plotly/PlotlyViewer.tsx
Normal file
19
visualization/src/visualizers/plotly/PlotlyViewer.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Plot from "react-plotly.js";
|
||||
|
||||
@observer
|
||||
export class PlotlyViewer extends React.Component<{
|
||||
data: Partial<Plotly.Data>[];
|
||||
}> {
|
||||
render() {
|
||||
return (
|
||||
<Plot
|
||||
style={{ width: "100%" }}
|
||||
data={this.props.data}
|
||||
layout={{}}
|
||||
config={{ responsive: true }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
30
visualization/src/visualizers/plotly/index.tsx
Normal file
30
visualization/src/visualizers/plotly/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { makeLazyLoadable } from "../../LazyLoadable";
|
||||
|
||||
const PlotlyViewerLazyLoadable = makeLazyLoadable(
|
||||
async () => (await import("./PlotlyViewer")).PlotlyViewer
|
||||
);
|
||||
|
||||
export class PlotlyVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { plotly: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("plotly"),
|
||||
name: "plotly",
|
||||
priority: 1001,
|
||||
render() {
|
||||
return <PlotlyViewerLazyLoadable data={data.data} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
74
visualization/src/visualizers/style.scss
Normal file
74
visualization/src/visualizers/style.scss
Normal file
@@ -0,0 +1,74 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.component-AstTree {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.part-tree2 {
|
||||
flex: 2;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.part-editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
border-top: solid gray 1px;
|
||||
|
||||
.marked {
|
||||
background: orange;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: yellow;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/*display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
||||
.part-tree2 {
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
.part-editor {
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
background: red;
|
||||
}*/
|
||||
}
|
||||
|
||||
.component-Grid {
|
||||
.part-data {
|
||||
border: 1px solid lightgray;
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
.part-header {
|
||||
background: transparentize(white, 0.5);
|
||||
}
|
||||
|
||||
.part-empty {
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { autorun, observable } from "mobx";
|
||||
|
||||
export class MonacoTextVisualizer extends Visualizer {
|
||||
visualize(data: ExtractedData, collector: VisualizationCollector): void {
|
||||
if (isCommonDataType(data, { text: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("monaco-text"),
|
||||
name: "Monaco Editor",
|
||||
priority: 95,
|
||||
render() {
|
||||
let id = "text";
|
||||
if (data.fileName) {
|
||||
id = getLanguageId(data.fileName);
|
||||
}
|
||||
|
||||
return <MonacoEditor text={data.text} languageId={id} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getLanguageId(fileName: string): string {
|
||||
const l = monaco.languages.getLanguages();
|
||||
const result = l.find(l => {
|
||||
if (l.filenamePatterns) {
|
||||
for (const p of l.filenamePatterns) {
|
||||
if (new RegExp(p).test(fileName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (l.extensions) {
|
||||
for (const p of l.extensions) {
|
||||
if (fileName.endsWith(p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result.id;
|
||||
}
|
||||
return "text";
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MonacoEditor extends React.Component<{
|
||||
text: string;
|
||||
languageId: string;
|
||||
}> {
|
||||
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private model: monaco.editor.ITextModel | undefined = undefined;
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateText = autorun(() => {
|
||||
if (this.editor) {
|
||||
const model = monaco.editor.createModel(
|
||||
this.props.text,
|
||||
this.props.languageId,
|
||||
undefined
|
||||
);
|
||||
|
||||
this.editor.setModel(model);
|
||||
|
||||
/* this.editor!.updateOptions({ readOnly: false });
|
||||
|
||||
setTimeout(() => {
|
||||
const a = this.editor!.getAction(
|
||||
"editor.action.formatDocument"
|
||||
);
|
||||
a.run().then(() => {
|
||||
this.editor!.updateOptions({ readOnly: true });
|
||||
});
|
||||
}, 200);*/
|
||||
|
||||
if (this.model) {
|
||||
this.model.dispose();
|
||||
}
|
||||
this.model = model;
|
||||
}
|
||||
});
|
||||
|
||||
private readonly setEditorDiv = (editorDiv: HTMLDivElement) => {
|
||||
if (!editorDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor = monaco.editor.create(editorDiv, {
|
||||
model: null,
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
Visualizer,
|
||||
VisualizationCollector,
|
||||
asVisualizationId,
|
||||
} from "../../Visualizer";
|
||||
import {
|
||||
ExtractedData,
|
||||
isCommonDataType,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import * as React from "react";
|
||||
|
||||
export class TextVisualizer extends Visualizer {
|
||||
visualize(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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user