diff --git a/extension/README.md b/extension/README.md index c9c2d81..508df73 100644 --- a/extension/README.md +++ b/extension/README.md @@ -25,6 +25,7 @@ See [demos](../demos/) for demos. These languages and debuggers are verified to - C++ using `cppdbg` debug adapter: [✅ Basic Support](../demos/cpp) - Swift using `lldb` debug adapter: [✅ Basic Support](../demos/swift) - Rust using `lldb` debug adapter: [✅ Basic Support](../demos/rust) +- Ruby using `rdbg` debug adapter: [✅ Basic Support] All other languages and debuggers might work too. For languages with _Basic Support_, only JSON strings can be visualized - you must implement some logic that builds this JSON for your data structure! diff --git a/extension/src/VisualizationBackend/RbVisualizationSupport.ts b/extension/src/VisualizationBackend/RbVisualizationSupport.ts new file mode 100644 index 0000000..434cede --- /dev/null +++ b/extension/src/VisualizationBackend/RbVisualizationSupport.ts @@ -0,0 +1,142 @@ +import { DataExtractionResult, DataResult } from "@hediet/debug-visualizer-data-extraction"; +import { hotClass, registerUpdateReconciler } from "@hediet/node-reload"; +import { Config } from "../Config"; +import { DebuggerViewProxy } from "../proxies/DebuggerViewProxy"; +import { DebugSessionProxy } from "../proxies/DebugSessionProxy"; +import { FormattedMessage } from "../webviewContract"; +import { DebugSessionVisualizationSupport, GetVisualizationDataArgs, VisualizationBackend, VisualizationBackendBase } from "./VisualizationBackend"; + +registerUpdateReconciler(module); + +@hotClass(module) +export class RbEvaluationEngine implements DebugSessionVisualizationSupport { + constructor( + private readonly debuggerView: DebuggerViewProxy, + private readonly config: Config + ) { } + + createBackend( + session: DebugSessionProxy + ): VisualizationBackend | undefined { + const supportedDebugAdapters = ['rdbg']; + + if (supportedDebugAdapters.indexOf(session.session.type) !== -1) { + return new RbVisualizationBackend( + session, + this.debuggerView, + this.config + ); + } + return undefined; + } +} + +class RbVisualizationBackend extends VisualizationBackendBase { + public readonly expressionLanguageId = 'ruby'; + constructor( + debugSession: DebugSessionProxy, + debuggerView: DebuggerViewProxy, + private readonly config: Config + ) { + super(debugSession, debuggerView) + } + + private readonly defaultContext = "repl"; + + public async getVisualizationData( + args: GetVisualizationDataArgs + ): Promise< + | { kind: "data"; result: DataExtractionResult; } + | { kind: "error"; message: FormattedMessage; } + > { + const result = await this._getVisualizationData(args); + return result; + } + + private async _getVisualizationData({ + expression, + preferredExtractorId + }: GetVisualizationDataArgs): Promise< + | { kind: "data"; result: DataExtractionResult } + | { kind: "error"; message: FormattedMessage } + > { + try { + if (expression.length === 0) throw new Error("No extractors"); + + const frameId = this.debuggerView.getActiveStackFrameId( + this.debugSession + ); + const initialReply = await this.debugSession.evaluate({ + expression: "require 'debugvisualizer'", + frameId, + context: this.defaultContext + }); + if (initialReply.result.includes('LoadError')) { + return { + kind: "error", + message: { + kind: "list", + items: [ + "LoadError: Failed to load debugvisualizer.", + { + kind: "inlineList", + items: [ + "Install the gem by executing:", + { kind: "code", content: "$ bundle add debugvisualizer" }, + ], + }, + { + kind: "inlineList", + items: [ + "If bundler is not being used, install the gem by executing:", + { kind: "code", content: "$ gem install debugvisualizer" }, + ], + } + ], + } + }; + } + const preferredId = preferredExtractorId || ''; + const wrappedExpr = ` + DebugVisualizer.to_debug_visualizer_protocol_json("${preferredId}", ${expression}) + `; + const reply = await this.debugSession.evaluate({ + expression: wrappedExpr, + frameId, + context: this.defaultContext + }) + + let dataResult: DataResult; + try { + // Debuggee converts result to a JSON string twice. + dataResult = JSON.parse(JSON.parse(reply.result)) as DataResult; + } catch (error) { + let message = error.message + // The `reply.result` is as follows when error occurs in the evaluation of an expression and parsing will fail. + // e.g. "#" + // In this case, it is more beneficial to display this error message. + if (reply.result.includes('Error')) message = reply.result; + throw new Error(message) + } + + switch (dataResult.kind) { + case 'NoExtractors': + throw new Error("No extractors"); + case 'Error': + throw new Error(dataResult.message); + case 'Data': + return { + kind: "data", + result: dataResult.extractionResult, + }; + default: + throw new Error("Invalid Data"); + } + } catch (error) { + return { + kind: "error", + message: error.message + }; + } + } +} diff --git a/extension/src/VisualizationBackend/index.ts b/extension/src/VisualizationBackend/index.ts index 2afe268..6558860 100644 --- a/extension/src/VisualizationBackend/index.ts +++ b/extension/src/VisualizationBackend/index.ts @@ -3,5 +3,6 @@ export * from "./VisualizationBackend"; export { ComposedVisualizationSupport } from "./ComposedVisualizationSupport"; export { JsEvaluationEngine } from "./JsVisualizationSupport"; export { PyEvaluationEngine } from "./PyVisualizationSupport"; +export { RbEvaluationEngine } from './RbVisualizationSupport'; export { GenericVisualizationSupport } from "./GenericVisualizationSupport"; export { ConfigurableVisualizationSupport } from "./ConfigurableVisualizationSupport"; diff --git a/extension/src/extension.ts b/extension/src/extension.ts index b577b1e..fb6deb2 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -22,6 +22,7 @@ import { ComposedVisualizationSupport, JsEvaluationEngine, PyEvaluationEngine, + RbEvaluationEngine, GenericVisualizationSupport, ConfigurableVisualizationSupport, } from "./VisualizationBackend"; @@ -53,6 +54,7 @@ export class Extension { ), new JsEvaluationEngine(this.debuggerView, this.config), new PyEvaluationEngine(this.debuggerView, this.config), + new RbEvaluationEngine(this.debuggerView, this.config), new GenericVisualizationSupport(this.debuggerView), ]), this.debuggerView @@ -102,7 +104,7 @@ export class Extension { if ( match.index <= selection.start.character && selection.start.character <= - match.index + match[0].length + match.index + match[0].length ) { selectedText = match[1]; }