Adds code view to AST tree view.

This commit is contained in:
Henning Dieterichs
2019-09-06 16:28:08 +02:00
parent 611dcbe80d
commit d9f77bb52c
9 changed files with 369 additions and 32 deletions

View File

@@ -0,0 +1,210 @@
import {
VisualizationProvider,
VisualizationCollector,
asVisualizationId,
} from "./Visualizer";
import {
ExtractedData,
isCommonDataType,
CommonDataTypes,
} from "@hediet/debug-visualizer-data-extraction";
import { createTreeViewModelFromTreeNodeData } from "./TreeVisualizer/Visualizer";
import {
TreeWithPathView,
TreeViewModel,
TreeView,
} from "./TreeVisualizer/Views";
import React = require("react");
import { observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun, trace } from "mobx";
import * as monaco from "monaco-editor";
import { getLanguageId } from "./MonacoTextVisualizer";
export class AstVisualizer extends VisualizationProvider {
getVisualizations(
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);
return <AstTree model={m} data={data} />;
},
});
}
}
}
interface NodeInfo {
position: number;
length: number;
}
@observer
export class AstTree extends React.Component<{
model: TreeViewModel<NodeInfo>;
data: CommonDataTypes.AstData;
}> {
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
model={model}
languageId={languageId}
text={data.text}
/>
</div>
</div>
);
}
}
function nodeInfoToRange(
info: NodeInfo,
model: monaco.editor.ITextModel
): monaco.IRange {
const start = model.getPositionAt(info.position);
const end = model.getPositionAt(info.position + info.length);
const range = monaco.Range.fromPositions(start, end);
return range;
}
@observer
export class MonacoEditor extends React.Component<{
text: string;
languageId: string;
model: TreeViewModel<NodeInfo>;
}> {
@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 =>
nodeInfoToRange(s.data, editorModel)
);
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 = nodeInfoToRange(selected.data, this.model);
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>
);
}
}

View File

@@ -35,7 +35,7 @@ export class MonacoTextVisualizer extends VisualizationProvider {
}
}
function getLanguageId(fileName: string): string {
export function getLanguageId(fileName: string): string {
const l = monaco.languages.getLanguages();
const result = l.find(l => {
if (l.filenamePatterns) {

View File

@@ -6,7 +6,7 @@ import { Rectangle, Point } from "../../Components/Point";
import { SvgLine } from "../../Components/SvgElements";
import "./style.scss";
export class TreeViewModel {
export class TreeViewModel<TData = unknown> {
private started = true;
start() {
this.started = true;
@@ -19,7 +19,7 @@ export class TreeViewModel {
private version = 0;
private lastUpdateVersion = -1;
@observable public root: TreeNodeViewModel | undefined = undefined;
@observable public root: TreeNodeViewModel<TData> | undefined = undefined;
public invalidateOffsets() {
this.version++;
@@ -42,9 +42,25 @@ export class TreeViewModel {
this.root.updateOffsets();
}
@observable selected: TreeNodeViewModel | undefined = undefined;
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;
}
@action public select(current: TreeNodeViewModel | undefined) {
@observable selected: TreeNodeViewModel<TData> | undefined = undefined;
@action public select(current: TreeNodeViewModel<TData> | undefined) {
if (this.selected) {
this.selected.isSelected = false;
}
@@ -54,10 +70,20 @@ export class TreeViewModel {
this.selected = current;
}
private selectOnHoverEnabled = false;
private selectedBeforeHover: TreeNodeViewModel | undefined = undefined;
@action public toggleSelect(current: TreeNodeViewModel<TData>) {
if (this.selected === current) {
this.select(undefined);
} else {
this.select(current);
}
}
@action public hover(current: TreeNodeViewModel | undefined) {
private selectOnHoverEnabled = false;
private selectedBeforeHover:
| TreeNodeViewModel<TData>
| undefined = undefined;
@action public hover(current: TreeNodeViewModel<TData> | undefined) {
if (this.selectOnHoverEnabled) {
if (current) {
this.select(current);
@@ -84,16 +110,17 @@ export class TreeViewModel {
let i = 0;
export class TreeNodeViewModel {
public parent: TreeNodeViewModel | null = null;
export class TreeNodeViewModel<TData = unknown> {
public parent: TreeNodeViewModel<TData> | null = null;
constructor(
public readonly treeViewModel: TreeViewModel,
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[]
public readonly children: TreeNodeViewModel<TData>[],
public readonly data: TData
) {}
@observable expanderOffsetRect: Rectangle | undefined = undefined;
@@ -393,7 +420,7 @@ export class TreeNodeView extends React.Component<{
}> {
@action.bound
private clickHandler() {
this.props.model.treeViewModel.select(this.props.model);
this.props.model.treeViewModel.toggleSelect(this.props.model);
}
private mouseEnterOrLeaveHandler(enter: boolean) {

View File

@@ -8,36 +8,37 @@ import {
CommonDataTypes,
ExtractedData,
isCommonDataType,
TreeNode,
} 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();
export function createTreeViewModelFromTreeNodeData<TData>(
root: TreeNode<TData>
): TreeViewModel<TData> {
const m = new TreeViewModel<TData>();
m.root = recurse(root, m);
return m;
function recurse(
node: CommonDataTypes.TreeNodeData["root"],
viewModel: TreeViewModel
): TreeNodeViewModel {
const children: TreeNodeViewModel[] = node.children.map(c =>
node: TreeNode<TData>,
viewModel: TreeViewModel<TData>
): TreeNodeViewModel<TData> {
const children: TreeNodeViewModel<TData>[] = node.children.map(c =>
recurse(c, viewModel)
);
const model = new TreeNodeViewModel(
const model = new TreeNodeViewModel<TData>(
viewModel,
node.id,
node.name,
node.value,
node.emphasizedValue,
children
children,
node.data
);
model.isMarked = node.isMarked;
for (const c of children) {

View File

@@ -6,6 +6,7 @@ import { SvgVisualizer } from "./SvgVisualizer";
import { DotVisualizer } from "./DotVisualizer";
import { TextVisualizer } from "./TextVisualizer";
import { MonacoTextVisualizer } from "./MonacoTextVisualizer";
import { AstVisualizer } from "./AstViewer";
export const knownVisualizations = new ComposedVisualizationProvider([
new TreeVisualizer(),
@@ -14,4 +15,5 @@ export const knownVisualizations = new ComposedVisualizationProvider([
new DotVisualizer(),
new TextVisualizer(),
new MonacoTextVisualizer(),
new AstVisualizer(),
]);

View File

@@ -14,3 +14,46 @@
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;
}*/
}