Implements the debugVisualizer.debugAdapterConfigurations configuration option.

This commit is contained in:
Henning Dieterichs
2020-03-02 10:52:19 +01:00
parent f6a554c897
commit f12ea19369
19 changed files with 338 additions and 118 deletions

View File

@@ -1,4 +1,10 @@
{
"debugVisualizer.useChromeKioskMode": false,
"editor.minimap.enabled": false
"editor.minimap.enabled": false,
"debugVisualizer.debugAdapterConfigurations": {
"php": {
"context": "watch",
"expressionTemplate": "${expr}"
}
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 ./"

View File

@@ -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<boolean>(useChromeKioskModeKey);
if (b === undefined) {
return true;
this._useChromeKioskMode = mapUndefined(
c.get<boolean>(useChromeKioskModeKey),
true
);
this._debugAdapterConfigs = mapUndefined(
c.get<DebugAdapterConfigs>(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<T>(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, string>
): string {
let result = template;
for (const [key, val] of Object.entries(data)) {
result = result.split(`\${${key}}`).join(val);
}
return result;
}

View File

@@ -76,7 +76,7 @@ export class ConnectionHandler {
});
let opened = false;
if (config.useChromeKioskMode()) {
if (config.useChromeKioskMode) {
opened = await launchChrome(url);
}
if (!opened) {

View File

@@ -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,
});
}
}

View File

@@ -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;
}

View File

@@ -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";
}
}

View File

@@ -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(

View File

@@ -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";

View File

@@ -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}`
);
}
}

View File

@@ -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;

View File

@@ -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
);
}

View File

@@ -1 +1 @@
export * from "./Sources";
export * from "./DataSourceImpl";

View File

@@ -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,

View File

@@ -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 };
}

View File

@@ -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() {

View File

@@ -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"