initial commit
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import * as React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun, trace, computed } from "mobx";
|
||||
import * as monacoTypes from "monaco-editor";
|
||||
import { getLanguageId } from "../monaco-text-visualizer/MonacoEditor";
|
||||
import {
|
||||
TreeWithPathView,
|
||||
TreeViewModel,
|
||||
TreeNodeViewModel,
|
||||
} from "../tree-visualizer/Views";
|
||||
import { sAstTree } from ".";
|
||||
import Measure from "react-measure";
|
||||
import { MonacoEditor, getLoadedMonaco } from "@hediet/monaco-editor-react";
|
||||
import { Theme } from "@hediet/visualization-core";
|
||||
|
||||
export interface NodeInfo {
|
||||
start: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AstTree extends React.Component<{
|
||||
model: TreeViewModel<NodeInfo>;
|
||||
data: typeof sAstTree.T;
|
||||
nodeInfoToRange: (info: NodeInfo) => monacoTypes.IRange;
|
||||
posToIndex: (pos: monacoTypes.IPosition) => number;
|
||||
theme: Theme;
|
||||
}> {
|
||||
private readonly editorRef = React.createRef<SourceCodeView>();
|
||||
|
||||
render() {
|
||||
const { model, data, theme } = this.props;
|
||||
|
||||
let languageId = "text";
|
||||
if (data.fileName) {
|
||||
languageId = getLanguageId(data.fileName);
|
||||
}
|
||||
return (
|
||||
<Measure client>
|
||||
{({ measureRef, contentRect }) => {
|
||||
return (
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="component-AstTree"
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection:
|
||||
contentRect.client &&
|
||||
contentRect.client.width > 1100
|
||||
? "row"
|
||||
: "column",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="part-tree2"
|
||||
style={{ flex: 1, minHeight: 0, minWidth: 0 }}
|
||||
>
|
||||
<TreeWithPathView model={model} />
|
||||
</div>
|
||||
<div
|
||||
className="part-editor"
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<SourceCodeView
|
||||
theme={theme}
|
||||
ref={this.editorRef}
|
||||
nodeInfoToRange={this.props.nodeInfoToRange}
|
||||
posToIndex={this.props.posToIndex}
|
||||
model={model}
|
||||
languageId={languageId}
|
||||
text={data.text}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class SourceCodeView extends React.Component<{
|
||||
text: string;
|
||||
languageId: string;
|
||||
model: TreeViewModel<NodeInfo>;
|
||||
theme: Theme;
|
||||
nodeInfoToRange: (info: NodeInfo) => monacoTypes.IRange;
|
||||
posToIndex: (pos: monacoTypes.IPosition) => number;
|
||||
}> {
|
||||
@observable private editor:
|
||||
| monacoTypes.editor.IStandaloneCodeEditor
|
||||
| undefined;
|
||||
|
||||
private markedDecorations: string[] = [];
|
||||
private selectedDecorations: string[] = [];
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
layout() {
|
||||
if (this.editor) {
|
||||
this.editor.layout();
|
||||
}
|
||||
}
|
||||
|
||||
@computed.struct
|
||||
private get markedRanges(): monacoTypes.IRange[] {
|
||||
if (!this.editor || !this.model) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const editorModel = this.model;
|
||||
const ranges = this.props.model.marked.map(s =>
|
||||
this.props.nodeInfoToRange(s.data)
|
||||
);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateMarkedDecorations = autorun(
|
||||
() => {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
const ranges = this.markedRanges;
|
||||
this.markedDecorations = this.editor.deltaDecorations(
|
||||
this.markedDecorations,
|
||||
ranges.map(range => ({
|
||||
range,
|
||||
options: { className: "marked" },
|
||||
}))
|
||||
);
|
||||
if (ranges.length > 0) {
|
||||
this.editor.revealRange(
|
||||
ranges[0],
|
||||
getLoadedMonaco().editor.ScrollType.Smooth
|
||||
);
|
||||
}
|
||||
},
|
||||
{ name: "updateMarkedDecorations" }
|
||||
);
|
||||
|
||||
@computed.struct
|
||||
private get selected(): monacoTypes.IRange | undefined {
|
||||
if (this.editor && this.model) {
|
||||
const selected = this.props.model.selected;
|
||||
if (selected) {
|
||||
const range = this.props.nodeInfoToRange(selected.data);
|
||||
return range;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateSelectedDecoration = autorun(
|
||||
() => {
|
||||
console.log("_updateSelectedDecoration");
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
const range = this.selected;
|
||||
if (range) {
|
||||
this.selectedDecorations = this.editor.deltaDecorations(
|
||||
this.selectedDecorations,
|
||||
[
|
||||
{
|
||||
range,
|
||||
options: { className: "selected" },
|
||||
},
|
||||
]
|
||||
);
|
||||
this.editor.revealRange(
|
||||
range,
|
||||
getLoadedMonaco().editor.ScrollType.Smooth
|
||||
);
|
||||
} else {
|
||||
this.selectedDecorations = this.editor.deltaDecorations(
|
||||
this.selectedDecorations,
|
||||
[]
|
||||
);
|
||||
}
|
||||
},
|
||||
{ name: "updateDecorations" }
|
||||
);
|
||||
|
||||
@computed.struct
|
||||
private get modelData(): { text: string; languageId: string } {
|
||||
const { text, languageId } = this.props;
|
||||
return { text, languageId };
|
||||
}
|
||||
|
||||
private lastModel: monacoTypes.editor.ITextModel | undefined;
|
||||
@computed get model(): monacoTypes.editor.ITextModel {
|
||||
const last = this.lastModel;
|
||||
this.lastModel = getLoadedMonaco().editor.createModel(
|
||||
this.modelData.text,
|
||||
this.modelData.languageId,
|
||||
undefined
|
||||
);
|
||||
if (last) {
|
||||
setTimeout(() => {
|
||||
// we don't want to dispose the current model
|
||||
last.dispose();
|
||||
});
|
||||
}
|
||||
return this.lastModel;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MonacoEditor
|
||||
model={this.model}
|
||||
theme={this.props.theme.kind === "dark" ? "vs-dark" : "vs"}
|
||||
onEditorLoaded={e => {
|
||||
this.editor = e;
|
||||
e.onDidChangeCursorSelection(e => {
|
||||
const selectionStart = this.props.posToIndex(
|
||||
e.selection.getStartPosition()
|
||||
);
|
||||
const selectionEnd = this.props.posToIndex(
|
||||
e.selection.getEndPosition()
|
||||
);
|
||||
|
||||
const findNode = (
|
||||
m: TreeNodeViewModel<NodeInfo>
|
||||
): TreeNodeViewModel<NodeInfo> | undefined => {
|
||||
const nodeStart = m.data.start;
|
||||
const nodeEnd = nodeStart + m.data.length;
|
||||
|
||||
if (
|
||||
!(
|
||||
nodeStart <= selectionStart &&
|
||||
selectionEnd <= nodeEnd
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const c of m.children) {
|
||||
const r = findNode(c);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
if (this.props.model.root) {
|
||||
const n = findNode(this.props.model.root);
|
||||
if (n) {
|
||||
this.props.model.select(n);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
readOnly={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
126
visualization-bundle/src/visualizers/ast-visualizer/index.tsx
Normal file
126
visualization-bundle/src/visualizers/ast-visualizer/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Serializer,
|
||||
sLazy,
|
||||
sOpenObject,
|
||||
sArrayOf,
|
||||
sString,
|
||||
sOptionalProp,
|
||||
sUnion,
|
||||
sLiteral,
|
||||
sBoolean,
|
||||
sProp,
|
||||
sNumber,
|
||||
} from "@hediet/semantic-json";
|
||||
import { visualizationNs } from "../../consts";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { createTreeViewModelFromTreeNodeData } from "../tree-visualizer";
|
||||
import { AstTree, NodeInfo } from "./AstVisualizer";
|
||||
import React from "react";
|
||||
import LineColumn from "line-column";
|
||||
import * as monacoTypes from "monaco-editor";
|
||||
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
|
||||
|
||||
interface AstTreeNode {
|
||||
children: AstTreeNode[];
|
||||
items: Item[];
|
||||
segment?: string; // like "root", ".name" or "[0]"
|
||||
isMarked?: boolean;
|
||||
span: NodeInfo;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
text: string;
|
||||
emphasis?: "style1" | "style2" | "style3" | string;
|
||||
}
|
||||
|
||||
export const sAstTreeNode: Serializer<AstTreeNode> = sLazy(() =>
|
||||
sOpenObject({
|
||||
children: sArrayOf(sAstTreeNode),
|
||||
items: sArrayOf(
|
||||
sOpenObject({
|
||||
text: sString(),
|
||||
emphasis: sOptionalProp(
|
||||
sUnion(
|
||||
[
|
||||
sLiteral("style1"),
|
||||
sLiteral("style2"),
|
||||
sLiteral("style3"),
|
||||
sString(),
|
||||
],
|
||||
{ inclusive: true }
|
||||
)
|
||||
),
|
||||
}).defineAs(visualizationNs("TreeNodeItem"))
|
||||
),
|
||||
segment: sOptionalProp(sString()),
|
||||
isMarked: sOptionalProp(sBoolean()),
|
||||
span: sOpenObject({
|
||||
start: sNumber(),
|
||||
length: sNumber(),
|
||||
}),
|
||||
}).defineAs(visualizationNs("TreeNode"))
|
||||
);
|
||||
|
||||
export const sAstTree = sOpenObject({
|
||||
kind: sOpenObject({
|
||||
tree: sLiteral(true),
|
||||
text: sLiteral(true),
|
||||
}),
|
||||
root: sAstTreeNode,
|
||||
text: sString(),
|
||||
fileName: sOptionalProp(sString()),
|
||||
});
|
||||
|
||||
export const astVisualizer = createVisualizer({
|
||||
id: "ast",
|
||||
name: "AST",
|
||||
serializer: sAstTree,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1500 }, theme => {
|
||||
const m = createTreeViewModelFromTreeNodeData(
|
||||
data.root,
|
||||
node => node.span
|
||||
);
|
||||
const l = LineColumn(data.text);
|
||||
function translatePosition(pos: number): monacoTypes.IPosition {
|
||||
let r = l.fromIndex(pos);
|
||||
if (!r) {
|
||||
r = l.fromIndex(data.text.length - 1);
|
||||
r.col++;
|
||||
}
|
||||
return {
|
||||
column: r.col,
|
||||
lineNumber: r.line,
|
||||
};
|
||||
}
|
||||
const nodeInfoToRange = (info: NodeInfo): monacoTypes.IRange => {
|
||||
const start = translatePosition(info.start);
|
||||
const end = translatePosition(info.start + info.length);
|
||||
const range = getLoadedMonaco().Range.fromPositions(start, end);
|
||||
return range;
|
||||
};
|
||||
function posToIndex(pos: monacoTypes.IPosition): number {
|
||||
const i = l.toIndex(pos.lineNumber, pos.column);
|
||||
if (i == -1) {
|
||||
return data.text.length;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
return (
|
||||
<AstTree
|
||||
model={m}
|
||||
theme={theme}
|
||||
data={data}
|
||||
nodeInfoToRange={nodeInfoToRange}
|
||||
posToIndex={posToIndex}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(astVisualizer);
|
||||
@@ -0,0 +1,23 @@
|
||||
.theme-dark {
|
||||
--visualizer-ast-marked-background: rgba(234, 92, 0, 0.33);
|
||||
--visualizer-ast-selected-background: #515c6a;
|
||||
}
|
||||
.theme-light {
|
||||
--visualizer-ast-marked-background: orange;
|
||||
--visualizer-ast-selected-background: yellow;
|
||||
}
|
||||
|
||||
.component-AstTree {
|
||||
.part-editor {
|
||||
//border-top: solid gray 1px;
|
||||
|
||||
.marked {
|
||||
background: var(--visualizer-ast-marked-background);
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: var(--visualizer-ast-selected-background);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
visualization-bundle/src/visualizers/ast-visualizer/types.d.ts
vendored
Normal file
13
visualization-bundle/src/visualizers/ast-visualizer/types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
declare module "line-column" {
|
||||
export = function(
|
||||
text: string
|
||||
): {
|
||||
fromIndex(
|
||||
idx: number
|
||||
): {
|
||||
line: number;
|
||||
col: number;
|
||||
};
|
||||
toIndex(line: number, column: number): number;
|
||||
} {};
|
||||
}
|
||||
Reference in New Issue
Block a user