Some progress.
This commit is contained in:
@@ -24,11 +24,11 @@ export type CommonDataType =
|
||||
| CommonDataTypes.GraphData;
|
||||
|
||||
export module CommonDataTypes {
|
||||
export interface Text extends ExtractedData {
|
||||
export interface Text {
|
||||
kind: { text: true };
|
||||
text: string;
|
||||
// what should be used here? File type (.ts, .txt) or mime type?
|
||||
fileType?: string;
|
||||
mimeType?: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
@@ -52,7 +52,7 @@ export module CommonDataTypes {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface TreeNodeData extends ExtractedData {
|
||||
export interface TreeNodeData {
|
||||
kind: { tree: true };
|
||||
root: TreeNode<{}>;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export function selfContainedInitDataExtractorApi(): boolean {
|
||||
.filter(([key, value]) => key.startsWith(prefix))
|
||||
.map(([key, value]) => value as DataExtractor<ExtractedData>);
|
||||
}
|
||||
obj["$asData"] = (x: unknown) => x;
|
||||
obj[key] = api = {
|
||||
registerExtractor(extractor) {
|
||||
obj[prefix + extractor.id] = extractor;
|
||||
|
||||
@@ -18,7 +18,7 @@ export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
|
||||
extractionCollector.addExtraction({
|
||||
id: "as-is",
|
||||
name: "As Is",
|
||||
priority: 100,
|
||||
priority: 500,
|
||||
extractData() {
|
||||
return obj;
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as ts from "typescript";
|
||||
import { DataExtractor, ExtractionCollector } from "../DataExtractor";
|
||||
import { CommonDataTypes } from "../../CommonDataTypes";
|
||||
|
||||
// This class is self contained and can be injected into both nodejs and browser environments.
|
||||
export class TypeScriptAstDataExtractor
|
||||
implements DataExtractor<CommonDataTypes.AstData> {
|
||||
readonly id = "TypeScriptAst";
|
||||
@@ -83,14 +84,12 @@ export class TypeScriptAstDataExtractor
|
||||
id: memberName,
|
||||
children: children,
|
||||
data: {
|
||||
length: 0,
|
||||
position: 0,
|
||||
length: node.end - node.pos,
|
||||
position: node.pos,
|
||||
},
|
||||
emphasizedValue: emphasizedValueFn(node),
|
||||
isMarked: marked.has(node),
|
||||
value,
|
||||
// startPos: node.pos,
|
||||
// endPos: node.end
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,7 +138,7 @@ export class TypeScriptAstDataExtractor
|
||||
kind: { text: true, tree: true, ast: true },
|
||||
root: toTreeNode(root, "root", marked, fn),
|
||||
text: root.text,
|
||||
fileType: "ts",
|
||||
fileName: "index.ts",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,18 +10,20 @@ import {
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
enableHotReload();
|
||||
import * as ts from "typescript";
|
||||
import { liveLogId, liveLog } from "@hediet/live-debug";
|
||||
//import { liveLogId, liveLog } from "@hediet/live-debug";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
registerAll();
|
||||
|
||||
/*
|
||||
let i = 0;
|
||||
setInterval(() => {
|
||||
i++;
|
||||
liveLog("bla" + i * 2);
|
||||
liveLog("test" + i);
|
||||
}, 1000);
|
||||
*/
|
||||
|
||||
@hotClass(module)
|
||||
class Main {
|
||||
@@ -29,18 +31,10 @@ class Main {
|
||||
const sf = ts.createSourceFile(
|
||||
"test",
|
||||
`
|
||||
class Test {
|
||||
public test() {
|
||||
console.log("aaa");
|
||||
const x = e;
|
||||
}
|
||||
}
|
||||
class Test {
|
||||
public test() {
|
||||
console.log("aaa");
|
||||
const x = e;
|
||||
}
|
||||
class uiae {
|
||||
|
||||
}
|
||||
|
||||
`,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
|
||||
@@ -44,16 +44,24 @@ export class ConnectionHandler {
|
||||
},
|
||||
openInBrowser: async ({}) => {
|
||||
try {
|
||||
await launchChrome(server.indexUrl);
|
||||
} catch (e) {
|
||||
//await launchChrome(server.indexUrl);
|
||||
open(server.indexUrl);
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
setPreferredDataExtractor: async ({ dataExtractorId }) => {
|
||||
if (this.watcher) {
|
||||
this.watcher.setPreferredDataExtractor(dataExtractorId);
|
||||
}
|
||||
},
|
||||
getCompletions: async ({ text, column }) => {
|
||||
const completions = await sources.jsSource.getCompletions(
|
||||
text,
|
||||
column
|
||||
);
|
||||
return {
|
||||
completions,
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DataExtractionState } from "@hediet/debug-visualizer-vscode-shared";
|
||||
import {
|
||||
DataExtractionState,
|
||||
CompletionItem,
|
||||
} from "@hediet/debug-visualizer-vscode-shared";
|
||||
|
||||
export interface EvaluationWatcherOptions {
|
||||
preferredDataExtractor?: DataExtractorId | undefined;
|
||||
@@ -10,6 +13,7 @@ export interface DataSource {
|
||||
expression: string,
|
||||
options: EvaluationWatcherOptions
|
||||
): EvaluationWatcher;
|
||||
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
|
||||
}
|
||||
|
||||
export interface EvaluationWatcher {
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
ApiHasNotBeenInitializedCode,
|
||||
selfContainedInitDataExtractorApi,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DataExtractionState } from "@hediet/debug-visualizer-vscode-shared";
|
||||
import {
|
||||
DataExtractionState,
|
||||
CompletionItem,
|
||||
} from "@hediet/debug-visualizer-vscode-shared";
|
||||
|
||||
export function createJsDebuggerSource(): JsDataSource {
|
||||
return new JsDebuggerSourceImplementation();
|
||||
@@ -50,6 +53,28 @@ class JsDebuggerSourceImplementation implements JsDataSource {
|
||||
|
||||
registerDataExtractor(classExpression: JsCode): void {}
|
||||
|
||||
async getCompletions(
|
||||
text: string,
|
||||
column: number
|
||||
): Promise<CompletionItem[]> {
|
||||
const session = vscode.debug.activeDebugSession;
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const reply = await session.customRequest("completions", {
|
||||
text,
|
||||
frameId: this.lastFrameId,
|
||||
column,
|
||||
});
|
||||
return reply.targets;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public createEvaluationWatcher(
|
||||
expression: string,
|
||||
options: EvaluationWatcherOptions
|
||||
@@ -79,10 +104,10 @@ class JsDebuggerSourceImplementation implements JsDataSource {
|
||||
? `"${w.preferredDataExtractor}"`
|
||||
: "undefined";
|
||||
|
||||
const expression = `(${fnSrc})().getData(${
|
||||
w.expression
|
||||
}, expr => eval(expr), ${preferredExtractor})`;
|
||||
// uia
|
||||
const expression =
|
||||
`(${fnSrc})().getData(${w.expression},` +
|
||||
`expr => eval(expr), ${preferredExtractor})`;
|
||||
|
||||
const reply = await session.customRequest("evaluate", {
|
||||
expression,
|
||||
frameId: this.lastFrameId,
|
||||
|
||||
84
extension/ui/src/Components/ExpressionInput.tsx
Normal file
84
extension/ui/src/Components/ExpressionInput.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React = require("react");
|
||||
import { Model } from "../Model/Model";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, action, autorun } from "mobx";
|
||||
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
export function getModel(): monaco.editor.ITextModel {
|
||||
return monaco.editor.createModel(
|
||||
"",
|
||||
"javascript",
|
||||
monaco.Uri.parse(`file:///main.ts`)
|
||||
);
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ExpressionInput extends React.Component<{ model: Model }> {
|
||||
@action.bound
|
||||
submit() {
|
||||
const val = this.model.getValue();
|
||||
console.log(val);
|
||||
this.props.model.setExpression(val);
|
||||
}
|
||||
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
|
||||
private model = getModel();
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.dispose();
|
||||
}
|
||||
this.model.dispose();
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateText = autorun(() => {
|
||||
const val = this.model.getValue();
|
||||
const newVal = this.props.model.expression;
|
||||
if (val !== newVal) {
|
||||
this.model.setValue(newVal);
|
||||
}
|
||||
});
|
||||
|
||||
private readonly setEditorDiv = (editorDiv: HTMLDivElement) => {
|
||||
if (!editorDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor = monaco.editor.create(editorDiv, {
|
||||
model: this.model,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: { enabled: false },
|
||||
fixedOverflowWidgets: true,
|
||||
lineNumbers: "off",
|
||||
glyphMargin: false,
|
||||
folding: false,
|
||||
lineDecorationsWidth: 0,
|
||||
lineNumbersMinChars: 0,
|
||||
automaticLayout: true,
|
||||
scrollbar: {
|
||||
horizontal: "hidden",
|
||||
vertical: "hidden",
|
||||
horizontalScrollbarSize: 0,
|
||||
verticalScrollbarSize: 0,
|
||||
},
|
||||
});
|
||||
|
||||
this.editor.onKeyDown(e => {
|
||||
if (e.keyCode == monaco.KeyCode.Enter) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="component-monaco-editor2">
|
||||
<div className="part-editor" ref={this.setEditorDiv} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
47
extension/ui/src/Components/NoData.tsx
Normal file
47
extension/ui/src/Components/NoData.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React = require("react");
|
||||
import { observer } from "mobx-react";
|
||||
import { observable } from "mobx";
|
||||
import Measure from "react-measure";
|
||||
|
||||
@observer
|
||||
export class NoData extends React.Component<{ label: string }> {
|
||||
@observable height = 0;
|
||||
@observable width = 0;
|
||||
|
||||
render() {
|
||||
const { width, height } = this;
|
||||
return (
|
||||
<Measure
|
||||
client={true}
|
||||
onResize={e => {
|
||||
if (e.client) {
|
||||
this.height = e.client.height;
|
||||
this.width = e.client.width;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ measureRef }) => (
|
||||
<div className="component-NoData" ref={measureRef}>
|
||||
<svg>
|
||||
<line x1={0} y1={0} x2={width} y2={height} />
|
||||
<line x1={width} y1={0} x2={0} y2={height} />
|
||||
<rect
|
||||
x={width / 2 - 50}
|
||||
y={height / 2 - 20}
|
||||
width={100}
|
||||
height={40}
|
||||
/>
|
||||
<text
|
||||
x={width / 2}
|
||||
y={height / 2}
|
||||
textAnchor="middle"
|
||||
alignmentBaseline="central"
|
||||
children={this.props.label}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -88,15 +88,3 @@ export function SvgRect(
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
export function Svg(props: { position: Point } & SvgAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
x={props.position.x}
|
||||
y={props.position.y}
|
||||
{...omit(props, ["position"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
36
extension/ui/src/Components/Visualizer.tsx
Normal file
36
extension/ui/src/Components/Visualizer.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React = require("react");
|
||||
import { Model } from "../Model/Model";
|
||||
import { observer } from "mobx-react";
|
||||
import { NoData } from "./NoData";
|
||||
|
||||
@observer
|
||||
export class Visualizer extends React.Component<{ model: Model }> {
|
||||
render() {
|
||||
return (
|
||||
<div className="component-Visualizer">{this.renderContent()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent(): JSX.Element {
|
||||
const s = this.props.model.state;
|
||||
if (s.kind === "loading") {
|
||||
return <NoData label="Loading" />;
|
||||
} else if (s.kind === "error") {
|
||||
return <NoData label={"Error: " + s.message} />;
|
||||
} else if (s.kind === "noExpression") {
|
||||
return <NoData label="No Expression" />;
|
||||
} else if (s.kind === "noDebugSession") {
|
||||
return <NoData label="No Debug Session" />;
|
||||
} else if (s.kind === "data") {
|
||||
const vis = this.props.model.visualizations;
|
||||
if (!vis || !vis.visualization) {
|
||||
return <NoData label="No Visualization" />;
|
||||
}
|
||||
|
||||
return vis.visualization.render();
|
||||
} else {
|
||||
const nvr: never = s;
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import React = require("react");
|
||||
import { Model } from "../Model";
|
||||
import { Model } from "../Model/Model";
|
||||
import { observer } from "mobx-react";
|
||||
import { observable, action, computed } from "mobx";
|
||||
import { Popover, Icon, Button, Spinner, InputGroup } from "@blueprintjs/core";
|
||||
import { observable, computed } from "mobx";
|
||||
import { Popover, Button, Spinner } from "@blueprintjs/core";
|
||||
import classnames = require("classnames");
|
||||
import Measure from "react-measure";
|
||||
import { DataExtractorInfo } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { knownVisualizations } from "../Visualizers";
|
||||
import { Visualizer } from "./Visualizer";
|
||||
import { ExpressionInput } from "./ExpressionInput";
|
||||
|
||||
@observer
|
||||
export class GUI extends React.Component<{ model: Model }> {
|
||||
@observable text: string = "node";
|
||||
@@ -24,15 +25,23 @@ export class GUI extends React.Component<{ model: Model }> {
|
||||
/>
|
||||
<div className="part-header-content">
|
||||
<div className="part-header-main">
|
||||
<ExpressionInput model={m} />
|
||||
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="refresh"
|
||||
onClick={() => m.refresh()}
|
||||
/>
|
||||
<div className="part-expression-input ">
|
||||
<ExpressionInput model={m} />
|
||||
</div>
|
||||
<div style={{ width: 4 }} />
|
||||
{m.loading ? (
|
||||
<div style={{ padding: "0 4px" }}>
|
||||
<Spinner size={Spinner.SIZE_SMALL} />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
className="part-icon"
|
||||
icon="refresh"
|
||||
onClick={() => m.refresh()}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
minimal
|
||||
small
|
||||
@@ -122,134 +131,6 @@ export class ExpandedOptions extends React.Component<{ model: Model }> {
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ExpressionInput extends React.Component<{ model: Model }> {
|
||||
@observable editedExpression: string | undefined;
|
||||
@computed get currentExpression(): string {
|
||||
return this.editedExpression !== undefined
|
||||
? this.editedExpression
|
||||
: this.props.model.expression;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
updateCurrentExpression(newValue: string) {
|
||||
this.editedExpression = newValue;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
submit() {
|
||||
if (!this.editedExpression) {
|
||||
return;
|
||||
}
|
||||
this.props.model.setExpression(this.editedExpression);
|
||||
this.editedExpression = undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
return (
|
||||
<InputGroup
|
||||
small
|
||||
fill
|
||||
rightElement={
|
||||
model.loading ? (
|
||||
<Spinner size={Icon.SIZE_STANDARD} />
|
||||
) : (
|
||||
undefined
|
||||
)
|
||||
}
|
||||
value={this.currentExpression}
|
||||
onChange={
|
||||
((e: { target: { value: string } }) =>
|
||||
this.updateCurrentExpression(e.target.value)) as any
|
||||
}
|
||||
onBlur={this.submit}
|
||||
onKeyPress={e => {
|
||||
if (e.charCode === 13) {
|
||||
// enter
|
||||
this.submit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Visualizer extends React.Component<{ model: Model }> {
|
||||
render() {
|
||||
return (
|
||||
<div className="component-Visualizer">{this.renderContent()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent(): JSX.Element {
|
||||
const s = this.props.model.state;
|
||||
if (s.kind === "loading") {
|
||||
return <NoData label="Loading" />;
|
||||
} else if (s.kind === "error") {
|
||||
return <NoData label={"Error: " + s.message} />;
|
||||
} else if (s.kind === "noExpression") {
|
||||
return <NoData label="No Expression" />;
|
||||
} else if (s.kind === "noDebugSession") {
|
||||
return <NoData label="No Debug Session" />;
|
||||
} else if (s.kind === "data") {
|
||||
const vis = this.props.model.visualizations;
|
||||
if (!vis || !vis.visualization) {
|
||||
return <NoData label="No Visualization" />;
|
||||
}
|
||||
|
||||
return vis.visualization.render();
|
||||
} else {
|
||||
const nvr: never = s;
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class NoData extends React.Component<{ label: string }> {
|
||||
@observable height = 0;
|
||||
@observable width = 0;
|
||||
|
||||
render() {
|
||||
const { width, height } = this;
|
||||
return (
|
||||
<Measure
|
||||
client={true}
|
||||
onResize={e => {
|
||||
if (e.client) {
|
||||
this.height = e.client.height;
|
||||
this.width = e.client.width;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ measureRef }) => (
|
||||
<div className="component-NoData" ref={measureRef}>
|
||||
<svg>
|
||||
<line x1={0} y1={0} x2={width} y2={height} />
|
||||
<line x1={width} y1={0} x2={0} y2={height} />
|
||||
<rect
|
||||
x={width / 2 - 50}
|
||||
y={height / 2 - 20}
|
||||
width={100}
|
||||
height={40}
|
||||
/>
|
||||
<text
|
||||
x={width / 2}
|
||||
y={height / 2}
|
||||
textAnchor="middle"
|
||||
alignmentBaseline="central"
|
||||
children={this.props.label}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExpandButton extends React.Component<{
|
||||
expanded: boolean;
|
||||
setExpanded: (val: boolean) => void;
|
||||
|
||||
@@ -4,81 +4,17 @@ import {
|
||||
} from "@hediet/debug-visualizer-vscode-shared";
|
||||
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
||||
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
|
||||
import { observable, action, computed } from "mobx";
|
||||
import { Barrier } from "@hediet/std/synchronization";
|
||||
import { observable, action, computed, when } from "mobx";
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { Visualization, VisualizationId } from "./Visualizers/Visualizer";
|
||||
import { knownVisualizations } from "./Visualizers";
|
||||
|
||||
interface VsCodeApi {
|
||||
getState<TState>(): TState | undefined;
|
||||
setState(state: unknown): void;
|
||||
}
|
||||
import { Visualization, VisualizationId } from "../Visualizers/Visualizer";
|
||||
import { knownVisualizations } from "../Visualizers";
|
||||
import { getApi as getVsCodeApi } from "./VsCodeApi";
|
||||
import { MonacoBridge } from "./MonacoBridge";
|
||||
|
||||
declare const window: Window & {
|
||||
serverPort?: number;
|
||||
VsCodeApi?: VsCodeApi;
|
||||
};
|
||||
|
||||
interface VSCodeApi<TState> {
|
||||
getState(): Promise<TState | undefined>;
|
||||
setState(state: TState): Promise<void>;
|
||||
}
|
||||
|
||||
type IncomingCommand = {
|
||||
command: "getStateResult";
|
||||
state: unknown;
|
||||
};
|
||||
|
||||
type OutgoingCommand =
|
||||
| {
|
||||
command: "setState";
|
||||
state: unknown;
|
||||
}
|
||||
| { command: "getState" };
|
||||
|
||||
class IFrameVSCodeApi<TState> implements VSCodeApi<TState> {
|
||||
private currentGetStatePromise: Barrier<TState> | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("message", event => {
|
||||
const data = event.data as IncomingCommand;
|
||||
if (data.command === "getStateResult") {
|
||||
this.currentGetStatePromise!.unlock(data.state as any);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sendCommand(command: OutgoingCommand) {
|
||||
window.parent.postMessage(command, "*");
|
||||
}
|
||||
|
||||
getState(): Promise<TState | undefined> {
|
||||
this.sendCommand({ command: "getState" });
|
||||
if (this.currentGetStatePromise) {
|
||||
throw new Error("Get state already in progress.");
|
||||
}
|
||||
this.currentGetStatePromise = new Barrier();
|
||||
return this.currentGetStatePromise.onUnlocked;
|
||||
}
|
||||
|
||||
async setState(state: TState): Promise<void> {
|
||||
this.sendCommand({ command: "setState", state });
|
||||
}
|
||||
}
|
||||
|
||||
class DirectVSCodeApi<TState> implements VSCodeApi<TState> {
|
||||
constructor(private readonly vsCodeApi: VsCodeApi) {}
|
||||
|
||||
async getState(): Promise<TState | undefined> {
|
||||
return this.vsCodeApi.getState<TState | undefined>();
|
||||
}
|
||||
|
||||
async setState(state: TState): Promise<void> {
|
||||
this.vsCodeApi.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
export class Model {
|
||||
port: number;
|
||||
|
||||
@@ -113,13 +49,12 @@ export class Model {
|
||||
}
|
||||
}
|
||||
|
||||
private server:
|
||||
@observable.ref
|
||||
public server:
|
||||
| typeof debugVisualizerUIContract.TServerInterface
|
||||
| undefined = undefined;
|
||||
|
||||
private readonly vsCodeApi = window.VsCodeApi
|
||||
? new DirectVSCodeApi<{ expression: string }>(window.VsCodeApi)
|
||||
: new IFrameVSCodeApi<{ expression: string }>();
|
||||
private readonly vsCodeApi = getVsCodeApi<{ expression: string }>();
|
||||
|
||||
@observable private _loading = false;
|
||||
|
||||
@@ -127,6 +62,8 @@ export class Model {
|
||||
return this._loading;
|
||||
}
|
||||
|
||||
private readonly bridge = new MonacoBridge(this);
|
||||
|
||||
constructor() {
|
||||
if (window.serverPort) {
|
||||
this.port = window.serverPort;
|
||||
@@ -137,6 +74,11 @@ export class Model {
|
||||
throw new Error("No port given.");
|
||||
}
|
||||
this.port = parseInt(portStr);
|
||||
|
||||
const expr = url.searchParams.get("expression");
|
||||
if (expr) {
|
||||
this.setExpression(expr);
|
||||
}
|
||||
}
|
||||
|
||||
this.stayConnected();
|
||||
@@ -151,13 +93,17 @@ export class Model {
|
||||
@action
|
||||
setExpression(newExpression: string) {
|
||||
this.expression = newExpression;
|
||||
if (this.server) {
|
||||
this.server.setExpression({ newExpression });
|
||||
}
|
||||
when(() => !!this.server).then(() => {
|
||||
this.server!.setExpression({ newExpression });
|
||||
});
|
||||
|
||||
this.vsCodeApi.setState({
|
||||
expression: newExpression,
|
||||
});
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("expression", newExpression);
|
||||
history.replaceState(null, document.title, url.toString());
|
||||
}
|
||||
|
||||
setPreferredExtractorId(id: DataExtractorId) {
|
||||
@@ -187,7 +133,7 @@ export class Model {
|
||||
stream,
|
||||
new ConsoleRpcLogger(),
|
||||
{
|
||||
updateState: ({ newState }) => {
|
||||
updateState: async ({ newState }) => {
|
||||
this._loading = newState.kind === "loading";
|
||||
if (!this._loading) {
|
||||
this.state = newState;
|
||||
104
extension/ui/src/Model/MonacoBridge.ts
Normal file
104
extension/ui/src/Model/MonacoBridge.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as monaco from "monaco-editor";
|
||||
import { Model } from "./Model";
|
||||
|
||||
export class MonacoBridge {
|
||||
constructor(private readonly model: Model) {
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
});
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES5,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const text = require("!!raw-loader!./lib.es5.d.ts.txt")
|
||||
.default as string;
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(text);
|
||||
|
||||
const commonTypes = require("!!raw-loader!@hediet/debug-visualizer-data-extraction/src/CommonDataTypes.ts")
|
||||
.default as string;
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
commonTypes,
|
||||
`file:///node_modules/CommonDataTypes/index.ts`
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
`declare function $asData(data: import("CommonDataTypes").CommonDataType): import("CommonDataTypes").CommonDataType;`,
|
||||
`file:///types.d.ts`
|
||||
);
|
||||
|
||||
monaco.languages.registerCompletionItemProvider("javascript", {
|
||||
triggerCharacters: ["."],
|
||||
provideCompletionItems: async (
|
||||
textModel,
|
||||
position
|
||||
): Promise<monaco.languages.CompletionList> => {
|
||||
const expression = textModel.getValue();
|
||||
|
||||
if (!this.model.server) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
const completions = (await this.model.server.getCompletions({
|
||||
text: expression,
|
||||
column: position.column,
|
||||
})).completions;
|
||||
|
||||
const map = {
|
||||
method: monaco.languages.CompletionItemKind.Method,
|
||||
function: monaco.languages.CompletionItemKind.Function,
|
||||
constructor:
|
||||
monaco.languages.CompletionItemKind.Constructor,
|
||||
field: monaco.languages.CompletionItemKind.Field,
|
||||
variable: monaco.languages.CompletionItemKind.Variable,
|
||||
class: monaco.languages.CompletionItemKind.Class,
|
||||
interface: monaco.languages.CompletionItemKind.Interface,
|
||||
module: monaco.languages.CompletionItemKind.Module,
|
||||
property: monaco.languages.CompletionItemKind.Property,
|
||||
unit: monaco.languages.CompletionItemKind.Unit,
|
||||
value: monaco.languages.CompletionItemKind.Value,
|
||||
enum: monaco.languages.CompletionItemKind.Enum,
|
||||
keyword: monaco.languages.CompletionItemKind.Keyword,
|
||||
snippet: monaco.languages.CompletionItemKind.Snippet,
|
||||
text: monaco.languages.CompletionItemKind.Text,
|
||||
color: monaco.languages.CompletionItemKind.Color,
|
||||
file: monaco.languages.CompletionItemKind.File,
|
||||
reference: monaco.languages.CompletionItemKind.Reference,
|
||||
customcolor:
|
||||
monaco.languages.CompletionItemKind.Customcolor,
|
||||
};
|
||||
|
||||
const p = textModel.getWordAtPosition(position);
|
||||
|
||||
return {
|
||||
suggestions: completions.map<
|
||||
monaco.languages.CompletionItem
|
||||
>(c => {
|
||||
const startColumn =
|
||||
c.start || (p && p.startColumn) || position.column;
|
||||
|
||||
return {
|
||||
insertText: c.text || c.label,
|
||||
label: c.label,
|
||||
kind: map[c.type || "text"],
|
||||
range: {
|
||||
startColumn,
|
||||
endColumn: startColumn + (c.length || 0),
|
||||
startLineNumber: 0,
|
||||
endLineNumber: 0,
|
||||
},
|
||||
sortText:
|
||||
c.type === "property" ? "zzzzzzz" : c.label,
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
75
extension/ui/src/Model/VsCodeApi.ts
Normal file
75
extension/ui/src/Model/VsCodeApi.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Barrier } from "@hediet/std/synchronization";
|
||||
|
||||
export interface VSCodeApi<TState> {
|
||||
getState(): Promise<TState | undefined>;
|
||||
setState(state: TState): Promise<void>;
|
||||
}
|
||||
|
||||
type IncomingCommand = {
|
||||
command: "getStateResult";
|
||||
state: unknown;
|
||||
};
|
||||
|
||||
type OutgoingCommand =
|
||||
| {
|
||||
command: "setState";
|
||||
state: unknown;
|
||||
}
|
||||
| { command: "getState" };
|
||||
|
||||
interface VsCodeApi {
|
||||
getState<TState>(): TState | undefined;
|
||||
setState(state: unknown): void;
|
||||
}
|
||||
|
||||
export function getApi<TState>(): VSCodeApi<TState> {
|
||||
return window.VsCodeApi
|
||||
? new DirectVSCodeApi<TState>(window.VsCodeApi)
|
||||
: new IFrameVSCodeApi<TState>();
|
||||
}
|
||||
|
||||
declare const window: Window & {
|
||||
VsCodeApi?: VsCodeApi;
|
||||
};
|
||||
|
||||
export class IFrameVSCodeApi<TState> implements VSCodeApi<TState> {
|
||||
private currentGetStatePromise: Barrier<TState> | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("message", event => {
|
||||
const data = event.data as IncomingCommand;
|
||||
if (data.command === "getStateResult") {
|
||||
this.currentGetStatePromise!.unlock(data.state as any);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sendCommand(command: OutgoingCommand) {
|
||||
window.parent.postMessage(command, "*");
|
||||
}
|
||||
|
||||
getState(): Promise<TState | undefined> {
|
||||
this.sendCommand({ command: "getState" });
|
||||
if (this.currentGetStatePromise) {
|
||||
throw new Error("Get state already in progress.");
|
||||
}
|
||||
this.currentGetStatePromise = new Barrier();
|
||||
return this.currentGetStatePromise.onUnlocked;
|
||||
}
|
||||
|
||||
async setState(state: TState): Promise<void> {
|
||||
this.sendCommand({ command: "setState", state });
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectVSCodeApi<TState> implements VSCodeApi<TState> {
|
||||
constructor(private readonly vsCodeApi: VsCodeApi) {}
|
||||
|
||||
async getState(): Promise<TState | undefined> {
|
||||
return this.vsCodeApi.getState<TState | undefined>();
|
||||
}
|
||||
|
||||
async setState(state: TState): Promise<void> {
|
||||
this.vsCodeApi.setState(state);
|
||||
}
|
||||
}
|
||||
4312
extension/ui/src/Model/lib.es5.d.ts.txt
Normal file
4312
extension/ui/src/Model/lib.es5.d.ts.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,61 @@ import React = require("react");
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { autorun, observable } from "mobx";
|
||||
|
||||
export class MonacoTextVisualizer extends VisualizationProvider {
|
||||
getVisualizations(
|
||||
data: ExtractedData,
|
||||
collector: VisualizationCollector
|
||||
): void {
|
||||
if (isCommonDataType(data, { text: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("monaco-text"),
|
||||
name: "Monaco Editor",
|
||||
priority: 95,
|
||||
render() {
|
||||
let id = "text";
|
||||
if (data.fileName) {
|
||||
id = getLanguageId(data.fileName);
|
||||
}
|
||||
|
||||
return <MonacoEditor text={data.text} languageId={id} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLanguageId(fileName: string): string {
|
||||
const l = monaco.languages.getLanguages();
|
||||
const result = l.find(l => {
|
||||
if (l.filenamePatterns) {
|
||||
for (const p of l.filenamePatterns) {
|
||||
if (new RegExp(p).test(fileName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (l.extensions) {
|
||||
for (const p of l.extensions) {
|
||||
if (fileName.endsWith(p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result.id;
|
||||
}
|
||||
return "text";
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MonacoEditor extends React.Component<{ text: string }> {
|
||||
export class MonacoEditor extends React.Component<{
|
||||
text: string;
|
||||
languageId: string;
|
||||
}> {
|
||||
@observable private editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -22,10 +75,34 @@ export class MonacoEditor extends React.Component<{ text: string }> {
|
||||
}
|
||||
}
|
||||
|
||||
private model: monaco.editor.ITextModel | undefined = undefined;
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateText = autorun(() => {
|
||||
if (this.editor) {
|
||||
this.editor.setValue(this.props.text);
|
||||
const model = monaco.editor.createModel(
|
||||
this.props.text,
|
||||
this.props.languageId,
|
||||
undefined
|
||||
);
|
||||
|
||||
this.editor.setModel(model);
|
||||
|
||||
/* this.editor!.updateOptions({ readOnly: false });
|
||||
|
||||
setTimeout(() => {
|
||||
const a = this.editor!.getAction(
|
||||
"editor.action.formatDocument"
|
||||
);
|
||||
a.run().then(() => {
|
||||
this.editor!.updateOptions({ readOnly: true });
|
||||
});
|
||||
}, 200);*/
|
||||
|
||||
if (this.model) {
|
||||
this.model.dispose();
|
||||
}
|
||||
this.model = model;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,7 +112,7 @@ export class MonacoEditor extends React.Component<{ text: string }> {
|
||||
}
|
||||
|
||||
this.editor = monaco.editor.create(editorDiv, {
|
||||
value: this.props.text,
|
||||
model: null,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: { enabled: false },
|
||||
@@ -52,21 +129,3 @@ export class MonacoEditor extends React.Component<{ text: string }> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MonacoTextVisualizer extends VisualizationProvider {
|
||||
getVisualizations(
|
||||
data: ExtractedData,
|
||||
collector: VisualizationCollector
|
||||
): void {
|
||||
if (isCommonDataType(data, { text: true })) {
|
||||
collector.addVisualization({
|
||||
id: asVisualizationId("monaco-text"),
|
||||
name: "Monaco Editor",
|
||||
priority: 95,
|
||||
render() {
|
||||
return <MonacoEditor text={data.text} />;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import "./style.scss";
|
||||
import { Model } from "./Model";
|
||||
import { Model } from "./Model/Model";
|
||||
import Components = require("./Components");
|
||||
|
||||
const model = new Model();
|
||||
|
||||
@@ -80,6 +80,7 @@ body,
|
||||
|
||||
.part-header-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
transition: flex 0.3s ease-out;
|
||||
|
||||
@@ -89,8 +90,9 @@ body,
|
||||
align-items: center;
|
||||
|
||||
.part-expression-input {
|
||||
min-width: 0;
|
||||
//color: rgb(155, 70, 176);
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
//font-family: var(--vscode-editor-font-family);
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -118,6 +120,31 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
.component-monaco-editor2 {
|
||||
height: 23px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
background-color: white;
|
||||
|
||||
// overflow: hidden;
|
||||
|
||||
.part-editor {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
|
||||
.current-line {
|
||||
border: none !important;
|
||||
}
|
||||
.selectionHighlight {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.decorator-collapse-icon {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23646465' d='M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z'/%3E%3C/svg%3E");
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { contract, notificationContract, types } from "@hediet/typed-json-rpc";
|
||||
import {
|
||||
contract,
|
||||
notificationContract,
|
||||
types,
|
||||
requestContract,
|
||||
JSONObject,
|
||||
} from "@hediet/typed-json-rpc";
|
||||
import {
|
||||
DataExtractionResult,
|
||||
DataExtractorId,
|
||||
@@ -22,6 +28,57 @@ export type DataExtractionState =
|
||||
result: DataExtractionResult;
|
||||
};
|
||||
|
||||
// https://microsoft.github.io/debug-adapter-protocol/specification#CompletionItem
|
||||
type CompletionItemType =
|
||||
| "method"
|
||||
| "function"
|
||||
| "constructor"
|
||||
| "field"
|
||||
| "variable"
|
||||
| "class"
|
||||
| "interface"
|
||||
| "module"
|
||||
| "property"
|
||||
| "unit"
|
||||
| "value"
|
||||
| "enum"
|
||||
| "keyword"
|
||||
| "snippet"
|
||||
| "text"
|
||||
| "color"
|
||||
| "file"
|
||||
| "reference"
|
||||
| "customcolor";
|
||||
|
||||
export interface CompletionItem {
|
||||
/**
|
||||
* The label of this completion item. By default this is also the text that is inserted when selecting this completion.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* If text is not falsy then it is inserted instead of the label.
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* The item's type. Typically the client uses this information to render the item in the UI with an icon.
|
||||
*/
|
||||
type?: CompletionItemType;
|
||||
|
||||
/**
|
||||
* This value determines the location (in the CompletionsRequest's 'text' attribute) where the completion text is added.
|
||||
* If missing the text is added at the location specified by the CompletionsRequest's 'column' attribute.
|
||||
*/
|
||||
start?: number;
|
||||
|
||||
/**
|
||||
* This value determines how many characters are overwritten by the completion text.
|
||||
* If missing the value 0 is assumed which results in the completion text being inserted.
|
||||
*/
|
||||
length?: number;
|
||||
}
|
||||
|
||||
export const debugVisualizerUIContract = contract({
|
||||
client: {
|
||||
updateState: notificationContract({
|
||||
@@ -43,5 +100,15 @@ export const debugVisualizerUIContract = contract({
|
||||
}),
|
||||
refresh: notificationContract({}),
|
||||
openInBrowser: notificationContract({}),
|
||||
|
||||
getCompletions: requestContract({
|
||||
params: types.type({
|
||||
text: types.string,
|
||||
column: types.number,
|
||||
}),
|
||||
result: types.type({
|
||||
completions: types.array(unchecked<CompletionItem>()),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user