diff --git a/.vscode/settings.json b/.vscode/settings.json index 560c672..424a8b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" } diff --git a/data-extraction/package.json b/data-extraction/package.json index 0d103e5..b6da623 100644 --- a/data-extraction/package.json +++ b/data-extraction/package.json @@ -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": { diff --git a/data-extraction/src/js/api/LoadDataExtractorsFn.ts b/data-extraction/src/js/api/LoadDataExtractorsFn.ts new file mode 100644 index 0000000..54904ed --- /dev/null +++ b/data-extraction/src/js/api/LoadDataExtractorsFn.ts @@ -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; diff --git a/data-extraction/src/js/api/default-extractors/InjectedExtractor.ts b/data-extraction/src/js/api/default-extractors/InjectedExtractor.ts new file mode 100644 index 0000000..68f1d98 --- /dev/null +++ b/data-extraction/src/js/api/default-extractors/InjectedExtractor.ts @@ -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(); + + for (const fn of injectedExtractorsFns) { + fn(extractor => { + extractors.push(extractor); + }, helpers); + } + + for (const extractor of extractors) { + extractor.getExtractions(data, extractionCollector, context); + } + } +} diff --git a/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts b/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts index 9ba7e82..86a7ab9 100644 --- a/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts +++ b/data-extraction/src/js/api/default-extractors/registerDefaultDataExtractors.ts @@ -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, - }); - }, - }); } diff --git a/data-extraction/src/js/api/index.ts b/data-extraction/src/js/api/index.ts index 5ebc4c1..6d9bc75 100644 --- a/data-extraction/src/js/api/index.ts +++ b/data-extraction/src/js/api/index.ts @@ -1,3 +1,4 @@ export * from "./DataExtractorApi"; export * from "./injection"; export { DataExtractorApiImpl } from "./DataExtractorApiImpl"; +export * from "./LoadDataExtractorsFn"; diff --git a/data-extraction/src/js/api/injection.ts b/data-extraction/src/js/api/injection.ts index f7f6d1f..b8cb175 100644 --- a/data-extraction/src/js/api/injection.ts +++ b/data-extraction/src/js/api/injection.ts @@ -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; } diff --git a/demos/js/.vscode/settings.json b/demos/js/.vscode/settings.json index ea591b4..dea1596 100644 --- a/demos/js/.vscode/settings.json +++ b/demos/js/.vscode/settings.json @@ -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", + ] } \ No newline at end of file diff --git a/demos/js/custom-visualizer.js b/demos/js/custom-visualizer.js new file mode 100644 index 0000000..5a1a54a --- /dev/null +++ b/demos/js/custom-visualizer.js @@ -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 + ); +} diff --git a/demos/js/src/demo_doubly-linked-list.ts b/demos/js/src/demo_doubly-linked-list.ts index 03ca946..8f775c6 100644 --- a/demos/js/src/demo_doubly-linked-list.ts +++ b/demos/js/src/demo_doubly-linked-list.ts @@ -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), })); diff --git a/demos/js/src/demo_sorting.ts b/demos/js/src/demo_sorting.ts index a6f250a..592f978 100644 --- a/demos/js/src/demo_sorting.ts +++ b/demos/js/src/demo_sorting.ts @@ -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, i: number, j: number) { [array[i], array[j]] = [array[j], array[i]]; diff --git a/demos/js/yarn.lock b/demos/js/yarn.lock index 6fa925d..608eaf3 100644 --- a/demos/js/yarn.lock +++ b/demos/js/yarn.lock @@ -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" diff --git a/docs/custom-script-map.png b/docs/custom-script-map.png new file mode 100644 index 0000000..bb1945d Binary files /dev/null and b/docs/custom-script-map.png differ diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md index 869733f..2d8d0b6 100644 --- a/extension/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -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. diff --git a/extension/README.md b/extension/README.md index f510e74..6e06b88 100644 --- a/extension/README.md +++ b/extension/README.md @@ -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, + })), + }); + }, + }); + }, + }); + }; + ``` + + ![](../docs/custom-script-map.png) + # See Also This extension works very well together with my library [`@hediet/node-reload`](https://github.com/hediet/node-reload) for TypeScript/JavaScript. diff --git a/extension/package.json b/extension/package.json index fa333b0..1d0e09d 100644 --- a/extension/package.json +++ b/extension/package.json @@ -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, diff --git a/extension/src/Config.ts b/extension/src/Config.ts index 6aed039..bd1f396 100644 --- a/extension/src/Config.ts +++ b/extension/src/Config.ts @@ -21,6 +21,29 @@ export class Config { { serializer: serializerWithDefault({}) } ); + private readonly _customScriptPaths = new VsCodeSetting( + "debugVisualizer.js.customScriptPaths", + { serializer: serializerWithDefault([]) } + ); + + 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 { + return this.str.replace(/\$\{([a-zA-Z0-9]+)\}/g, (substr, grp1) => { + return data[grp1](); + }); + } +} diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts deleted file mode 100644 index 9563aa8..0000000 --- a/extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts +++ /dev/null @@ -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; - } -} diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts deleted file mode 100644 index 65a828f..0000000 --- a/extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts +++ /dev/null @@ -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, - }); - } -} diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts deleted file mode 100644 index 166acb5..0000000 --- a/extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts +++ /dev/null @@ -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; -} diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts deleted file mode 100644 index 5ccd924..0000000 --- a/extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts +++ /dev/null @@ -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(); - 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 { - 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; - } - } -} diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/index.ts b/extension/src/EvaluationWatchService/EvaluationEngine/index.ts deleted file mode 100644 index 1d57263..0000000 --- a/extension/src/EvaluationWatchService/EvaluationEngine/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./EvaluationEngine"; - -export { ComposedEvaluationEngine } from "./ComposedEvaluationEngine"; -export { JsEvaluationEngine } from "./JsEvaluationEngine"; -export { GenericEvaluationEngine } from "./GenericEvaluationEngine"; -export { ConfiguredEvaluationEngine } from "./ConfiguredEvaluationEngine"; diff --git a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts b/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts deleted file mode 100644 index 47b6041..0000000 --- a/extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts +++ /dev/null @@ -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(); - - 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 { - 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 { - 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); - } -} diff --git a/extension/src/EvaluationWatchService/index.ts b/extension/src/EvaluationWatchService/index.ts deleted file mode 100644 index 4062c46..0000000 --- a/extension/src/EvaluationWatchService/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./EvaluationWatchServiceImpl"; -export * from "./EvaluationWatchService"; diff --git a/extension/src/VisualizationBackend/ComposedVisualizationSupport.ts b/extension/src/VisualizationBackend/ComposedVisualizationSupport.ts new file mode 100644 index 0000000..096acce --- /dev/null +++ b/extension/src/VisualizationBackend/ComposedVisualizationSupport.ts @@ -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; + } +} diff --git a/extension/src/VisualizationBackend/ConfigurableVisualizationSupport.ts b/extension/src/VisualizationBackend/ConfigurableVisualizationSupport.ts new file mode 100644 index 0000000..921ede9 --- /dev/null +++ b/extension/src/VisualizationBackend/ConfigurableVisualizationSupport.ts @@ -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, + }); + } +} diff --git a/extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts b/extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts new file mode 100644 index 0000000..1cc40b4 --- /dev/null +++ b/extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts @@ -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 { + return ( + (await this.activeVisualizationBackend?.getCompletions( + text, + column + )) || [] + ); + } +} diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts b/extension/src/VisualizationBackend/GenericVisualizationSupport.ts similarity index 60% rename from extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts rename to extension/src/VisualizationBackend/GenericVisualizationSupport.ts index 126b1f0..3f44e89 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts +++ b/extension/src/VisualizationBackend/GenericVisualizationSupport.ts @@ -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 { // 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); diff --git a/extension/src/VisualizationBackend/JsVisualizationSupport.ts b/extension/src/VisualizationBackend/JsVisualizationSupport.ts new file mode 100644 index 0000000..58b6823 --- /dev/null +++ b/extension/src/VisualizationBackend/JsVisualizationSupport.ts @@ -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 | 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(); + 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 | undefined { + if (this.apiInitialized) { + return; + } + + if (this.initializedPromise) { + return this.initializedPromise; + } else { + this.initializedPromise = this._ensureRuntimeIsInjected(); + } + } + + private async _ensureRuntimeIsInjected(): Promise { + 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 { + 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 { + await this.debugSession.evaluate({ + expression: `global["${injectedScriptsKey}"] = {};`, + frameId: undefined, + context: "repl", + }); + + const watchers = new Map(); + 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>(); + + const activePaths = new Set(); + 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 { + 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 { + 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(); + } +} diff --git a/extension/src/VisualizationBackend/VisualizationBackend.ts b/extension/src/VisualizationBackend/VisualizationBackend.ts new file mode 100644 index 0000000..4c0c509 --- /dev/null +++ b/extension/src/VisualizationBackend/VisualizationBackend.ts @@ -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; +} + +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 { + return await this.debugSession.getCompletions({ + text, + frameId: this.debuggerView.getActiveStackFrameId(this.debugSession), + column, + }); + } +} diff --git a/extension/src/VisualizationBackend/index.ts b/extension/src/VisualizationBackend/index.ts new file mode 100644 index 0000000..0f7ae00 --- /dev/null +++ b/extension/src/VisualizationBackend/index.ts @@ -0,0 +1,6 @@ +export * from "./VisualizationBackend"; + +export { ComposedVisualizationSupport } from "./ComposedVisualizationSupport"; +export { JsEvaluationEngine } from "./JsVisualizationSupport"; +export { GenericVisualizationSupport } from "./GenericVisualizationSupport"; +export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport"; diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/parseEvaluationResultFromGenericDebugAdapter.ts b/extension/src/VisualizationBackend/parseEvaluationResultFromGenericDebugAdapter.ts similarity index 97% rename from extension/src/EvaluationWatchService/EvaluationEngine/parseEvaluationResultFromGenericDebugAdapter.ts rename to extension/src/VisualizationBackend/parseEvaluationResultFromGenericDebugAdapter.ts index b0306b9..f19a249 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/parseEvaluationResultFromGenericDebugAdapter.ts +++ b/extension/src/VisualizationBackend/parseEvaluationResultFromGenericDebugAdapter.ts @@ -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; diff --git a/extension/src/EvaluationWatchService/EvaluationWatchService.ts b/extension/src/VisualizationWatchModel/VisualizationWatchModel.ts similarity index 76% rename from extension/src/EvaluationWatchService/EvaluationWatchService.ts rename to extension/src/VisualizationWatchModel/VisualizationWatchModel.ts index 4ccf6f3..25ad5b7 100644 --- a/extension/src/EvaluationWatchService/EvaluationWatchService.ts +++ b/extension/src/VisualizationWatchModel/VisualizationWatchModel.ts @@ -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; @@ -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; diff --git a/extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts b/extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts new file mode 100644 index 0000000..ad798f5 --- /dev/null +++ b/extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts @@ -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(); + + 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 { + 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 { + /*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; + } +} diff --git a/extension/src/VisualizationWatchModel/index.ts b/extension/src/VisualizationWatchModel/index.ts new file mode 100644 index 0000000..50e1941 --- /dev/null +++ b/extension/src/VisualizationWatchModel/index.ts @@ -0,0 +1,2 @@ +export * from "./VisualizationWatchModelImpl"; +export * from "./VisualizationWatchModel"; diff --git a/extension/src/debugger/VsCodeDebuggerView.ts b/extension/src/debugger/VsCodeDebuggerView.ts deleted file mode 100644 index bf864b0..0000000 --- a/extension/src/debugger/VsCodeDebuggerView.ts +++ /dev/null @@ -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; - } - } -} diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 48375f4..0faaeb2 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -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); diff --git a/extension/src/debugger/EnhancedDebugSession.ts b/extension/src/proxies/DebugSessionProxy.ts similarity index 96% rename from extension/src/debugger/EnhancedDebugSession.ts rename to extension/src/proxies/DebugSessionProxy.ts index 21acb9c..1f1b259 100644 --- a/extension/src/debugger/EnhancedDebugSession.ts +++ b/extension/src/proxies/DebugSessionProxy.ts @@ -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) {} diff --git a/extension/src/debugger/VsCodeDebugger.ts b/extension/src/proxies/DebuggerProxy.ts similarity index 83% rename from extension/src/debugger/VsCodeDebugger.ts rename to extension/src/proxies/DebuggerProxy.ts index 4880a7a..6b3cb5e 100644 --- a/extension/src/debugger/VsCodeDebugger.ts +++ b/extension/src/proxies/DebuggerProxy.ts @@ -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(); + private readonly sessions = new Map(); 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; } } diff --git a/extension/src/proxies/DebuggerViewProxy.ts b/extension/src/proxies/DebuggerViewProxy.ts new file mode 100644 index 0000000..6087374 --- /dev/null +++ b/extension/src/proxies/DebuggerViewProxy.ts @@ -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; + } +} diff --git a/extension/src/webview/InternalWebviewManager.ts b/extension/src/webview/InternalWebviewManager.ts index 312547c..1b29bc8 100644 --- a/extension/src/webview/InternalWebviewManager.ts +++ b/extension/src/webview/InternalWebviewManager.ts @@ -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() }, "*"); } } diff --git a/extension/src/webview/WebviewConnection.ts b/extension/src/webview/WebviewConnection.ts index 094db82..1f48a6a 100644 --- a/extension/src/webview/WebviewConnection.ts +++ b/extension/src/webview/WebviewConnection.ts @@ -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 ({}) => { diff --git a/extension/src/webview/WebviewServer.ts b/extension/src/webview/WebviewServer.ts index b085678..0488aa3 100644 --- a/extension/src/webview/WebviewServer.ts +++ b/extension/src/webview/WebviewServer.ts @@ -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(); 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 => { diff --git a/webview/src/components/App.tsx b/webview/src/components/App.tsx index bc160fa..42ea4ef 100644 --- a/webview/src/components/App.tsx +++ b/webview/src/components/App.tsx @@ -17,7 +17,6 @@ export class App extends React.Component { require("../vscode-dark.scss"); } } - console.log(this.model.theme); } render() { diff --git a/webview/src/components/ExpressionInput.tsx b/webview/src/components/ExpressionInput.tsx index a250390..a75b10b 100644 --- a/webview/src/components/ExpressionInput.tsx +++ b/webview/src/components/ExpressionInput.tsx @@ -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); } } diff --git a/webview/src/model/MonacoBridge.ts b/webview/src/model/MonacoBridge.ts index 10207f1..a1261bb 100644 --- a/webview/src/model/MonacoBridge.ts +++ b/webview/src/model/MonacoBridge.ts @@ -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,