diff --git a/demos/java/.project b/demos/java/.project index f1e2727..2e6ed36 100644 --- a/demos/java/.project +++ b/demos/java/.project @@ -1,17 +1,28 @@ - + - java - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - \ No newline at end of file + java + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + + + 1599063072264 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts b/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts index 40e08d5..126b1f0 100644 --- a/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts +++ b/extension/src/EvaluationWatchService/EvaluationEngine/GenericEvaluationEngine.ts @@ -1,6 +1,8 @@ import { DataExtractionResult, DataExtractorId, + GraphNode, + GraphVisualizationData, } from "@hediet/debug-visualizer-data-extraction"; import { EnhancedDebugSession } from "../../debugger/EnhancedDebugSession"; import { @@ -38,13 +40,36 @@ export class GenericEvaluator implements Evaluator { expression, preferredExtractorId, }); - let reply; + let reply: { result: string, variablesReference: number }; try { reply = await this.session.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.session.session.configuration.type, + }); + } } catch (error) { return { kind: "error", @@ -65,10 +90,87 @@ export class GenericEvaluator implements Evaluator { }, }; } + } - return parseEvaluationResultFromGenericDebugAdapter(reply.result, { - debugAdapterType: this.session.session.configuration.type, - }); + /** + * 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 { + // 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.session.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: { diff --git a/extension/src/debugger/EnhancedDebugSession.ts b/extension/src/debugger/EnhancedDebugSession.ts index 4cf7a3c..5e8a00e 100644 --- a/extension/src/debugger/EnhancedDebugSession.ts +++ b/extension/src/debugger/EnhancedDebugSession.ts @@ -46,6 +46,23 @@ export class EnhancedDebugSession { } } + public async getVariables(args: { + variablesReference: number + }): Promise { + try { + const reply = await this.session.customRequest("variables", { + variablesReference: args.variablesReference + }); + if (!reply) { + return []; + } + return reply.variables; + } catch (error) { + console.error(error); + return []; + } + } + /** * Evaluates the given expression. * If context is "watch", long results are usually shortened. @@ -55,15 +72,22 @@ export class EnhancedDebugSession { expression: string; frameId: number | undefined; context: "watch" | "repl" | "copy"; - }): Promise<{ result: string }> { + }): Promise<{ result: string, variablesReference: number }> { const reply = await this.session.customRequest("evaluate", { expression: args.expression, frameId: args.frameId, context: args.context, }); - return { result: reply.result }; + return { result: reply.result, variablesReference: reply.variablesReference }; } } + +interface Variable { + name: string; + value: string; + variablesReference: number; +} + interface StackFrame { id: number; name: string;