From d22e5d4111ad8ba1525e1e52659dc3ddab19581c Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 18 Jun 2021 20:26:51 +0200 Subject: [PATCH] Version 2.2.0. * Adds support for custom injected scripts with automatic reload * Refactors extension --- .vscode/settings.json | 8 +- data-extraction/package.json | 2 +- .../src/js/api/LoadDataExtractorsFn.ts | 7 + .../default-extractors/InjectedExtractor.ts | 34 ++ .../registerDefaultDataExtractors.ts | 97 +---- data-extraction/src/js/api/index.ts | 1 + data-extraction/src/js/api/injection.ts | 23 +- demos/js/.vscode/settings.json | 6 +- demos/js/custom-visualizer.js | 152 +++++++ demos/js/src/demo_doubly-linked-list.ts | 6 +- demos/js/src/demo_sorting.ts | 10 +- demos/js/yarn.lock | 47 ++- docs/custom-script-map.png | Bin 0 -> 38346 bytes extension/CHANGELOG.md | 4 + extension/README.md | 63 ++- extension/package.json | 15 +- extension/src/Config.ts | 33 ++ .../ComposedEvaluationEngine.ts | 16 - .../ConfiguredEvaluationEngine.ts | 47 --- .../EvaluationEngine/EvaluationEngine.ts | 30 -- .../EvaluationEngine/JsEvaluationEngine.ts | 169 -------- .../EvaluationEngine/index.ts | 6 - .../EvaluationWatchServiceImpl.ts | 175 -------- extension/src/EvaluationWatchService/index.ts | 2 - .../ComposedVisualizationSupport.ts | 22 + .../ConfigurableVisualizationSupport.ts | 62 +++ .../DispatchingVisualizationBackend.ts | 134 ++++++ .../GenericVisualizationSupport.ts} | 116 +++-- .../JsVisualizationSupport.ts | 395 ++++++++++++++++++ .../VisualizationBackend.ts | 80 ++++ extension/src/VisualizationBackend/index.ts | 6 + ...EvaluationResultFromGenericDebugAdapter.ts | 2 +- .../VisualizationWatchModel.ts} | 15 +- .../VisualizationWatchModelImpl.ts | 162 +++++++ .../src/VisualizationWatchModel/index.ts | 2 + extension/src/debugger/VsCodeDebuggerView.ts | 48 --- extension/src/extension.ts | 38 +- .../DebugSessionProxy.ts} | 4 +- .../DebuggerProxy.ts} | 22 +- extension/src/proxies/DebuggerViewProxy.ts | 41 ++ .../src/webview/InternalWebviewManager.ts | 4 +- extension/src/webview/WebviewConnection.ts | 21 +- extension/src/webview/WebviewServer.ts | 6 +- webview/src/components/App.tsx | 1 - webview/src/components/ExpressionInput.tsx | 2 +- webview/src/model/MonacoBridge.ts | 1 - 46 files changed, 1421 insertions(+), 716 deletions(-) create mode 100644 data-extraction/src/js/api/LoadDataExtractorsFn.ts create mode 100644 data-extraction/src/js/api/default-extractors/InjectedExtractor.ts create mode 100644 demos/js/custom-visualizer.js create mode 100644 docs/custom-script-map.png delete mode 100644 extension/src/EvaluationWatchService/EvaluationEngine/ComposedEvaluationEngine.ts delete mode 100644 extension/src/EvaluationWatchService/EvaluationEngine/ConfiguredEvaluationEngine.ts delete mode 100644 extension/src/EvaluationWatchService/EvaluationEngine/EvaluationEngine.ts delete mode 100644 extension/src/EvaluationWatchService/EvaluationEngine/JsEvaluationEngine.ts delete mode 100644 extension/src/EvaluationWatchService/EvaluationEngine/index.ts delete mode 100644 extension/src/EvaluationWatchService/EvaluationWatchServiceImpl.ts delete mode 100644 extension/src/EvaluationWatchService/index.ts create mode 100644 extension/src/VisualizationBackend/ComposedVisualizationSupport.ts create mode 100644 extension/src/VisualizationBackend/ConfigurableVisualizationSupport.ts create mode 100644 extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts rename extension/src/{EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts => VisualizationBackend/GenericVisualizationSupport.ts} (60%) create mode 100644 extension/src/VisualizationBackend/JsVisualizationSupport.ts create mode 100644 extension/src/VisualizationBackend/VisualizationBackend.ts create mode 100644 extension/src/VisualizationBackend/index.ts rename extension/src/{EvaluationWatchService/EvaluationEngine => VisualizationBackend}/parseEvaluationResultFromGenericDebugAdapter.ts (97%) rename extension/src/{EvaluationWatchService/EvaluationWatchService.ts => VisualizationWatchModel/VisualizationWatchModel.ts} (76%) create mode 100644 extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts create mode 100644 extension/src/VisualizationWatchModel/index.ts delete mode 100644 extension/src/debugger/VsCodeDebuggerView.ts rename extension/src/{debugger/EnhancedDebugSession.ts => proxies/DebugSessionProxy.ts} (96%) rename extension/src/{debugger/VsCodeDebugger.ts => proxies/DebuggerProxy.ts} (83%) create mode 100644 extension/src/proxies/DebuggerViewProxy.ts 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 0000000000000000000000000000000000000000..bb1945d5c37fe1c5872c70f0ea6075367e74fa55 GIT binary patch literal 38346 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 { + 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,