From e289bd1e3bdd460667e7bb21b5e25550aead4bcd Mon Sep 17 00:00:00 2001 From: Neel Gondalia Date: Mon, 5 Jun 2023 14:07:32 -0400 Subject: [PATCH] Implemented external API This commit provides an external API that can be used by other extensions to communicate with the base trace extension. The current API provides a way to retrieve the active experiment as well as fire events and register callbacks. This is the initial API and will be further expanded in future commits. Fixes #140 Signed-off-by: Neel Gondalia --- README.md | 64 +++++++++++++++ vscode-trace-extension/src/extension.ts | 7 +- .../src/external-api/external-api.ts | 64 +++++++++++++++ ...plorer-available-views-webview-provider.ts | 2 + ...explorer-opened-traces-webview-provider.ts | 4 +- ...plorer-properties-view-webview-provider.ts | 2 + .../trace-viewer-webview-panel.ts | 6 ++ .../utils/trace-extension-webview-manager.ts | 78 +++++++++++++++++++ 8 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 vscode-trace-extension/src/external-api/external-api.ts create mode 100644 vscode-trace-extension/src/utils/trace-extension-webview-manager.ts diff --git a/README.md b/README.md index 4e0f171f..830c79e8 100644 --- a/README.md +++ b/README.md @@ -215,3 +215,67 @@ yarn playwright test --retries [vscode-messages]: https://code.visualstudio.com/api/extension-guides/webview#passing-messages-from-an-extension-to-a-webview [vscode-webview]: https://github.com/rebornix/vscode-webview-react [vscode-webview-react]: https://github.com/rebornix/vscode-webview-react + +## Using the External API + +VSCode Trace Extension provides an external API that adopter extensions can rely on for communication. Currently the API is limited to the following: + +```javascript +getActiveExperiment(): Experiment | undefined +getActiveWebviewPanels(): { [key: string]: TraceViewerPanel | undefined; } +getActiveWebviews(): vscode.WebviewView[] +onWebviewCreated(listener: (data: vscode.WebviewView) => void): void +onWebviewPanelCreated(listener: (data: vscode.WebviewPanel) => void): void +``` + +### Using the API from Adopter Extensions + +```javascript +//The following retrieves the API object from the vscode-trace-extension +const ext = vscode.extensions.getExtension("tracecompass-community.vscode-trace-extension"); +const importedApi = ext.exports; +``` + +Once you have the API object, you can proceed to make API calls. For example, if you wish to retrieve the active experiment in the Trace Viewer, the following API call can be used: + +```javascript +const experiment = importedApi.getActiveExperiment(); +``` + +The API provides getters to retrieve the active webviews and panels. This can be useful for scenarios when webviews/panels were created before the adopter extension was activated but the adopter extension still wants to handle messages from them. + +```javascript +for (const webview of importedApi.getActiveWebviews()) { + webview.webview.onDidReceiveMessage((message) => { + switch (message.command) { + case "webviewReady": + console.log("From adopter extension - webviewReady signal received"); + break; + default: + break; + } + }); +} +``` + +The API also provides a way to attach a listener for when webview or webview panel is created. Note that this listener will not be called for webviews and panels created before the registration of the listener. It is recommended to register the listeners during the activation of the adopter extensions. + +```javascript +importedApi.onWebviewPanelCreated(_panel => { + // For newly created panel, handle messages from webviews + _panel.webview.onDidReceiveMessage((message) => { + switch (message.command) { + case "webviewReady": + console.log("From adopter extension - webviewReady signal received"); + break; + default: + break; + } + }); + _panel.onDidDispose(() => { + console.log("panel disposed"); + }); +}); +``` + +As a general rule, adopter extensions should retrieve and handle the webviews and webview panels once during their activation by calling `getActiveWebviews` and `getActiveWebviewPanels`. This ensures that the webviews and panels created before the activation of the adopter extension are handled. To handle any new webviews and panels created afterwards, listeners can be registered by calling `onWebviewCreated` and `onWebviewPanelCreated`. diff --git a/vscode-trace-extension/src/extension.ts b/vscode-trace-extension/src/extension.ts index 4b158771..b7d2043c 100644 --- a/vscode-trace-extension/src/extension.ts +++ b/vscode-trace-extension/src/extension.ts @@ -8,14 +8,17 @@ import { fileHandler, openOverviewHandler, resetZoomHandler, undoRedoHandler, zo import { TraceServerConnectionStatusService } from './utils/trace-server-status'; import { getTspClientUrl, updateTspClient } from './utils/tspClient'; import { TraceExtensionLogger } from './utils/trace-extension-logger'; +import { ExternalAPI, traceExtensionAPI} from './external-api/external-api'; +import { TraceExtensionWebviewManager } from './utils/trace-extension-webview-manager'; import { VSCODE_MESSAGES } from 'vscode-trace-common/lib/messages/vscode-message-manager'; import { TraceViewerPanel } from './trace-viewer-panel/trace-viewer-webview-panel'; import { TspClientProvider } from 'vscode-trace-common/lib/client/tsp-client-provider-impl'; export let traceLogger: TraceExtensionLogger; +export const traceExtensionWebviewManager: TraceExtensionWebviewManager = new TraceExtensionWebviewManager(); const tspClientProvider = new TspClientProvider(getTspClientUrl(), undefined); -export function activate(context: vscode.ExtensionContext): void { +export function activate(context: vscode.ExtensionContext): ExternalAPI { traceLogger = new TraceExtensionLogger('Trace Extension'); const serverStatusBarItemPriority = 1; @@ -117,10 +120,12 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands.executeCommand('setContext', 'traceViewer.markerSetsPresent', false); vscode.commands.executeCommand('setContext', 'traceViewer.markerCategoriesPresent', false); + return traceExtensionAPI; } export function deactivate(): void { traceLogger.disposeChannel(); + traceExtensionWebviewManager.dispose(); } async function startTraceServerIfAvailable(): Promise { diff --git a/vscode-trace-extension/src/external-api/external-api.ts b/vscode-trace-extension/src/external-api/external-api.ts new file mode 100644 index 00000000..cf927eb0 --- /dev/null +++ b/vscode-trace-extension/src/external-api/external-api.ts @@ -0,0 +1,64 @@ +/*************************************************************************************** + * Copyright (c) 2023 BlackBerry Limited and others. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +import { Experiment } from 'tsp-typescript-client/lib/models/experiment'; +import { TraceViewerPanel } from '../trace-viewer-panel/trace-viewer-webview-panel'; +import * as vscode from 'vscode'; +import { traceExtensionWebviewManager } from '../extension'; + +export interface ExternalAPI { + getActiveExperiment(): Experiment | undefined, + getActiveWebviewPanels(): { [key: string]: TraceViewerPanel | undefined; }, + getActiveWebviews(): vscode.WebviewView[], + onWebviewCreated(listener: (data: vscode.WebviewView) => void): void, + onWebviewPanelCreated(listener: (data: vscode.WebviewPanel) => void): void, +} + +export const traceExtensionAPI: ExternalAPI = { + /** + * Retrieves the currently active experiment + * + * @returns Experiment if one is currently active, otherwise undefined + */ + getActiveExperiment(): Experiment | undefined { + return TraceViewerPanel.getCurrentExperiment(); + }, + + /** + * Retrieves active trace panels + * + * @returns Key value pairs where the value is of TraceViewerPanel type if panel with that key is active, otherwise undefined + */ + getActiveWebviewPanels(): { [key: string]: TraceViewerPanel | undefined; } { + return TraceViewerPanel.activePanels; + }, + + /** + * Retrieves active webviews + * + * @returns List of active webviews + */ + getActiveWebviews(): vscode.WebviewView[] { + return traceExtensionWebviewManager.getAllActiveWebviews(); + }, + + /** + * Registers an event listener for onWebviewCreated event + * + * @param listener event listener + */ + onWebviewCreated(listener: (data: vscode.WebviewView) => void): void { + traceExtensionWebviewManager.onWebviewCreated(listener); + }, + + /** + * Registers an event listener for onWebviewPanelCreated event + * + * @param listener event listener + */ + onWebviewPanelCreated(listener: (data: vscode.WebviewPanel) => void): void { + traceExtensionWebviewManager.onWebviewPanelCreated(listener); + } +}; diff --git a/vscode-trace-extension/src/trace-explorer/available-views/trace-explorer-available-views-webview-provider.ts b/vscode-trace-extension/src/trace-explorer/available-views/trace-explorer-available-views-webview-provider.ts index b787265e..ca87e2a5 100644 --- a/vscode-trace-extension/src/trace-explorer/available-views/trace-explorer-available-views-webview-provider.ts +++ b/vscode-trace-extension/src/trace-explorer/available-views/trace-explorer-available-views-webview-provider.ts @@ -8,6 +8,7 @@ import { TraceServerConnectionStatusService } from '../../utils/trace-server-sta import { getTraceServerUrl, getTspClientUrl } from '../../utils/tspClient'; import { convertSignalExperiment } from 'vscode-trace-common/lib/signals/vscode-signal-converter'; import { VSCODE_MESSAGES } from 'vscode-trace-common/lib/messages/vscode-message-manager'; +import { traceExtensionWebviewManager } from 'vscode-trace-extension/src/extension'; const JSONBig = JSONBigConfig({ useNativeBigInt: true, @@ -47,6 +48,7 @@ export class TraceExplorerAvailableViewsProvider implements vscode.WebviewViewPr }; webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + traceExtensionWebviewManager.fireWebviewCreated(webviewView); // Handle messages from the webview webviewView.webview.onDidReceiveMessage(message => { diff --git a/vscode-trace-extension/src/trace-explorer/opened-traces/trace-explorer-opened-traces-webview-provider.ts b/vscode-trace-extension/src/trace-explorer/opened-traces/trace-explorer-opened-traces-webview-provider.ts index 0722e7da..4203414a 100644 --- a/vscode-trace-extension/src/trace-explorer/opened-traces/trace-explorer-opened-traces-webview-provider.ts +++ b/vscode-trace-extension/src/trace-explorer/opened-traces/trace-explorer-opened-traces-webview-provider.ts @@ -7,6 +7,7 @@ import { TraceViewerPanel } from '../../trace-viewer-panel/trace-viewer-webview- import { TraceServerConnectionStatusService } from '../../utils/trace-server-status'; import { getTraceServerUrl, getTspClientUrl } from '../../utils/tspClient'; import { VSCODE_MESSAGES } from 'vscode-trace-common/lib/messages/vscode-message-manager'; +import { traceExtensionWebviewManager } from 'vscode-trace-extension/src/extension'; const JSONBig = JSONBigConfig({ useNativeBigInt: true, @@ -20,7 +21,7 @@ export class TraceExplorerOpenedTracesViewProvider implements vscode.WebviewView private _disposables: vscode.Disposable[] = []; private _selectedExperiment: Experiment | undefined; - constructor( + constructor( private readonly _extensionUri: vscode.Uri, private readonly _statusService: TraceServerConnectionStatusService, ) {} @@ -69,6 +70,7 @@ export class TraceExplorerOpenedTracesViewProvider implements vscode.WebviewView }; webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + traceExtensionWebviewManager.fireWebviewCreated(webviewView); // Handle messages from the webview webviewView.webview.onDidReceiveMessage(message => { diff --git a/vscode-trace-extension/src/trace-explorer/properties/trace-explorer-properties-view-webview-provider.ts b/vscode-trace-extension/src/trace-explorer/properties/trace-explorer-properties-view-webview-provider.ts index 9b98b842..4c595704 100644 --- a/vscode-trace-extension/src/trace-explorer/properties/trace-explorer-properties-view-webview-provider.ts +++ b/vscode-trace-extension/src/trace-explorer/properties/trace-explorer-properties-view-webview-provider.ts @@ -4,6 +4,7 @@ * Licensed under the MIT license. See LICENSE file in the project root for details. ***************************************************************************************/ import * as vscode from 'vscode'; +import { traceExtensionWebviewManager } from 'vscode-trace-extension/src/extension'; import { getTraceServerUrl } from 'vscode-trace-extension/src/utils/tspClient'; export class TraceExplorerItemPropertiesProvider implements vscode.WebviewViewProvider { @@ -26,6 +27,7 @@ export class TraceExplorerItemPropertiesProvider implements vscode.WebviewViewPr ] }; webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + traceExtensionWebviewManager.fireWebviewCreated(webviewView); } postMessagetoWebview(_command: string, _data: unknown): void { diff --git a/vscode-trace-extension/src/trace-viewer-panel/trace-viewer-webview-panel.ts b/vscode-trace-extension/src/trace-viewer-panel/trace-viewer-webview-panel.ts index 4cd93f93..d1ebcfbe 100644 --- a/vscode-trace-extension/src/trace-viewer-panel/trace-viewer-webview-panel.ts +++ b/vscode-trace-extension/src/trace-viewer-panel/trace-viewer-webview-panel.ts @@ -9,6 +9,7 @@ import { VSCODE_MESSAGES } from 'vscode-trace-common/lib/messages/vscode-message import { MarkerSet } from 'tsp-typescript-client/lib/models/markerset'; import JSONBigConfig from 'json-bigint'; import * as fs from 'fs'; +import { traceExtensionWebviewManager } from '../extension'; const JSONBig = JSONBigConfig({ useNativeBigInt: true, @@ -99,6 +100,10 @@ export class TraceViewerPanel { TraceViewerPanel.currentPanel?.showMarkersFilter(); } + public static getCurrentExperiment(): Experiment | undefined { + return TraceViewerPanel.currentPanel?._experiment; + } + private static async saveTraceCsv(csvData: string, defaultFileName: string) { const saveDialogOptions = { defaultUri: vscode.workspace.workspaceFolders @@ -143,6 +148,7 @@ export class TraceViewerPanel { // Set the webview's initial html content this._panel.webview.html = this._getHtmlForWebview(); + traceExtensionWebviewManager.fireWebviewPanelCreated(this._panel); // Listen for when the panel is disposed // This happens when the user closes the panel or when the panel is closed programmatically diff --git a/vscode-trace-extension/src/utils/trace-extension-webview-manager.ts b/vscode-trace-extension/src/utils/trace-extension-webview-manager.ts new file mode 100644 index 00000000..3a8374f6 --- /dev/null +++ b/vscode-trace-extension/src/utils/trace-extension-webview-manager.ts @@ -0,0 +1,78 @@ +/*************************************************************************************** + * Copyright (c) 2023 BlackBerry Limited and others. + * + * Licensed under the MIT license. See LICENSE file in the project root for details. + ***************************************************************************************/ +import * as vscode from 'vscode'; + +/** + * Manages webview panels and webviews creation events + */ +export class TraceExtensionWebviewManager { + private webviews: vscode.WebviewView[] = []; + private webviewCreated: vscode.EventEmitter = new vscode.EventEmitter(); + private webviewPanelCreated: vscode.EventEmitter = new vscode.EventEmitter(); + private isManagerDisposed = false; + + getAllActiveWebviews(): vscode.WebviewView[] { + if (!this.isDisposed()) { + return this.webviews; + } + return []; + } + + fireWebviewCreated(_webview: vscode.WebviewView): void { + if (!this.isDisposed()) { + this.addWebview(_webview); + this.webviewCreated.fire(_webview); + } + } + + fireWebviewPanelCreated(_webviewPanel: vscode.WebviewPanel): void { + if (!this.isDisposed()) { + this.webviewPanelCreated.fire(_webviewPanel); + } + } + + onWebviewCreated(listener: (data: vscode.WebviewView) => unknown): void { + if (!this.isDisposed()) { + this.webviewCreated.event(listener); + } + } + + onWebviewPanelCreated(listener: (data: vscode.WebviewPanel) => unknown): void { + if (!this.isDisposed()) { + this.webviewPanelCreated.event(listener); + } + } + + dispose(): void { + if (!this.isDisposed()) { + this.webviews = []; + this.webviewCreated.dispose(); + this.webviewPanelCreated.dispose(); + this.isManagerDisposed = true; + } + } + + isDisposed(): boolean { + return this.isManagerDisposed; + } + + private addWebview(webview: vscode.WebviewView): void { + // Remove it from the array when the webview disposes + webview.onDidDispose(() => { + this.removeWebview(webview); + }); + this.webviews.push(webview); + } + + private removeWebview(_webview: vscode.WebviewView): void { + this.webviews.filter(webview => webview === _webview).forEach(webview => { + const index = this.webviews.indexOf(webview); + if (index !== -1) { + this.webviews.splice(index, 1); + } + }); + } +}