Next Iteration
This commit is contained in:
22
extension/src/DataSource/DataSource.ts
Normal file
22
extension/src/DataSource/DataSource.ts
Normal 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;
|
||||
}
|
||||
157
extension/src/DataSource/JsDebuggerSource.ts
Normal file
157
extension/src/DataSource/JsDebuggerSource.ts
Normal 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
224
extension/src/extension.ts
Normal 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() {}
|
||||
Reference in New Issue
Block a user