Files
vscode-debug-visualizer/extension/src/DataSource/JsDebuggerSource.ts
2019-10-06 11:34:04 +02:00

210 lines
5.2 KiB
TypeScript

import {
DataSource,
EvaluationWatcher,
EvaluationWatcherOptions,
} from "./DataSource";
import { observable } from "mobx";
import * as vscode from "vscode";
import { Disposable } from "@hediet/std/disposable";
import {
selfContainedGetInitializedDataExtractorApi,
DataResult,
DataExtractorId,
ApiHasNotBeenInitializedCode,
selfContainedInitDataExtractorApi,
TypeScriptAstDataExtractor,
AsIsDataExtractor,
} from "@hediet/debug-visualizer-data-extraction";
import {
DataExtractionState,
CompletionItem,
} from "@hediet/debug-visualizer-vscode-shared";
import { hotClass } from "@hediet/node-reload";
export function createJsDebuggerSource(): JsDataSource {
return new JsDebuggerSourceImplementation();
}
export interface JsCode extends String {
__brand: "JsCode";
}
export interface JsDataSource extends DataSource {
registerDataExtractor(classExpression: JsCode): void;
}
@hotClass(module)
export 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 {}
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
): EvaluationWatcher {
const w = new ObservableEvaluationWatcher(expression, this, options);
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 fnSrc = selfContainedGetInitializedDataExtractorApi.toString();
const preferredExtractor = w.preferredDataExtractor
? `"${w.preferredDataExtractor}"`
: "undefined";
const body = `(${fnSrc})().getData(${w.expression}, expr => eval(expr), ${preferredExtractor})`;
const expression = `(() => { try { return ${body}; } catch (e) { return JSON.stringify({ kind: "Error", message: e.message, stack: e.stack }); } })()`;
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");
} else if (result.kind === "Error") {
throw new Error(result.message);
}
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 {
let expression = `(${selfContainedInitDataExtractorApi.toString()})();`;
const es = [TypeScriptAstDataExtractor, AsIsDataExtractor].map(
e => `new (${e.toString()})()`
);
expression += `(${selfContainedGetInitializedDataExtractorApi.toString()})()`;
expression += `.registerExtractors([${es.join(",")}])`;
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,
options: EvaluationWatcherOptions
) {
this._preferredDataExtractor = options.preferredDataExtractor;
}
@observable
_preferredDataExtractor: DataExtractorId | undefined = undefined;
get preferredDataExtractor(): DataExtractorId | undefined {
return this._preferredDataExtractor;
}
public setPreferredDataExtractor(id: DataExtractorId | undefined): 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);
}
}