Version 2.2.0.

* Adds support for custom injected scripts with automatic reload
* Refactors extension
This commit is contained in:
Henning Dieterichs
2021-06-18 20:26:51 +02:00
parent 6ce5d666a3
commit d22e5d4111
46 changed files with 1421 additions and 716 deletions

View File

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

View File

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

View File

@@ -0,0 +1,134 @@
import { DataExtractionResult } from "@hediet/debug-visualizer-data-extraction";
import { CompletionItem, FormattedMessage } from "../webviewContract";
import {
DebugSessionVisualizationSupport,
GetVisualizationDataArgs,
VisualizationBackend,
} from "./VisualizationBackend";
import { EventEmitter, EventSource } from "@hediet/std/events";
import { debug } from "vscode";
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
import { Disposable } from "@hediet/std/disposable";
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
import { autorun, reaction } from "mobx";
export class DispatchingVisualizationBackend implements VisualizationBackend {
public readonly dispose = Disposable.fn();
private readonly visualizationBackends = new Map<
DebugSessionProxy,
VisualizationBackend
>();
protected readonly onChangeEmitter = new EventEmitter();
public readonly onChange = this.onChangeEmitter;
constructor(
private readonly visualizationSupport: DebugSessionVisualizationSupport,
private readonly debuggerView: DebuggerViewProxy
) {
this.dispose.track(
debug.onDidTerminateDebugSession(session => {
const existing = [...this.visualizationBackends].find(
v => v[0].session === session
);
if (existing) {
this.visualizationBackends.delete(existing[0]);
existing[1].dispose();
}
})
);
this.dispose.track({
dispose: () => {
for (const backend of this.visualizationBackends.values()) {
backend.dispose();
}
this.visualizationBackends.clear();
},
});
this.dispose.track({
dispose: reaction(
() => this.debuggerView.activeDebugSession,
debugSession => {
if (debugSession !== undefined) {
this.onChangeEmitter.emit();
}
}
),
});
}
private getVisualizationBackend(
debugSession: DebugSessionProxy
): VisualizationBackend | undefined {
const existing = this.visualizationBackends.get(debugSession);
if (existing) {
return existing;
}
const newBackend = this.visualizationSupport.createBackend(
debugSession
);
if (!newBackend) {
return undefined;
}
newBackend.onChange.sub(() => {
this.onChangeEmitter.emit();
});
this.visualizationBackends.set(debugSession, newBackend);
return newBackend;
}
private get activeVisualizationBackend(): VisualizationBackend | undefined {
const activeDebugSession = this.debuggerView.activeDebugSession;
if (!activeDebugSession) {
return undefined;
}
const backend = this.getVisualizationBackend(activeDebugSession);
return backend;
}
public async getVisualizationData(
args: GetVisualizationDataArgs
): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: FormattedMessage }
> {
const activeDebugSession = this.debuggerView.activeDebugSession;
if (!activeDebugSession) {
return {
kind: "error",
message: "No active debug session.",
};
}
const backend = this.getVisualizationBackend(activeDebugSession);
if (!backend) {
return {
kind: "error",
message: `The debug adapter "${activeDebugSession.session.type}" is not supported.`,
};
}
return await backend.getVisualizationData(args);
}
public get expressionLanguageId(): string | undefined {
return this.activeVisualizationBackend?.expressionLanguageId;
}
public async getCompletions(
text: string,
column: number
): Promise<CompletionItem[]> {
return (
(await this.activeVisualizationBackend?.getCompletions(
text,
column
)) || []
);
}
}

View File

