Adds code view to AST tree view.
This commit is contained in:
@@ -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;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
210
extension/ui/src/Visualizers/AstViewer.tsx
Normal file
210
extension/ui/src/Visualizers/AstViewer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
}*/
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user