Version 2.2.0.
* Adds support for custom injected scripts with automatic reload * Refactors extension
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
DebugSessionVisualizationSupport,
|
||||
VisualizationBackend,
|
||||
} from "./VisualizationBackend";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
|
||||
export class ComposedVisualizationSupport
|
||||
implements DebugSessionVisualizationSupport {
|
||||
constructor(public readonly engines: DebugSessionVisualizationSupport[]) {}
|
||||
|
||||
createBackend(
|
||||
session: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
for (const f of this.engines) {
|
||||
const evaluator = f.createBackend(session);
|
||||
if (evaluator) {
|
||||
return evaluator;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
import {
|
||||
DebugSessionVisualizationSupport,
|
||||
VisualizationBackend,
|
||||
} from "./VisualizationBackend";
|
||||
import { Config, DebugAdapterConfig } from "../Config";
|
||||
import { GenericVisualizationBackend } from "./GenericVisualizationSupport";
|
||||
import { registerUpdateReconciler, hotClass } from "@hediet/node-reload";
|
||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
@hotClass(module)
|
||||
export class ConfigurableVisualizationSupport
|
||||
implements DebugSessionVisualizationSupport {
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly debuggerView: DebuggerViewProxy
|
||||
) {}
|
||||
|
||||
createBackend(
|
||||
session: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
const config = this.config.getDebugAdapterConfig(session.session.type);
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
return new ConfiguredVisualizationBackend(
|
||||
session,
|
||||
this.debuggerView,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfiguredVisualizationBackend extends GenericVisualizationBackend {
|
||||
constructor(
|
||||
debugSession: DebugSessionProxy,
|
||||
debuggerView: DebuggerViewProxy,
|
||||
private readonly config: DebugAdapterConfig
|
||||
) {
|
||||
super(debugSession, debuggerView);
|
||||
}
|
||||
|
||||
protected getContext() {
|
||||
return this.config.context;
|
||||
}
|
||||
|
||||
protected getFinalExpression({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
}: {
|
||||
expression: string;
|
||||
preferredExtractorId: DataExtractorId | undefined;
|
||||
}) {
|
||||
return this.config.getFinalExpression({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { DataExtractionResult } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { CompletionItem, FormattedMessage } from "../webviewContract";
|
||||
import {
|
||||
DebugSessionVisualizationSupport,
|
||||
GetVisualizationDataArgs,
|
||||
VisualizationBackend,
|
||||
} from "./VisualizationBackend";
|
||||
import { EventEmitter, EventSource } from "@hediet/std/events";
|
||||
import { debug } from "vscode";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||
import { autorun, reaction } from "mobx";
|
||||
|
||||
export class DispatchingVisualizationBackend implements VisualizationBackend {
|
||||
public readonly dispose = Disposable.fn();
|
||||
private readonly visualizationBackends = new Map<
|
||||
DebugSessionProxy,
|
||||
VisualizationBackend
|
||||
>();
|
||||
|
||||
protected readonly onChangeEmitter = new EventEmitter();
|
||||
public readonly onChange = this.onChangeEmitter;
|
||||
|
||||
constructor(
|
||||
private readonly visualizationSupport: DebugSessionVisualizationSupport,
|
||||
private readonly debuggerView: DebuggerViewProxy
|
||||
) {
|
||||
this.dispose.track(
|
||||
debug.onDidTerminateDebugSession(session => {
|
||||
const existing = [...this.visualizationBackends].find(
|
||||
v => v[0].session === session
|
||||
);
|
||||
if (existing) {
|
||||
this.visualizationBackends.delete(existing[0]);
|
||||
existing[1].dispose();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.dispose.track({
|
||||
dispose: () => {
|
||||
for (const backend of this.visualizationBackends.values()) {
|
||||
backend.dispose();
|
||||
}
|
||||
this.visualizationBackends.clear();
|
||||
},
|
||||
});
|
||||
|
||||
this.dispose.track({
|
||||
dispose: reaction(
|
||||
() => this.debuggerView.activeDebugSession,
|
||||
debugSession => {
|
||||
if (debugSession !== undefined) {
|
||||
this.onChangeEmitter.emit();
|
||||
}
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private getVisualizationBackend(
|
||||
debugSession: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
const existing = this.visualizationBackends.get(debugSession);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const newBackend = this.visualizationSupport.createBackend(
|
||||
debugSession
|
||||
);
|
||||
|
||||
if (!newBackend) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
newBackend.onChange.sub(() => {
|
||||
this.onChangeEmitter.emit();
|
||||
});
|
||||
|
||||
this.visualizationBackends.set(debugSession, newBackend);
|
||||
|
||||
return newBackend;
|
||||
}
|
||||
|
||||
private get activeVisualizationBackend(): VisualizationBackend | undefined {
|
||||
const activeDebugSession = this.debuggerView.activeDebugSession;
|
||||
if (!activeDebugSession) {
|
||||
return undefined;
|
||||
}
|
||||
const backend = this.getVisualizationBackend(activeDebugSession);
|
||||
return backend;
|
||||
}
|
||||
|
||||
public async getVisualizationData(
|
||||
args: GetVisualizationDataArgs
|
||||
): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
> {
|
||||
const activeDebugSession = this.debuggerView.activeDebugSession;
|
||||
if (!activeDebugSession) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: "No active debug session.",
|
||||
};
|
||||
}
|
||||
const backend = this.getVisualizationBackend(activeDebugSession);
|
||||
if (!backend) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: `The debug adapter "${activeDebugSession.session.type}" is not supported.`,
|
||||
};
|
||||
}
|
||||
return await backend.getVisualizationData(args);
|
||||
}
|
||||
|
||||
public get expressionLanguageId(): string | undefined {
|
||||
return this.activeVisualizationBackend?.expressionLanguageId;
|
||||
}
|
||||
|
||||
public async getCompletions(
|
||||
text: string,
|
||||
column: number
|
||||
): Promise<CompletionItem[]> {
|
||||
return (
|
||||
(await this.activeVisualizationBackend?.getCompletions(
|
||||
text,
|
||||
column
|
||||
)) || []
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
DataExtractionResult,
|
||||
DataExtractorId,
|
||||
GraphNode,
|
||||
GraphVisualizationData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
import {
|
||||
DebugSessionVisualizationSupport,
|
||||
VisualizationBackend,
|
||||
GetVisualizationDataArgs,
|
||||
VisualizationBackendBase,
|
||||
} from "./VisualizationBackend";
|
||||
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
|
||||
import { FormattedMessage } from "../webviewContract";
|
||||
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
@hotClass(module)
|
||||
export class GenericVisualizationSupport
|
||||
implements DebugSessionVisualizationSupport {
|
||||
constructor(private readonly debuggerView: DebuggerViewProxy) {}
|
||||
|
||||
createBackend(
|
||||
session: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
return new GenericVisualizationBackend(session, this.debuggerView);
|
||||
}
|
||||
}
|
||||
|
||||
export class GenericVisualizationBackend extends VisualizationBackendBase {
|
||||
public readonly expressionLanguageId = "text";
|
||||
|
||||
constructor(
|
||||
debugSession: DebugSessionProxy,
|
||||
debuggerView: DebuggerViewProxy
|
||||
) {
|
||||
super(debugSession, debuggerView);
|
||||
}
|
||||
|
||||
public async getVisualizationData({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
}: GetVisualizationDataArgs): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
> {
|
||||
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||
this.debugSession
|
||||
);
|
||||
|
||||
const finalExpression = this.getFinalExpression({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
});
|
||||
let reply: { result: string; variablesReference: number };
|
||||
try {
|
||||
reply = await this.debugSession.evaluate({
|
||||
expression: finalExpression,
|
||||
frameId,
|
||||
context: this.getContext(),
|
||||
});
|
||||
|
||||
// Use structural information about variables
|
||||
// from the evaluation response if present.
|
||||
if (reply.variablesReference) {
|
||||
const graph = await this.constructGraphFromVariablesReference(
|
||||
reply.result,
|
||||
reply.variablesReference
|
||||
);
|
||||
|
||||
return {
|
||||
kind: "data",
|
||||
result: {
|
||||
availableExtractors: [],
|
||||
usedExtractor: {
|
||||
id: "generic" as any,
|
||||
name: "Generic",
|
||||
priority: 1,
|
||||
},
|
||||
data: graph,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return parseEvaluationResultFromGenericDebugAdapter(
|
||||
reply.result,
|
||||
{
|
||||
debugAdapterType: this.debugSession.session
|
||||
.configuration.type,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: {
|
||||
kind: "list",
|
||||
items: [
|
||||
"An error occurred while evaluating the expression:",
|
||||
error.message,
|
||||
`Used debug adapter: ${this.debugSession.session.configuration.type}`,
|
||||
{
|
||||
kind: "inlineList",
|
||||
items: [
|
||||
"Evaluated expression is",
|
||||
{ kind: "code", content: finalExpression },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs GraphVisualizationData from a DAP variables
|
||||
* reference by successively querying the debug adapter for
|
||||
* variables. Objects are considered to be equivalent if
|
||||
* they share the same variables reference (this is important
|
||||
* for representing cyclic relationships).
|
||||
*
|
||||
* @param rootLabel - The root object's label
|
||||
* @param rootVariablesReference - The root object's DAP variables reference
|
||||
* @param maxDepth - The maximum depth to search at
|
||||
* @param maxKnownNodes - The maximum number of nodes
|
||||
*/
|
||||
private async constructGraphFromVariablesReference(
|
||||
rootLabel: string,
|
||||
rootVariablesReference: number,
|
||||
maxDepth: number = 30,
|
||||
maxKnownNodes: number = 50
|
||||
): Promise<GraphVisualizationData> {
|
||||
// Perform a breadth-first search on the object to construct the graph
|
||||
|
||||
const graph: GraphVisualizationData = {
|
||||
kind: { graph: true },
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
const knownNodeIds: { [ref: number]: string } = {};
|
||||
const bfsQueue: {
|
||||
source: { id: string; name: string } | undefined;
|
||||
label: string;
|
||||
variablesReference: number;
|
||||
depth: number;
|
||||
}[] = [
|
||||
{
|
||||
source: undefined,
|
||||
label: rootLabel,
|
||||
variablesReference: rootVariablesReference,
|
||||
depth: 0,
|
||||
},
|
||||
];
|
||||
|
||||
let knownCount: number = 0;
|
||||
|
||||
do {
|
||||
const variable = bfsQueue.shift()!;
|
||||
const hasChilds = variable.variablesReference > 0;
|
||||
|
||||
if (variable.depth > maxDepth) {
|
||||
break;
|
||||
}
|
||||
|
||||
let nodeId: string;
|
||||
|
||||
if (!hasChilds || !(variable.variablesReference in knownNodeIds)) {
|
||||
// The variable is a leaf or an unvisited object: create the node.
|
||||
|
||||
const node: GraphNode = {
|
||||
id: hasChilds
|
||||
? `${variable.variablesReference}`
|
||||
: `__${variable.label}@${knownCount}__`,
|
||||
label: variable.label,
|
||||
color: variable.depth == 0 ? "lightblue" : undefined,
|
||||
shape: "box",
|
||||
};
|
||||
|
||||
graph.nodes.push(node);
|
||||
knownCount++;
|
||||
|
||||
if (hasChilds) {
|
||||
knownNodeIds[variable.variablesReference] = node.id;
|
||||
|
||||
for (const child of await this.debugSession.getVariables({
|
||||
variablesReference: variable.variablesReference,
|
||||
})) {
|
||||
bfsQueue.push({
|
||||
source: { id: node.id, name: child.name },
|
||||
label: child.value,
|
||||
variablesReference: child.variablesReference,
|
||||
depth: variable.depth + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
nodeId = node.id;
|
||||
} else {
|
||||
// The variable is a visited object (e.g. due to a cyclic reference)
|
||||
|
||||
nodeId = knownNodeIds[variable.variablesReference];
|
||||
}
|
||||
|
||||
if (variable.source) {
|
||||
graph.edges.push({
|
||||
from: variable.source.id,
|
||||
to: nodeId,
|
||||
label: variable.source.name,
|
||||
});
|
||||
}
|
||||
} while (bfsQueue.length > 0 && knownCount <= maxKnownNodes);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
protected getFinalExpression(args: {
|
||||
expression: string;
|
||||
preferredExtractorId: DataExtractorId | undefined;
|
||||
}): string {
|
||||
return args.expression;
|
||||
}
|
||||
|
||||
protected getContext(): "watch" | "repl" {
|
||||
// we will use "repl" as default so that results are not truncated.
|
||||
return "repl";
|
||||
}
|
||||
}
|
||||
395
extension/src/VisualizationBackend/JsVisualizationSupport.ts
Normal file
395
extension/src/VisualizationBackend/JsVisualizationSupport.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import {
|
||||
getExpressionForDataExtractorApi,
|
||||
DataResult,
|
||||
getExpressionToInitializeDataExtractorApi,
|
||||
DataExtractionResult,
|
||||
getExpressionToDetectDataExtractorApiPresence,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
import {
|
||||
DebugSessionVisualizationSupport,
|
||||
VisualizationBackend,
|
||||
GetVisualizationDataArgs,
|
||||
VisualizationBackendBase,
|
||||
} from "./VisualizationBackend";
|
||||
import { FormattedMessage } from "../webviewContract";
|
||||
import { registerUpdateReconciler, hotClass } from "@hediet/node-reload";
|
||||
import { Config } from "../Config";
|
||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { reaction, observable } from "mobx";
|
||||
import {
|
||||
workspace,
|
||||
FileSystemWatcher,
|
||||
CancellationTokenSource,
|
||||
CancellationToken,
|
||||
window,
|
||||
} from "vscode";
|
||||
import { ResettableTimeout } from "@hediet/std/timer";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
@hotClass(module)
|
||||
export class JsEvaluationEngine implements DebugSessionVisualizationSupport {
|
||||
constructor(
|
||||
private readonly debuggerView: DebuggerViewProxy,
|
||||
private readonly config: Config
|
||||
) {}
|
||||
|
||||
createBackend(
|
||||
session: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
const supportedDebugAdapters = [
|
||||
"node",
|
||||
"node2",
|
||||
"extensionHost",
|
||||
"chrome",
|
||||
"pwa-chrome",
|
||||
"pwa-node",
|
||||
"pwa-extensionHost",
|
||||
"node-terminal",
|
||||
"pwa-msedge",
|
||||
];
|
||||
|
||||
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
|
||||
return new JsVisualizationBackend(
|
||||
session,
|
||||
this.debuggerView,
|
||||
this.config
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class JsVisualizationBackend extends VisualizationBackendBase {
|
||||
public readonly expressionLanguageId = "javascript";
|
||||
private apiInitialized = false;
|
||||
private initializedPromise: Promise<void> | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
debugSession: DebugSessionProxy,
|
||||
debuggerView: DebuggerViewProxy,
|
||||
private readonly config: Config
|
||||
) {
|
||||
super(debugSession, debuggerView);
|
||||
}
|
||||
|
||||
private getContext(): "copy" | "repl" {
|
||||
if (this.debugSession.session.type.startsWith("pwa-")) {
|
||||
return "copy";
|
||||
}
|
||||
return "repl";
|
||||
}
|
||||
|
||||
public async getVisualizationData({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
}: GetVisualizationDataArgs): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
> {
|
||||
try {
|
||||
await this.ensureRuntimeIsInjected();
|
||||
|
||||
const frameId = this.debuggerView.getActiveStackFrameId(
|
||||
this.debugSession
|
||||
);
|
||||
|
||||
const variableNames = new Array<string>();
|
||||
if (frameId) {
|
||||
const scopes = await this.debugSession.getScopes({ frameId });
|
||||
const scopeVariables = await Promise.all(
|
||||
scopes
|
||||
.filter(s => !s.expensive)
|
||||
.map(s =>
|
||||
this.debugSession.getVariables({
|
||||
variablesReference: s.variablesReference,
|
||||
})
|
||||
)
|
||||
);
|
||||
for (const variables of scopeVariables) {
|
||||
variableNames.push(
|
||||
...variables
|
||||
.filter(v => v.value !== "undefined")
|
||||
.map(v => v.name)
|
||||
);
|
||||
}
|
||||
|
||||
if (variableNames.length > 100) {
|
||||
variableNames.length = 100;
|
||||
}
|
||||
}
|
||||
|
||||
const preferredExtractorExpr = preferredExtractorId
|
||||
? `"${preferredExtractorId}"`
|
||||
: "undefined";
|
||||
|
||||
const body = `${getExpressionForDataExtractorApi()}.getData(
|
||||
e => (${expression}),
|
||||
expr => eval(expr),
|
||||
${preferredExtractorExpr},
|
||||
{${variableNames.map(n => `${n}: ${n}`).join(",")}},
|
||||
)`;
|
||||
|
||||
const wrappedExpr = `
|
||||
(() => {
|
||||
try {
|
||||
return ${body};
|
||||
} catch (e) {
|
||||
return JSON.stringify({
|
||||
kind: "Error",
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
});
|
||||
}
|
||||
})()
|
||||
`;
|
||||
|
||||
const reply = await this.debugSession.evaluate({
|
||||
expression: wrappedExpr,
|
||||
frameId,
|
||||
context: this.getContext(),
|
||||
});
|
||||
const resultStr = reply.result;
|
||||
const jsonData =
|
||||
this.getContext() === "copy"
|
||||
? resultStr
|
||||
: 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);
|
||||
} else if (result.kind === "Data") {
|
||||
return {
|
||||
kind: "data",
|
||||
result: result.extractionResult,
|
||||
};
|
||||
} else {
|
||||
throw new Error("Invalid Data");
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private ensureRuntimeIsInjected(): Promise<void> | undefined {
|
||||
if (this.apiInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.initializedPromise) {
|
||||
return this.initializedPromise;
|
||||
} else {
|
||||
this.initializedPromise = this._ensureRuntimeIsInjected();
|
||||
}
|
||||
}
|
||||
|
||||
private async _ensureRuntimeIsInjected(): Promise<void> {
|
||||
const body = getExpressionToDetectDataExtractorApiPresence();
|
||||
|
||||
const reply = await this.debugSession.evaluate({
|
||||
expression: body,
|
||||
frameId: undefined,
|
||||
context: this.getContext(),
|
||||
});
|
||||
const isApiInitialized = reply.result === "true";
|
||||
|
||||
if (!isApiInitialized) {
|
||||
await this.initializeApi();
|
||||
}
|
||||
|
||||
await this.initializeCustomScripts();
|
||||
}
|
||||
|
||||
private async initializeApi(): Promise<boolean> {
|
||||
try {
|
||||
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
|
||||
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
|
||||
|
||||
await this.debugSession.evaluate({
|
||||
expression,
|
||||
frameId: undefined,
|
||||
context: this.getContext(),
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@observable
|
||||
private filesVersionId: number = 0;
|
||||
|
||||
private readonly fileChangeDebouncedScheduler = new DebouncedScheduler(100);
|
||||
|
||||
private async initializeCustomScripts(): Promise<void> {
|
||||
await this.debugSession.evaluate({
|
||||
expression: `global["${injectedScriptsKey}"] = {};`,
|
||||
frameId: undefined,
|
||||
context: "repl",
|
||||
});
|
||||
|
||||
const watchers = new Map<string, WatchedFile>();
|
||||
this.dispose.track({
|
||||
dispose: () => {
|
||||
for (const w of watchers.values()) {
|
||||
w.watcher.dispose();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
reaction(
|
||||
() => ({
|
||||
customScriptPaths: [...this.config.customScriptPaths],
|
||||
version: this.filesVersionId,
|
||||
}),
|
||||
async ({ customScriptPaths }) => {
|
||||
const promises = new Array<Promise<[Result, WatchedFile]>>();
|
||||
|
||||
const activePaths = new Set<string>();
|
||||
for (const fsPath of customScriptPaths) {
|
||||
activePaths.add(fsPath);
|
||||
let entry = watchers.get(fsPath);
|
||||
if (!entry) {
|
||||
entry = new WatchedFile(fsPath, this.debugSession);
|
||||
watchers.set(fsPath, entry);
|
||||
entry.watcher.onDidChange(() => {
|
||||
this.fileChangeDebouncedScheduler.run(() => {
|
||||
this.filesVersionId++;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
promises.push(entry.update().then(e => [e, entry!]));
|
||||
}
|
||||
|
||||
for (const [fsPath, entry] of watchers) {
|
||||
if (!activePaths.has(fsPath)) {
|
||||
promises.push(entry.remove().then(e => [e, entry]));
|
||||
entry.watcher.dispose();
|
||||
watchers.delete(fsPath);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const errors = results.filter(([r]) => r.error);
|
||||
for (const [e, watcher] of errors) {
|
||||
window.showErrorMessage(
|
||||
`Error while evaluating "${watcher.fsPath}": ${
|
||||
e.error!.message
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
if (promises.length > 0) {
|
||||
this.onChangeEmitter.emit();
|
||||
}
|
||||
},
|
||||
{
|
||||
fireImmediately: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const injectedScriptsKey = `@hediet/debug-visualizer/injectedScripts`;
|
||||
|
||||
interface Result {
|
||||
error?: { message: string };
|
||||
}
|
||||
|
||||
class WatchedFile {
|
||||
public static nextId = 0;
|
||||
public readonly watcher = workspace.createFileSystemWatcher(this.fsPath);
|
||||
public readonly id = WatchedFile.nextId++;
|
||||
public lastContent: string | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
public readonly fsPath: string,
|
||||
private readonly debugSession: DebugSessionProxy
|
||||
) {}
|
||||
|
||||
public async update(): Promise<Result> {
|
||||
if (!existsSync(this.fsPath)) {
|
||||
return {
|
||||
error: { message: `File does not exist.` },
|
||||
};
|
||||
}
|
||||
|
||||
const scriptContent = readFileSync(this.fsPath, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
if (this.lastContent === scriptContent) {
|
||||
return {};
|
||||
}
|
||||
this.lastContent = scriptContent;
|
||||
|
||||
const expression = `
|
||||
(
|
||||
load => {
|
||||
var module = {};
|
||||
load(module);
|
||||
global["${injectedScriptsKey}"]["${this.id}"] = module.exports;
|
||||
}
|
||||
)
|
||||
(
|
||||
function (module) { ${scriptContent} }
|
||||
)`;
|
||||
try {
|
||||
await this.debugSession.evaluate({
|
||||
expression,
|
||||
frameId: undefined,
|
||||
context: "repl",
|
||||
});
|
||||
} catch (e) {
|
||||
return { error: { message: e.message } };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public async remove(): Promise<Result> {
|
||||
const expression = `(() => { delete global["${injectedScriptsKey}"]["${this.id}"]; })()`;
|
||||
try {
|
||||
await this.debugSession.evaluate({
|
||||
expression,
|
||||
frameId: undefined,
|
||||
context: "repl",
|
||||
});
|
||||
} catch (e) {
|
||||
return { error: { message: e.message } };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
class DebouncedScheduler {
|
||||
private timeout: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor(private readonly debounceTimeout: number) {}
|
||||
|
||||
public run(action: () => void): void {
|
||||
this.clear();
|
||||
this.timeout = setTimeout(action, this.debounceTimeout);
|
||||
}
|
||||
|
||||
private clear() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
80
extension/src/VisualizationBackend/VisualizationBackend.ts
Normal file
80
extension/src/VisualizationBackend/VisualizationBackend.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
DataExtractionResult,
|
||||
DataExtractorId,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
import { CompletionItem, FormattedMessage } from "../webviewContract";
|
||||
import { reaction } from "mobx";
|
||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||
import { EventEmitter, EventSource } from "@hediet/std/events";
|
||||
|
||||
export interface DebugSessionVisualizationSupport {
|
||||
createBackend(session: DebugSessionProxy): VisualizationBackend | undefined;
|
||||
}
|
||||
|
||||
export interface VisualizationBackend extends Disposable {
|
||||
readonly onChange: EventSource;
|
||||
|
||||
getVisualizationData(
|
||||
args: GetVisualizationDataArgs
|
||||
): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
>;
|
||||
|
||||
/**
|
||||
* The language that expressions must be written in.
|
||||
*/
|
||||
readonly expressionLanguageId: string | undefined;
|
||||
|
||||
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
|
||||
}
|
||||
|
||||
export interface GetVisualizationDataArgs {
|
||||
expression: string;
|
||||
preferredExtractorId: DataExtractorId | undefined;
|
||||
}
|
||||
|
||||
export abstract class VisualizationBackendBase implements VisualizationBackend {
|
||||
public readonly dispose = Disposable.fn();
|
||||
|
||||
protected readonly onChangeEmitter = new EventEmitter();
|
||||
public readonly onChange = this.onChangeEmitter;
|
||||
|
||||
constructor(
|
||||
protected readonly debugSession: DebugSessionProxy,
|
||||
protected readonly debuggerView: DebuggerViewProxy
|
||||
) {
|
||||
this.dispose.track({
|
||||
dispose: reaction(
|
||||
() => debuggerView.getActiveStackFrameId(debugSession),
|
||||
activeStackFrameId => {
|
||||
if (activeStackFrameId !== undefined) {
|
||||
this.onChangeEmitter.emit();
|
||||
}
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
public abstract readonly expressionLanguageId: string | undefined;
|
||||
|
||||
public abstract getVisualizationData(
|
||||
args: GetVisualizationDataArgs
|
||||
): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
>;
|
||||
|
||||
public async getCompletions(
|
||||
text: string,
|
||||
column: number
|
||||
): Promise<CompletionItem[]> {
|
||||
return await this.debugSession.getCompletions({
|
||||
text,
|
||||
frameId: this.debuggerView.getActiveStackFrameId(this.debugSession),
|
||||
column,
|
||||
});
|
||||
}
|
||||
}
|
||||
6
extension/src/VisualizationBackend/index.ts
Normal file
6
extension/src/VisualizationBackend/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./VisualizationBackend";
|
||||
|
||||
export { ComposedVisualizationSupport } from "./ComposedVisualizationSupport";
|
||||
export { JsEvaluationEngine } from "./JsVisualizationSupport";
|
||||
export { GenericVisualizationSupport } from "./GenericVisualizationSupport";
|
||||
export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport";
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
DataExtractionResult,
|
||||
isVisualizationData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { FormattedMessage } from "../webviewContract";
|
||||
|
||||
export interface ParseEvaluationResultContext {
|
||||
debugAdapterType: string;
|
||||
}
|
||||
|
||||
export function parseEvaluationResultFromGenericDebugAdapter(
|
||||
resultText: string,
|
||||
context: ParseEvaluationResultContext
|
||||
):
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage } {
|
||||
const jsonData = resultText.trim();
|
||||
|
||||
let resultObj;
|
||||
try {
|
||||
try {
|
||||
let jsonData2;
|
||||
// Remove optionally enclosing characters.
|
||||
if (
|
||||
isEnclosedWith(jsonData, '"') ||
|
||||
isEnclosedWith(jsonData, "'")
|
||||
) {
|
||||
// In case of JavaScript: `"{ "kind": { ... }, "text": "some\ntext" }"`
|
||||
jsonData2 = jsonData.substr(1, jsonData.length - 2);
|
||||
} else {
|
||||
// Just in case no quoting is done.
|
||||
jsonData2 = jsonData;
|
||||
}
|
||||
resultObj = parseJson(jsonData2, context);
|
||||
} catch (e) {
|
||||
// in case of C++: `"{ \"kind\": { ... }, \"text\": \"some\\ntext\" }"`
|
||||
const str = parseJson(jsonData, context);
|
||||
// str is now `{ "kind": { ... }, "text": "some\ntext" }"`
|
||||
resultObj = parseJson(str, context);
|
||||
// result is now { kind: { ... }, text: "some\ntext" }
|
||||
}
|
||||
|
||||
if (!isVisualizationData(resultObj)) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: {
|
||||
kind: "list",
|
||||
items: [
|
||||
"Evaluation result does not match ExtractedData interface.",
|
||||
{
|
||||
kind: "inlineList",
|
||||
items: [
|
||||
"Evaluation result was:",
|
||||
{
|
||||
kind: "code",
|
||||
content: JSON.stringify(
|
||||
resultObj,
|
||||
undefined,
|
||||
4
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "data",
|
||||
result: {
|
||||
availableExtractors: [],
|
||||
usedExtractor: {
|
||||
id: "generic" as any,
|
||||
name: "Generic",
|
||||
priority: 1,
|
||||
},
|
||||
data: resultObj,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isEnclosedWith(str: string, char: string): boolean {
|
||||
return str.startsWith(char) && str.endsWith(char);
|
||||
}
|
||||
|
||||
function parseJson(str: string, context: ParseEvaluationResultContext) {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (error) {
|
||||
throw new FormattedError({
|
||||
kind: "list",
|
||||
items: [
|
||||
"Could not parse evaluation result as JSON:",
|
||||
error.message,
|
||||
{
|
||||
kind: "inlineList",
|
||||
items: [
|
||||
"Evaluation result was:",
|
||||
{
|
||||
kind: "code",
|
||||
content: str,
|
||||
},
|
||||
],
|
||||
},
|
||||
`Used debug adapter: ${context.debugAdapterType}`,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class FormattedError {
|
||||
constructor(public readonly message: FormattedMessage) {}
|
||||
}
|
||||
Reference in New Issue
Block a user