From 0506f7f736db79b4f5acc28ca7e36fdf5272f44f Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 25 Sep 2019 15:10:28 -0700 Subject: [PATCH] Make sure image views (and custom editors) work properly on the web This fixes an issue where webviews for custom editors did not have any associated extension information, which caused them try reading `file:` uri resources instead of remote uri resources --- .../api/browser/mainThreadCodeInsets.ts | 2 +- .../api/browser/mainThreadWebview.ts | 6 ++++- .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 2 ++ src/vs/workbench/api/common/extHostWebview.ts | 22 ++++++++------- .../customEditor/browser/customEditorInput.ts | 2 +- .../browser/dynamicWebviewEditorOverlay.ts | 12 +++++++++ .../contrib/webview/browser/webview.ts | 8 +++--- .../webview/browser/webviewEditorInput.ts | 16 ++++------- .../webview/browser/webviewEditorService.ts | 9 ++++--- .../contrib/webview/browser/webviewElement.ts | 11 +++++--- .../contrib/webview/common/portMapping.ts | 5 ++-- .../electron-browser/webviewElement.ts | 27 +++++++++++-------- 13 files changed, 76 insertions(+), 48 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index 859e9b337e8..cefcf818f89 100644 --- a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts +++ b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts @@ -90,11 +90,11 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape { const webview = this._webviewService.createWebview('' + handle, { enableFindWidget: false, - extension: { id: extensionId, location: URI.revive(extensionLocation) } }, { allowScripts: options.enableScripts, localResourceRoots: options.localResourceRoots ? options.localResourceRoots.map(uri => URI.revive(uri)) : undefined }); + webview.extension = { id: extensionId, location: URI.revive(extensionLocation) }; const webviewZone = new EditorWebviewZone(editor, line, height, webview); diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 8b4e16a1fb8..0b8b855adfc 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -8,7 +8,6 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; import { startsWith } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -173,6 +172,11 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews webview.webview.contentOptions = reviveWebviewOptions(options as any /*todo@mat */); } + public $setExtension(handle: WebviewPanelHandle, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void { + const webview = this.getWebviewEditorInput(handle); + webview.webview.extension = { id: extensionId, location: URI.revive(extensionLocation) }; + } + public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void { const webview = this.getWebviewEditorInput(handle); if (webview.isDisposed()) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 59edb3be7bd..3a2852eaa14 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -532,7 +532,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, registerWebviewEditorProvider: (viewType: string, provider: vscode.WebviewEditorProvider) => { checkProposedApiEnabled(extension); - return extHostWebviews.registerWebviewEditorProvider(viewType, provider); + return extHostWebviews.registerWebviewEditorProvider(extension, viewType, provider); }, registerDecorationProvider(provider: vscode.DecorationProvider) { checkProposedApiEnabled(extension); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f2bc8f0e717..8a7ca779ce1 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -550,6 +550,8 @@ export interface MainThreadWebviewsShape extends IDisposable { $setHtml(handle: WebviewPanelHandle, value: string): void; $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void; + $setExtension(handle: WebviewPanelHandle, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void; + $postMessage(handle: WebviewPanelHandle, value: any): Promise; $registerSerializer(viewType: string): void; diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 7d43a551a1c..b038cdf037e 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -264,7 +264,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { private readonly _proxy: MainThreadWebviewsShape; private readonly _webviewPanels = new Map(); private readonly _serializers = new Map(); - private readonly _editorProviders = new Map(); + private readonly _editorProviders = new Map(); constructor( mainContext: IMainContext, @@ -313,14 +313,15 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } public registerWebviewEditorProvider( + extension: IExtensionDescription, viewType: string, - provider: vscode.WebviewEditorProvider + provider: vscode.WebviewEditorProvider, ): vscode.Disposable { if (this._editorProviders.has(viewType)) { throw new Error(`Editor provider for '${viewType}' already registered`); } - this._editorProviders.set(viewType, provider); + this._editorProviders.set(viewType, { extension, provider, }); this._proxy.$registerEditorProvider(viewType); return new Disposable(() => { @@ -415,21 +416,24 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { async $resolveWebviewEditor( resource: UriComponents, - webviewHandle: WebviewPanelHandle, + handle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions ): Promise { - const provider = this._editorProviders.get(viewType); - if (!provider) { + const entry = this._editorProviders.get(viewType); + if (!entry) { return Promise.reject(new Error(`No provider found for '${viewType}'`)); } + const { provider, extension } = entry; - const webview = new ExtHostWebview(webviewHandle, this._proxy, options, this.initData); - const revivedPanel = new ExtHostWebviewEditor(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); - this._webviewPanels.set(webviewHandle, revivedPanel); + this._proxy.$setExtension(handle, extension.identifier, extension.extensionLocation); + + const webview = new ExtHostWebview(handle, this._proxy, options, this.initData); + const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + this._webviewPanels.set(handle, revivedPanel); return Promise.resolve(provider.resolveWebviewEditor(URI.revive(resource), revivedPanel)); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 2a5c59429a3..f128dcbd725 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -38,7 +38,7 @@ export class CustomFileEditorInput extends WebviewInput { @IExtensionService private readonly _extensionService: IExtensionService, @IDialogService private readonly dialogService: IDialogService, ) { - super(id, viewType, '', undefined, webview); + super(id, viewType, '', webview); this._editorResource = resource; } diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index d85e4df4791..e052f0cccaa 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -24,6 +24,11 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd private _html: string = ''; private _initialScrollProgress: number = 0; private _state: string | undefined = undefined; + private _extension: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + } | undefined; + private _owner: any = undefined; public constructor( @@ -82,6 +87,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd this._webview.value = webview; webview.state = this._state; webview.html = this._html; + webview.extension = this._extension; if (this.options.tryRestoreScrollPosition) { webview.initialScrollProgress = this._initialScrollProgress; } @@ -133,6 +139,12 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewEd this.withWebview(webview => webview.contentOptions = value); } + public get extension() { return this._extension; } + public set extension(value) { + this._extension = value; + this.withWebview(webview => webview.extension = value); + } + private readonly _onDidFocus = this._register(new Emitter()); public readonly onDidFocus: Event = this._onDidFocus.event; diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 46635e42144..a689becdf0d 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -43,10 +43,6 @@ export interface IWebviewService { export const WebviewResourceScheme = 'vscode-resource'; export interface WebviewOptions { - readonly extension?: { - readonly location: URI; - readonly id?: ExtensionIdentifier; - }; readonly enableFindWidget?: boolean; readonly tryRestoreScrollPosition?: boolean; readonly retainContextWhenHidden?: boolean; @@ -63,6 +59,10 @@ export interface Webview extends IDisposable { html: string; contentOptions: WebviewContentOptions; + extension: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + } | undefined; initialScrollProgress: number; state: string | undefined; diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index d89a890249f..ee87e913ab7 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -6,7 +6,6 @@ import * as dom from 'vs/base/browser/dom'; import { memoize } from 'vs/base/common/decorators'; import { URI } from 'vs/base/common/uri'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { EditorInput, EditorModel, GroupIdentifier, IEditorInput, Verbosity } from 'vs/workbench/common/editor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { UnownedDisposable as Unowned } from 'vs/base/common/lifecycle'; @@ -67,16 +66,11 @@ export class WebviewInput extends EditorInput { public readonly id: string, public readonly viewType: string, name: string, - public readonly extension: undefined | { - readonly location: URI; - readonly id: ExtensionIdentifier; - }, webview: Unowned ) { super(); this._name = name; - this.extension = extension; this._webview = this._register(webview.acquire()); // The input owns this webview } @@ -113,6 +107,10 @@ export class WebviewInput extends EditorInput { return this._webview; } + public get extension() { + return this._webview.extension; + } + public get iconPath() { return this._iconPath; } @@ -150,14 +148,10 @@ export class RevivedWebviewEditorInput extends WebviewInput { id: string, viewType: string, name: string, - extension: undefined | { - readonly location: URI; - readonly id: ExtensionIdentifier - }, private readonly reviver: (input: WebviewInput) => Promise, webview: Unowned ) { - super(id, viewType, name, extension, webview); + super(id, viewType, name, webview); } public async resolve(): Promise { diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts index f1f379dd0ef..5e48e6bc5da 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts @@ -150,7 +150,7 @@ export class WebviewEditorService implements IWebviewEditorService { ): WebviewInput { const webview = this.createWebiew(id, extension, options); - const webviewInput = this._instantiationService.createInstance(WebviewInput, id, viewType, title, extension, new UnownedDisposable(webview), undefined); + const webviewInput = this._instantiationService.createInstance(WebviewInput, id, viewType, title, new UnownedDisposable(webview), undefined); this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus, @@ -197,7 +197,7 @@ export class WebviewEditorService implements IWebviewEditorService { const webview = this.createWebiew(id, extension, options); webview.state = state; - const webviewInput = new RevivedWebviewEditorInput(id, viewType, title, extension, async (webview: WebviewInput): Promise => { + const webviewInput = new RevivedWebviewEditorInput(id, viewType, title, async (webview: WebviewInput): Promise => { const didRevive = await this.tryRevive(webview); if (didRevive) { return Promise.resolve(undefined); @@ -263,14 +263,15 @@ export class WebviewEditorService implements IWebviewEditorService { } private createWebiew(id: string, extension: { location: URI; id: ExtensionIdentifier; } | undefined, options: WebviewInputOptions) { - return this._webviewService.createWebviewEditorOverlay(id, { - extension: extension, + const webview = this._webviewService.createWebviewEditorOverlay(id, { enableFindWidget: options.enableFindWidget, retainContextWhenHidden: options.retainContextWhenHidden }, { ...options, localResourceRoots: options.localResourceRoots || this.getDefaultLocalResourceRoots(extension), }); + webview.extension = extension; + return webview; } private getDefaultLocalResourceRoots(extension: undefined | { diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index db8dca5e362..15e9264acda 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -36,9 +36,14 @@ export class IFrameWebview extends Disposable implements Webview { private readonly _portMappingManager: WebviewPortMappingManager; + public extension: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + } | undefined; + constructor( private readonly id: string, - private _options: WebviewOptions, + _options: WebviewOptions, contentOptions: WebviewContentOptions, @IThemeService themeService: IThemeService, @ITunnelService tunnelService: ITunnelService, @@ -52,7 +57,7 @@ export class IFrameWebview extends Disposable implements Webview { } this._portMappingManager = this._register(new WebviewPortMappingManager( - this._options.extension ? this._options.extension.location : undefined, + () => this.extension ? this.extension.location : undefined, () => this.content.options.portMapping || [], tunnelService )); @@ -307,7 +312,7 @@ export class IFrameWebview extends Disposable implements Webview { private async loadResource(requestPath: string, uri: URI) { try { - const result = await loadLocalResource(uri, this.fileService, this._options.extension ? this._options.extension.location : undefined, + const result = await loadLocalResource(uri, this.fileService, this.extension ? this.extension.location : undefined, () => (this.content.options.localResourceRoots || [])); if (result.type === 'success') { diff --git a/src/vs/workbench/contrib/webview/common/portMapping.ts b/src/vs/workbench/contrib/webview/common/portMapping.ts index 30041eb213b..241a9c7de17 100644 --- a/src/vs/workbench/contrib/webview/common/portMapping.ts +++ b/src/vs/workbench/contrib/webview/common/portMapping.ts @@ -14,7 +14,7 @@ export class WebviewPortMappingManager extends Disposable { private readonly _tunnels = new Map>(); constructor( - private readonly extensionLocation: URI | undefined, + private readonly getExtensionLocation: () => URI | undefined, private readonly mappings: () => ReadonlyArray, private readonly tunnelService: ITunnelService ) { @@ -30,7 +30,8 @@ export class WebviewPortMappingManager extends Disposable { for (const mapping of this.mappings()) { if (mapping.webviewPort === requestLocalHostInfo.port) { - if (this.extensionLocation && this.extensionLocation.scheme === REMOTE_HOST_SCHEME) { + const extensionLocation = this.getExtensionLocation(); + if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { const tunnel = await this.getOrCreateTunnel(mapping.extensionHostPort); if (tunnel) { return encodeURI(uri.with({ diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index e891c95527e..406eb4927b4 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -92,7 +92,7 @@ class WebviewSession extends Disposable { class WebviewProtocolProvider extends Disposable { constructor( webview: WebviewTag, - private readonly _extensionLocation: URI | undefined, + private readonly _getExtensionLocation: () => URI | undefined, private readonly _getLocalResourceRoots: () => ReadonlyArray, private readonly _fileService: IFileService, ) { @@ -111,7 +111,7 @@ class WebviewProtocolProvider extends Disposable { return; } - registerFileProtocol(contents, WebviewResourceScheme, this._fileService, this._extensionLocation, () => + registerFileProtocol(contents, WebviewResourceScheme, this._fileService, this._getExtensionLocation(), () => this._getLocalResourceRoots() ); } @@ -123,12 +123,12 @@ class WebviewPortMappingProvider extends Disposable { constructor( session: WebviewSession, - extensionLocation: URI | undefined, + getExtensionLocation: () => URI | undefined, mappings: () => ReadonlyArray, tunnelService: ITunnelService, ) { super(); - this._manager = this._register(new WebviewPortMappingManager(extensionLocation, mappings, tunnelService)); + this._manager = this._register(new WebviewPortMappingManager(getExtensionLocation, mappings, tunnelService)); session.onBeforeRequest(async details => { const redirect = await this._manager.getRedirect(details.url); @@ -233,8 +233,13 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview, private readonly _onDidFocus = this._register(new Emitter()); public readonly onDidFocus: Event = this._onDidFocus.event; + public extension: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + } | undefined; + constructor( - private readonly _options: WebviewOptions, + options: WebviewOptions, contentOptions: WebviewContentOptions, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -279,13 +284,13 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview, this._register(new WebviewProtocolProvider( this._webview, - this._options.extension ? this._options.extension.location : undefined, + () => this.extension ? this.extension.location : undefined, () => (this.content.options.localResourceRoots || []), fileService)); this._register(new WebviewPortMappingProvider( session, - _options.extension ? _options.extension.location : undefined, + () => this.extension ? this.extension.location : undefined, () => (this.content.options.portMapping || []), tunnelService, )); @@ -382,7 +387,7 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview, this._send('devtools-opened'); })); - if (_options.enableFindWidget) { + if (options.enableFindWidget) { this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); this._register(addDisposableListener(this._webview, 'found-in-page', e => { @@ -529,9 +534,9 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview, } this._hasAlertedAboutMissingCsp = true; - if (this._options.extension && this._options.extension.id) { + if (this.extension && this.extension.id) { if (this._environementService.isExtensionDevelopment) { - this._onMissingCsp.fire(this._options.extension.id); + this._onMissingCsp.fire(this.extension.id); } type TelemetryClassification = { @@ -542,7 +547,7 @@ export class ElectronWebviewBasedWebview extends Disposable implements Webview, }; this._telemetryService.publicLog2('webviewMissingCsp', { - extension: this._options.extension.id.value + extension: this.extension.id.value }); } }