@@ -0,0 +1,229 @@
import {
DataExtractionResult,
DataExtractorId,
GraphNode,
GraphVisualizationData,
} from "@hediet/debug-visualizer-data-extraction";
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
import {
DebugSessionVisualizationSupport,
VisualizationBackend,
GetVisualizationDataArgs,
VisualizationBackendBase,
} from "./VisualizationBackend";
import { parseEvaluationResultFromGenericDebugAdapter } from "./parseEvaluationResultFromGenericDebugAdapter";
import { FormattedMessage } from "../webviewContract";
import { hotClass, registerUpdateReconciler } from "@hediet/node-reload";
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
registerUpdateReconciler(module);
@hotClass(module)
export class GenericVisualizationSupport
implements DebugSessionVisualizationSupport {
constructor(private readonly debuggerView: DebuggerViewProxy) {}
createBackend(
session: DebugSessionProxy
): VisualizationBackend | undefined {
return new GenericVisualizationBackend(session, this.debuggerView);
}
}
export class GenericVisualizationBackend extends VisualizationBackendBase {
public readonly expressionLanguageId = "text";
constructor(
debugSession: DebugSessionProxy,
debuggerView: DebuggerViewProxy
) {
super(debugSession, debuggerView);
}
public async getVisualizationData({
expression,
preferredExtractorId,
}: 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 };
try {
reply = await this.debugSession.evaluate({
expression: finalExpression,
frameId,
context: this.getContext(),
});
// Use structural information about variables
// from the evaluation response if present.
if (reply.variablesReference) {
const graph = await this.constructGraphFromVariablesReference(
reply.result,
reply.variablesReference
);
return {
kind: "data",
result: {
availableExtractors: [],
usedExtractor: {
id: "generic" as any,
name: "Generic",
priority: 1,
},
data: graph,
},
};
} else {
return parseEvaluationResultFromGenericDebugAdapter(
reply.result,
{
debugAdapterType: this.debugSession.session
.configuration.type,
}
);
}
} catch (error) {
return {
kind: "error",
message: {
kind: "list",
items: [
"An error occurred while evaluating the expression:",
error.message,
`Used debug adapter: ${this.debugSession.session.configuration.type}`,
{
kind: "inlineList",
items: [
"Evaluated expression is",
{ kind: "code", content: finalExpression },
],
},
],
},
};
}
}
/**
* Constructs GraphVisualizationData from a DAP variables
* reference by successively querying the debug adapter for
* 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
* @param maxKnownNodes - The maximum number of nodes
*/
private async constructGraphFromVariablesReference(
rootLabel: string,
rootVariablesReference: number,
maxDepth: number = 30,
maxKnownNodes: number = 50
): Promise<GraphVisualizationData> {
// Perform a breadth-first search on the object to construct the graph
const graph: GraphVisualizationData = {
kind: { graph: true },
nodes: [],
edges: [],
};
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;
do {
const variable = bfsQueue.shift()!;
const hasChilds = variable.variablesReference > 0;
if (variable.depth > maxDepth) {
break;
}
let nodeId: string;
if (!hasChilds || !(variable.variablesReference in knownNodeIds)) {
// The variable is a leaf or an unvisited object: create the node.
const node: GraphNode = {
id: hasChilds
? `${variable.variablesReference}`
: `__${variable.label}@${knownCount}__`,
label: variable.label,
color: variable.depth == 0 ? "lightblue" : undefined,
shape: "box",
};
graph.nodes.push(node);
knownCount++;
if (hasChilds) {
knownNodeIds[variable.variablesReference] = node.id;
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,
});
}
}
nodeId = node.id;
} else {
// The variable is a visited object (e.g. due to a cyclic reference)
nodeId = knownNodeIds[variable.variablesReference];
}
if (variable.source) {
graph.edges.push({
from: variable.source.id,
to: nodeId,
label: variable.source.name,
});
}
} while (bfsQueue.length > 0 && knownCount <= maxKnownNodes);
return graph;
}
protected getFinalExpression(args: {
expression: string;
preferredExtractorId: DataExtractorId | undefined;
}): string {
return args.expression;
}
protected getContext(): "watch" | "repl" {
// we will use "repl" as default so that results are not truncated.
return "repl";
}
}

View File

