Files
vscode-debug-visualizer/webview/src/model/Model.ts
2020-07-20 19:40:12 +02:00

268 lines
5.9 KiB
TypeScript

import {
debugVisualizerUIContract,
DataExtractionState,
} from "debug-visualizer/src/contract";
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
import { observable, action, computed, when, runInAction } from "mobx";
import {
DataExtractorId,
ExtractedData,
} from "@hediet/debug-visualizer-data-extraction";
import {
Visualization,
VisualizationId,
knownVisualizations,
} from "@hediet/visualization";
import { getApi as getVsCodeApi } from "./VsCodeApi";
import { MonacoBridge } from "./MonacoBridge";
import { Disposable } from "@hediet/std/disposable";
import { startInterval, EventTimer } from "@hediet/std/timer";
declare const window: Window & {
webViewData?: {
serverSecret: string;
serverPort: number;
publicPath: string;
expression: string | undefined;
};
};
declare let __webpack_public_path__: string;
export class Model {
public readonly dispose = Disposable.fn();
public readonly runningMode: "standalone" | "webView" | "webViewIFrame" =
"webView";
@observable
public theme: "dark" | "light" = "light";
private port: number;
private serverSecret: string;
@observable expression: string = "";
@observable state:
| DataExtractionState
| { kind: "noExpression" }
| { kind: "visualizationError"; data: ExtractedData } = {
kind: "noExpression",
};
@observable languageId: string = "text";
@observable private preferredVisualizationId:
| VisualizationId
| undefined = undefined;
@observable isPolling = false;
private readonly pollingTimer = this.dispose.track(
new EventTimer(500, "stopped")
);
@action
public setPreferredVisualizationId(id: VisualizationId) {
this.preferredVisualizationId = id;
}
@action
public setVisualizationError(data: ExtractedData) {
this.state = { kind: "visualizationError", data: data };
}
@action
public setPolling(value: boolean) {
this.isPolling = value;
if (value) {
this.pollingTimer.start();
} else {
this.pollingTimer.stop();
}
}
@computed get visualizations():
| {
visualization: Visualization | undefined;
allVisualizations: Visualization[];
}
| undefined {
if (this.state.kind === "data") {
const vis = knownVisualizations.getBestVisualization(
this.state.result.data,
this.preferredVisualizationId
);
return vis;
} else {
return undefined;
}
}
@observable.ref
public server:
| typeof debugVisualizerUIContract.TServerInterface
| undefined = undefined;
private readonly vsCodeApi = getVsCodeApi<{ expression: string }>();
@observable private _loading = false;
public get loading(): boolean {
return this._loading;
}
private readonly _bridge = new MonacoBridge(this);
constructor() {
if (window.webViewData) {
const data = window.webViewData;
this.port = data.serverPort;
this.serverSecret = data.serverSecret;
this.runningMode = "webView";
__webpack_public_path__ = data.publicPath;
const updateTheme = () => {
const isLight = document.body.classList.contains(
"vscode-light"
);
this.theme = isLight ? "light" : "dark";
};
updateTheme();
if (data.expression !== undefined) {
this.setExpression(data.expression);
}
this.dispose.track(
startInterval(1000, () => {
updateTheme();
})
);
} else {
const url = new URL(window.location.href);
const portStr = url.searchParams.get("serverPort");
if (!portStr) {
throw new Error("No port given.");
}
this.port = parseInt(portStr);
const expr = url.searchParams.get("expression");
if (expr) {
this.setExpression(expr);
}
const secret = url.searchParams.get("serverSecret");
if (!secret) {
throw new Error("Server secret not set.");
}
this.serverSecret = secret;
const theme = url.searchParams.get("theme");
if (theme && theme === "dark") {
this.theme = "dark";
} else {
this.theme = "light";
}
const mode = url.searchParams.get("mode");
if (mode) {
this.runningMode = mode as any;
} else {
this.runningMode = "standalone";
}
}
this.stayConnected();
this.vsCodeApi.getState().then(state => {
if (state) {
this.setExpression(state.expression);
}
});
this.pollingTimer.onTick.sub(() => {
this.refresh();
});
}
@action
setExpression(newExpression: string) {
this.expression = 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) {
if (this.server) {
this.server.setPreferredDataExtractor({
dataExtractorId: id,
});
}
}
refresh() {
if (this.server) {
this.server.refresh();
}
}
async stayConnected(): Promise<void> {
while (true) {
try {
const stream = await WebSocketStream.connectTo({
host: "localhost",
port: this.port,
});
const {
server,
} = debugVisualizerUIContract.getServerFromStream(
stream,
new ConsoleRpcLogger(),
{
updateState: async ({ newState }) => {
runInAction(() => {
this._loading = newState.kind === "loading";
if (!this._loading) {
this.state = newState;
}
});
},
setExpression: async ({ expression }) => {
this.setExpression(expression);
},
updateLanguageId: async ({ languageId }) => {
this.languageId = languageId || "text";
},
}
);
try {
await server.authenticate({ secret: this.serverSecret });
} catch (e) {
console.error(e);
}
this.server = server;
await stream.onClosed;
} catch (e) {}
}
}
openBrowser() {
if (this.server) {
this.server.openInBrowser();
}
}
useSelectionAsExpression() {
throw new Error("Method not implemented.");
}
}