Skip to content

Commit

Permalink
Implement vscode WebviewView API (eclipse-theia#10705)
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew authored and mcgordonite committed May 9, 2022
1 parent b919dfe commit 58e7db2
Show file tree
Hide file tree
Showing 12 changed files with 692 additions and 8 deletions.
6 changes: 4 additions & 2 deletions packages/editor/src/browser/editor-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,13 +755,15 @@ const codeEditorPreferenceProperties = {
'editor.peekWidgetDefaultFocus': {
'enumDescriptions': [
'Focus the tree when opening peek',
'Focus the editor when opening peek'
'Focus the editor when opening peek',
'Focus the webview when opening peek'
],
'description': 'Controls whether to focus the inline editor or the tree in the peek widget.',
'type': 'string',
'enum': [
'tree',
'editor'
'editor',
'webview'
],
'default': 'tree'
},
Expand Down
23 changes: 23 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,27 @@ export interface WebviewsMain {
$unregisterSerializer(viewType: string): void;
}

export interface WebviewViewsExt {
$resolveWebviewView(handle: string,
viewType: string,
title: string | undefined,
state: any,
cancellation: CancellationToken): Promise<void>;
$onDidChangeWebviewViewVisibility(handle: string, visible: boolean): void;
$disposeWebviewView(handle: string): void;
}

export interface WebviewViewsMain extends Disposable {
$registerWebviewViewProvider(viewType: string,
options: { retainContextWhenHidden?: boolean, serializeBuffersForPostMessage: boolean }): void;
$unregisterWebviewViewProvider(viewType: string): void;

$setWebviewViewTitle(handle: string, value: string | undefined): void;
$setWebviewViewDescription(handle: string, value: string | undefined): void;

$show(handle: string, preserveFocus: boolean): void;
}

export interface CustomEditorsExt {
$resolveWebviewEditor(
resource: UriComponents,
Expand Down Expand Up @@ -1718,6 +1739,7 @@ export const PLUGIN_RPC_CONTEXT = {
CONNECTION_MAIN: createProxyIdentifier<ConnectionMain>('ConnectionMain'),
WEBVIEWS_MAIN: createProxyIdentifier<WebviewsMain>('WebviewsMain'),
CUSTOM_EDITORS_MAIN: createProxyIdentifier<CustomEditorsMain>('CustomEditorsMain'),
WEBVIEW_VIEWS_MAIN: createProxyIdentifier<WebviewViewsMain>('WebviewViewsMain'),
STORAGE_MAIN: createProxyIdentifier<StorageMain>('StorageMain'),
TASKS_MAIN: createProxyIdentifier<TasksMain>('TasksMain'),
DEBUG_MAIN: createProxyIdentifier<DebugMain>('DebugMain'),
Expand Down Expand Up @@ -1752,6 +1774,7 @@ export const MAIN_RPC_CONTEXT = {
CONNECTION_EXT: createProxyIdentifier<ConnectionExt>('ConnectionExt'),
WEBVIEWS_EXT: createProxyIdentifier<WebviewsExt>('WebviewsExt'),
CUSTOM_EDITORS_EXT: createProxyIdentifier<CustomEditorsExt>('CustomEditorsExt'),
WEBVIEW_VIEWS_EXT: createProxyIdentifier<WebviewViewsExt>('WebviewViewsExt'),
STORAGE_EXT: createProxyIdentifier<StorageExt>('StorageExt'),
TASKS_EXT: createProxyIdentifier<TasksExt>('TasksExt'),
DEBUG_EXT: createProxyIdentifier<DebugExt>('DebugExt'),
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,16 @@ export interface PluginPackageViewContainer {
icon: string;
}

export enum PluginViewType {
Tree = 'tree',
Webview = 'webview'
}

export interface PluginPackageView {
id: string;
name: string;
when?: string;
type?: string;
}

export interface PluginPackageViewWelcome {
Expand Down Expand Up @@ -659,6 +665,7 @@ export interface View {
id: string;
name: string;
when?: string;
type?: string;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,8 @@ export class TheiaPluginScanner implements PluginScanner {
const result: View = {
id: rawView.id,
name: rawView.name,
when: rawView.when
when: rawView.when,
type: rawView.type
};

return result;
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-ext/src/main/browser/main-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { ThemingMainImpl } from './theming-main';
import { CommentsMainImp } from './comments/comments-main';
import { CustomEditorsMainImpl } from './custom-editors/custom-editors-main';
import { NotificationExtImpl } from '../../plugin/notification';
import { WebviewViewsMainImpl } from './webview-views/webview-views-main';

export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void {
const authenticationMain = new AuthenticationMainImpl(rpc, container);
Expand Down Expand Up @@ -130,6 +131,9 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container
const customEditorsMain = new CustomEditorsMainImpl(rpc, container, webviewsMain);
rpc.set(PLUGIN_RPC_CONTEXT.CUSTOM_EDITORS_MAIN, customEditorsMain);

const webviewViewsMain = new WebviewViewsMainImpl(rpc, container, webviewsMain);
rpc.set(PLUGIN_RPC_CONTEXT.WEBVIEW_VIEWS_MAIN, webviewViewsMain);

const storageMain = new StorageMainImpl(container);
rpc.set(PLUGIN_RPC_CONTEXT.STORAGE_MAIN, storageMain);

Expand Down
78 changes: 73 additions & 5 deletions packages/plugin-ext/src/main/browser/view/plugin-view-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
ViewContainerIdentifier, ViewContainerTitleOptions, Widget, FrontendApplicationContribution,
StatefulWidget, CommonMenus, BaseWidget, TreeViewWelcomeWidget
} from '@theia/core/lib/browser';
import { ViewContainer, View, ViewWelcome } from '../../../common';
import { ViewContainer, View, ViewWelcome, PluginViewType } from '../../../common';
import { PluginSharedStyle } from '../plugin-shared-style';
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { PluginViewWidget, PluginViewWidgetIdentifier } from './plugin-view-widget';
Expand All @@ -32,7 +32,7 @@ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposa
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { QuickViewService } from '@theia/core/lib/browser/quick-view-service';
import { Emitter } from '@theia/core/lib/common/event';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { SearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
import { ViewContextKeyService } from './view-context-key-service';
Expand All @@ -42,6 +42,11 @@ import { DebugConsoleContribution } from '@theia/debug/lib/browser/console/debug
import { TERMINAL_WIDGET_FACTORY_ID } from '@theia/terminal/lib/browser/terminal-widget-impl';
import { TreeViewWidget } from './tree-view-widget';

import { WebviewView, WebviewViewResolver } from '../webview-views/webview-views';
import { WebviewWidget, WebviewWidgetIdentifier } from '../webview/webview';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { v4 } from 'uuid';

export const PLUGIN_VIEW_FACTORY_ID = 'plugin-view';
export const PLUGIN_VIEW_CONTAINER_FACTORY_ID = 'plugin-view-container';
export const PLUGIN_VIEW_DATA_FACTORY_ID = 'plugin-view-data';
Expand Down Expand Up @@ -96,6 +101,8 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
private readonly viewDataProviders = new Map<string, ViewDataProvider>();
private readonly viewDataState = new Map<string, object>();

private readonly webviewViewResolvers = new Map<string, WebviewViewResolver>();

@postConstruct()
protected init(): void {
// VS Code Viewlets
Expand Down Expand Up @@ -314,6 +321,63 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
return toDispose;
}

async registerWebviewView(viewId: string, resolver: WebviewViewResolver): Promise<Disposable> {
if (this.webviewViewResolvers.has(viewId)) {
throw new Error(`View resolver already registered for ${viewId}`);
}
this.webviewViewResolvers.set(viewId, resolver);

const webviewView = await this.createNewWebviewView();
const token = CancellationToken.None;
this.getView(viewId).then(async view => {
if (view) {
if (view.isVisible) {
await this.prepareView(view, webviewView.webview.identifier.id);
} else {
const toDisposeOnDidExpandView = new DisposableCollection(this.onDidExpandView(async id => {
if (id === viewId) {
dispose();
await this.prepareView(view, webviewView.webview.identifier.id);
}
}));
const dispose = () => toDisposeOnDidExpandView.dispose();
view.disposed.connect(dispose);
toDisposeOnDidExpandView.push(Disposable.create(() => view.disposed.disconnect(dispose)));
}
}
});

resolver.resolve(webviewView, token);

return Disposable.create(() => {
this.webviewViewResolvers.delete(viewId);
});
}

async createNewWebviewView(): Promise<WebviewView> {
const webview = await this.widgetManager.getOrCreateWidget<WebviewWidget>(
WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id: v4() });
webview.setContentOptions({ allowScripts: true });

let _description: string | undefined;

return {
webview,

get onDidChangeVisibility(): Event<boolean> { return webview.onDidChangeVisibility; },
get onDidDispose(): Event<void> { return webview.onDidDispose; },

get title(): string | undefined { return webview.title.label; },
set title(value: string | undefined) { webview.title.label = value || ''; },

get description(): string | undefined { return _description; },
set description(value: string | undefined) { _description = value; },

dispose: webview.dispose,
show: webview.show
};
}

registerViewWelcome(viewWelcome: ViewWelcome): Disposable {
const toDispose = new DisposableCollection();

Expand Down Expand Up @@ -390,15 +454,15 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
return this.getView(viewId);
}

protected async prepareView(widget: PluginViewWidget): Promise<void> {
protected async prepareView(widget: PluginViewWidget, webviewId?: string): Promise<void> {
const data = this.views.get(widget.options.viewId);
if (!data) {
return;
}
const [, view] = data;
widget.title.label = view.name;
const currentDataWidget = widget.widgets[0];
const viewDataWidget = await this.createViewDataWidget(view.id);
const viewDataWidget = await this.createViewDataWidget(view.id, webviewId);
if (widget.isDisposed) {
// eslint-disable-next-line no-unused-expressions
viewDataWidget?.dispose();
Expand Down Expand Up @@ -633,8 +697,12 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
return toDispose;
}

protected async createViewDataWidget(viewId: string): Promise<Widget | undefined> {
protected async createViewDataWidget(viewId: string, webviewId?: string): Promise<Widget | undefined> {
const view = this.views.get(viewId);
if (view?.[1]?.type === PluginViewType.Webview) {
const webviewWidget = this.widgetManager.getWidget(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id: webviewId });
return webviewWidget;
}
const provider = this.viewDataProviders.get(viewId);
if (!view || !provider) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/********************************************************************************
* Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/e1f0f8f51390dea5df9096718fb6b647ed5a9534/src/vs/workbench/api/browser/mainThreadWebviewViews.ts

import { inject, interfaces } from '@theia/core/shared/inversify';
import { WebviewViewsMain, MAIN_RPC_CONTEXT, WebviewViewsExt } from '../../../common/plugin-api-rpc';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { Disposable, DisposableCollection, ILogger } from '@theia/core';
import { WebviewView } from './webview-views';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { WebviewsMainImpl } from '../webviews-main';
import { Widget, WidgetManager } from '@theia/core/lib/browser';
import { PluginViewRegistry } from '../view/plugin-view-registry';

export class WebviewViewsMainImpl implements WebviewViewsMain, Disposable {

protected readonly proxy: WebviewViewsExt;
protected readonly toDispose = new DisposableCollection(
Disposable.create(() => { /* mark as not disposed */ })
);

protected readonly webviewViews = new Map<string, WebviewView>();
protected readonly webviewViewProviders = new Map<string, Disposable>();
protected readonly widgetManager: WidgetManager;
protected readonly pluginViewRegistry: PluginViewRegistry;

@inject(ILogger)
protected readonly logger: ILogger;

constructor(rpc: RPCProtocol,
container: interfaces.Container,
readonly webviewsMain: WebviewsMainImpl
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEW_VIEWS_EXT);
this.widgetManager = container.get(WidgetManager);
this.pluginViewRegistry = container.get(PluginViewRegistry);
}

dispose(): void {
this.toDispose.dispose();
}

async $registerWebviewViewProvider(viewType: string, options: { retainContextWhenHidden?: boolean, serializeBuffersForPostMessage: boolean }): Promise<void> {

if (this.webviewViewProviders.has(viewType)) {
throw new Error(`View provider for ${viewType} already registered`);
}

const registration = await this.pluginViewRegistry.registerWebviewView(viewType, {
resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => {
const handle = webviewView.webview.identifier.id;
this.webviewViews.set(handle, webviewView);
this.webviewsMain.hookWebview(webviewView.webview);

let state: string | undefined;
if (webviewView.webview.state) {
try {
state = JSON.parse(webviewView.webview.state);
console.log(state);
} catch (e) {
console.error('Could not load webview state', e, webviewView.webview.state);
}
}
if (options) {
webviewView.webview.options = options;
}

webviewView.onDidChangeVisibility(visible => {
this.proxy.$onDidChangeWebviewViewVisibility(handle, visible);
});

webviewView.onDidDispose(() => {
this.proxy.$disposeWebviewView(handle);
this.webviewViews.delete(handle);
});

try {
this.proxy.$resolveWebviewView(handle, viewType, webviewView.title, state, cancellation);
} catch (error) {
this.logger.error(`Error resolving webview view '${viewType}': ${error}`);
webviewView.webview.setHTML('failed to load plugin webview view');
}
}
});

this.webviewViewProviders.set(viewType, registration);
}

protected getWebview(handle: string): Widget | undefined {
return this.widgetManager.tryGetWidget(handle);
}

$unregisterWebviewViewProvider(viewType: string): void {
const provider = this.webviewViewProviders.get(viewType);
if (!provider) {
throw new Error(`No view provider for ${viewType} registered`);
}
provider.dispose();
this.webviewViewProviders.delete(viewType);
}

$setWebviewViewTitle(handle: string, value: string | undefined): void {
const webviewView = this.getWebviewView(handle);
webviewView.title = value;
}

$setWebviewViewDescription(handle: string, value: string | undefined): void {
const webviewView = this.getWebviewView(handle);
webviewView.description = value;
}

$show(handle: string, preserveFocus: boolean): void {
const webviewView = this.getWebviewView(handle);
webviewView.show(preserveFocus);
}

protected getWebviewView(handle: string): WebviewView {
const webviewView = this.webviewViews.get(handle);
if (!webviewView) {
throw new Error(`No webview view registered for handle '${handle}'`);
}
return webviewView;
}

}
Loading

0 comments on commit 58e7db2

Please sign in to comment.