diff --git a/extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts b/extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts index 1cc40b4..f5c0751 100644 --- a/extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts +++ b/extension/src/VisualizationBackend/DispatchingVisualizationBackend.ts @@ -131,4 +131,14 @@ export class DispatchingVisualizationBackend implements VisualizationBackend { )) || [] ); } + + public async evaluateForEffect( + expression: string + ): Promise<{ result: string } | { error: string }> { + const backend = this.activeVisualizationBackend; + if (!backend) { + return { error: "No active debug session." }; + } + return await backend.evaluateForEffect(expression); + } } diff --git a/extension/src/VisualizationBackend/VisualizationBackend.ts b/extension/src/VisualizationBackend/VisualizationBackend.ts index b1a38c5..145041f 100644 --- a/extension/src/VisualizationBackend/VisualizationBackend.ts +++ b/extension/src/VisualizationBackend/VisualizationBackend.ts @@ -29,6 +29,14 @@ export interface VisualizationBackend extends Disposable { readonly expressionLanguageId: string | undefined; getCompletions(text: string, column: number): Promise; + + /** + * Evaluates `expression` in the current frame for its side effect + * (e.g. an assignment) rather than to extract visualization data. + */ + evaluateForEffect( + expression: string + ): Promise<{ result: string } | { error: string }>; } export interface GetVisualizationDataArgs { @@ -80,4 +88,22 @@ export abstract class VisualizationBackendBase implements VisualizationBackend { column, }); } + + public async evaluateForEffect( + expression: string + ): Promise<{ result: string } | { error: string }> { + try { + const frameId = this.debuggerView.getActiveStackFrameId( + this.debugSession + ); + const reply = await this.debugSession.evaluate({ + expression, + frameId, + context: "repl", + }); + return { result: reply.result }; + } catch (error: any) { + return { error: error.message }; + } + } } diff --git a/extension/src/VisualizationWatchModel/VisualizationWatchModel.ts b/extension/src/VisualizationWatchModel/VisualizationWatchModel.ts index 84de9a6..5685811 100644 --- a/extension/src/VisualizationWatchModel/VisualizationWatchModel.ts +++ b/extension/src/VisualizationWatchModel/VisualizationWatchModel.ts @@ -15,6 +15,14 @@ export interface VisualizationWatchModel { * This field is observable. */ readonly languageId: string | undefined; + + /** + * Evaluates `expression` in the current frame for its side effect + * (e.g. an assignment) rather than to extract visualization data. + */ + evaluateForEffect( + expression: string + ): Promise<{ result: string } | { error: string }>; } export interface VisualizationWatchOptions { diff --git a/extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts b/extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts index 76df626..c4a1a06 100644 --- a/extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts +++ b/extension/src/VisualizationWatchModel/VisualizationWatchModelImpl.ts @@ -44,6 +44,12 @@ export class VisualizationWatchModelImpl implements VisualizationWatchModel { public getCompletions(text: string, column: number): Promise { return this.visualizationBackend.getCompletions(text, column); } + + public evaluateForEffect( + expression: string + ): Promise<{ result: string } | { error: string }> { + return this.visualizationBackend.evaluateForEffect(expression); + } } class ObservableVisualizationWatch implements VisualizationWatch { diff --git a/extension/src/webview/WebviewConnection.ts b/extension/src/webview/WebviewConnection.ts index 05c6655..25fc9b0 100644 --- a/extension/src/webview/WebviewConnection.ts +++ b/extension/src/webview/WebviewConnection.ts @@ -103,6 +103,14 @@ export class WebviewConnection { completions, }; }, + evaluateForEffect: async ({ expression }) => { + throwIfNotAuthenticated(); + + const reply = await evaluationWatchService.evaluateForEffect(expression); + return "error" in reply + ? { success: false, message: reply.error } + : { success: true, message: reply.result }; + }, }); this.client = client; diff --git a/extension/src/webviewContract.ts b/extension/src/webviewContract.ts index 296629d..6757b3d 100644 --- a/extension/src/webviewContract.ts +++ b/extension/src/webviewContract.ts @@ -90,6 +90,23 @@ export const webviewContract = contract({ completions: types.array(unchecked()), }), }), + + // Evaluates `expression` for its side effect (e.g. an assignment + // like `x = 5`) in the active debug session's current frame, using + // the same DebugSessionProxy.evaluate() the read-only visualization + // path already uses (context "repl", so debug adapters that only + // truncate/sanitize "watch" context results still run this in full). + // Used by custom visualizer scripts (see vis-plugins/node-table- + // visualizer.js) to support editing values in place. + evaluateForEffect: requestContract({ + params: types.type({ + expression: types.string, + }), + result: types.type({ + success: types.boolean, + message: types.string, + }), + }), }, }); diff --git a/webview/src/model/Model.ts b/webview/src/model/Model.ts index adf5551..427dc5c 100644 --- a/webview/src/model/Model.ts +++ b/webview/src/model/Model.ts @@ -161,6 +161,24 @@ export class Model { this.pollingTimer.onTick.sub(() => { this.refresh(); }); + + // Exposed globally (not passed into custom visualizer scripts' + // closures) for the same reason setVisualizationModule is below: + // setCustomVisualizerScript's `eval()` runs plugin code in this + // same window/global scope, so a plugin (see vis-plugins/node- + // table-visualizer.js) can reach these to support editing values + // in place, without threading a new parameter through the whole + // register()/getVisualization()/render() plugin contract. + (globalThis as any).debugVisualizerSetValue = (expression: string) => + this.evaluateForEffect(expression); + (globalThis as any).debugVisualizerRefresh = () => this.refresh(); + } + + async evaluateForEffect(expression: string): Promise<{ success: boolean; message: string }> { + if (!this.server) { + return { success: false, message: "Not connected to the debug visualizer server." }; + } + return this.server.evaluateForEffect({ expression }); } @action