@@ -0,0 +1,395 @@
import {
getExpressionForDataExtractorApi,
DataResult,
getExpressionToInitializeDataExtractorApi,
DataExtractionResult,
getExpressionToDetectDataExtractorApiPresence,
} from "@hediet/debug-visualizer-data-extraction";
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
import {
DebugSessionVisualizationSupport,
VisualizationBackend,
GetVisualizationDataArgs,
VisualizationBackendBase,
} from "./VisualizationBackend";
import { FormattedMessage } from "../webviewContract";
import { registerUpdateReconciler, hotClass } from "@hediet/node-reload";
import { Config } from "../Config";
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
import { readFileSync, existsSync } from "fs";
import { reaction, observable } from "mobx";
import {
workspace,
FileSystemWatcher,
CancellationTokenSource,
CancellationToken,
window,
} from "vscode";
import { ResettableTimeout } from "@hediet/std/timer";
registerUpdateReconciler(module);
@hotClass(module)
export class JsEvaluationEngine implements DebugSessionVisualizationSupport {
constructor(
private readonly debuggerView: DebuggerViewProxy,
private readonly config: Config
) {}
createBackend(
session: DebugSessionProxy
): VisualizationBackend | undefined {
const supportedDebugAdapters = [
"node",
"node2",
"extensionHost",
"chrome",
"pwa-chrome",
"pwa-node",
"pwa-extensionHost",
"node-terminal",
"pwa-msedge",
];
if (supportedDebugAdapters.indexOf(session.session.type) !== -1) {
return new JsVisualizationBackend(
session,
this.debuggerView,
this.config
);
}
return undefined;
}
}
class JsVisualizationBackend extends VisualizationBackendBase {
public readonly expressionLanguageId = "javascript";
private apiInitialized = false;
private initializedPromise: Promise<void> | undefined = undefined;
constructor(
debugSession: DebugSessionProxy,
debuggerView: DebuggerViewProxy,
private readonly config: Config
) {
super(debugSession, debuggerView);
}
private getContext(): "copy" | "repl" {
if (this.debugSession.session.type.startsWith("pwa-")) {
return "copy";
}
return "repl";
}
public async getVisualizationData({
expression,
preferredExtractorId,
}: GetVisualizationDataArgs): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: FormattedMessage }
> {
try {
await this.ensureRuntimeIsInjected();
const frameId = this.debuggerView.getActiveStackFrameId(
this.debugSession
);
const variableNames = new Array<string>();
if (frameId) {
const scopes = await this.debugSession.getScopes({ frameId });
const scopeVariables = await Promise.all(
scopes
.filter(s => !s.expensive)
.map(s =>
this.debugSession.getVariables({
variablesReference: s.variablesReference,
})
)
);
for (const variables of scopeVariables) {
variableNames.push(
...variables
.filter(v => v.value !== "undefined")
.map(v => v.name)
);
}
if (variableNames.length > 100) {
variableNames.length = 100;
}
}
const preferredExtractorExpr = preferredExtractorId
? `"${preferredExtractorId}"`
: "undefined";
const body = `${getExpressionForDataExtractorApi()}.getData(
e => (${expression}),
expr => eval(expr),
${preferredExtractorExpr},
{${variableNames.map(n => `${n}: ${n}`).join(",")}},
)`;
const wrappedExpr = `
(() => {
try {
return ${body};
} catch (e) {
return JSON.stringify({
kind: "Error",
message: e.message,
stack: e.stack
});
}
})()
`;
const reply = await this.debugSession.evaluate({
expression: wrappedExpr,
frameId,
context: this.getContext(),
});
const resultStr = reply.result;
const jsonData =
this.getContext() === "copy"
? resultStr
: resultStr.substr(1, resultStr.length - 2);
const result = JSON.parse(jsonData) as DataResult;
if (result.kind === "NoExtractors") {
throw new Error("No extractors");
} else if (result.kind === "Error") {
throw new Error(result.message);
} else if (result.kind === "Data") {
return {
kind: "data",
result: result.extractionResult,
};
} else {
throw new Error("Invalid Data");
}
} catch (error) {
return {
kind: "error",
message: error.message,
};
}
}
private ensureRuntimeIsInjected(): Promise<void> | undefined {
if (this.apiInitialized) {
return;
}
if (this.initializedPromise) {
return this.initializedPromise;
} else {
this.initializedPromise = this._ensureRuntimeIsInjected();
}
}
private async _ensureRuntimeIsInjected(): Promise<void> {
const body = getExpressionToDetectDataExtractorApiPresence();
const reply = await this.debugSession.evaluate({
expression: body,
frameId: undefined,
context: this.getContext(),
});
const isApiInitialized = reply.result === "true";
if (!isApiInitialized) {
await this.initializeApi();
}
await this.initializeCustomScripts();
}
private async initializeApi(): Promise<boolean> {
try {
// prefer existing is true, so that manually registered (possibly newer) extractors are not overwritten.
const expression = `${getExpressionToInitializeDataExtractorApi()}.registerDefaultExtractors(true);`;
await this.debugSession.evaluate({
expression,
frameId: undefined,
context: this.getContext(),
});
return true;
} catch (error) {
return false;
}
}
@observable
private filesVersionId: number = 0;
private readonly fileChangeDebouncedScheduler = new DebouncedScheduler(100);
private async initializeCustomScripts(): Promise<void> {
await this.debugSession.evaluate({
expression: `global["${injectedScriptsKey}"] = {};`,
frameId: undefined,
context: "repl",
});
const watchers = new Map<string, WatchedFile>();
this.dispose.track({
dispose: () => {
for (const w of watchers.values()) {
w.watcher.dispose();
}
},
});
reaction(
() => ({
customScriptPaths: [...this.config.customScriptPaths],
version: this.filesVersionId,
}),
async ({ customScriptPaths }) => {
const promises = new Array<Promise<[Result, WatchedFile]>>();
const activePaths = new Set<string>();
for (const fsPath of customScriptPaths) {
activePaths.add(fsPath);
let entry = watchers.get(fsPath);
if (!entry) {
entry = new WatchedFile(fsPath, this.debugSession);
watchers.set(fsPath, entry);
entry.watcher.onDidChange(() => {
this.fileChangeDebouncedScheduler.run(() => {
this.filesVersionId++;
});
});
}
promises.push(entry.update().then(e => [e, entry!]));
}
for (const [fsPath, entry] of watchers) {
if (!activePaths.has(fsPath)) {
promises.push(entry.remove().then(e => [e, entry]));
entry.watcher.dispose();
watchers.delete(fsPath);
}
}
const results = await Promise.all(promises);
const errors = results.filter(([r]) => r.error);
for (const [e, watcher] of errors) {
window.showErrorMessage(
`Error while evaluating "${watcher.fsPath}": ${
e.error!.message
}`
);
}
if (promises.length > 0) {
this.onChangeEmitter.emit();
}
},
{
fireImmediately: true,
}
);
}
}
const injectedScriptsKey = `@hediet/debug-visualizer/injectedScripts`;
interface Result {
error?: { message: string };
}
class WatchedFile {
public static nextId = 0;
public readonly watcher = workspace.createFileSystemWatcher(this.fsPath);
public readonly id = WatchedFile.nextId++;
public lastContent: string | undefined = undefined;
constructor(
public readonly fsPath: string,
private readonly debugSession: DebugSessionProxy
) {}
public async update(): Promise<Result> {
if (!existsSync(this.fsPath)) {
return {
error: { message: `File does not exist.` },
};
}
const scriptContent = readFileSync(this.fsPath, {
encoding: "utf-8",
});
if (this.lastContent === scriptContent) {
return {};
}
this.lastContent = scriptContent;
const expression = `
(
load => {
var module = {};
load(module);
global["${injectedScriptsKey}"]["${this.id}"] = module.exports;
}
)
(
function (module) { ${scriptContent} }
)`;
try {
await this.debugSession.evaluate({
expression,
frameId: undefined,
context: "repl",
});
} catch (e) {
return { error: { message: e.message } };
}
return {};
}
public async remove(): Promise<Result> {
const expression = `(() => { delete global["${injectedScriptsKey}"]["${this.id}"]; })()`;
try {
await this.debugSession.evaluate({
expression,
frameId: undefined,
context: "repl",
});
} catch (e) {
return { error: { message: e.message } };
}
return {};
}
}
class DebouncedScheduler {
private timeout: NodeJS.Timeout | undefined;
constructor(private readonly debounceTimeout: number) {}
public run(action: () => void): void {
this.clear();
this.timeout = setTimeout(action, this.debounceTimeout);
}
private clear() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = undefined;
}
}
public dispose(): void {
this.clear();
}
}

