Version 2.2.0.
* Adds support for custom injected scripts with automatic reload * Refactors extension
This commit is contained in:
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -7,7 +7,11 @@
|
||||
"plantuml.exportOutDir": "docs/exported",
|
||||
"typescript.updateImportsOnFileMove.enabled": "prompt",
|
||||
"mochaExplorer.files": "./data-extraction/test/**/*.test.ts",
|
||||
"mochaExplorer.require": ["ts-node/register", "source-map-support/register"],
|
||||
"mochaExplorer.require": [
|
||||
"ts-node/register",
|
||||
"source-map-support/register"
|
||||
],
|
||||
"tasksStatusbar.taskLabelFilter": "dev",
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hediet/debug-visualizer-data-extraction",
|
||||
"description": "A library that helps implementing data extractors for the Debug Visualizer VS Code extension.",
|
||||
"version": "0.13.1",
|
||||
"version": "0.14.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"author": {
|
||||
|
||||
7
data-extraction/src/js/api/LoadDataExtractorsFn.ts
Normal file
7
data-extraction/src/js/api/LoadDataExtractorsFn.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { DataExtractor } from "./DataExtractorApi";
|
||||
import type * as helpersTypes from "../helpers"
|
||||
|
||||
export type LoadDataExtractorsFn = (
|
||||
register: (extractor: DataExtractor) => void,
|
||||
helpers: typeof helpersTypes
|
||||
) => void;
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
DataExtractor,
|
||||
ExtractionCollector,
|
||||
DataExtractorContext,
|
||||
LoadDataExtractorsFn,
|
||||
} from "..";
|
||||
import * as helpers from "../../helpers";
|
||||
|
||||
export class InjectedExtractor implements DataExtractor {
|
||||
readonly id = "injected-extractor";
|
||||
|
||||
getExtractions(
|
||||
data: unknown,
|
||||
extractionCollector: ExtractionCollector,
|
||||
context: DataExtractorContext
|
||||
): void {
|
||||
const key = "@hediet/debug-visualizer/injectedScripts";
|
||||
const injectedExtractorsFns = Object.values(
|
||||
(global as any)[key]
|
||||
) as LoadDataExtractorsFn[];
|
||||
|
||||
const extractors = new Array<DataExtractor>();
|
||||
|
||||
for (const fn of injectedExtractorsFns) {
|
||||
fn(extractor => {
|
||||
extractors.push(extractor);
|
||||
}, helpers);
|
||||
}
|
||||
|
||||
for (const extractor of extractors) {
|
||||
extractor.getExtractions(data, extractionCollector, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { getDataExtractorApi } from "../injection";
|
||||
import { GridExtractor } from "./GridExtractor";
|
||||
import { TableDataExtractor } from "./TableExtractor";
|
||||
import { StringDiffExtractor } from "./StringDiffExtractor";
|
||||
import { find } from "../../helpers";
|
||||
import { InjectedExtractor } from "./InjectedExtractor";
|
||||
|
||||
/**
|
||||
* The default data extractors should be registered by VS Code automatically.
|
||||
@@ -28,101 +28,8 @@ export function registerDefaultExtractors(
|
||||
new GridExtractor(),
|
||||
new TableDataExtractor(),
|
||||
new StringDiffExtractor(),
|
||||
new InjectedExtractor(),
|
||||
]) {
|
||||
api.registerExtractor(item);
|
||||
}
|
||||
|
||||
function isRange(
|
||||
value: unknown
|
||||
): value is {
|
||||
startColumn: number;
|
||||
startLineNumber: number;
|
||||
endColumn: number;
|
||||
endLineNumber: number;
|
||||
} {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
!!value &&
|
||||
"startColumn" in value &&
|
||||
"startLineNumber" in value
|
||||
);
|
||||
}
|
||||
|
||||
function isPosition(
|
||||
value: unknown
|
||||
): value is { column: number; lineNumber: number } {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
!!value &&
|
||||
"column" in value &&
|
||||
"lineNumber" in value
|
||||
);
|
||||
}
|
||||
|
||||
// hedietDbgVis.find(x => x.constructor.name === 'TextModel').getValue()
|
||||
api.registerExtractor({
|
||||
id: "positionInTextModel",
|
||||
getExtractions(data, collector, context) {
|
||||
let range: {
|
||||
start: {
|
||||
line: number;
|
||||
column: number;
|
||||
};
|
||||
end: {
|
||||
line: number;
|
||||
column: number;
|
||||
};
|
||||
};
|
||||
if (isRange(data)) {
|
||||
range = {
|
||||
start: {
|
||||
line: data.startLineNumber - 1,
|
||||
column: data.startColumn - 1,
|
||||
},
|
||||
end: {
|
||||
line: data.endLineNumber - 1,
|
||||
column: data.endColumn - 1,
|
||||
},
|
||||
};
|
||||
} else if (isPosition(data)) {
|
||||
range = {
|
||||
start: {
|
||||
line: data.lineNumber - 1,
|
||||
column: data.column - 1,
|
||||
},
|
||||
end: {
|
||||
line: data.lineNumber - 1,
|
||||
column: data.column - 1,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRange(data) && !isPosition(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textModel: any = find(
|
||||
x =>
|
||||
typeof x === "object" &&
|
||||
!!x &&
|
||||
"constructor" in x &&
|
||||
(x as any).constructor.name === "TextModel"
|
||||
);
|
||||
|
||||
collector.addExtraction({
|
||||
id: "positionInTextModel",
|
||||
name: "Position In TextModel",
|
||||
extractData() {
|
||||
return {
|
||||
kind: { text: true },
|
||||
text: textModel.getValue(),
|
||||
decorations: [{ range }],
|
||||
};
|
||||
},
|
||||
priority: 1000,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./DataExtractorApi";
|
||||
export * from "./injection";
|
||||
export { DataExtractorApiImpl } from "./DataExtractorApiImpl";
|
||||
export * from "./LoadDataExtractorsFn";
|
||||
|
||||
@@ -20,16 +20,18 @@ export function getExpressionToInitializeDataExtractorApi(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns standalone JS code representing an expression returns the data extraction API.
|
||||
* Returns standalone JS code representing an expression that returns the data extraction API.
|
||||
* This expression returns an object of type `DataExtractorApi`.
|
||||
* If the API must be initialized with `getExpressionToInitializeDataExtractorApi()`,
|
||||
* it throws an exception containing the text of `ApiHasNotBeenInitializedCode`.
|
||||
* This function is called in the VS Code extension, the expression is evaluated in the debugee.
|
||||
*/
|
||||
export function getExpressionForDataExtractorApi(): string {
|
||||
return `((${selfContainedGetInitializedDataExtractorApi.toString()})())`;
|
||||
}
|
||||
|
||||
export function getExpressionToDetectDataExtractorApiPresence(): string {
|
||||
return `((${selfContainedIsDataExtractorApiInitialized.toString()})())`;
|
||||
}
|
||||
|
||||
const apiKey = "@hediet/data-extractor-api/v2";
|
||||
|
||||
export function getDataExtractorApi(): DataExtractorApi {
|
||||
@@ -43,10 +45,15 @@ export function getDataExtractorApi(): DataExtractorApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* This code is used to detect if the API has not been initialized yet.
|
||||
* @internal
|
||||
*/
|
||||
export const ApiHasNotBeenInitializedCode = "EgH0cybXij1jYUozyakO" as const;
|
||||
function selfContainedIsDataExtractorApiInitialized(): boolean {
|
||||
const globalObj =
|
||||
typeof window === "object" ? (window as any) : (global as any);
|
||||
const key: typeof apiKey = "@hediet/data-extractor-api/v2";
|
||||
let api: DataExtractorApi | undefined = globalObj[key];
|
||||
return api !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -57,11 +64,7 @@ function selfContainedGetInitializedDataExtractorApi(): DataExtractorApi {
|
||||
const key: typeof apiKey = "@hediet/data-extractor-api/v2";
|
||||
let api: DataExtractorApi | undefined = globalObj[key];
|
||||
if (!api) {
|
||||
const code: typeof ApiHasNotBeenInitializedCode =
|
||||
"EgH0cybXij1jYUozyakO";
|
||||
throw new Error(
|
||||
`Data Extractor API has not been initialized. Code: ${code}`
|
||||
);
|
||||
throw new Error(`Data Extractor API has not been initialized.`);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
6
demos/js/.vscode/settings.json
vendored
6
demos/js/.vscode/settings.json
vendored
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"editor.minimap.enabled": false,
|
||||
"git.enabled": false
|
||||
"git.enabled": false,
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"debugVisualizer.js.customScriptPaths": [
|
||||
"${workspaceFolder}/custom-visualizer.js",
|
||||
]
|
||||
}
|
||||
152
demos/js/custom-visualizer.js
Normal file
152
demos/js/custom-visualizer.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* @type {import("@hediet/debug-visualizer-data-extraction").LoadDataExtractorsFn}
|
||||
*/
|
||||
module.exports = (register, helpers) => {
|
||||
register({
|
||||
id: "map",
|
||||
getExtractions(data, collector, context) {
|
||||
if (!(data instanceof Map)) {
|
||||
return;
|
||||
}
|
||||
|
||||
collector.addExtraction({
|
||||
priority: 1000,
|
||||
id: "map",
|
||||
name: "Map",
|
||||
extractData() {
|
||||
return helpers.asData({
|
||||
kind: { table: true },
|
||||
rows: [...data].map(([k, v]) => ({ key: k, value: v }))
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
register({
|
||||
id: "binaryViewer",
|
||||
getExtractions(data, collector, context) {
|
||||
if (typeof data !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
collector.addExtraction({
|
||||
priority: 100,
|
||||
id: "binary",
|
||||
name: "Bits of Integer",
|
||||
extractData() {
|
||||
return helpers.asData({
|
||||
kind: { text: true },
|
||||
text: data.toString(2),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
(register, helpers) => {
|
||||
register({
|
||||
id: "positionOrRangeInTextModel",
|
||||
getExtractions(data, collector, context) {
|
||||
collector.addExtraction({
|
||||
priority: 100,
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
extractData() {
|
||||
return helpers.asData({
|
||||
kind: { text: true },
|
||||
text: Object.keys(context.variablesInScope).join(", "),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
register({
|
||||
id: "positionOrRangeInTextModel",
|
||||
getExtractions(data, collector, context) {
|
||||
/** @type {{ start: { line: number; column: number; }; end: { line: number; column: number; }; }} */
|
||||
let range;
|
||||
if (isRange(data)) {
|
||||
range = {
|
||||
start: {
|
||||
line: data.startLineNumber - 1,
|
||||
column: data.startColumn - 1,
|
||||
},
|
||||
end: {
|
||||
line: data.endLineNumber - 1,
|
||||
column: data.endColumn - 1,
|
||||
},
|
||||
};
|
||||
} else if (isPosition(data)) {
|
||||
range = {
|
||||
start: {
|
||||
line: data.lineNumber - 1,
|
||||
column: data.column - 1,
|
||||
},
|
||||
end: { line: data.lineNumber - 1, column: data.column - 1 },
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRange(data) && !isPosition(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {any} */
|
||||
const textModel = helpers.find(
|
||||
x =>
|
||||
typeof x === "object" &&
|
||||
!!x &&
|
||||
"constructor" in x &&
|
||||
x.constructor.name === "TextModel"
|
||||
);
|
||||
|
||||
collector.addExtraction({
|
||||
id: "positionOrRangeInTextModel",
|
||||
name: "Position/Range In TextModel",
|
||||
extractData() {
|
||||
return helpers.asData({
|
||||
kind: { text: true },
|
||||
text: textModel.getValue(),
|
||||
decorations: [{ range }],
|
||||
});
|
||||
},
|
||||
priority: 1000,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {value is { startColumn: number; startLineNumber: number; endColumn: number; endLineNumber: number; }}
|
||||
*/
|
||||
function isRange(value) {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
!!value &&
|
||||
"startColumn" in value &&
|
||||
"startLineNumber" in value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {value is { column: number; lineNumber: number }}
|
||||
*/
|
||||
function isPosition(value) {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
!!value &&
|
||||
"column" in value &&
|
||||
"lineNumber" in value
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,9 @@ setTimeout(() => {
|
||||
}, 0);
|
||||
|
||||
class Main {
|
||||
/** @pure */
|
||||
run() {
|
||||
id = 0;
|
||||
const head = new DoublyLinkedListNode("1");
|
||||
head.setNext(new DoublyLinkedListNode("2"));
|
||||
head.next!.setNext(new DoublyLinkedListNode("3"));
|
||||
@@ -34,9 +36,9 @@ function reverse(list: DoublyLinkedList) {
|
||||
i => ({
|
||||
id: i.id,
|
||||
label: i.name,
|
||||
color: finished.has(i) ? "lime" : undefined,
|
||||
color: finished.has(i) ? "lime" : "lightblue",
|
||||
edges: [
|
||||
{ to: i.next!, label: "next" },
|
||||
{ to: i.next!, label: "next", color: "lightblue", },
|
||||
{ to: i.prev!, label: "prev", color: "lightgray" },
|
||||
].filter(r => !!r.to),
|
||||
}));
|
||||
|
||||
@@ -13,9 +13,13 @@ hedietDbgVis.markedGrid(
|
||||
*/
|
||||
|
||||
// From https://github.com/AvraamMavridis/Algorithms-Data-Structures-in-Typescript/blob/master/algorithms/quickSort.md
|
||||
const array = [1, 2, 33, 31, 1, 2, 63, 123, 6, 32, 943, 346, 24];
|
||||
const sorted = quickSort(array, 0, array.length - 1);
|
||||
console.log(sorted);
|
||||
/** @pure */
|
||||
function main() {
|
||||
const array = [1, 2, 33, 31, 1, 2, 63, 123, 6, 32, 943, 346, 24];
|
||||
const sorted = quickSort(array, 0, array.length - 1);
|
||||
console.log(sorted);
|
||||
}
|
||||
main();
|
||||
|
||||
function swap(array: Array<number>, i: number, j: number) {
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
|
||||
@@ -271,6 +271,11 @@ fsevents@^2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a"
|
||||
integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
get-func-name@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
|
||||
@@ -296,6 +301,13 @@ has-flag@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
|
||||
|
||||
has@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
|
||||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
io-ts@^1.8.6:
|
||||
version "1.10.4"
|
||||
resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-1.10.4.tgz#cd5401b138de88e4f920adbcb7026e2d1967e6e2"
|
||||
@@ -310,6 +322,13 @@ is-binary-path@^2.1.0:
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-core-module@^2.2.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1"
|
||||
integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==
|
||||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
@@ -378,6 +397,11 @@ normalize-path@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
path-parse@^1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-to-regexp@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
|
||||
@@ -407,6 +431,14 @@ readdirp@^3.1.1:
|
||||
dependencies:
|
||||
picomatch "^2.0.4"
|
||||
|
||||
resolve@>=1.9.0:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
||||
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
|
||||
dependencies:
|
||||
is-core-module "^2.2.0"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
sinon@^7.2.7:
|
||||
version "7.3.2"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.3.2.tgz#82dba3a6d85f6d2181e1eca2c10d8657c2161f28"
|
||||
@@ -452,15 +484,22 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
ttypescript@^1.5.12:
|
||||
version "1.5.12"
|
||||
resolved "https://registry.yarnpkg.com/ttypescript/-/ttypescript-1.5.12.tgz#27a8356d7d4e719d0075a8feb4df14b52384f044"
|
||||
integrity sha512-1ojRyJvpnmgN9kIHmUnQPlEV1gq+VVsxVYjk/NfvMlHSmYxjK5hEvOOU2MQASrbekTUiUM7pR/nXeCc8bzvMOQ==
|
||||
dependencies:
|
||||
resolve ">=1.9.0"
|
||||
|
||||
type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||
|
||||
typescript@^3.8.2:
|
||||
version "3.9.7"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
|
||||
integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
|
||||
typescript@^3.9.0:
|
||||
version "3.9.9"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674"
|
||||
integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==
|
||||
|
||||
ws@^7.0.0:
|
||||
version "7.1.1"
|
||||
|
||||
BIN
docs/custom-script-map.png
Normal file
BIN
docs/custom-script-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
## 2.2.0
|
||||
|
||||
- Adds setting `debugVisualizer.js.customScriptPaths` to inject custom scripts when debugging JavaScript. Such scripts are reloaded automatically when changed.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
- Adds text diff and text decoration viewer.
|
||||
|
||||
@@ -16,18 +16,18 @@ Click [here](https://hediet.github.io/visualization/) to explore all available v
|
||||
See [demos](../demos/) for demos. These languages and debuggers are verified to work with this extension:
|
||||
|
||||
- JavaScript/TypeScript/... using `node`/`node2`/`extensionHost`/`chrome`/`pwa-chrome`/`pwa-node` debug adapter: [⭐ Full Support](../demos/js)
|
||||
- Dart/Flutter using `dart` debug adapter: [✅ Rudimentary Support](../demos/dart)
|
||||
- Go using `go` (Delve) debug adapter: [✅ Rudimentary Support](../demos/golang)
|
||||
- Python using `python` debug adapter: [✅ Rudimentary Support](../demos/python)
|
||||
- C# using `coreclr` debug adapter: [✅ Rudimentary Support](../demos/csharp) (work in progress for Full Support)
|
||||
- PHP using `php` debug adapter: [✅ Rudimentary Support](../demos/php)
|
||||
- Java using `java` debug adapter: [✅ Rudimentary Support](../demos/java)
|
||||
- C++ using `cppdbg` debug adapter: [✅ Rudimentary Support](../demos/cpp)
|
||||
- Swift using `lldb` debug adapter: [✅ Rudimentary Support](../demos/swift)
|
||||
- Rust using `lldb` debug adapter: [✅ Rudimentary Support](../demos/rust)
|
||||
- Dart/Flutter using `dart` debug adapter: [✅ Basic Support](../demos/dart)
|
||||
- Go using `go` (Delve) debug adapter: [✅ Basic Support](../demos/golang)
|
||||
- Python using `python` debug adapter: [✅ Basic Support](../demos/python)
|
||||
- C# using `coreclr` debug adapter: [✅ Basic Support](../demos/csharp) (work in progress for Full Support)
|
||||
- PHP using `php` debug adapter: [✅ Basic Support](../demos/php)
|
||||
- Java using `java` debug adapter: [✅ Basic Support](../demos/java)
|
||||
- C++ using `cppdbg` debug adapter: [✅ Basic Support](../demos/cpp)
|
||||
- Swift using `lldb` debug adapter: [✅ Basic Support](../demos/swift)
|
||||
- Rust using `lldb` debug adapter: [✅ Basic Support](../demos/rust)
|
||||
|
||||
All other languages and debuggers might work too.
|
||||
For languages with _Rudimentary Support_, only JSON strings can be visualized - you must implement some logic that builds this JSON for your data structure!
|
||||
For languages with _Basic Support_, only JSON strings can be visualized - you must implement some logic that builds this JSON for your data structure!
|
||||
Fully supported languages offer _Data Extractors_ which convert some well known data structures to json.
|
||||
|
||||
## Usage
|
||||
@@ -132,6 +132,49 @@ This extension provides these configuration options:
|
||||
|
||||
Specifies whether to pop out Debug Visualization Views with Chrome in Kiosk Mode. Uses the default browser otherwise or if Chrome is not found. Defaults to `true`.
|
||||
|
||||
- `debugVisualizer.js.customScriptPaths`
|
||||
|
||||
Specifies a list of JavaScript files that are injected into the debugee when debugging JavaScript.
|
||||
Each script must assign `module.exports` with a function of type `import("@hediet/debug-visualizer-data-extraction").LoadDataExtractorsFn`.
|
||||
Paths must be absolute and can use the variable `${workspaceFolder}`.
|
||||
Scripts are automatically reloaded when they are changed.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
// @ts-check
|
||||
/**
|
||||
* @type {import("@hediet/debug-visualizer-data-extraction").LoadDataExtractorsFn}
|
||||
*/
|
||||
module.exports = (register, helpers) => {
|
||||
register({
|
||||
id: "map",
|
||||
getExtractions(data, collector, context) {
|
||||
if (!(data instanceof Map)) {
|
||||
return;
|
||||
}
|
||||
|
||||
collector.addExtraction({
|
||||
priority: 1000,
|
||||
id: "map",
|
||||
name: "Map",
|
||||
extractData() {
|
||||
return helpers.asData({
|
||||
kind: { table: true },
|
||||
rows: [...data].map(([k, v]) => ({
|
||||
key: k,
|
||||
value: v,
|
||||
})),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||

|
||||
|
||||
# See Also
|
||||
|
||||
This extension works very well together with my library [`@hediet/node-reload`](https://github.com/hediet/node-reload) for TypeScript/JavaScript.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"displayName": "Debug Visualizer",
|
||||
"description": "A visual watch window that lets you visualize your data structures while debugging.",
|
||||
"icon": "docs/logo.drawio.png",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"license": "GPL-3.0",
|
||||
"engines": {
|
||||
"vscode": "^1.46.0"
|
||||
@@ -41,6 +41,11 @@
|
||||
"onDebug",
|
||||
"onWebviewPanel:debugVisualizer"
|
||||
],
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
"supported": false
|
||||
}
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
@@ -63,6 +68,14 @@
|
||||
"configuration": {
|
||||
"title": "Debug Visualizer",
|
||||
"properties": {
|
||||
"debugVisualizer.js.customScriptPaths": {
|
||||
"type": "array",
|
||||
"description": "Defines JavaScript sources that are injected into the debuggee.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "The file path to a custom js script. Must be absolute. You can use ${workspaceFolder}."
|
||||
}
|
||||
},
|
||||
"debugVisualizer.useChromeKioskMode": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
|
||||
@@ -21,6 +21,29 @@ export class Config {
|
||||
{ serializer: serializerWithDefault<DebugAdapterConfigs>({}) }
|
||||
);
|
||||
|
||||
private readonly _customScriptPaths = new VsCodeSetting(
|
||||
"debugVisualizer.js.customScriptPaths",
|
||||
{ serializer: serializerWithDefault<string[]>([]) }
|
||||
);
|
||||
|
||||
public get customScriptPaths(): string[] {
|
||||
return this._customScriptPaths.get().map(p => {
|
||||
const tpl = new SimpleTemplate(p);
|
||||
return tpl.render({
|
||||
workspaceFolder: () => {
|
||||
const workspaceFolder = (workspace.workspaceFolders ||
|
||||
[])[0];
|
||||
if (!workspaceFolder) {
|
||||
throw new Error(
|
||||
`Cannot get workspace folder - '${p}' cannot be evaluated!`
|
||||
);
|
||||
}
|
||||
return workspaceFolder.uri.fsPath;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@observable
|
||||
private _vsCodeTheme = window.activeColorTheme;
|
||||
|
||||
@@ -85,3 +108,13 @@ function evaluateTemplate(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class SimpleTemplate {
|
||||
constructor(private readonly str: string) {}
|
||||
|
||||
render(data: Record<string, () => string>): string {
|
||||
return this.str.replace(/\$\{([a-zA-Z0-9]+)\}/g, (substr, grp1) => {
|
||||
return data[grp1]();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { EvaluationEngine, Evaluator } from "./EvaluationEngine";
|
||||
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
|
||||
|
||||
export class ComposedEvaluationEngine implements EvaluationEngine {
|
||||
constructor(public readonly engines: EvaluationEngine[]) {}
|
||||
|
||||
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined {
|
||||
for (const f of this.engines) {
|
||||
const evaluator = f.createEvaluator(session);
|
||||
if (evaluator) {
|
||||
return evaluator;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
|
||||
import { EvaluationEngine, Evaluator } from "./EvaluationEngine";
|
||||
import { Config, DebugAdapterConfig } from "../../Config";
|
||||
import { GenericEvaluator } from "./GenericEvaluationEngine";
|
||||
import { registerUpdateReconciler, hotClass } from "@hediet/node-reload";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
@hotClass(module)
|
||||
export class ConfiguredEvaluationEngine implements EvaluationEngine {
|
||||
constructor(private readonly config: Config) {}
|
||||
|
||||
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined {
|
||||
const config = this.config.getDebugAdapterConfig(session.session.type);
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
return new ConfiguredEvaluator(session, config);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfiguredEvaluator extends GenericEvaluator {
|
||||
constructor(
|
||||
session: EnhancedDebugSession,
|
||||
private readonly config: DebugAdapterConfig
|
||||
) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
protected getContext() {
|
||||
return this.config.context;
|
||||
}
|
||||
|
||||
protected getFinalExpression({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
}: {
|
||||
expression: string;
|
||||
preferredExtractorId: DataExtractorId | undefined;
|
||||
}) {
|
||||
return this.config.getFinalExpression({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
DataExtractionResult,
|
||||
DataExtractorId,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
|
||||
import { FormattedMessage } from "../../webviewContract";
|
||||
|
||||
export interface EvaluationEngine {
|
||||
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined;
|
||||
}
|
||||
|
||||
export interface Evaluator {
|
||||
evaluate(
|
||||
args: EvaluationArgs
|
||||
): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
>;
|
||||
|
||||
/**
|
||||
* The language that expressions must be written in.
|
||||
*/
|
||||
readonly languageId: string | undefined;
|
||||
}
|
||||
|
||||
export interface EvaluationArgs {
|
||||
expression: string;
|
||||
preferredExtractorId: DataExtractorId | undefined;
|
||||
frameId: number | undefined;
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import {
|
||||
getExpressionForDataExtractorApi,
|
||||
DataResult,
|
||||
ApiHasNotBeenInitializedCode,
|
||||
getExpressionToInitializeDataExtractorApi,
|
||||
DataExtractionResult,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
|
||||
import {
|
||||
EvaluationEngine,
|
||||
Evaluator,
|
||||
EvaluationArgs,
|
||||
} from "./EvaluationEngine";
|
||||
import { FormattedMessage } from "../../webviewContract";
|
||||
import { registerUpdateReconciler, hotClass } from "@hediet/node-reload";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
@hotClass(module)
|
||||
export class JsEvaluationEngine implements EvaluationEngine {
|
||||
createEvaluator(session: EnhancedDebugSession): Evaluator | 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 JsEvaluator(session);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class JsEvaluator implements Evaluator {
|
||||
public readonly languageId = "javascript";
|
||||
|
||||
constructor(private readonly session: EnhancedDebugSession) {}
|
||||
|
||||
private getContext(): "copy" | "repl" {
|
||||
if (this.session.session.type.startsWith("pwa-")) {
|
||||
return "copy";
|
||||
}
|
||||
return "repl";
|
||||
}
|
||||
|
||||
public async evaluate({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
frameId,
|
||||
}: EvaluationArgs): Promise<
|
||||
| { kind: "data"; result: DataExtractionResult }
|
||||
| { kind: "error"; message: FormattedMessage }
|
||||
> {
|
||||
const variableNames = new Array<string>();
|
||||
if (frameId) {
|
||||
const scopes = await this.session.getScopes({ frameId });
|
||||
const scopeVariables = await Promise.all(
|
||||
scopes
|
||||
.filter(s => !s.expensive)
|
||||
.map(s =>
|
||||
this.session.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;
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
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.session.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) {
|
||||
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,
|
||||
context: this.getContext(),
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from "./EvaluationEngine";
|
||||
|
||||
export { ComposedEvaluationEngine } from "./ComposedEvaluationEngine";
|
||||
export { JsEvaluationEngine } from "./JsEvaluationEngine";
|
||||
export { GenericEvaluationEngine } from "./GenericEvaluationEngine";
|
||||
export { ConfiguredEvaluationEngine } from "./ConfiguredEvaluationEngine";
|
||||
@@ -1,175 +0,0 @@
|
||||
import {
|
||||
EvaluationWatchService,
|
||||
EvaluationWatcher,
|
||||
EvaluationWatcherOptions,
|
||||
} from "./EvaluationWatchService";
|
||||
import { observable, autorun, action } from "mobx";
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { CancellationTokenSource, CancellationToken } from "vscode";
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DataExtractionState, CompletionItem } from "../webviewContract";
|
||||
import { hotClass } from "@hediet/node-reload";
|
||||
import { VsCodeDebuggerView } from "../debugger/VsCodeDebuggerView";
|
||||
import { EvaluationEngine } from "./EvaluationEngine/EvaluationEngine";
|
||||
import { wait } from "@hediet/std/timer";
|
||||
|
||||
@hotClass(module)
|
||||
export class EvaluationWatchServiceImpl implements EvaluationWatchService {
|
||||
public readonly dispose = Disposable.fn();
|
||||
private readonly watchers = new Set<ObservableEvaluationWatcher>();
|
||||
|
||||
constructor(
|
||||
private readonly vsCodeDebuggerView: VsCodeDebuggerView,
|
||||
private readonly evaluationEngine: EvaluationEngine
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
private lastLanguageId: string | undefined = undefined;
|
||||
|
||||
get languageId(): string | undefined {
|
||||
const session = this.vsCodeDebuggerView.activeDebugSession;
|
||||
if (!session) {
|
||||
return this.lastLanguageId;
|
||||
}
|
||||
const evaluator = this.evaluationEngine.createEvaluator(session);
|
||||
if (!evaluator) {
|
||||
return this.lastLanguageId;
|
||||
}
|
||||
this.lastLanguageId = evaluator.languageId;
|
||||
return evaluator.languageId;
|
||||
}
|
||||
|
||||
private runningRefreshOperation: Disposable | undefined;
|
||||
|
||||
public refresh(w: ObservableEvaluationWatcher): void {
|
||||
if (this.runningRefreshOperation) {
|
||||
this.runningRefreshOperation.dispose();
|
||||
}
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
this.runningRefreshOperation = {
|
||||
dispose: () => {
|
||||
tokenSource.cancel();
|
||||
},
|
||||
};
|
||||
this._refresh(w, tokenSource.token);
|
||||
}
|
||||
|
||||
private async _refresh(
|
||||
w: ObservableEvaluationWatcher,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
const session = this.vsCodeDebuggerView.activeDebugSession;
|
||||
if (!session) {
|
||||
w._state = { kind: "noDebugSession" };
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = this.vsCodeDebuggerView.activeFrameId;
|
||||
|
||||
w._state = { kind: "loading" };
|
||||
|
||||
const evaluator = this.evaluationEngine.createEvaluator(session);
|
||||
if (!evaluator) {
|
||||
w._state = {
|
||||
kind: "error",
|
||||
message: `The debug adapter "${session.session.type}" is not supported.`,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await evaluator.evaluate({
|
||||
expression: w.expression,
|
||||
frameId,
|
||||
preferredExtractorId: w.preferredDataExtractor,
|
||||
});
|
||||
|
||||
if (result.kind === "error") {
|
||||
// give cancellation requested more time in case of errors
|
||||
await wait(330);
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
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: EvaluationWatchServiceImpl,
|
||||
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,2 +0,0 @@
|
||||
export * from "./EvaluationWatchServiceImpl";
|
||||
export * from "./EvaluationWatchService";
|
||||
@@ -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
|
||||
)) || []
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,45 +4,60 @@ import {
|
||||
GraphNode,
|
||||
GraphVisualizationData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession";
|
||||
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
|
||||
import {
|
||||
EvaluationEngine,
|
||||
Evaluator,
|
||||
EvaluationArgs,
|
||||
} from "./EvaluationEngine";
|
||||
DebugSessionVisualizationSupport,
|
||||
VisualizationBackend,
|
||||
GetVisualizationDataArgs,
|
||||
VisualizationBackendBase,
|
||||
} from "./VisualizationBackend";
|
||||
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
|
||||
import { FormattedMessage } from "../../webviewContract";
|
||||
import { FormattedMessage } from "../webviewContract";
|
||||
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
|
||||
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
|
||||
|
||||
registerUpdateReconciler(module);
|
||||
|
||||
@hotClass(module)
|
||||
export class GenericEvaluationEngine implements EvaluationEngine {
|
||||
createEvaluator(session: EnhancedDebugSession): Evaluator | undefined {
|
||||
return new GenericEvaluator(session);
|
||||
export class GenericVisualizationSupport
|
||||
implements DebugSessionVisualizationSupport {
|
||||
constructor(private readonly debuggerView: DebuggerViewProxy) {}
|
||||
|
||||
createBackend(
|
||||
session: DebugSessionProxy
|
||||
): VisualizationBackend | undefined {
|
||||
return new GenericVisualizationBackend(session, this.debuggerView);
|
||||
}
|
||||
}
|
||||
|
||||
export class GenericEvaluator implements Evaluator {
|
||||
public readonly languageId = "text";
|
||||
export class GenericVisualizationBackend extends VisualizationBackendBase {
|
||||
public readonly expressionLanguageId = "text";
|
||||
|
||||
constructor(private readonly session: EnhancedDebugSession) {}
|
||||
constructor(
|
||||
debugSession: DebugSessionProxy,
|
||||
debuggerView: DebuggerViewProxy
|
||||
) {
|
||||
super(debugSession, debuggerView);
|
||||
}
|
||||
|
||||
public async evaluate({
|
||||
public async getVisualizationData({
|
||||
expression,
|
||||
preferredExtractorId,
|
||||
frameId,
|
||||
}: EvaluationArgs): Promise<
|
||||
}: 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 };
|
||||
let reply: { result: string; variablesReference: number };
|
||||
try {
|
||||
reply = await this.session.evaluate({
|
||||
reply = await this.debugSession.evaluate({
|
||||
expression: finalExpression,
|
||||
frameId,
|
||||
context: this.getContext(),
|
||||
@@ -51,7 +66,10 @@ export class GenericEvaluator implements Evaluator {
|
||||
// Use structural information about variables
|
||||
// from the evaluation response if present.
|
||||
if (reply.variablesReference) {
|
||||
const graph = await this.constructGraphFromVariablesReference(reply.result, reply.variablesReference);
|
||||
const graph = await this.constructGraphFromVariablesReference(
|
||||
reply.result,
|
||||
reply.variablesReference
|
||||
);
|
||||
|
||||
return {
|
||||
kind: "data",
|
||||
@@ -64,11 +82,15 @@ export class GenericEvaluator implements Evaluator {
|
||||
},
|
||||
data: graph,
|
||||
},
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return parseEvaluationResultFromGenericDebugAdapter(reply.result, {
|
||||
debugAdapterType: this.session.session.configuration.type,
|
||||
});
|
||||
return parseEvaluationResultFromGenericDebugAdapter(
|
||||
reply.result,
|
||||
{
|
||||
debugAdapterType: this.debugSession.session
|
||||
.configuration.type,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -78,7 +100,7 @@ export class GenericEvaluator implements Evaluator {
|
||||
items: [
|
||||
"An error occurred while evaluating the expression:",
|
||||
error.message,
|
||||
`Used debug adapter: ${this.session.session.configuration.type}`,
|
||||
`Used debug adapter: ${this.debugSession.session.configuration.type}`,
|
||||
{
|
||||
kind: "inlineList",
|
||||
items: [
|
||||
@@ -98,7 +120,7 @@ export class GenericEvaluator implements Evaluator {
|
||||
* 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
|
||||
@@ -108,22 +130,29 @@ export class GenericEvaluator implements Evaluator {
|
||||
rootLabel: string,
|
||||
rootVariablesReference: number,
|
||||
maxDepth: number = 30,
|
||||
maxKnownNodes: number = 50,
|
||||
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: []
|
||||
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,
|
||||
}];
|
||||
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;
|
||||
|
||||
@@ -141,7 +170,9 @@ export class GenericEvaluator implements Evaluator {
|
||||
// The variable is a leaf or an unvisited object: create the node.
|
||||
|
||||
const node: GraphNode = {
|
||||
id: hasChilds ? `${variable.variablesReference}` : `__${variable.label}@${knownCount}__`,
|
||||
id: hasChilds
|
||||
? `${variable.variablesReference}`
|
||||
: `__${variable.label}@${knownCount}__`,
|
||||
label: variable.label,
|
||||
color: variable.depth == 0 ? "lightblue" : undefined,
|
||||
shape: "box",
|
||||
@@ -153,8 +184,15 @@ export class GenericEvaluator implements Evaluator {
|
||||
if (hasChilds) {
|
||||
knownNodeIds[variable.variablesReference] = node.id;
|
||||
|
||||
for (const child of await this.session.getVariables({ variablesReference: variable.variablesReference })) {
|
||||
bfsQueue.push({ source: { id: node.id, name: child.name }, label: child.value, variablesReference: child.variablesReference, depth: variable.depth + 1 });
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +204,11 @@ export class GenericEvaluator implements Evaluator {
|
||||
}
|
||||
|
||||
if (variable.source) {
|
||||
graph.edges.push({ from: variable.source.id, to: nodeId, label: variable.source.name });
|
||||
graph.edges.push({
|
||||
from: variable.source.id,
|
||||
to: nodeId,
|
||||
label: variable.source.name,
|
||||
});
|
||||
}
|
||||
} while (bfsQueue.length > 0 && knownCount <= maxKnownNodes);
|
||||
|
||||
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";
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
DataExtractionResult,
|
||||
isVisualizationData,
|
||||
} from "@hediet/debug-visualizer-data-extraction";
|
||||
import { FormattedMessage } from "../../webviewContract";
|
||||
import { FormattedMessage } from "../webviewContract";
|
||||
|
||||
export interface ParseEvaluationResultContext {
|
||||
debugAdapterType: string;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { DataExtractionState, CompletionItem } from "../webviewContract";
|
||||
|
||||
export interface EvaluationWatchService {
|
||||
createEvaluationWatcher(
|
||||
export interface VisualizationWatchModel {
|
||||
createWatch(
|
||||
expression: string,
|
||||
options: EvaluationWatcherOptions
|
||||
): EvaluationWatcher;
|
||||
options: VisualizationWatchOptions
|
||||
): VisualizationWatch;
|
||||
|
||||
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
|
||||
|
||||
@@ -17,14 +17,17 @@ export interface EvaluationWatchService {
|
||||
readonly languageId: string | undefined;
|
||||
}
|
||||
|
||||
export interface EvaluationWatcherOptions {
|
||||
export interface VisualizationWatchOptions {
|
||||
preferredDataExtractor?: DataExtractorId | undefined;
|
||||
}
|
||||
|
||||
export interface EvaluationWatcher {
|
||||
export interface VisualizationWatch {
|
||||
/** This field is constant. */
|
||||
readonly expression: string;
|
||||
|
||||
/** This field is observable */
|
||||
readonly state: DataExtractionState;
|
||||
|
||||
/** This field is observable */
|
||||
readonly preferredDataExtractor: DataExtractorId | undefined;
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction";
|
||||
import { hotClass } from "@hediet/node-reload";
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { EventEmitter } from "@hediet/std/events";
|
||||
import { wait } from "@hediet/std/timer";
|
||||
import { action, observable } from "mobx";
|
||||
import { CancellationToken, CancellationTokenSource } from "vscode";
|
||||
import { VisualizationBackend } from "../VisualizationBackend/VisualizationBackend";
|
||||
import { CompletionItem, DataExtractionState } from "../webviewContract";
|
||||
import {
|
||||
VisualizationWatch,
|
||||
VisualizationWatchModel,
|
||||
VisualizationWatchOptions,
|
||||
} from "./VisualizationWatchModel";
|
||||
|
||||
@hotClass(module)
|
||||
export class VisualizationWatchModelImpl implements VisualizationWatchModel {
|
||||
public readonly dispose = Disposable.fn();
|
||||
private readonly watchers = new Set<ObservableVisualizationWatch>();
|
||||
|
||||
constructor(private readonly visualizationBackend: VisualizationBackend) {
|
||||
this.dispose.track(
|
||||
visualizationBackend.onChange.sub(() => {
|
||||
if (visualizationBackend.expressionLanguageId !== undefined) {
|
||||
this.lastLanguageId =
|
||||
visualizationBackend.expressionLanguageId;
|
||||
}
|
||||
})
|
||||
);
|
||||
this.lastLanguageId = visualizationBackend.expressionLanguageId;
|
||||
}
|
||||
|
||||
public createWatch(
|
||||
expression: string,
|
||||
options: VisualizationWatchOptions
|
||||
): VisualizationWatch {
|
||||
const w = new ObservableVisualizationWatch(
|
||||
expression,
|
||||
options,
|
||||
this.visualizationBackend
|
||||
);
|
||||
w.onDispose.sub(() => {
|
||||
this.watchers.delete(w);
|
||||
});
|
||||
this.watchers.add(w);
|
||||
return w;
|
||||
}
|
||||
|
||||
@observable
|
||||
private lastLanguageId: string | undefined = undefined;
|
||||
|
||||
get languageId(): string | undefined {
|
||||
return this.lastLanguageId;
|
||||
}
|
||||
|
||||
public getCompletions(
|
||||
text: string,
|
||||
column: number
|
||||
): Promise<CompletionItem[]> {
|
||||
return this.visualizationBackend.getCompletions(text, column);
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableVisualizationWatch implements VisualizationWatch {
|
||||
public readonly dispose = Disposable.fn();
|
||||
private readonly onDisposeEmitter = new EventEmitter();
|
||||
public readonly onDispose = this.onDisposeEmitter.asEvent();
|
||||
|
||||
constructor(
|
||||
public readonly expression: string,
|
||||
options: VisualizationWatchOptions,
|
||||
private readonly visualizationBackend: VisualizationBackend
|
||||
) {
|
||||
this._preferredDataExtractor = options.preferredDataExtractor;
|
||||
|
||||
this.dispose.track(
|
||||
visualizationBackend.onChange.sub(() => {
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
|
||||
this.dispose.track({
|
||||
dispose: () => {
|
||||
this.onDisposeEmitter.emit();
|
||||
},
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private runningRefreshOperation: Disposable | undefined;
|
||||
|
||||
public refresh(): void {
|
||||
if (this.runningRefreshOperation) {
|
||||
this.runningRefreshOperation.dispose();
|
||||
}
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
this.runningRefreshOperation = {
|
||||
dispose: () => {
|
||||
tokenSource.cancel();
|
||||
},
|
||||
};
|
||||
this._refresh(tokenSource.token);
|
||||
}
|
||||
|
||||
private async _refresh(token: CancellationToken): Promise<void> {
|
||||
/*const session = this.debuggerView.activeDebugSession;
|
||||
if (!session) {
|
||||
this._state = { kind: "noDebugSession" };
|
||||
return;
|
||||
}*/
|
||||
|
||||
//const frameId = this.debuggerView.activeFrameId;
|
||||
|
||||
this._state = { kind: "loading" };
|
||||
|
||||
/*
|
||||
const visualizationBackend = this.getVisualizationBackend();
|
||||
if (!visualizationBackend) {
|
||||
this._state = {
|
||||
kind: "error",
|
||||
message: `The debug adapter "${session.session.type}" is not supported.`,
|
||||
};
|
||||
return;
|
||||
}*/
|
||||
|
||||
const result = await this.visualizationBackend.getVisualizationData({
|
||||
expression: this.expression,
|
||||
//frameId,
|
||||
preferredExtractorId: this.preferredDataExtractor,
|
||||
});
|
||||
|
||||
if (result.kind === "error") {
|
||||
// give cancellation requested more time in case of errors
|
||||
await wait(330);
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._state = result;
|
||||
}
|
||||
|
||||
@observable
|
||||
public _state: DataExtractionState = { kind: "loading" };
|
||||
public get state(): DataExtractionState {
|
||||
return this._state;
|
||||
}
|
||||
}
|
||||
2
extension/src/VisualizationWatchModel/index.ts
Normal file
2
extension/src/VisualizationWatchModel/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./VisualizationWatchModelImpl";
|
||||
export * from "./VisualizationWatchModel";
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { debug, DebugSession } from "vscode";
|
||||
import { observable, action } from "mobx";
|
||||
import { VsCodeDebugger } from "./VsCodeDebugger";
|
||||
import { EnhancedDebugSession } from "./EnhancedDebugSession";
|
||||
|
||||
/**
|
||||
* Tracks the currently active debug session and its currently active stack frame.
|
||||
* This is currently all faked. A proper API would be nice.
|
||||
*/
|
||||
export class VsCodeDebuggerView {
|
||||
public readonly dispose = Disposable.fn();
|
||||
|
||||
@observable private _activeDebugSession: EnhancedDebugSession | undefined;
|
||||
|
||||
public get activeDebugSession(): EnhancedDebugSession | undefined {
|
||||
return this._activeDebugSession;
|
||||
}
|
||||
|
||||
public get activeFrameId(): number | undefined {
|
||||
if (!this._activeDebugSession) {
|
||||
return undefined;
|
||||
} else {
|
||||
return this._activeDebugSession["activeStackFrame"];
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private vsCodeDebugger: VsCodeDebugger) {
|
||||
this.dispose.track(
|
||||
debug.onDidChangeActiveDebugSession(activeSession => {
|
||||
this.updateActiveDebugSession(activeSession);
|
||||
})
|
||||
);
|
||||
this.updateActiveDebugSession(debug.activeDebugSession);
|
||||
}
|
||||
|
||||
@action
|
||||
private updateActiveDebugSession(activeSession: DebugSession | undefined) {
|
||||
if (!activeSession) {
|
||||
this._activeDebugSession = undefined;
|
||||
} else {
|
||||
const s = this.vsCodeDebugger.getEnhancedDebugSession(
|
||||
activeSession
|
||||
);
|
||||
this._activeDebugSession = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,15 +15,16 @@ registerUpdateReconciler(module);
|
||||
import { InternalWebviewManager } from "./webview/InternalWebviewManager";
|
||||
import { WebviewServer } from "./webview/WebviewServer";
|
||||
import { Config } from "./Config";
|
||||
import { VsCodeDebugger } from "./debugger/VsCodeDebugger";
|
||||
import { VsCodeDebuggerView } from "./debugger/VsCodeDebuggerView";
|
||||
import { EvaluationWatchServiceImpl } from "./EvaluationWatchService";
|
||||
import { DebuggerProxy } from "./proxies/DebuggerProxy";
|
||||
import { DebuggerViewProxy } from "./proxies/DebuggerViewProxy";
|
||||
import { VisualizationWatchModelImpl } from "./VisualizationWatchModel";
|
||||
import {
|
||||
ComposedEvaluationEngine,
|
||||
ComposedVisualizationSupport,
|
||||
JsEvaluationEngine,
|
||||
GenericEvaluationEngine,
|
||||
ConfiguredEvaluationEngine,
|
||||
} from "./EvaluationWatchService/EvaluationEngine";
|
||||
GenericVisualizationSupport,
|
||||
ConfigurableVisualizationSupport,
|
||||
} from "./VisualizationBackend";
|
||||
import { DispatchingVisualizationBackend } from "./VisualizationBackend/DispatchingVisualizationBackend";
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
@@ -37,18 +38,23 @@ export class Extension {
|
||||
public readonly dispose = Disposable.fn();
|
||||
|
||||
private readonly config = new Config();
|
||||
private readonly debugger = this.dispose.track(new VsCodeDebugger());
|
||||
private readonly debugger = this.dispose.track(new DebuggerProxy());
|
||||
private readonly debuggerView = this.dispose.track(
|
||||
new VsCodeDebuggerView(this.debugger)
|
||||
new DebuggerViewProxy(this.debugger)
|
||||
);
|
||||
|
||||
public readonly dataSource = new EvaluationWatchServiceImpl(
|
||||
this.debuggerView,
|
||||
new ComposedEvaluationEngine([
|
||||
new ConfiguredEvaluationEngine(this.config),
|
||||
new JsEvaluationEngine(),
|
||||
new GenericEvaluationEngine(),
|
||||
])
|
||||
public readonly dataSource = new VisualizationWatchModelImpl(
|
||||
new DispatchingVisualizationBackend(
|
||||
new ComposedVisualizationSupport([
|
||||
new ConfigurableVisualizationSupport(
|
||||
this.config,
|
||||
this.debuggerView
|
||||
),
|
||||
new JsEvaluationEngine(this.debuggerView, this.config),
|
||||
new GenericVisualizationSupport(this.debuggerView),
|
||||
]),
|
||||
this.debuggerView
|
||||
)
|
||||
);
|
||||
|
||||
private readonly server = new WebviewServer(this.dataSource, this.config);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { DebugSession } from "vscode";
|
||||
import { CompletionItem } from "../webviewContract";
|
||||
import { observable } from "mobx";
|
||||
|
||||
export class EnhancedDebugSession {
|
||||
@observable protected activeStackFrame: number | undefined;
|
||||
export class DebugSessionProxy {
|
||||
@observable private _activeStackFrameId: number | undefined;
|
||||
|
||||
constructor(public readonly session: DebugSession) {}
|
||||
|
||||
@@ -2,26 +2,24 @@ import { Disposable } from "@hediet/std/disposable";
|
||||
import { debug, DebugSession } from "vscode";
|
||||
import { EventEmitter } from "@hediet/std/events";
|
||||
import { runInAction } from "mobx";
|
||||
import { EnhancedDebugSession } from "./EnhancedDebugSession";
|
||||
import { DebugSessionProxy } from "./DebugSessionProxy";
|
||||
|
||||
/**
|
||||
* Decorates DebugSession instances and emits an event for new debug sessions.
|
||||
*/
|
||||
export class VsCodeDebugger {
|
||||
export class DebuggerProxy {
|
||||
public readonly dispose = Disposable.fn();
|
||||
private readonly sessions = new Map<DebugSession, EnhancedDebugSession>();
|
||||
private readonly sessions = new Map<DebugSession, DebugSessionProxy>();
|
||||
|
||||
private readonly _onDidStartDebugSession = new EventEmitter<{
|
||||
session: EnhancedDebugSession;
|
||||
session: DebugSessionProxy;
|
||||
}>();
|
||||
public readonly onDidStartDebugSession = this._onDidStartDebugSession.asEvent();
|
||||
|
||||
public getEnhancedDebugSession(
|
||||
session: DebugSession
|
||||
): EnhancedDebugSession {
|
||||
public getDebugSessionProxy(session: DebugSession): DebugSessionProxy {
|
||||
let result = this.sessions.get(session);
|
||||
if (!result) {
|
||||
result = new EnhancedDebugSession(session);
|
||||
result = new DebugSessionProxy(session);
|
||||
this.sessions.set(session, result);
|
||||
}
|
||||
return result;
|
||||
@@ -38,9 +36,7 @@ export class VsCodeDebugger {
|
||||
}),
|
||||
debug.registerDebugAdapterTrackerFactory("*", {
|
||||
createDebugAdapterTracker: session => {
|
||||
const extendedSession = this.getEnhancedDebugSession(
|
||||
session
|
||||
);
|
||||
const extendedSession = this.getDebugSessionProxy(session);
|
||||
return {
|
||||
onDidSendMessage: async msg => {
|
||||
type Message =
|
||||
@@ -90,7 +86,7 @@ export class VsCodeDebugger {
|
||||
startFrame: 0,
|
||||
levels: 1,
|
||||
});
|
||||
extendedSession["activeStackFrame"] =
|
||||
extendedSession["_activeStackFrameId"] =
|
||||
r.stackFrames.length > 0
|
||||
? r.stackFrames[0].id
|
||||
: undefined;
|
||||
@@ -103,7 +99,7 @@ export class VsCodeDebugger {
|
||||
m.command === "stepOut"
|
||||
) {
|
||||
extendedSession[
|
||||
"activeStackFrame"
|
||||
"_activeStackFrameId"
|
||||
] = undefined;
|
||||
}
|
||||
}
|
||||
41
extension/src/proxies/DebuggerViewProxy.ts
Normal file
41
extension/src/proxies/DebuggerViewProxy.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Disposable } from "@hediet/std/disposable";
|
||||
import { debug, DebugSession } from "vscode";
|
||||
import { observable, action } from "mobx";
|
||||
import { DebuggerProxy } from "./DebuggerProxy";
|
||||
import { DebugSessionProxy } from "./DebugSessionProxy";
|
||||
|
||||
/**
|
||||
* Tracks the currently active debug session and its currently active stack frame.
|
||||
* This is currently all faked. A proper API would be nice.
|
||||
*/
|
||||
export class DebuggerViewProxy {
|
||||
public readonly dispose = Disposable.fn();
|
||||
|
||||
@observable private _activeDebugSession: DebugSessionProxy | undefined;
|
||||
|
||||
public get activeDebugSession(): DebugSessionProxy | undefined {
|
||||
return this._activeDebugSession;
|
||||
}
|
||||
|
||||
public getActiveStackFrameId(
|
||||
session: DebugSessionProxy
|
||||
): number | undefined {
|
||||
return session["_activeStackFrameId"];
|
||||
}
|
||||
|
||||
constructor(private debuggerProxy: DebuggerProxy) {
|
||||
this.dispose.track(
|
||||
debug.onDidChangeActiveDebugSession(activeSession => {
|
||||
this.updateActiveDebugSession(activeSession);
|
||||
})
|
||||
);
|
||||
this.updateActiveDebugSession(debug.activeDebugSession);
|
||||
}
|
||||
|
||||
@action
|
||||
private updateActiveDebugSession(activeSession: DebugSession | undefined) {
|
||||
this._activeDebugSession = activeSession
|
||||
? this.debuggerProxy.getDebugSessionProxy(activeSession)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
@@ -112,11 +112,11 @@ function getDebugVisualizerWebviewHtml(
|
||||
window.addEventListener('message', event => {
|
||||
if (event.source === window.frames[0]) {
|
||||
if (event.data.command === "setState") {
|
||||
console.log("setState", event.data.state);
|
||||
// console.log("setState", event.data.state);
|
||||
api.setState(event.data.state);
|
||||
}
|
||||
if (event.data.command === "getState") {
|
||||
console.log("getState, sent ", api.getState());
|
||||
// console.log("getState, sent ", api.getState());
|
||||
window.frames[0].postMessage({ command: "getStateResult", state: api.getState() }, "*");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Disposable } from "@hediet/std/disposable";
|
||||
import { webviewContract } from "../webviewContract";
|
||||
import { ConsoleRpcLogger, RequestHandlingError } from "@hediet/typed-json-rpc";
|
||||
import {
|
||||
EvaluationWatcher,
|
||||
EvaluationWatchService,
|
||||
} from "../EvaluationWatchService";
|
||||
VisualizationWatch,
|
||||
VisualizationWatchModel,
|
||||
} from "../VisualizationWatchModel";
|
||||
import { WebSocketStream } from "@hediet/typed-json-rpc-websocket";
|
||||
import { observable, autorun } from "mobx";
|
||||
import { WebviewServer } from "./WebviewServer";
|
||||
@@ -15,12 +15,12 @@ import { Config } from "../Config";
|
||||
export class WebviewConnection {
|
||||
public readonly dispose = Disposable.fn();
|
||||
@observable
|
||||
private watcher: EvaluationWatcher | undefined = undefined;
|
||||
private watcher: VisualizationWatch | undefined = undefined;
|
||||
|
||||
private readonly client: typeof webviewContract["TClientInterface"];
|
||||
|
||||
constructor(
|
||||
evaluationWatchService: EvaluationWatchService,
|
||||
evaluationWatchService: VisualizationWatchModel,
|
||||
stream: WebSocketStream,
|
||||
server: WebviewServer,
|
||||
config: Config,
|
||||
@@ -55,19 +55,16 @@ export class WebviewConnection {
|
||||
setExpression: async ({ newExpression }) => {
|
||||
throwIfNotAuthenticated();
|
||||
|
||||
let oldPreferredDataExtractor: EvaluationWatcher["preferredDataExtractor"];
|
||||
let oldPreferredDataExtractor: VisualizationWatch["preferredDataExtractor"];
|
||||
if (this.watcher) {
|
||||
oldPreferredDataExtractor = this.watcher
|
||||
.preferredDataExtractor;
|
||||
this.dispose.untrack(this.watcher).dispose();
|
||||
}
|
||||
this.watcher = this.dispose.track(
|
||||
evaluationWatchService.createEvaluationWatcher(
|
||||
newExpression,
|
||||
{
|
||||
preferredDataExtractor: oldPreferredDataExtractor,
|
||||
}
|
||||
)
|
||||
evaluationWatchService.createWatch(newExpression, {
|
||||
preferredDataExtractor: oldPreferredDataExtractor,
|
||||
})
|
||||
);
|
||||
},
|
||||
openInBrowser: async ({}) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as serveStatic from "serve-static";
|
||||
import { Config } from "../Config";
|
||||
import cryptoRandomString = require("crypto-random-string");
|
||||
import { distPath } from "debug-visualizer-webview";
|
||||
import { EvaluationWatchService } from "../EvaluationWatchService/EvaluationWatchService";
|
||||
import { VisualizationWatchModel } from "../VisualizationWatchModel/VisualizationWatchModel";
|
||||
import { URLSearchParams } from "url";
|
||||
import { autorun, reaction } from "mobx";
|
||||
|
||||
@@ -19,7 +19,7 @@ export class WebviewServer {
|
||||
public readonly connections = new Set<WebviewConnection>();
|
||||
|
||||
constructor(
|
||||
dataSource: EvaluationWatchService,
|
||||
dataSource: VisualizationWatchModel,
|
||||
private readonly config: Config
|
||||
) {
|
||||
const app = express();
|
||||
@@ -28,11 +28,13 @@ export class WebviewServer {
|
||||
|
||||
this.server = app.listen();
|
||||
|
||||
/*
|
||||
console.log(
|
||||
`Serving "${distPath}" on port ${
|
||||
(this.server.address() as AddressInfo).port
|
||||
}`
|
||||
);
|
||||
*/
|
||||
|
||||
const wss = new WebSocket.Server({ server: this.server });
|
||||
wss.on("connection", async ws => {
|
||||
|
||||
@@ -17,7 +17,6 @@ export class App extends React.Component {
|
||||
require("../vscode-dark.scss");
|
||||
}
|
||||
}
|
||||
console.log(this.model.theme);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -108,7 +108,7 @@ export class ExpressionInput extends React.Component<{ model: Model }> {
|
||||
@action.bound
|
||||
submit() {
|
||||
const val = this.model.getValue();
|
||||
console.log(val);
|
||||
//console.log(val);
|
||||
this.props.model.setExpression(val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ export class MonacoBridge {
|
||||
);
|
||||
|
||||
for (const file of commonTypes.keys()) {
|
||||
console.log(file);
|
||||
this.dispose.track(
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
commonTypes(file).default,
|
||||
|
||||
Reference in New Issue
Block a user