diff --git a/data-extraction/src/CommonDataTypes.ts b/data-extraction/src/CommonDataTypes.ts index e57ac05..386fcb6 100644 --- a/data-extraction/src/CommonDataTypes.ts +++ b/data-extraction/src/CommonDataTypes.ts @@ -52,17 +52,18 @@ export module CommonDataTypes { text: string; } - export interface TreeNodeData { + export interface TreeNodeData { kind: { tree: true }; - root: TreeNode<{}>; + root: TreeNode; } - 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; - }>; } } diff --git a/example-project/src/index.ts b/example-project/src/index.ts index 5c56566..ecc00ac 100644 --- a/example-project/src/index.ts +++ b/example-project/src/index.ts @@ -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 diff --git a/extension/README.md b/extension/README.md index 7d07361..7725a8c 100644 --- a/extension/README.md +++ b/extension/README.md @@ -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) diff --git a/extension/ui/src/Visualizers/AstViewer.tsx b/extension/ui/src/Visualizers/AstViewer.tsx new file mode 100644 index 0000000..c40af55 --- /dev/null +++ b/extension/ui/src/Visualizers/AstViewer.tsx @@ -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 ; + }, + }); + } + } +} + +interface NodeInfo { + position: number; + length: number; +} + +@observer +export class AstTree extends React.Component<{ + model: TreeViewModel; + 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 ( +
+
+ +
+
+ +
+
+ ); + } +} + +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; +}> { + @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 ( +
+
+
+ ); + } +} diff --git a/extension/ui/src/Visualizers/MonacoTextVisualizer.tsx b/extension/ui/src/Visualizers/MonacoTextVisualizer.tsx index 1da8ff1..19001cb 100644 --- a/extension/ui/src/Visualizers/MonacoTextVisualizer.tsx +++ b/extension/ui/src/Visualizers/MonacoTextVisualizer.tsx @@ -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) { diff --git a/extension/ui/src/Visualizers/TreeVisualizer/Views.tsx b/extension/ui/src/Visualizers/TreeVisualizer/Views.tsx index d2b6671..fa507e1 100644 --- a/extension/ui/src/Visualizers/TreeVisualizer/Views.tsx +++ b/extension/ui/src/Visualizers/TreeVisualizer/Views.tsx @@ -6,7 +6,7 @@ import { Rectangle, Point } from "../../Components/Point"; import { SvgLine } from "../../Components/SvgElements"; import "./style.scss"; -export class TreeViewModel { +export class TreeViewModel { 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 | 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[] { + const result = new Array>(); + function recurse(node: TreeNodeViewModel) { + 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 | undefined = undefined; + + @action public select(current: TreeNodeViewModel | 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) { + if (this.selected === current) { + this.select(undefined); + } else { + this.select(current); + } + } - @action public hover(current: TreeNodeViewModel | undefined) { + private selectOnHoverEnabled = false; + private selectedBeforeHover: + | TreeNodeViewModel + | undefined = undefined; + + @action public hover(current: TreeNodeViewModel | 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 { + public parent: TreeNodeViewModel | null = null; constructor( - public readonly treeViewModel: TreeViewModel, + public readonly treeViewModel: TreeViewModel, 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[], + 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) { diff --git a/extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx b/extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx index cc98ce2..faa907d 100644 --- a/extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx +++ b/extension/ui/src/Visualizers/TreeVisualizer/Visualizer.tsx @@ -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( + root: TreeNode +): TreeViewModel { + const m = new TreeViewModel(); 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, + viewModel: TreeViewModel + ): TreeNodeViewModel { + const children: TreeNodeViewModel[] = node.children.map(c => recurse(c, viewModel) ); - const model = new TreeNodeViewModel( + const model = new TreeNodeViewModel( viewModel, node.id, node.name, node.value, node.emphasizedValue, - children + children, + node.data ); model.isMarked = node.isMarked; for (const c of children) { diff --git a/extension/ui/src/Visualizers/index.ts b/extension/ui/src/Visualizers/index.ts index a53380a..eb125e4 100644 --- a/extension/ui/src/Visualizers/index.ts +++ b/extension/ui/src/Visualizers/index.ts @@ -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(), ]); diff --git a/extension/ui/src/Visualizers/style.scss b/extension/ui/src/Visualizers/style.scss index 8beb580..b231076 100644 --- a/extension/ui/src/Visualizers/style.scss +++ b/extension/ui/src/Visualizers/style.scss @@ -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; + }*/ +}