diff --git a/demo/.vscode/settings.json b/demo/.vscode/settings.json index 7f39cf5..d6894d7 100644 --- a/demo/.vscode/settings.json +++ b/demo/.vscode/settings.json @@ -1,4 +1,10 @@ { "debugVisualizer.useChromeKioskMode": false, - "editor.minimap.enabled": false + "editor.minimap.enabled": false, + "debugVisualizer.debugAdapterConfigurations": { + "php": { + "context": "watch", + "expressionTemplate": "${expr}" + } + } } diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md index bd4b5dc..a341aee 100644 --- a/extension/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## [Unreleased] + +- Adds "debugVisualizer.debugAdapterConfigurations" config to customize the expression template. +- Code Restructuring + ## 0.12.1 - Tries to parse evaluated strings twice as JSON in case of C++/C# escape sequences. diff --git a/extension/README.md b/extension/README.md index 6f4dd1e..673fa68 100644 --- a/extension/README.md +++ b/extension/README.md @@ -239,6 +239,30 @@ Creates Grid visualization data for an array. ![](../docs/multiline-expression.png) +## Configuration + +This extension provides these configuration options: + +- `debugVisualizer.debugAdapterConfigurations` + + Allows to set expression templates for specific debug adapter types. + Example: + + ```json + "debugVisualizer.debugAdapterConfigurations": { + "lldb": { + "expressionTemplate": "script to_json(\"${expr}\")", + "context": "repl" + } + } + ``` + + Configurations here overwrite the built-in support for the corresponding debug adapter type. + +* `debugVisualizer.useChromeKioskMode` + + 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`. + # 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 4d5ad3b..5098b0a 100644 --- a/extension/package.json +++ b/extension/package.json @@ -42,12 +42,33 @@ "type": "boolean", "default": true, "description": "Pop out Debug Visualization Views with Chrome in Kiosk Mode." + }, + "debugVisualizer.debugAdapterConfigurations": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "expressionTemplate": { + "type:": "string", + "description": "A template for the final expression sent to the debugger for evaluation. Use `${expr}` to refer to the currently visualized expression." + }, + "context": { + "type": "string", + "enum": [ + "watch", + "repl" + ], + "description": "The context to use for evaluating the expression." + } + } + } } } } }, "scripts": { "pub": "vsce publish --yarn --baseImagesUrl https://github.com/hediet/vscode-debug-visualizer/raw/master/extension --baseContentUrl https://github.com/hediet/vscode-debug-visualizer/raw/master/extension", + "package": "vsce package --yarn", "vscode:prepublish": "yarn build", "build": "webpack --mode production", "dev": "shx rm -rf dist && tsc -watch -p ./" diff --git a/extension/src/Config.ts b/extension/src/Config.ts index eeb9fb5..4aa7d1a 100644 --- a/extension/src/Config.ts +++ b/extension/src/Config.ts @@ -1,28 +1,99 @@ import { workspace } from "vscode"; import { Disposable } from "@hediet/std/disposable"; -import { EventEmitter, EventSource } from "@hediet/std/events"; +import { observable } from "mobx"; +import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction"; -export const useChromeKioskModeKey = "debugVisualizer.useChromeKioskMode"; +const useChromeKioskModeKey = "debugVisualizer.useChromeKioskMode"; +const debugAdapterConfigsKey = "debugVisualizer.debugAdapterConfigurations"; export class Config { - private changeEventEmitter = new EventEmitter(); - public readonly onChange: EventSource = this.changeEventEmitter; public dispose = Disposable.fn(); + @observable + private _useChromeKioskMode!: boolean; + + @observable + private _debugAdapterConfigs!: DebugAdapterConfigs; + + public get useChromeKioskMode(): boolean { + return this._useChromeKioskMode; + } + + public get debugAdapterConfigs(): DebugAdapterConfigs { + return this._debugAdapterConfigs; + } + constructor() { + this.updateConfig(); this.dispose.track( workspace.onDidChangeConfiguration(() => { - this.changeEventEmitter.emit(); + this.updateConfig(); }) ); } - public useChromeKioskMode(): boolean { + private updateConfig(): void { const c = workspace.getConfiguration(); - const b = c.get(useChromeKioskModeKey); - if (b === undefined) { - return true; + + this._useChromeKioskMode = mapUndefined( + c.get(useChromeKioskModeKey), + true + ); + + this._debugAdapterConfigs = mapUndefined( + c.get(debugAdapterConfigsKey), + {} + ); + } + + public getDebugAdapterConfig( + debugAdapterType: string + ): DebugAdapterConfig | undefined { + const c = this.debugAdapterConfigs[debugAdapterType]; + if (!c) { + return undefined; } - return b; + + return { + context: c.context || "watch", + getFinalExpression: ({ expression, preferredExtractorId }) => + evaluateTemplate(c.expressionTemplate || "${expr}", { + expr: expression, + preferredDataExtractorId: preferredExtractorId || "", + }), + }; } } + +function mapUndefined(val: T | undefined, defaultVal: T) { + if (val === undefined) { + return defaultVal; + } + return val; +} + +type DebugAdapterConfigs = { + [debugAdapter: string]: { + context?: "watch" | "repl"; + expressionTemplate?: "string"; + }; +}; + +export interface DebugAdapterConfig { + context: "watch" | "repl"; + getFinalExpression(vars: { + expression: string; + preferredExtractorId: DataExtractorId | undefined; + }): string; +} + +function evaluateTemplate( + template: string, + data: Record +): string { + let result = template; + for (const [key, val] of Object.entries(data)) { + result = result.split(`\${${key}}`).join(val); + } + return result; +} diff --git a/extension/src/ConnectionHandler.ts b/extension/src/ConnectionHandler.ts index 0f0cb5f..528fd16 100644 --- a/extension/src/ConnectionHandler.ts +++ b/extension/src/ConnectionHandler.ts @@ -76,7 +76,7 @@ export class ConnectionHandler { }); let opened = false; - if (config.useChromeKioskMode()) { + if (config.useChromeKioskMode) { opened = await launchChrome(url); } if (!opened) { diff --git a/extension/src/DataSource/DataExtractionProvider/ConfiguredDataExtractionProviderFactory.ts b/extension/src/DataSource/DataExtractionProvider/ConfiguredDataExtractionProviderFactory.ts new file mode 100644 index 0000000..1e720f2 --- /dev/null +++ b/extension/src/DataSource/DataExtractionProvider/ConfiguredDataExtractionProviderFactory.ts @@ -0,0 +1,49 @@ +import { DataExtractorId } from "@hediet/debug-visualizer-data-extraction"; +import { VsCodeDebugSession } from "../../VsCodeDebugger"; +import { + DataExtractionProviderFactory, + DataExtractionProvider, +} from "./DataExtractionProvider"; +import { Config, DebugAdapterConfig } from "../../Config"; +import { GenericDataExtractionProvider } from "./GenericDataExtractionProviderFactory"; + +export class ConfiguredDataExtractionProviderFactory + implements DataExtractionProviderFactory { + constructor(private readonly config: Config) {} + + createDataExtractionProvider( + session: VsCodeDebugSession + ): DataExtractionProvider | undefined { + const config = this.config.getDebugAdapterConfig(session.session.type); + if (!config) { + return undefined; + } + return new ConfiguredDataExtractionProvider(session, config); + } +} + +class ConfiguredDataExtractionProvider extends GenericDataExtractionProvider { + constructor( + session: VsCodeDebugSession, + 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/DataSource/DataExtractionProvider/DataExtractionProvider.ts b/extension/src/DataSource/DataExtractionProvider/DataExtractionProvider.ts index caae320..aed30bd 100644 --- a/extension/src/DataSource/DataExtractionProvider/DataExtractionProvider.ts +++ b/extension/src/DataSource/DataExtractionProvider/DataExtractionProvider.ts @@ -1,4 +1,7 @@ -import { DataExtractionResult } from "@hediet/debug-visualizer-data-extraction"; +import { + DataExtractionResult, + DataExtractorId, +} from "@hediet/debug-visualizer-data-extraction"; import { VsCodeDebugSession } from "../../VsCodeDebugger"; export interface DataExtractionProviderFactory { @@ -18,6 +21,6 @@ export interface DataExtractionProvider { export interface DataExtractionProviderArgs { expression: string; - preferredExtractor: string | undefined; + preferredExtractorId: DataExtractorId | undefined; frameId: number | undefined; } diff --git a/extension/src/DataSource/DataExtractionProvider/GenericDataExtractionProviderFactory.ts b/extension/src/DataSource/DataExtractionProvider/GenericDataExtractionProviderFactory.ts index ad34eec..14e1e0e 100644 --- a/extension/src/DataSource/DataExtractionProvider/GenericDataExtractionProviderFactory.ts +++ b/extension/src/DataSource/DataExtractionProvider/GenericDataExtractionProviderFactory.ts @@ -1,8 +1,6 @@ import { - DataResult, DataExtractionResult, - ExtractedData, - isExtractedData, + DataExtractorId, } from "@hediet/debug-visualizer-data-extraction"; import { VsCodeDebugSession } from "../../VsCodeDebugger"; import { @@ -10,6 +8,7 @@ import { DataExtractionProvider, DataExtractionProviderArgs, } from "./DataExtractionProvider"; +import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter"; export class GenericDataExtractionProviderFactory implements DataExtractionProviderFactory { @@ -20,84 +19,46 @@ export class GenericDataExtractionProviderFactory } } -class GenericDataExtractionProvider implements DataExtractionProvider { +export class GenericDataExtractionProvider implements DataExtractionProvider { constructor(private readonly session: VsCodeDebugSession) {} public async evaluate({ expression, - preferredExtractor, + preferredExtractorId, frameId, }: DataExtractionProviderArgs): Promise< | { kind: "data"; result: DataExtractionResult } | { kind: "error"; message: string } > { + const finalExpression = this.getFinalExpression({ + expression, + preferredExtractorId, + }); + let reply; try { - const reply = await this.session.evaluate({ - expression, + reply = await this.session.evaluate({ + expression: finalExpression, frameId, + context: this.getContext(), }); - - function isEnclosedWith(str: string, char: string): boolean { - return str.startsWith(char) && str.endsWith(char); - } - - const jsonData = reply.result.trim(); - - function parseJson(str: string) { - try { - return JSON.parse(str); - } catch (error) { - throw new Error( - `Could not parse \`${str}\` as JSON.\n${error.message}` - ); - } - } - - let result; - - try { - let jsonData2; - // Remove optionally enclosing characters. - if ( - isEnclosedWith(jsonData, '"') || - isEnclosedWith(jsonData, "'") - ) { - // In case of JavaScript: `"{ "kind": { ... }, "text": "some\ntext" }"` - jsonData2 = jsonData.substr(1, jsonData.length - 2); - } else { - // Just in case no quoting is done. - jsonData2 = jsonData; - } - result = parseJson(jsonData2); - } catch (e) { - // in case of C++: `"{ \"kind\": { ... }, \"text\": \"some\\ntext\" }"` - const str = parseJson(jsonData); - // str is now `{ "kind": { ... }, "text": "some\ntext" }"` - result = parseJson(str); - // result is now { kind: { ... }, text: "some\ntext" } - } - - if (!isExtractedData(result)) { - throw new Error("Data does not match ExtractedData interface."); - } - - return { - kind: "data", - result: { - availableExtractors: [], - usedExtractor: { - id: "generic" as any, - name: "Generic", - priority: 1, - }, - data: result, - }, - }; } catch (error) { return { kind: "error", message: error.message, }; } + + return parseEvaluationResultFromGenericDebugAdapter(reply.result); + } + + protected getFinalExpression(args: { + expression: string; + preferredExtractorId: DataExtractorId | undefined; + }): string { + return args.expression; + } + + protected getContext(): "watch" | "repl" { + return "watch"; } } diff --git a/extension/src/DataSource/DataExtractionProvider/JsDataExtractionProviderFactory.ts b/extension/src/DataSource/DataExtractionProvider/JsDataExtractionProviderFactory.ts index f59bd1a..c26a92e 100644 --- a/extension/src/DataSource/DataExtractionProvider/JsDataExtractionProviderFactory.ts +++ b/extension/src/DataSource/DataExtractionProvider/JsDataExtractionProviderFactory.ts @@ -35,7 +35,7 @@ class JsDataEvaluator implements DataExtractionProvider { public async evaluate({ expression, - preferredExtractor, + preferredExtractorId, frameId, }: DataExtractionProviderArgs): Promise< | { kind: "data"; result: DataExtractionResult } @@ -43,8 +43,8 @@ class JsDataEvaluator implements DataExtractionProvider { > { while (true) { try { - const preferredExtractorExpr = preferredExtractor - ? `"${preferredExtractor}"` + const preferredExtractorExpr = preferredExtractorId + ? `"${preferredExtractorId}"` : "undefined"; const body = `${getExpressionForDataExtractorApi()}.getData( diff --git a/extension/src/DataSource/DataExtractionProvider/index.ts b/extension/src/DataSource/DataExtractionProvider/index.ts index 9902e81..c9de44d 100644 --- a/extension/src/DataSource/DataExtractionProvider/index.ts +++ b/extension/src/DataSource/DataExtractionProvider/index.ts @@ -1,12 +1,6 @@ export * from "./DataExtractionProvider"; -import { ComposedDataExtractionProviderFactory } from "./ComposedDataExtractionProviderFactory"; -import { JsDataExtractionProviderFactory } from "./JsDataExtractionProviderFactory"; -import { GenericDataExtractionProviderFactory } from "./GenericDataExtractionProviderFactory"; - -export const defaultDataExtractionProviderFactory = new ComposedDataExtractionProviderFactory( - [ - new JsDataExtractionProviderFactory(), - new GenericDataExtractionProviderFactory(), - ] -); +export { ComposedDataExtractionProviderFactory } from "./ComposedDataExtractionProviderFactory"; +export { JsDataExtractionProviderFactory } from "./JsDataExtractionProviderFactory"; +export { GenericDataExtractionProviderFactory } from "./GenericDataExtractionProviderFactory"; +export { ConfiguredDataExtractionProviderFactory } from "./ConfiguredDataExtractionProviderFactory"; diff --git a/extension/src/DataSource/DataExtractionProvider/parseEvaluationResultFromGenericDebugAdapter.ts b/extension/src/DataSource/DataExtractionProvider/parseEvaluationResultFromGenericDebugAdapter.ts new file mode 100644 index 0000000..213534b --- /dev/null +++ b/extension/src/DataSource/DataExtractionProvider/parseEvaluationResultFromGenericDebugAdapter.ts @@ -0,0 +1,76 @@ +import { + DataExtractionResult, + isExtractedData, +} from "@hediet/debug-visualizer-data-extraction"; + +export function parseEvaluationResultFromGenericDebugAdapter( + resultText: string +): + | { kind: "data"; result: DataExtractionResult } + | { kind: "error"; message: string } { + const jsonData = resultText.trim(); + + let resultObj; + try { + try { + let jsonData2; + // Remove optionally enclosing characters. + if ( + isEnclosedWith(jsonData, '"') || + isEnclosedWith(jsonData, "'") + ) { + // In case of JavaScript: `"{ "kind": { ... }, "text": "some\ntext" }"` + jsonData2 = jsonData.substr(1, jsonData.length - 2); + } else { + // Just in case no quoting is done. + jsonData2 = jsonData; + } + resultObj = parseJson(jsonData2); + } catch (e) { + // in case of C++: `"{ \"kind\": { ... }, \"text\": \"some\\ntext\" }"` + const str = parseJson(jsonData); + // str is now `{ "kind": { ... }, "text": "some\ntext" }"` + resultObj = parseJson(str); + // result is now { kind: { ... }, text: "some\ntext" } + } + + if (!isExtractedData(resultObj)) { + return { + kind: "error", + message: "Data does not match ExtractedData interface.", + }; + } + } catch (e) { + return { + kind: "error", + message: e.message, + }; + } + + return { + kind: "data", + result: { + availableExtractors: [], + usedExtractor: { + id: "generic" as any, + name: "Generic", + priority: 1, + }, + data: resultObj, + }, + }; +} + +function isEnclosedWith(str: string, char: string): boolean { + return str.startsWith(char) && str.endsWith(char); +} + +function parseJson(str: string) { + try { + return JSON.parse(str); + } catch (error) { + throw new Error( + `Could not parse \`${str}\` as JSON.\n${error.message}` + ); + } +} diff --git a/extension/src/DataSource/DataSourceImpl.ts b/extension/src/DataSource/DataSourceImpl.ts index 7f27b98..6c42c3a 100644 --- a/extension/src/DataSource/DataSourceImpl.ts +++ b/extension/src/DataSource/DataSourceImpl.ts @@ -73,7 +73,7 @@ export class DataSourceImpl implements DataSource { const result = await extractionProvider.evaluate({ expression: w.expression, frameId, - preferredExtractor: w.preferredDataExtractor, + preferredExtractorId: w.preferredDataExtractor, }); w._state = result; diff --git a/extension/src/DataSource/Sources.ts b/extension/src/DataSource/Sources.ts deleted file mode 100644 index d5ca2df..0000000 --- a/extension/src/DataSource/Sources.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Disposable } from "@hediet/std/disposable"; -import { VsCodeDebugger, VsCodeDebuggerView } from "../VsCodeDebugger"; -import { DataSourceImpl } from "./DataSourceImpl"; -import { defaultDataExtractionProviderFactory } from "./DataExtractionProvider"; - -export class Sources { - public readonly dispose = Disposable.fn(); - private readonly debugger = this.dispose.track(new VsCodeDebugger()); - private readonly debuggerView = this.dispose.track( - new VsCodeDebuggerView(this.debugger) - ); - - public readonly dataSource = new DataSourceImpl( - this.debuggerView, - defaultDataExtractionProviderFactory - ); -} diff --git a/extension/src/DataSource/index.ts b/extension/src/DataSource/index.ts index fe0b90f..15e8db1 100644 --- a/extension/src/DataSource/index.ts +++ b/extension/src/DataSource/index.ts @@ -1 +1 @@ -export * from "./Sources"; +export * from "./DataSourceImpl"; diff --git a/extension/src/Server.ts b/extension/src/Server.ts index 0136b49..2ec7ef7 100644 --- a/extension/src/Server.ts +++ b/extension/src/Server.ts @@ -2,19 +2,19 @@ import { WebSocketStream } from "@hediet/typed-json-rpc-websocket"; import { AddressInfo } from "net"; import WebSocket = require("ws"); import { ConnectionHandler } from "./ConnectionHandler"; -import { Sources } from "./DataSource"; import * as express from "express"; import * as http from "http"; import * as serveStatic from "serve-static"; import { Config } from "./Config"; import cryptoRandomString = require("crypto-random-string"); import { distPath } from "debug-visualizer-webview"; +import { DataSource } from "./DataSource/DataSource"; export class Server { private server: http.Server; public readonly secret = cryptoRandomString({ length: 20 }); - constructor(sources: Sources, config: Config) { + constructor(dataSource: DataSource, config: Config) { const app = express(); app.use(serveStatic(distPath)); @@ -31,7 +31,7 @@ export class Server { wss.on("connection", ws => { const stream = new WebSocketStream(ws); new ConnectionHandler( - sources.dataSource, + dataSource, stream, this, config, diff --git a/extension/src/VsCodeDebugger.ts b/extension/src/VsCodeDebugger.ts index 97f0895..9628544 100644 --- a/extension/src/VsCodeDebugger.ts +++ b/extension/src/VsCodeDebugger.ts @@ -162,11 +162,12 @@ export class VsCodeDebugSession { public async evaluate(args: { expression: string; frameId: number | undefined; + context?: "watch" | "repl"; }): Promise<{ result: string }> { const reply = await this.session.customRequest("evaluate", { expression: args.expression, frameId: args.frameId, - context: "watch", + context: args.context || "watch", }); return { result: reply.result }; } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 7dd23b1..379467e 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -15,14 +15,35 @@ registerUpdateReconciler(module); import { WebViews } from "./WebViews"; import { Server } from "./Server"; import { Config } from "./Config"; -import { Sources } from "./DataSource"; +import { VsCodeDebugger, VsCodeDebuggerView } from "./VsCodeDebugger"; +import { DataSourceImpl } from "./DataSource"; +import { + ComposedDataExtractionProviderFactory, + JsDataExtractionProviderFactory, + GenericDataExtractionProviderFactory, + ConfiguredDataExtractionProviderFactory, +} from "./DataSource/DataExtractionProvider"; export class Extension { - public dispose = Disposable.fn(); + public readonly dispose = Disposable.fn(); private readonly config = new Config(); - private readonly sources = this.dispose.track(new Sources()); - private readonly server = new Server(this.sources, this.config); + + private readonly debugger = this.dispose.track(new VsCodeDebugger()); + private readonly debuggerView = this.dispose.track( + new VsCodeDebuggerView(this.debugger) + ); + + public readonly dataSource = new DataSourceImpl( + this.debuggerView, + new ComposedDataExtractionProviderFactory([ + new ConfiguredDataExtractionProviderFactory(this.config), + new JsDataExtractionProviderFactory(), + new GenericDataExtractionProviderFactory(), + ]) + ); + + private readonly server = new Server(this.dataSource, this.config); private readonly views = this.dispose.track(new WebViews(this.server)); constructor() { diff --git a/yarn.lock b/yarn.lock index 3f344f8..00181e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -92,6 +92,11 @@ stack-trace "^0.0.10" ws "^7.0.0" +"@hediet/semantic-json@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@hediet/semantic-json/-/semantic-json-0.1.0.tgz#ca3831778f1e59e555d0112decd5fe598437fdd3" + integrity sha512-lNdXabUmyxXnysXadTKyiKK9ik5L8b9SXu/QshgvE271QObPtp2Ubmh+rLNNCEAO65DE2VAxcesWmRKYfaQSJg== + "@hediet/std@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@hediet/std/-/std-0.6.0.tgz#f7d2ffc1180ecef6ff272d0d6780f0c0d9337e4e"