View File

@@ -0,0 +1,80 @@
import {
DataExtractionResult,
DataExtractorId,
} from "@hediet/debug-visualizer-data-extraction";
import { Disposable } from "@hediet/std/disposable";
import { DebugSessionProxy } from "../proxies/DebugSessionProxy";
import { CompletionItem, FormattedMessage } from "../webviewContract";
import { reaction } from "mobx";
import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy";
import { EventEmitter, EventSource } from "@hediet/std/events";
export interface DebugSessionVisualizationSupport {
createBackend(session: DebugSessionProxy): VisualizationBackend | undefined;
}
export interface VisualizationBackend extends Disposable {
readonly onChange: EventSource;
getVisualizationData(
args: GetVisualizationDataArgs
): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: FormattedMessage }
>;
/**
* The language that expressions must be written in.
*/
readonly expressionLanguageId: string | undefined;
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
}
export interface GetVisualizationDataArgs {
expression: string;
preferredExtractorId: DataExtractorId | undefined;
}
export abstract class VisualizationBackendBase implements VisualizationBackend {
public readonly dispose = Disposable.fn();
protected readonly onChangeEmitter = new EventEmitter();
public readonly onChange = this.onChangeEmitter;
constructor(
protected readonly debugSession: DebugSessionProxy,
protected readonly debuggerView: DebuggerViewProxy
) {
this.dispose.track({
dispose: reaction(
() => debuggerView.getActiveStackFrameId(debugSession),
activeStackFrameId => {
if (activeStackFrameId !== undefined) {
this.onChangeEmitter.emit();
}
}
),
});
}
public abstract readonly expressionLanguageId: string | undefined;
public abstract getVisualizationData(
args: GetVisualizationDataArgs
): Promise<
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: FormattedMessage }
>;
public async getCompletions(
text: string,
column: number
): Promise<CompletionItem[]> {
return await this.debugSession.getCompletions({
text,
frameId: this.debuggerView.getActiveStackFrameId(this.debugSession),
column,
});
}
}

