Files
vscode-debug-visualizer/webview/src/model/Model.ts
Henning Dieterichs a9656e1e12 Update
2023-09-07 15:00:50 +02:00

284 lines
7.0 KiB
TypeScript

import { DataExtractorId, VisualizationData } from "@hediet/debug-visualizer-data-extraction";
import { Disposable } from "@hediet/std/disposable";
import { EventTimer } from "@hediet/std/timer";
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import "@hediet/visualization-bundle";
import {
RegisterVisualizerFn,
Visualization,
VisualizationFactory,
VisualizationId,
createVisualizer,
globalVisualizationFactory,
libImplementation,
} from "@hediet/visualization-core";
import {
DataExtractionState,
WebviewUrlParams,
WindowWithWebviewData,
webviewContract,
} from "debug-visualizer/src/webviewContract";
import { ObservableMap, action, computed, observable, runInAction, when } from "mobx";
import { MonacoBridge } from "./MonacoBridge";
import { getApi as getVsCodeApi } from "./VsCodeApi";
declare const window: Window & WindowWithWebviewData;
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.ref state:
| DataExtractionState
| { kind: "noExpression" }
| { kind: "visualizationError"; data: VisualizationData } = {
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: VisualizationData) {
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 = visualizerRegistry
.get()
.getVisualizations(this.state.result.data, this.preferredVisualizationId);
return {
visualization: vis.bestVisualization,
allVisualizations: vis.allVisualizations,
};
} else {
return undefined;
}
}
@observable.ref
public server: typeof webviewContract.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;
this.theme = window.webviewData.theme;
if (data.expression !== undefined) {
this.setExpression(data.expression);
}
} else {
const url = new URL(window.location.href);
const urlParams = Object.fromEntries(url.searchParams.entries()) as unknown as WebviewUrlParams;
const portStr = urlParams.serverPort;
if (!portStr) {
throw new Error("No port given.");
}
this.port = parseInt(portStr);
const expr = urlParams.expression;
if (expr) {
this.setExpression(expr);
}
const secret = urlParams.serverSecret;
if (!secret) {
throw new Error("Server secret not set.");
}
this.serverSecret = secret;
const theme = urlParams.theme;
if (theme) {
this.theme = theme;
}
const mode = urlParams.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 } = webviewContract.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";
},
setTheme: async ({ theme }) => {
this.theme = theme;
},
setCustomVisualizerScript: async ({ id, jsSource }) => {
eval(`
((load) => {
let fn = undefined;
if (load) {
const module = {};
load(module);
fn = module.exports;
}
setVisualizationModule(${JSON.stringify(id)}, fn);
})(
${jsSource ? `function (module) { ${jsSource} }` : "undefined"}
)
`);
},
});
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.");
}
}
const fns = new ObservableMap<string, RegisterVisualizerFn>();
const visualizerRegistry = computed(() => {
const result = new VisualizationFactory();
for (const visualization of globalVisualizationFactory.getRegisteredVisualizers()) {
result.addVisualizer(visualization);
}
for (const visualization of globalVisualizationFactory.getRegisteredHiddenVisualizer()) {
result.addHiddenVisualizer(visualization);
}
fns.forEach((fn) => {
fn((options) => result.addVisualizer(createVisualizer(options)), libImplementation);
});
return result;
});
function setVisualizationModule(id: string, moduleFn: RegisterVisualizerFn | undefined) {
if (moduleFn) {
fns.set(id, moduleFn);
} else {
fns.delete(id);
}
}
(globalThis as any).setVisualizationModule = setVisualizationModule;