Add evaluateForEffect method to support expression evaluation for side effects
Some checks failed
/ build (macos-latest) (push) Has been cancelled
/ build (ubuntu-latest) (push) Has been cancelled
/ build (windows-latest) (push) Has been cancelled

This commit is contained in:
mynameisdeleted
2026-07-10 03:57:54 -04:00
parent 96c26e5388
commit 18acf1d7f4
7 changed files with 93 additions and 0 deletions

View File

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

View File

@@ -29,6 +29,14 @@ export interface VisualizationBackend extends Disposable {
readonly expressionLanguageId: string | undefined;
getCompletions(text: string, column: number): Promise<CompletionItem[]>;
/**
* 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 };
}
}
}

View File

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

View File

@@ -44,6 +44,12 @@ export class VisualizationWatchModelImpl implements VisualizationWatchModel {
public getCompletions(text: string, column: number): Promise<CompletionItem[]> {
return this.visualizationBackend.getCompletions(text, column);
}
public evaluateForEffect(
expression: string
): Promise<{ result: string } | { error: string }> {
return this.visualizationBackend.evaluateForEffect(expression);
}
}
class ObservableVisualizationWatch implements VisualizationWatch {

View File

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

View File

@@ -90,6 +90,23 @@ export const webviewContract = contract({
completions: types.array(unchecked<CompletionItem>()),
}),
}),
// 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,
}),
}),
},
});

View File

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