Next Iteration

This commit is contained in:
Henning Dieterichs
2019-07-31 11:41:10 +02:00
parent 6175e8f55e
commit 922eb0b845
71 changed files with 19606 additions and 14862 deletions

View File

@@ -0,0 +1,22 @@
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
import { DataExtractionState } from "@hediet/debug-visualizer-vscode-shared";
export interface DataSource {
createEvaluationWatcher(expression: string): EvaluationWatcher;
}
export interface EvaluationWatcher {
readonly expression: string;
// is observable
readonly preferredDataExtractor: DataExtractorId | undefined;
setPreferredDataExtractor(id: DataExtractorId): void;
refresh(): void;
// is observable
state: DataExtractionState;
dispose(): void;
}

View File

@@ -0,0 +1,157 @@
import { DataSource, EvaluationWatcher } from "./DataSource";
import { observable } from "mobx";
import * as vscode from "vscode";
import { Disposable } from "@hediet/std/disposable";
import {
selfContainedGetInitializedDataExtractorApi,
DataResult,
DataExtractorId,
ApiHasNotBeenInitializedCode,
selfContainedInitDataExtractorApi,
} from "@hediet/debug-visualizer-data-extraction";
import { DataExtractionState } from "@hediet/debug-visualizer-vscode-shared";
export function createJsDebuggerSource(): JsDataSource {
return new JsDebuggerSourceImplementation();
}
export interface JsCode extends String {
__brand: "JsCode";
}
export interface JsDataSource extends DataSource {
registerDataExtractor(classExpression: JsCode): void;
}
class JsDebuggerSourceImplementation implements JsDataSource {
private lastFrameId: number | null = null;
private readonly watchers = new Set<ObservableEvaluationWatcher>();
public readonly dispose = Disposable.fn();
constructor() {
this.dispose.track(
vscode.debug.onDidReceiveDebugSessionCustomEvent(e => {
if (e.event === "paused") {
const frameId = e.body.currentFrameId;
this.lastFrameId = frameId;
for (const w of this.watchers) {
w.refresh();
}
}
})
);
}
registerDataExtractor(classExpression: JsCode): void {}
public createEvaluationWatcher(expression: string): EvaluationWatcher {
const w = new ObservableEvaluationWatcher(expression, this);
this.watchers.add(w);
this.refresh(w);
return w;
}
public removeWatcher(w: ObservableEvaluationWatcher) {
this.watchers.delete(w);
}
public async refresh(w: ObservableEvaluationWatcher): Promise<void> {
const session = vscode.debug.activeDebugSession;
if (!session) {
w._state = { kind: "noDebugSession" };
return;
}
try {
w._state = { kind: "loading" };
const expression = `(${selfContainedGetInitializedDataExtractorApi.toString()})().getData(${
w.expression
})`;
const reply = await session.customRequest("evaluate", {
expression,
frameId: this.lastFrameId,
context: "watch",
});
const resultStr = reply.result as string;
const jsonData = resultStr.substr(1, resultStr.length - 2);
const result = JSON.parse(jsonData) as DataResult;
if (result.kind === "NoExtractors") {
throw new Error("No extractors");
}
w._state = {
kind: "data",
result: result.extractionResult,
};
} catch (error) {
const msg = error.message as string | undefined;
if (msg && msg.includes(ApiHasNotBeenInitializedCode)) {
if (await this.initializeApi(session)) {
await this.refresh(w);
return;
}
}
w._state = {
kind: "error",
message: error.message,
};
}
}
private async initializeApi(
session: vscode.DebugSession
): Promise<boolean> {
try {
const expression = `(${selfContainedInitDataExtractorApi.toString()})()`;
const reply = await session.customRequest("evaluate", {
expression,
frameId: this.lastFrameId,
context: "watch",
});
if (reply.result === "true") {
// register extractors
}
return true;
} catch (error) {
return false;
}
}
}
class ObservableEvaluationWatcher implements EvaluationWatcher {
constructor(
public readonly expression: string,
private readonly source: JsDebuggerSourceImplementation
) {}
@observable
_preferredDataExtractor: DataExtractorId | undefined = undefined;
get preferredDataExtractor(): DataExtractorId | undefined {
return this._preferredDataExtractor;
}
public setPreferredDataExtractor(id: DataExtractorId): void {
this._preferredDataExtractor = id;
this.refresh();
}
public refresh(): void {
this.source.refresh(this);
}
@observable
public _state: DataExtractionState = { kind: "loading" };
public get state(): DataExtractionState {
return this._state;
}
public dispose(): void {
this.source.removeWatcher(this);
}
}

224
extension/src/extension.ts Normal file
View File

