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

@@ -52,17 +52,18 @@ export module CommonDataTypes {
text: string;
}
export interface TreeNodeData {
export interface TreeNodeData<TData = unknown> {
kind: { tree: true };
root: TreeNode<{}>;
root: TreeNode<TData>;
}
export interface AstData extends TreeNodeData, Text {
export interface AstData
extends TreeNodeData<{
position: number;
length: number;
}>,
Text {
kind: { text: true; tree: true; ast: true };
root: TreeNode<{
position: number;
length: number;
}>;
}
}

View File

@@ -27,6 +27,58 @@ class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
class Test1 {
}
class Test2 {
}
`,
ts.ScriptTarget.Latest,
true

View File

@@ -4,7 +4,8 @@
Provides infrastructure to visualize values while debugging.
This extension is still in development. Also, for the best user experience some VS Code APIs are missing.
This extension is still in development. Also, for the best user experience some VS Code APIs are missing -
[please give your thumbs up here](https://github.com/microsoft/vscode/issues/78663) so that these APIs might be provided at some time.
![](docs/demo.gif)

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;
}*/
}