View File

@@ -0,0 +1,6 @@
export * from "./VisualizationBackend";
export { ComposedVisualizationSupport } from "./ComposedVisualizationSupport";
export { JsEvaluationEngine } from "./JsVisualizationSupport";
export { GenericVisualizationSupport } from "./GenericVisualizationSupport";
export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport";

View File

@@ -0,0 +1,120 @@
import {
DataExtractionResult,
isVisualizationData,
} from "@hediet/debug-visualizer-data-extraction";
import { FormattedMessage } from "../webviewContract";
export interface ParseEvaluationResultContext {
debugAdapterType: string;
}
export function parseEvaluationResultFromGenericDebugAdapter(
resultText: string,
context: ParseEvaluationResultContext
):
| { kind: "data"; result: DataExtractionResult }
| { kind: "error"; message: FormattedMessage } {
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, context);
} catch (e) {
// in case of C++: `"{ \"kind\": { ... }, \"text\": \"some\\ntext\" }"`
const str = parseJson(jsonData, context);
// str is now `{ "kind": { ... }, "text": "some\ntext" }"`
resultObj = parseJson(str, context);
// result is now { kind: { ... }, text: "some\ntext" }
}
if (!isVisualizationData(resultObj)) {
return {
kind: "error",
message: {
kind: "list",
items: [
"Evaluation result does not match ExtractedData interface.",
{
kind: "inlineList",
items: [
"Evaluation result was:",
{
kind: "code",
content: JSON.stringify(
resultObj,
undefined,
4
),
},
],
},
],
},
};
}
} 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, context: ParseEvaluationResultContext) {
try {
return JSON.parse(str);
} catch (error) {
throw new FormattedError({
kind: "list",
items: [
"Could not parse evaluation result as JSON:",
error.message,
{
kind: "inlineList",
items: [
"Evaluation result was:",
{
kind: "code",
content: str,
},
],
},
`Used debug adapter: ${context.debugAdapterType}`,
],
});
}
}
class FormattedError {
constructor(public readonly message: FormattedMessage) {}
}