diff --git a/extensions/ql-vscode/src/common/interface-types.ts b/extensions/ql-vscode/src/common/interface-types.ts index 9306fde51c1..2d8a07fe690 100644 --- a/extensions/ql-vscode/src/common/interface-types.ts +++ b/extensions/ql-vscode/src/common/interface-types.ts @@ -598,8 +598,7 @@ export type FromModelEditorMessage = | SetModeledMethodMessage; export type FromMethodModelingMessage = - | TelemetryMessage - | UnhandledErrorMessage + | CommonFromViewMessages | SetModeledMethodMessage; interface SetMethodMessage { diff --git a/extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts b/extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts new file mode 100644 index 00000000000..05bcab3082a --- /dev/null +++ b/extensions/ql-vscode/src/common/vscode/abstract-webview-view-provider.ts @@ -0,0 +1,85 @@ +import * as vscode from "vscode"; +import { Uri, WebviewViewProvider } from "vscode"; +import { WebviewKind, WebviewMessage, getHtmlForWebview } from "./webview-html"; +import { Disposable } from "../disposable-object"; +import { App } from "../app"; + +export abstract class AbstractWebviewViewProvider< + ToMessage extends WebviewMessage, + FromMessage extends WebviewMessage, +> implements WebviewViewProvider +{ + protected webviewView: vscode.WebviewView | undefined = undefined; + private disposables: Disposable[] = []; + + constructor( + private readonly app: App, + private readonly webviewKind: WebviewKind, + ) {} + + /** + * This is called when a view first becomes visible. This may happen when the view is + * first loaded or when the user hides and then shows a view again. + */ + public resolveWebviewView( + webviewView: vscode.WebviewView, + _context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [Uri.file(this.app.extensionPath)], + }; + + const html = getHtmlForWebview( + this.app, + webviewView.webview, + this.webviewKind, + { + allowInlineStyles: true, + allowWasmEval: false, + }, + ); + + webviewView.webview.html = html; + + this.webviewView = webviewView; + + webviewView.webview.onDidReceiveMessage(async (msg) => this.onMessage(msg)); + webviewView.onDidDispose(() => this.dispose()); + } + + protected get isShowingView() { + return this.webviewView?.visible ?? false; + } + + protected async postMessage(msg: ToMessage): Promise { + await this.webviewView?.webview.postMessage(msg); + } + + protected dispose() { + while (this.disposables.length > 0) { + const disposable = this.disposables.pop()!; + disposable.dispose(); + } + + this.webviewView = undefined; + } + + protected push(obj: T): T { + if (obj !== undefined) { + this.disposables.push(obj); + } + return obj; + } + + protected abstract onMessage(msg: FromMessage): Promise; + + /** + * This is called when a view first becomes visible. This may happen when the view is + * first loaded or when the user hides and then shows a view again. + */ + protected onWebViewLoaded(): void { + // Do nothing by default. + } +} diff --git a/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-panel.ts b/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-panel.ts index 6a361ca6839..2136bda48fa 100644 --- a/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-panel.ts +++ b/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-panel.ts @@ -12,7 +12,6 @@ export class MethodModelingPanel extends DisposableObject { super(); this.provider = new MethodModelingViewProvider(app, modelingStore); - this.push(this.provider); this.push( window.registerWebviewViewProvider( MethodModelingViewProvider.viewType, diff --git a/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts b/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts index 7e7e11e743a..e87375dc468 100644 --- a/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts +++ b/extensions/ql-vscode/src/model-editor/method-modeling/method-modeling-view-provider.ts @@ -1,82 +1,51 @@ -import * as vscode from "vscode"; -import { Uri, WebviewViewProvider } from "vscode"; -import { getHtmlForWebview } from "../../common/vscode/webview-html"; -import { FromMethodModelingMessage } from "../../common/interface-types"; +import { + FromMethodModelingMessage, + ToMethodModelingMessage, +} from "../../common/interface-types"; import { telemetryListener } from "../../common/vscode/telemetry"; import { showAndLogExceptionWithTelemetry } from "../../common/logging/notifications"; import { extLogger } from "../../common/logging/vscode/loggers"; import { App } from "../../common/app"; import { redactableError } from "../../common/errors"; import { Method } from "../method"; -import { DisposableObject } from "../../common/disposable-object"; import { ModelingStore } from "../modeling-store"; +import { AbstractWebviewViewProvider } from "../../common/vscode/abstract-webview-view-provider"; -export class MethodModelingViewProvider - extends DisposableObject - implements WebviewViewProvider -{ +export class MethodModelingViewProvider extends AbstractWebviewViewProvider< + ToMethodModelingMessage, + FromMethodModelingMessage +> { public static readonly viewType = "codeQLMethodModeling"; - private webviewView: vscode.WebviewView | undefined = undefined; - private method: Method | undefined = undefined; constructor( - private readonly app: App, + app: App, private readonly modelingStore: ModelingStore, ) { - super(); + super(app, "method-modeling"); } - /** - * This is called when a view first becomes visible. This may happen when the view is - * first loaded or when the user hides and then shows a view again. - */ - public resolveWebviewView( - webviewView: vscode.WebviewView, - _context: vscode.WebviewViewResolveContext, - _token: vscode.CancellationToken, - ) { - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [Uri.file(this.app.extensionPath)], - }; - - const html = getHtmlForWebview( - this.app, - webviewView.webview, - "method-modeling", - { - allowInlineStyles: true, - allowWasmEval: false, - }, - ); - - webviewView.webview.html = html; - - webviewView.webview.onDidReceiveMessage(async (msg) => this.onMessage(msg)); - - this.webviewView = webviewView; - - this.setInitialState(webviewView); + protected override onWebViewLoaded(): void { + this.setInitialState(); this.registerToModelingStoreEvents(); } public async setMethod(method: Method): Promise { this.method = method; - if (this.webviewView) { - await this.webviewView.webview.postMessage({ + if (this.isShowingView) { + await this.postMessage({ t: "setMethod", method, }); } } - private setInitialState(webviewView: vscode.WebviewView): void { + private setInitialState(): void { const selectedMethod = this.modelingStore.getSelectedMethodDetails(); if (selectedMethod) { - void webviewView.webview.postMessage({ + void this.postMessage({ t: "setSelectedMethod", method: selectedMethod.method, modeledMethod: selectedMethod.modeledMethod, @@ -85,24 +54,18 @@ export class MethodModelingViewProvider } } - private async onMessage(msg: FromMethodModelingMessage): Promise { + protected override async onMessage( + msg: FromMethodModelingMessage, + ): Promise { switch (msg.t) { - case "setModeledMethod": { - const activeState = this.modelingStore.getStateForActiveDb(); - if (!activeState) { - throw new Error("No active state found in modeling store"); - } - this.modelingStore.updateModeledMethod( - activeState.databaseItem, - msg.method, - ); + case "viewLoaded": + this.onWebViewLoaded(); break; - } - case "telemetry": { + case "telemetry": telemetryListener?.sendUIInteraction(msg.action); break; - } + case "unhandledError": void showAndLogExceptionWithTelemetry( extLogger, @@ -112,6 +75,18 @@ export class MethodModelingViewProvider )`Unhandled error in method modeling view: ${msg.error.message}`, ); break; + + case "setModeledMethod": { + const activeState = this.modelingStore.getStateForActiveDb(); + if (!activeState) { + throw new Error("No active state found in modeling store"); + } + this.modelingStore.updateModeledMethod( + activeState.databaseItem, + msg.method, + ); + break; + } } }