Implements GenericDataExtractionProviderFactory. With this, the debug visualizer can be used with every language that can be debugged in VS Code.
This commit is contained in:
@@ -4,7 +4,9 @@ export type DataExtractionResult = {
|
||||
availableExtractors: DataExtractorInfo[];
|
||||
};
|
||||
|
||||
// Instances must be valid json values.
|
||||
/**
|
||||
* Instances must be valid json values.
|
||||
*/
|
||||
export type ExtractedData = {
|
||||
kind: Record<string, true>;
|
||||
};
|
||||
@@ -18,3 +20,16 @@ export type DataExtractorInfo = {
|
||||
export type DataExtractorId = {
|
||||
__brand: "DataExtractorId";
|
||||
} & string;
|
||||
|
||||
export function isExtractedData(val: unknown): val is ExtractedData {
|
||||
if (typeof val !== "object" || !val || !("kind" in val)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const obj = val as any;
|
||||
if (typeof obj.kind !== "object" || !obj.kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.values(obj.kind).every(val => val === true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExtractedData } from "../../../DataExtractionResult";
|
||||
import { ExtractedData, isExtractedData } from "../../../DataExtractionResult";
|
||||
import {
|
||||
DataExtractor,
|
||||
ExtractionCollector,
|
||||
@@ -12,17 +12,7 @@ export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
|
||||
extractionCollector: ExtractionCollector<ExtractedData>,
|
||||
context: DataExtractorContext
|
||||
): void {
|
||||
if (typeof data !== "object" || !data || !("kind" in data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = data as any;
|
||||
if (typeof obj.kind !== "object" || !obj.kind) {
|
||||
return;
|
||||
}
|
||||
|
||||
const areAllTrue = Object.values(obj.kind).every(val => val === true);
|
||||
if (!areAllTrue) {
|
||||
if (!isExtractedData(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,7 +21,7 @@ export class AsIsDataExtractor implements DataExtractor<ExtractedData> {
|
||||
name: "As Is",
|
||||
priority: 500,
|
||||
extractData() {
|
||||
return obj;
|
||||
return data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
22
example-project/.vscode/launch.json
vendored
Normal file
22
example-project/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Listen for XDebug",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"port": 9000
|
||||
},
|
||||
{
|
||||
"name": "Launch currently open script",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"cwd": "${fileDirname}",
|
||||
"port": 9000
|
||||
}
|
||||
]
|
||||
}
|
||||
18
example-project/src/demo.php
Normal file
18
example-project/src/demo.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
|
||||
// { kind: { graph: true }, nodes: [ { id: "1", label: "1" }, { id: "2", label: "2" } ], edges: [{ from: "1", to: "2", label: "edge" }]}
|
||||
$data = json_encode(array(
|
||||
"kind" => array("graph" => true),
|
||||
"nodes" => [
|
||||
array("id" => "1", "label" => "1"),
|
||||
array("id" => "2", "label" => "2"),
|
||||
],
|
||||
"edges" => [
|
||||
array("from" => "1", "to" => "2", "label" => "edge")
|
||||
]
|
||||
));
|
||||
|
||||
echo "test" . $i;
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { debugVisualizerUIContract } from "@hediet/debug-visualizer-vscode-shared";
|
||||
import { ConsoleRpcLogger, RequestHandlingError } from "@hediet/typed-json-rpc";
|
||||
import { EvaluationWatcher } from "./DataSource/DataSource";
|
||||
import { EvaluationWatcher, DataSource } from "./DataSource/DataSource";
|
||||
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
||||
import { observable, autorun } from "mobx";
|
||||
import { Sources } from "./DataSource";
|
||||
import { Server } from "./Server";
|
||||
import * as open from "open";
|
||||
import chromeLauncher = require("chrome-launcher");
|
||||
@@ -16,7 +15,7 @@ export class ConnectionHandler {
|
||||
private watcher: EvaluationWatcher | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
sources: Sources,
|
||||
dataSource: DataSource,
|
||||
stream: WebSocketStream,
|
||||
server: Server,
|
||||
config: Config,
|
||||
@@ -61,12 +60,9 @@ export class ConnectionHandler {
|
||||
this.dispose.untrack(this.watcher).dispose();
|
||||
}
|
||||
this.watcher = this.dispose.track(
|
||||
sources.jsSource.createEvaluationWatcher(
|
||||
newExpression,
|
||||
{
|
||||
preferredDataExtractor: oldPreferredDataExtractor,
|
||||
}
|
||||
)
|
||||
dataSource.createEvaluationWatcher(newExpression, {
|
||||
preferredDataExtractor: oldPreferredDataExtractor,
|
||||
})
|
||||
);
|
||||
},
|
||||
openInBrowser: async ({}) => {
|
||||
@@ -96,7 +92,7 @@ export class ConnectionHandler {
|
||||
getCompletions: async ({ text, column }) => {
|
||||
throwIfNotAuthenticated();
|
||||
|
||||
const completions = await sources.jsSource.getCompletions(
|
||||
const completions = await dataSource.getCompletions(
|
||||
text,
|
||||
column
|
||||
);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
DataExtractionProviderFactory,
|
||||
DataExtractionProvider,
|
||||
} from "./DataExtractionProvider";
|
||||
import { VsCodeDebugSession } from "../../VsCodeDebugger";
|
||||
|
||||
export class ComposedDataExtractionProviderFactory
|
||||
implements DataExtractionProviderFactory {
|
||||
constructor(public readonly factories: DataExtractionProviderFactory[]) {}
|
||||
|
||||
createDataExtractionProvider(
|
||||
session: VsCodeDebugSession
|
||||
): DataExtractionProvider | undefined {
|
||||
for (const f of this.factories) {
|
||||
const provider = f.createDataExtractionProvider(session);
|
||||
if (provider) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { DataExtractionResult } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { VsCodeDebugSession } from "../../VsCodeDebugger";
|
||||
|
||||
export interface DataExtractionProviderFactory {
|
||||
createDataExtractionProvider(
|
||||
session: VsCodeDebugSession
|
||||
): DataExtractionProvider | undefined;
|
||||
}
|
||||
|
||||
export interface DataExtractionProvider {
|
||||
evaluate(
|
||||
args: DataExtractionProviderArgs
|
||||
): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: string }
|
||||
>;
|
||||
}
|
||||
|
||||
export interface DataExtractionProviderArgs {
|
||||
expression: string;
|
||||
preferredExtractor: string | undefined;
|
||||
frameId: number | undefined;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
DataResult,
|
||||
DataExtractionResult,
|
||||
ExtractedData,
|
||||
isExtractedData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { VsCodeDebugSession } from "../../VsCodeDebugger";
|
||||
import {
|
||||
DataExtractionProviderFactory,
|
||||
DataExtractionProvider,
|
||||
DataExtractionProviderArgs,
|
||||
} from "./DataExtractionProvider";
|
||||
|
||||
export class GenericDataExtractionProviderFactory
|
||||
implements DataExtractionProviderFactory {
|
||||
createDataExtractionProvider(
|
||||
session: VsCodeDebugSession
|
||||
): DataExtractionProvider | undefined {
|
||||
return new GenericDataExtractionProvider(session);
|
||||
}
|
||||
}
|
||||
|
||||
class GenericDataExtractionProvider implements DataExtractionProvider {
|
||||
constructor(private readonly session: VsCodeDebugSession) {}
|
||||
|
||||
public async evaluate({
|
||||
expression,
|
||||
preferredExtractor,
|
||||
frameId,
|
||||
}: DataExtractionProviderArgs): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: string }
|
||||
> {
|
||||
try {
|
||||
const reply = await this.session.evaluate({
|
||||
expression,
|
||||
frameId,
|
||||
});
|
||||
const resultStr = reply.result;
|
||||
const jsonData = resultStr.substr(1, resultStr.length - 2);
|
||||
const result = JSON.parse(jsonData) as ExtractedData;
|
||||
|
||||
if (!isExtractedData(result)) {
|
||||
throw new Error("Invalid Data");
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "data",
|
||||
result: {
|
||||
availableExtractors: [],
|
||||
usedExtractor: {
|
||||
id: "generic" as any,
|
||||
name: "Generic",
|
||||
priority: 1,
|
||||
},
|
||||
data: result,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
kind: "error",
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
getExpressionForDataExtractorApi,
|
||||
DataResult,
|
||||
ApiHasNotBeenInitializedCode,
|
||||
getExpressionToInitializeDataExtractorApi,
|
||||
DataExtractionResult,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { VsCodeDebugSession } from "../../VsCodeDebugger";
|
||||
import {
|
||||
DataExtractionProviderFactory,
|
||||
DataExtractionProvider,
|
||||
DataExtractionProviderArgs,
|
||||
} from "./DataExtractionProvider";
|
||||
|
||||
export class JsDataExtractionProviderFactory
|
||||
implements DataExtractionProviderFactory {
|
||||
createDataExtractionProvider(
|
||||
session: VsCodeDebugSession
|
||||
): DataExtractionProvider | undefined {
|
||||
const supportedDebugAdapters = [
|
||||
"node",
|
||||
"node2",
|
||||
"extensionHost",
|
||||
"chrome",
|
||||
];
|
||||
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
|
||||
return new JsDataEvaluator(session);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class JsDataEvaluator implements DataExtractionProvider {
|
||||
constructor(private readonly session: VsCodeDebugSession) {}
|
||||
|
||||
public async evaluate({
|
||||
expression,
|
||||
preferredExtractor,
|
||||
frameId,
|
||||
}: DataExtractionProviderArgs): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: string }
|
||||
> {
|
||||
while (true) {
|
||||
try {
|
||||
const preferredExtractorExpr = preferredExtractor
|
||||
? `"${preferredExtractor}"`
|
||||
: "undefined";
|
||||
|
||||
const body = `${getExpressionForDataExtractorApi()}.getData(
|
||||
e => (${expression}),
|
||||
expr => eval(expr),
|
||||
${preferredExtractorExpr}
|
||||
)`;
|
||||
|
||||
const wrappedExpr = `
|
||||
(() => {
|
||||
try {
|
||||
return ${body};
|
||||
} catch (e) {
|
||||
return JSON.stringify({
|
||||
kind: "Error",
|
||||
message: e.message,
|
||||
stack: e.stack
|
||||
});
|
||||
}
|
||||
})()
|
||||
`;
|
||||
|
||||
const reply = await this.session.evaluate({
|
||||
expression: wrappedExpr,
|
||||
frameId,
|
||||
});
|
||||
const resultStr = reply.result;
|
||||
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);
|
||||
} else if (result.kind === "Data") {
|
||||
return {
|
||||
kind: "data",
|
||||
result: result.extractionResult,
|
||||
};
|
||||
} else {
|
||||
throw new Error("Invalid Data");
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = error.message as string | undefined;
|
||||
if (msg && msg.includes(ApiHasNotBeenInitializedCode)) {
|
||||
if (await this.initializeApi(frameId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "error",
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeApi(frameId: number | undefined): Promise<boolean> {
|
||||
try {
|
||||
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
|
||||
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
|
||||
|
||||
await this.session.evaluate({
|
||||
expression,
|
||||
frameId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
extension/src/DataSource/DataExtractionProvider/index.ts
Normal file
12
extension/src/DataSource/DataExtractionProvider/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from "./DataExtractionProvider";
|
||||
|
||||
import { ComposedDataExtractionProviderFactory } from "./ComposedDataExtractionProviderFactory";
|
||||
import { JsDataExtractionProviderFactory } from "./JsDataExtractionProviderFactory";
|
||||
import { GenericDataExtractionProviderFactory } from "./GenericDataExtractionProviderFactory";
|
||||
|
||||
export const defaultDataExtractionProviderFactory = new ComposedDataExtractionProviderFactory(
|
||||
[
|
||||
new JsDataExtractionProviderFactory(),
|
||||
new GenericDataExtractionProviderFactory(),
|
||||
]
|
||||
);
|
||||
@@ -4,30 +4,27 @@ import {
|
||||
CompletionItem,
|
||||
} from "@hediet/debug-visualizer-vscode-shared";
|
||||
|
||||
export interface EvaluationWatcherOptions {
|
||||
preferredDataExtractor?: DataExtractorId | undefined;
|
||||
}
|
||||
|
||||
export interface DataSource {
|
||||
createEvaluationWatcher(
|
||||
expression: string,
|
||||
options: EvaluationWatcherOptions
|
||||
): EvaluationWatcher;
|
||||
|
||||
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
|
||||
}
|
||||
|
||||
export interface EvaluationWatcherOptions {
|
||||
preferredDataExtractor?: DataExtractorId | undefined;
|
||||
}
|
||||
|
||||
export interface EvaluationWatcher {
|
||||
readonly expression: string;
|
||||
|
||||
// is observable
|
||||
/** This field is observable */
|
||||
readonly state: DataExtractionState;
|
||||
/** This field is observable */
|
||||
readonly preferredDataExtractor: DataExtractorId | undefined;
|
||||
|
||||
setPreferredDataExtractor(id: DataExtractorId | undefined): void;
|
||||
|
||||
refresh(): void;
|
||||
|
||||
// is observable
|
||||
state: DataExtractionState;
|
||||
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
136
extension/src/DataSource/DataSourceImpl.ts
Normal file
136
extension/src/DataSource/DataSourceImpl.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
DataSource,
|
||||
EvaluationWatcher,
|
||||
EvaluationWatcherOptions,
|
||||
} from "./DataSource";
|
||||
import { observable, autorun, action } from "mobx";
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import {
|
||||
DataExtractionState,
|
||||
CompletionItem,
|
||||
} from "@hediet/debug-visualizer-vscode-shared";
|
||||
import { hotClass } from "@hediet/node-reload";
|
||||
import { VsCodeDebuggerView } from "../VsCodeDebugger";
|
||||
import { DataExtractionProviderFactory } from "./DataExtractionProvider/DataExtractionProvider";
|
||||
|
||||
@hotClass(module)
|
||||
export class DataSourceImpl implements DataSource {
|
||||
public readonly dispose = Disposable.fn();
|
||||
private readonly watchers = new Set<ObservableEvaluationWatcher>();
|
||||
|
||||
constructor(
|
||||
private readonly vsCodeDebuggerView: VsCodeDebuggerView,
|
||||
private readonly dataExtractionProviderFactory: DataExtractionProviderFactory
|
||||
) {
|
||||
this.dispose.track({
|
||||
dispose: autorun(() => {
|
||||
if (
|
||||
vsCodeDebuggerView.activeDebugSession &&
|
||||
vsCodeDebuggerView.activeFrameId !== undefined
|
||||
) {
|
||||
for (const w of this.watchers) {
|
||||
w.refresh();
|
||||
}
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
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 = this.vsCodeDebuggerView.activeDebugSession;
|
||||
if (!session) {
|
||||
w._state = { kind: "noDebugSession" };
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = this.vsCodeDebuggerView.activeFrameId;
|
||||
|
||||
w._state = { kind: "loading" };
|
||||
|
||||
const extractionProvider = this.dataExtractionProviderFactory.createDataExtractionProvider(
|
||||
session
|
||||
);
|
||||
if (!extractionProvider) {
|
||||
w._state = {
|
||||
kind: "error",
|
||||
message: `The debug adapter "${session.session.type}" is not supported.`,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await extractionProvider.evaluate({
|
||||
expression: w.expression,
|
||||
frameId,
|
||||
preferredExtractor: w.preferredDataExtractor,
|
||||
});
|
||||
|
||||
w._state = result;
|
||||
}
|
||||
|
||||
public async getCompletions(
|
||||
text: string,
|
||||
column: number
|
||||
): Promise<CompletionItem[]> {
|
||||
const session = this.vsCodeDebuggerView.activeDebugSession;
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
return await session.getCompletions({
|
||||
text,
|
||||
frameId: this.vsCodeDebuggerView.activeFrameId,
|
||||
column,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableEvaluationWatcher implements EvaluationWatcher {
|
||||
constructor(
|
||||
public readonly expression: string,
|
||||
private readonly source: DataSourceImpl,
|
||||
options: EvaluationWatcherOptions
|
||||
) {
|
||||
this._preferredDataExtractor = options.preferredDataExtractor;
|
||||
}
|
||||
|
||||
@observable
|
||||
private _preferredDataExtractor: DataExtractorId | undefined = undefined;
|
||||
|
||||
public get preferredDataExtractor(): DataExtractorId | undefined {
|
||||
return this._preferredDataExtractor;
|
||||
}
|
||||
|
||||
@action
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
import {
|
||||
DataSource,
|
||||
EvaluationWatcher,
|
||||
EvaluationWatcherOptions,
|
||||
} from "./DataSource";
|
||||
import { observable, autorun, action } from "mobx";
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import {
|
||||
getExpressionForDataExtractorApi,
|
||||
DataResult,
|
||||
DataExtractorId,
|
||||
ApiHasNotBeenInitializedCode,
|
||||
getExpressionToInitializeDataExtractorApi,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import {
|
||||
DataExtractionState,
|
||||
CompletionItem,
|
||||
} from "@hediet/debug-visualizer-vscode-shared";
|
||||
import { hotClass } from "@hediet/node-reload";
|
||||
import { VsCodeDebuggerView, VsCodeDebugSession } from "../VsCodeDebugger";
|
||||
|
||||
export function createJsDebuggerSource(args: {
|
||||
vsCodeDebuggerView: VsCodeDebuggerView;
|
||||
}): JsDataSource {
|
||||
return new JsDebuggerSourceImplementation(args.vsCodeDebuggerView);
|
||||
}
|
||||
|
||||
export interface JsCode extends String {
|
||||
__brand: "JsCode";
|
||||
}
|
||||
|
||||
export interface JsDataSource extends DataSource {
|
||||
registerDataExtractor(classExpression: JsCode): void;
|
||||
}
|
||||
|
||||
@hotClass(module)
|
||||
export class JsDebuggerSourceImplementation implements JsDataSource {
|
||||
public readonly dispose = Disposable.fn();
|
||||
private readonly watchers = new Set<ObservableEvaluationWatcher>();
|
||||
|
||||
constructor(private readonly vsCodeDebuggerView: VsCodeDebuggerView) {
|
||||
this.dispose.track({
|
||||
dispose: autorun(() => {
|
||||
if (
|
||||
vsCodeDebuggerView.activeDebugSession &&
|
||||
vsCodeDebuggerView.activeFrameId !== undefined
|
||||
) {
|
||||
for (const w of this.watchers) {
|
||||
w.refresh();
|
||||
}
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
registerDataExtractor(classExpression: JsCode): void {
|
||||
// TODO implement
|
||||
}
|
||||
|
||||
public async getCompletions(
|
||||
text: string,
|
||||
column: number
|
||||
): Promise<CompletionItem[]> {
|
||||
const session = this.vsCodeDebuggerView.activeDebugSession;
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
return await session.getCompletions({
|
||||
text,
|
||||
frameId: this.vsCodeDebuggerView.activeFrameId,
|
||||
column,
|
||||
});
|
||||
}
|
||||
|
||||
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 = this.vsCodeDebuggerView.activeDebugSession;
|
||||
if (!session) {
|
||||
w._state = { kind: "noDebugSession" };
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = this.vsCodeDebuggerView.activeFrameId;
|
||||
|
||||
try {
|
||||
w._state = { kind: "loading" };
|
||||
|
||||
const preferredExtractor = w.preferredDataExtractor
|
||||
? `"${w.preferredDataExtractor}"`
|
||||
: "undefined";
|
||||
|
||||
const body = `${getExpressionForDataExtractorApi()}.getData(
|
||||
e => (${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.evaluate({
|
||||
expression,
|
||||
frameId,
|
||||
});
|
||||
const resultStr = reply.result;
|
||||
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, frameId)) {
|
||||
await this.refresh(w);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
w._state = {
|
||||
kind: "error",
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeApi(
|
||||
session: VsCodeDebugSession,
|
||||
frameId: number | undefined
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
|
||||
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
|
||||
|
||||
await session.evaluate({
|
||||
expression,
|
||||
frameId,
|
||||
});
|
||||
|
||||
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
|
||||
private _preferredDataExtractor: DataExtractorId | undefined = undefined;
|
||||
|
||||
public get preferredDataExtractor(): DataExtractorId | undefined {
|
||||
return this._preferredDataExtractor;
|
||||
}
|
||||
|
||||
@action
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { VsCodeDebugger, VsCodeDebuggerView } from "../VsCodeDebugger";
|
||||
import { createJsDebuggerSource } from "./JsDebuggerSource";
|
||||
import { DataSourceImpl } from "./DataSourceImpl";
|
||||
import { defaultDataExtractionProviderFactory } from "./DataExtractionProvider";
|
||||
|
||||
export class Sources {
|
||||
public readonly dispose = Disposable.fn();
|
||||
@@ -9,7 +10,8 @@ export class Sources {
|
||||
new VsCodeDebuggerView(this.debugger)
|
||||
);
|
||||
|
||||
public readonly jsSource = createJsDebuggerSource({
|
||||
vsCodeDebuggerView: this.debuggerView,
|
||||
});
|
||||
public readonly dataSource = new DataSourceImpl(
|
||||
this.debuggerView,
|
||||
defaultDataExtractionProviderFactory
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,13 @@ export class Server {
|
||||
const wss = new WebSocket.Server({ server: this.server });
|
||||
wss.on("connection", ws => {
|
||||
const stream = new WebSocketStream(ws);
|
||||
new ConnectionHandler(sources, stream, this, config, this.secret);
|
||||
new ConnectionHandler(
|
||||
sources.dataSource,
|
||||
stream,
|
||||
this,
|
||||
config,
|
||||
this.secret
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user