Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support VSCode WebviewView API #10705

Merged
merged 1 commit into from
Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -900,13 +900,15 @@ const codeEditorPreferenceProperties = {
'editor.peekWidgetDefaultFocus': {
'enumDescriptions': [
nls.localizeByDefault('Focus the tree when opening peek'),
nls.localizeByDefault('Focus the editor when opening peek')
nls.localizeByDefault('Focus the editor when opening peek'),
nls.localizeByDefault('Focus the webview when opening peek')
],
'description': nls.localizeByDefault('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 @@ -1549,6 +1549,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 @@ -1743,6 +1764,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 @@ -1777,6 +1799,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 @@ -128,10 +128,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 @@ -696,6 +702,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 @@ -572,7 +572,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 { SecretsMainImpl } from './secrets-main';
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 @@ -127,6 +128,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, codicon, ViewContainerPart
} 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 @@ -31,7 +31,7 @@ import { DebugFrontendApplicationContribution } from '@theia/debug/lib/browser/d
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
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 { ViewContextKeyService } from './view-context-key-service';
import { PROBLEMS_WIDGET_ID } from '@theia/markers/lib/browser/problem/problem-widget';
Expand All @@ -41,6 +41,11 @@ import { TERMINAL_WIDGET_FACTORY_ID } from '@theia/terminal/lib/browser/terminal
import { TreeViewWidget } from './tree-view-widget';
import { SEARCH_VIEW_CONTAINER_ID } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';

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 @@ -95,6 +100,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 @@ -333,6 +340,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 @@ -409,7 +473,7 @@ 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;
Expand All @@ -419,7 +483,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
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 @@ -701,8 +765,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