Compare commits

...

6 commits

Author SHA1 Message Date
Matt Bierner 95179f9a4a Move webview view to new directory under workbench 2020-08-19 10:18:32 -07:00
Matt Bierner 8f465e11db Adding documentation 2020-08-17 18:59:20 -07:00
Matt Bierner a0bc12bd6c Fix resize observer 2020-08-17 18:46:42 -07:00
Matt Bierner abb8c62bda Transparent background 2020-08-17 18:46:42 -07:00
Matt Bierner 02311f26dd Use proper activation event 2020-08-17 18:46:42 -07:00
Matt Bierner f5e898f40e Add proposed webview view API
For #46585

This adds a new `WebviewView` proposed api to VS Code that lets webview be used inside views. Webview views can be contributed using a contribution point such as :

```json
    "views": {
      "explorer": [
        {
          "type": "webview",
          "id": "cats.cat",
          "name": "Cats",
          "visibility": "visible"
        }
      ]
    },
```
2020-08-17 18:46:42 -07:00
13 changed files with 611 additions and 19 deletions

View file

@ -24,7 +24,8 @@
"activationEvents": [
"onCustomEditor:imagePreview.previewEditor",
"onCommand:imagePreview.zoomIn",
"onCommand:imagePreview.zoomOut"
"onCommand:imagePreview.zoomOut",
"onView:cats.cat"
],
"contributes": {
"customEditors": [
@ -39,6 +40,16 @@
]
}
],
"views": {
"explorer": [
{
"type": "webview",
"id": "cats.cat",
"name": "Cat",
"visibility": "visible"
}
]
},
"commands": [
{
"command": "imagePreview.zoomIn",

View file

@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry';
import { PreviewManager } from './preview';
import { SizeStatusBarEntry } from './sizeStatusBarEntry';
import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry';
import { ZoomStatusBarEntry } from './zoomStatusBarEntry';
export function activate(context: vscode.ExtensionContext) {
@ -32,4 +32,23 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand('imagePreview.zoomOut', () => {
previewManager.activePreview?.zoomOut();
}));
vscode.window.registerWebviewViewProvider('cats.cat', new class implements vscode.WebviewViewProvider {
async resolveWebviewView(webviewView: vscode.WebviewView, _state: unknown) {
await new Promise(resolve => setTimeout(resolve, 2000));
webviewView.webview.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body style="background-color: transparent">
<img src="https://media.giphy.com/media/E6jscXfv3AkWQ/giphy.gif">
</body>
</html>`;
}
});
}

View file

@ -2048,4 +2048,116 @@ declare module 'vscode' {
notebook: NotebookDocument | undefined;
}
//#endregion
//#region https://github.com/microsoft/vscode/issues/46585
/**
* A webview based view.
*/
export interface WebviewView {
/**
* Identifies the type of the webview view, such as `'hexEditor.dataView'`.
*/
readonly viewType: string;
/**
* The underlying webview for the view.
*/
readonly webview: Webview;
/**
* Event fired when the view is disposed.
*
* Views are disposed of in a few cases:
*
* - When a view is collapsed and `retainContextWhenHidden` has not been set.
* - When a view is hidden by a user.
*
* Trying to use the view after it has been disposed throws an exception.
*/
readonly onDidDispose: Event<void>;
/**
* Tracks if the webview is currently visible.
*
* Views are visible when they are on the screen and expanded.
*/
readonly visible: boolean;
/**
* Event fired when the visibility of the view changes
*/
readonly onDidChangeVisibility: Event<void>;
}
interface WebviewViewResolveContext<T = unknown> {
/**
* Persisted state from the webview content.
*
* To save resources, VS Code normally deallocates webview views that are not visible. For example, if the user
* collapse a view or switching to another top level activity, the underlying webview document is deallocates.
*
* You can prevent this behavior by setting `retainContextWhenHidden` in the `WebviewOptions`. However this
* increases resource usage and should be avoided wherever possible. Instead, you can use persisted state to
* save off a webview's state so that it can be quickly recreated as needed.
*
* To save off a persisted state, inside the webview call `acquireVsCodeApi().setState()` with
* any json serializable object. To restore the state again, call `getState()`. For example:
*
* ```js
* // Within the webview
* const vscode = acquireVsCodeApi();
*
* // Get existing state
* const oldState = vscode.getState() || { value: 0 };
*
* // Update state
* setState({ value: oldState.value + 1 })
* ```
*
* VS Code ensures that the persisted state is saved correctly when a webview is hidden and across
* editor restarts.
*/
readonly state: T | undefined;
}
/**
* Provider for creating `WebviewView` elements.
*/
export interface WebviewViewProvider {
/**
* Revolves a webview view.
*
* `resolveWebviewView` 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.
*
* @param webviewView Webview panel to restore. The serializer should take ownership of this panel. The
* provider must set the webview's `.html` and hook up all webview events it is interested in.
* @param context Additional metadata about the view being resolved.
* @param token Cancellation token indicating that the view being provided is no longer needed.
*
* @return Optional promise indicating that the view has been fully resolved.
*/
resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): Promise<void> | void;
}
namespace window {
/**
* Register a new provider for webview views.
*
* @param viewId Unique id of the view. This should match the `id` from the
* `views` contribution in the package.json.
* @param provider Provider for the webview views.
*
* @return Disposable that unregisters the provider.
*/
export function registerWebviewViewProvider(viewId: string, provider: WebviewViewProvider, options?: {
/**
* Content settings for the webview created for this view.
*/
readonly webviewOptions?: WebviewPanelOptions;
}): Disposable;
}
//#endregion
}

View file

@ -33,9 +33,10 @@ import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/cus
import { CustomDocumentBackupData } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory';
import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel';
import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview';
import { Webview, WebviewExtensionDescription, WebviewIcons, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService';
import { IWebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@ -119,6 +120,10 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
private readonly _proxy: extHostProtocol.ExtHostWebviewsShape;
private readonly _webviewInputs = new WebviewInputStore();
private readonly _revivers = new Map<string, IDisposable>();
private readonly _webviewViewProviders = new Map<string, IDisposable>();
private readonly _webviewViews = new Map<string, WebviewOverlay>();
private readonly _editorProviders = new Map<string, IDisposable>();
private readonly _webviewFromDiffEditorHandles = new Set<string>();
@ -136,6 +141,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IBackupFileService private readonly _backupService: IBackupFileService,
@IWebviewViewService private readonly _webviewViewService: IWebviewViewService,
) {
super();
@ -212,7 +218,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
const extension = reviveWebviewExtension(extensionData);
const webview = this._webviewWorkbenchService.createWebview(handle, webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension);
this.hookupWebviewEventDelegate(handle, webview);
this.hookupWebviewEventDelegate(handle, webview.webview);
this._webviewInputs.add(handle, webview);
@ -240,8 +246,8 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
}
public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void {
const webview = this.getWebviewInput(handle);
webview.webview.html = value;
const webview = this.getWebview(handle);
webview.html = value;
}
public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void {
@ -285,7 +291,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
const handle = webviewInput.id;
this._webviewInputs.add(handle, webviewInput);
this.hookupWebviewEventDelegate(handle, webviewInput);
this.hookupWebviewEventDelegate(handle, webviewInput.webview);
let state = undefined;
if (webviewInput.webview.state) {
@ -316,6 +322,37 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
this._revivers.delete(viewType);
}
public $registerWebviewViewProvider(viewType: string): void {
if (this._webviewViewProviders.has(viewType)) {
throw new Error(`View provider for ${viewType} already registered`);
}
this._webviewViewService.register(viewType, {
resolve: async (webview: WebviewOverlay, cancellation: CancellationToken) => {
this._webviewViews.set(viewType, webview);
const handle = viewType;
this.hookupWebviewEventDelegate(handle, webview);
let state = undefined;
if (webview.state) {
try {
state = JSON.parse(webview.state);
} catch (e) {
console.error('Could not load webview state', e, webview.state);
}
}
try {
await this._proxy.$resolveWebviewView(handle, viewType, state, cancellation);
} catch (error) {
onUnexpectedError(error);
webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType);
}
}
});
}
public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void {
this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities, true);
}
@ -353,7 +390,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
const resource = webviewInput.resource;
this._webviewInputs.add(handle, webviewInput);
this.hookupWebviewEventDelegate(handle, webviewInput);
this.hookupWebviewEventDelegate(handle, webviewInput.webview);
webviewInput.webview.options = options;
webviewInput.webview.extension = extension;
@ -460,14 +497,14 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
model.changeContent();
}
private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) {
private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, webview: WebviewOverlay) {
const disposables = new DisposableStore();
disposables.add(input.webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri)));
disposables.add(input.webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); }));
disposables.add(input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value)));
disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri)));
disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); }));
disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value)));
disposables.add(input.webview.onDispose(() => {
disposables.add(webview.onDispose(() => {
disposables.dispose();
this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
@ -554,6 +591,14 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command;
}
private getWebview(handle: extHostProtocol.WebviewPanelHandle): Webview {
const webview = this.tryGetWebviewInput(handle)?.webview ?? this._webviewViews.get(handle);
if (!webview) {
throw new Error(`Unknown webview handle:${handle}`);
}
return webview;
}
private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput {
const webview = this.tryGetWebviewInput(handle);
if (!webview) {

View file

@ -32,6 +32,7 @@ import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneCont
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Codicon } from 'vs/base/common/codicons';
import { CustomTreeView } from 'vs/workbench/contrib/views/browser/treeView';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
export interface IUserFriendlyViewsContainerDescriptor {
id: string;
@ -77,6 +78,8 @@ export const viewsContainersContribution: IJSONSchema = {
};
interface IUserFriendlyViewDescriptor {
type?: 'tree' | 'webview';
id: string;
name: string;
when?: string;
@ -208,11 +211,18 @@ const viewsContribution: IJSONSchema = {
}
};
export interface ICustomViewDescriptor extends ITreeViewDescriptor {
export interface ICustomTreeViewDescriptor extends ITreeViewDescriptor {
readonly extensionId: ExtensionIdentifier;
readonly originalContainerId: string;
}
export interface ICustomWebviewViewDescriptor extends IViewDescriptor {
readonly extensionId: ExtensionIdentifier;
readonly originalContainerId: string;
}
export type ICustomViewDescriptor = ICustomTreeViewDescriptor | ICustomWebviewViewDescriptor;
type ViewContainerExtensionPointType = { [loc: string]: IUserFriendlyViewsContainerDescriptor[] };
const viewsContainersExtensionPoint: IExtensionPoint<ViewContainerExtensionPointType> = ExtensionsRegistry.registerExtensionPoint<ViewContainerExtensionPointType>({
extensionPoint: 'viewsContainers',
@ -226,19 +236,76 @@ const viewsExtensionPoint: IExtensionPoint<ViewExtensionPointType> = ExtensionsR
jsonSchema: viewsContribution
});
export interface IViewResolver {
resolve(viewDescriptor: Partial<IViewDescriptor>): IViewDescriptor;
}
export namespace Extensions {
export const ViewResolverRegistry = 'workbench.registry.viewResolver';
}
export interface IViewResolverRegistry {
register(type: string, resolver: IViewResolver): IDisposable;
tryResolve(viewDescriptor: Partial<IViewDescriptor>): IViewDescriptor | undefined;
}
class ViewResolverRegistry extends Disposable implements IViewResolverRegistry {
private readonly _views = new Map<string, IViewResolver>();
register(type: string, resolver: IViewResolver): IDisposable {
if (this._views.has(type)) {
throw new Error(`View resolver already registered for ${type}`);
}
this._views.set(type, resolver);
return toDisposable(() => {
this._views.delete(type);
});
}
tryResolve(viewDescriptor: Partial<IViewDescriptor>): IViewDescriptor | undefined {
const resolver = this._views.get(viewDescriptor.type!);
if (!resolver) {
return undefined;
}
return resolver.resolve(viewDescriptor);
}
}
Registry.add(Extensions.ViewResolverRegistry, new ViewResolverRegistry());
const treeViewType = 'tree';
const TEST_VIEW_CONTAINER_ORDER = 6;
class ViewsExtensionHandler implements IWorkbenchContribution {
export class ViewsExtensionHandler implements IWorkbenchContribution {
private viewContainersRegistry: IViewContainersRegistry;
private viewsRegistry: IViewsRegistry;
private readonly viewResolverRegistry: IViewResolverRegistry;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService
@IInstantiationService instantiationService: IInstantiationService
) {
this.viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
this.viewsRegistry = Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry);
this.viewResolverRegistry = Registry.as<IViewResolverRegistry>(Extensions.ViewResolverRegistry);
this.handleAndRegisterCustomViewContainers();
this.handleAndRegisterCustomViews();
this.viewResolverRegistry.register(treeViewType, {
resolve: (viewDescriptor: IViewDescriptor): ITreeViewDescriptor => {
return {
...viewDescriptor,
ctorDescriptor: new SyncDescriptor(TreeViewPane),
treeView: instantiationService.createInstance(CustomTreeView, viewDescriptor.id!, viewDescriptor.name!),
};
}
});
}
private handleAndRegisterCustomViewContainers() {
@ -442,16 +509,16 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
const icon = item.icon ? resources.joinPath(extension.description.extensionLocation, item.icon) : undefined;
const initialVisibility = this.convertInitialVisibility(item.visibility);
const viewDescriptor = <ICustomViewDescriptor>{
const partialViewDescriptor = <Partial<ICustomTreeViewDescriptor>>{
type: !item.type ? treeViewType : item.type,
id: item.id,
name: item.name,
ctorDescriptor: new SyncDescriptor(TreeViewPane),
when: ContextKeyExpr.deserialize(item.when),
containerIcon: icon || viewContainer?.icon,
containerTitle: item.contextualTitle || viewContainer?.name,
canToggleVisibility: true,
canMoveView: true,
treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name),
collapsed: this.showCollapsed(container) || initialVisibility === InitialVisibility.Collapsed,
order: order,
extensionId: extension.description.identifier,
@ -461,6 +528,11 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
hideByDefault: initialVisibility === InitialVisibility.Hidden
};
const viewDescriptor = this.viewResolverRegistry.tryResolve(partialViewDescriptor);
if (!viewDescriptor) {
return null;
}
viewIds.add(viewDescriptor.id);
return viewDescriptor;
}));

View file

@ -612,6 +612,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
},
onDidChangeActiveColorTheme(listener, thisArg?, disposables?) {
return extHostTheming.onDidChangeActiveColorTheme(listener, thisArg, disposables);
},
registerWebviewViewProvider(viewId: string, provider: vscode.WebviewViewProvider) {
checkProposedApiEnabled(extension);
return extHostWebviews.registerWebviewViewProvider(extension, viewId, provider);
}
};

View file

@ -629,6 +629,8 @@ export interface MainThreadWebviewsShape extends IDisposable {
$onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void;
$onContentChange(resource: UriComponents, viewType: string): void;
$registerWebviewViewProvider(viewType: string): void;
}
export interface WebviewPanelViewStateData {
@ -662,6 +664,8 @@ export interface ExtHostWebviewsShape {
$backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string>;
$onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise<void>;
$resolveWebviewView(webviewHandle: WebviewPanelHandle, viewType: string, state: any, cancellation: CancellationToken): Promise<void>;
}
export enum CellKind {

View file

@ -267,6 +267,57 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa
}
}
export class ExtHostWebviewView extends Disposable implements vscode.WebviewView {
readonly #viewType: string;
readonly #webview: ExtHostWebview;
#isDisposed = false;
constructor(
handle: extHostProtocol.WebviewPanelHandle,
proxy: extHostProtocol.MainThreadWebviewsShape,
viewType: string,
webview: ExtHostWebview
) {
super();
this.#viewType = viewType;
this.#webview = webview;
}
public dispose() {
if (this.#isDisposed) {
return;
}
this.#isDisposed = true;
this.#onDidDispose.fire();
super.dispose();
}
readonly #onDidChangeVisibility = this._register(new Emitter<void>());
public readonly onDidChangeVisibility = this.#onDidChangeVisibility.event;
readonly #onDidDispose = this._register(new Emitter<void>());
public readonly onDidDispose = this.#onDidDispose.event;
get visible() {
// todo
return true;
}
get webview() {
return this.#webview;
}
get viewType(): string {
return this.#viewType;
}
}
class CustomDocumentStoreEntry {
private _backupCounter = 1;
@ -412,6 +463,11 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
readonly extension: IExtensionDescription;
}>();
private readonly _viewProviders = new Map<string, {
readonly provider: vscode.WebviewViewProvider;
readonly extension: IExtensionDescription;
}>();
private readonly _editorProviders = new EditorProviderStore();
private readonly _documents = new CustomDocumentStore();
@ -468,6 +524,24 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
});
}
public registerWebviewViewProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.WebviewViewProvider,
): vscode.Disposable {
if (this._viewProviders.has(viewType)) {
throw new Error(`View provider for '${viewType}' already registered`);
}
this._viewProviders.set(viewType, { provider, extension });
this._proxy.$registerWebviewViewProvider(viewType);
return new extHostTypes.Disposable(() => {
this._viewProviders.delete(viewType);
this._proxy.$unregisterSerializer(viewType);
});
}
public registerCustomEditorProvider(
extension: IExtensionDescription,
viewType: string,
@ -583,6 +657,24 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
await serializer.deserializeWebviewPanel(revivedPanel, state);
}
async $resolveWebviewView(
webviewHandle: string,
viewType: string,
state: any,
cancellation: CancellationToken,
): Promise<void> {
const entry = this._viewProviders.get(viewType);
if (!entry) {
throw new Error(`No view provider found for '${viewType}'`);
}
const { provider, extension } = entry;
const webview = new ExtHostWebview(webviewHandle, this._proxy, reviveOptions({ /* todo */ }), this.initData, this.workspace, extension, this._deprecationService);
const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, webview);
await provider.resolveWebviewView(revivedView, { state }, cancellation);
}
async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) {
const entry = this._editorProviders.get(viewType);
if (!entry) {

View file

@ -196,6 +196,8 @@ Registry.add(Extensions.ViewContainersRegistry, new ViewContainersRegistryImpl()
export interface IViewDescriptor {
readonly type?: string;
readonly id: string;
readonly name: string;

View file

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as ViewsExtensions, IViewResolverRegistry } from 'vs/workbench/api/browser/viewsExtensionPoint';
import { IViewDescriptor } from 'vs/workbench/common/views';
import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane';
import { IWebviewViewService, WebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService';
registerSingleton(IWebviewViewService, WebviewViewService, true);
Registry.as<IViewResolverRegistry>(ViewsExtensions.ViewResolverRegistry)
.register('webview', {
resolve: (viewDescriptor: IViewDescriptor): IViewDescriptor => {
return {
...viewDescriptor,
ctorDescriptor: new SyncDescriptor(WebviewViewPane)
};
}
});

View file

@ -0,0 +1,131 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { toDisposable } from 'vs/base/common/lifecycle';
import { generateUuid } from 'vs/base/common/uuid';
import { MenuId } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { IWebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
declare const ResizeObserver: any;
export class WebviewViewPane extends ViewPane {
private _webview?: WebviewOverlay;
private _activated = false;
private _container?: HTMLElement;
private _resizeObserver?: any;
constructor(
options: IViewletViewOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IInstantiationService instantiationService: IInstantiationService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
@IExtensionService private readonly extensionService: IExtensionService,
@IProgressService private readonly progressService: IProgressService,
@IWebviewService private readonly webviewService: IWebviewService,
@IWebviewViewService private readonly webviewViewService: IWebviewViewService,
) {
super({ ...options, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility()));
this.updateTreeVisibility();
}
focus(): void {
super.focus();
this._webview?.focus();
}
renderBody(container: HTMLElement): void {
super.renderBody(container);
this._container = container;
if (!this._resizeObserver) {
this._resizeObserver = new ResizeObserver(() => {
setImmediate(() => {
if (this._container) {
this._webview?.layoutWebviewOverElement(this._container);
}
});
});
this._register(toDisposable(() => {
this._resizeObserver.disconnect();
}));
this._resizeObserver.observe(container);
}
}
protected layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
if (!this._webview) {
return;
}
if (this._container) {
this._webview.layoutWebviewOverElement(this._container, { width, height });
}
}
private updateTreeVisibility() {
if (this.isBodyVisible()) {
this.activate();
this._webview?.claim(this);
} else {
this._webview?.release(this);
}
}
private activate() {
if (!this._activated) {
this._activated = true;
const webview = this.webviewService.createWebviewOverlay(generateUuid(), {}, {}, undefined);
this._webview = webview;
this._register(toDisposable(() => {
this._webview?.release(this);
}));
const source = this._register(new CancellationTokenSource());
this.withProgress(async () => {
await this.extensionService.activateByEvent(`onView:${this.id}`);
await this.webviewViewService.resolve(this.id, webview, source.token);
});
}
}
private async withProgress(task: () => Promise<void>): Promise<void> {
return this.progressService.withProgress({ location: this.id, delay: 500 }, task);
}
}

View file

@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Webview } from 'vs/workbench/contrib/webview/browser/webview';
export const IWebviewViewService = createDecorator<IWebviewViewService>('webviewViewService');
export interface IWebviewViewResolver {
resolve(webview: Webview, cancellation: CancellationToken): Promise<void>;
}
export interface IWebviewViewService {
readonly _serviceBrand: undefined;
register(type: string, resolver: IWebviewViewResolver): IDisposable;
resolve(viewType: string, webview: Webview, cancellation: CancellationToken): Promise<void>;
}
export class WebviewViewService extends Disposable implements IWebviewViewService {
readonly _serviceBrand: undefined;
private readonly _views = new Map<string, IWebviewViewResolver>();
private readonly _awaitingRevival = new Map<string, { webview: Webview, resolve: () => void }>();
constructor() {
super();
}
register(viewType: string, resolver: IWebviewViewResolver): IDisposable {
if (this._views.has(viewType)) {
throw new Error(`View resolver already registered for ${viewType}`);
}
this._views.set(viewType, resolver);
const pending = this._awaitingRevival.get(viewType);
if (pending) {
resolver.resolve(pending.webview, CancellationToken.None).then(() => {
this._awaitingRevival.delete(viewType);
pending.resolve();
});
}
return toDisposable(() => {
this._views.delete(viewType);
});
}
resolve(viewType: string, webview: Webview, cancellation: CancellationToken): Promise<void> {
const resolver = this._views.get(viewType);
if (!resolver) {
if (this._awaitingRevival.has(viewType)) {
throw new Error('View already awaiting revival');
}
let resolve: () => void;
const p = new Promise<void>(r => resolve = r);
this._awaitingRevival.set(viewType, { webview, resolve: resolve! });
return p;
}
return resolver.resolve(webview, cancellation);
}
}

View file

@ -199,6 +199,7 @@ import 'vs/workbench/contrib/url/browser/url.contribution';
// Webview
import 'vs/workbench/contrib/webview/browser/webview.contribution';
import 'vs/workbench/contrib/webviewView/browser/webviewView.contribution';
import 'vs/workbench/contrib/customEditor/browser/customEditor.contribution';
// Extensions Management