@@ -0,0 +1,224 @@
import {
window,
OutputChannel,
ExtensionContext,
StatusBarAlignment,
commands,
workspace,
ViewColumn,
WebviewPanel,
} from "vscode";
import {
enableHotReload,
hotRequireExportedFn,
registerUpdateReconciler,
getReloadCount,
} from "@hediet/node-reload";
enableHotReload({ entryModule: module });
import { Disposable } from "@hediet/std/disposable";
import { createJsDebuggerSource } from "./DataSource/JsDebuggerSource";
import { debugVisualizerUIContract } from "@hediet/debug-visualizer-vscode-shared";
import { ConsoleRpcLogger } from "@hediet/typed-json-rpc";
import { EvaluationWatcher } from "./DataSource/DataSource";
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
import { observable, autorun } from "mobx";
import * as express from "express";
import * as http from "http";
import * as serveStatic from "serve-static";
import { AddressInfo } from "net";
import WebSocket = require("ws");
import { join } from "path";
registerUpdateReconciler(module);
class WebView {
private expression: string | null = null;
private lastFrameId: number | null = null;
constructor(private readonly webviewPanel: WebviewPanel) {}
}
const debugVisualizer = "debugVisualizer";
function getHtml(server: Server) {
const isDev = true;
return `
<html>
<head>
<style>
html { height: 100%; width: 100%; padding: 0; margin: 0; }
body { height: 100%; width: 100%; padding: 0; margin: 0; }
iframe { height: 100%; width: 100%; padding: 0; margin: 0; border: 0; display: block; }
</style>
</head>
<body>
<script>
window.serverPort = ${server.port};
</script>
${
isDev
? `<iframe src="http://localhost:8080/?serverPort=${
server.port
}" tabindex="0"></iframe>`
: `<script type="text/javascript" src="${
server.mainBundle
}"></script>`
}
</body>
</html>
`;
}
class WebViews {
private readonly debugVisualizations = new Map<WebviewPanel, WebView>();
constructor(private readonly server: Server) {}
public createNew() {
const panel = window.createWebviewPanel(
debugVisualizer,
"Debug Visualizer",
ViewColumn.Two,
{ enableScripts: true }
);
this.setupView(panel);
}
public restore(webviewPanel: WebviewPanel) {
this.setupView(webviewPanel);
}
private setupView(webviewPanel: WebviewPanel) {
webviewPanel.webview.html = getHtml(this.server);
const view = new WebView(webviewPanel);
this.debugVisualizations.set(webviewPanel, view);
webviewPanel.onDidDispose(() => {
this.debugVisualizations.delete(webviewPanel);
});
}
public dispose() {
for (const panel of this.debugVisualizations.keys()) {
panel.dispose();
}
}
}
// js-debug value-printer value-viewer
// #src: bla
// editor-src typescript-ast-viewer
import * as open from "open";
class ConnectionHandler {
public readonly dispose = Disposable.fn();
@observable
private watcher: EvaluationWatcher | undefined = undefined;
constructor(sources: Sources, stream: WebSocketStream, server: Server) {
const {
client,
channel,
} = debugVisualizerUIContract.registerServerToStream(
stream,
new ConsoleRpcLogger(),
{
refresh: async () => {
if (this.watcher) {
this.watcher.refresh();
}
},
setExpression: async ({ newExpression }) => {
if (this.watcher) {
this.dispose.untrack(this.watcher).dispose();
}
this.watcher = this.dispose.track(
sources.jsSource.createEvaluationWatcher(newExpression)
);
},
openInBrowser: async ({}) => {
open(server.indexUrl);
},
}
);
this.dispose.track(
Disposable.create(
autorun(() => {
if (this.watcher) {
client.updateState({
newState: this.watcher.state,
});
}
})
)
);
stream.onClosed.then(() => {
this.dispose();
});
}
}
class Server {
private server: http.Server;
constructor(sources: Sources) {
const app = express();
app.use(serveStatic(join(__dirname, "../../ui/dist")));
this.server = app.listen();
const wss = new WebSocket.Server({ server: this.server });
wss.on("connection", ws => {
const stream = new WebSocketStream(ws);
new ConnectionHandler(sources, stream, this);
});
console.log(this.port);
}
public get indexUrl(): string {
return `http://localhost:${this.port}/index.html?serverPort=${
this.port
}`;
}
public get mainBundle(): string {
return `http://localhost:${this.port}/main-efe6bdc4b62fdc7971ee.js`;
}
public get port(): number {
const httpPort = (this.server.address() as AddressInfo).port;
return httpPort;
}
}
export class Sources {
public readonly jsSource = createJsDebuggerSource();
}
export class Extension {
public dispose = Disposable.fn();
private readonly sources = new Sources();
private readonly server = new Server(this.sources);
private readonly views = this.dispose.track(new WebViews(this.server));
constructor() {
const i = this.dispose.track(window.createStatusBarItem());
i.text = "reload" + getReloadCount(module);
i.show();
this.views.createNew();
}
}
export function activate(context: ExtensionContext) {
context.subscriptions.push(
hotRequireExportedFn(module, Extension, Extension => new Extension())
);
}
export function deactivate() {}