Switch entirely to iframe based webviews on desktop

Fixes #83188
Part of #92164

Removes our usage of the electron `<webview>` tag
This commit is contained in:
Matt Bierner 2021-07-06 12:40:51 -07:00
parent d0b9f5f8cf
commit 2222f3cc1d
No known key found for this signature in database
GPG Key ID: 099C331567E11888
11 changed files with 7 additions and 528 deletions

View File

@ -65,7 +65,6 @@ const vscodeResources = [
'out-build/vs/workbench/contrib/debug/**/*.json',
'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt',
'out-build/vs/workbench/contrib/webview/browser/pre/*.js',
'out-build/vs/workbench/contrib/webview/electron-browser/pre/*.js',
'out-build/vs/**/markdown.css',
'out-build/vs/workbench/contrib/tasks/**/*.json',
'out-build/vs/platform/files/**/*.exe',

View File

@ -40,10 +40,7 @@ export class WebviewProtocolProvider extends Disposable {
const uri = URI.parse(request.url);
const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path);
if (typeof entry === 'string') {
const relativeResourcePath = uri.path.startsWith('/electron-browser')
? `vs/workbench/contrib/webview/electron-browser/pre/${entry}`
: `vs/workbench/contrib/webview/browser/pre/${entry}`;
const relativeResourcePath = `vs/workbench/contrib/webview/browser/pre/${entry}`;
const url = FileAccess.asFileUri(relativeResourcePath, require);
return callback(decodeURIComponent(url.fsPath));
}

View File

@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
(function () {
'use strict';
const { ipcRenderer, contextBridge } = require('electron');
/**
* @type {import('../../browser/pre/main').WebviewHost & {isInDevelopmentMode: boolean}}
*/
const host = {
onElectron: true,
useParentPostMessage: true,
postMessage: (channel, data) => {
ipcRenderer.sendToHost(channel, data);
},
onMessage: (channel, handler) => {
ipcRenderer.on(channel, handler);
},
focusIframeOnCreate: true,
isInDevelopmentMode: false
};
host.onMessage('devtools-opened', () => {
host.isInDevelopmentMode = true;
});
document.addEventListener('DOMContentLoaded', e => {
// Forward messages from the embedded iframe
window.onmessage = (/** @type {MessageEvent} */ event) => {
ipcRenderer.sendToHost(event.data.command, event.data.data);
};
});
contextBridge.exposeInMainWorld('vscodeHost', host);
}());

View File

@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="width: 100%; height: 100%">
<head>
<title>Virtual Document</title>
</head>
<body style="margin: 0; overflow: hidden; width: 100%; height: 100%" role="document">
<script async type="module">
import { createWebviewManager } from './main.js';
createWebviewManager({
...window.vscodeHost,
onIframeLoaded: (newFrame) => {
newFrame.contentWindow.onbeforeunload = () => {
if (window.vscodeHost.isInDevelopmentMode) { // Allow reloads while developing a webview
window.vscodeHost.postMessage('do-reload');
return false;
}
// Block navigation when not in development mode
console.log('prevented webview navigation');
return false;
};
// Electron 4 eats mouseup events from inside webviews
// https://github.com/microsoft/vscode/issues/75090
// Try to fix this by rebroadcasting mouse moves and mouseups so that we can
// emulate these on the main window
let isMouseDown = false;
newFrame.contentWindow.addEventListener('mousedown', () => {
isMouseDown = true;
});
const tryDispatchSyntheticMouseEvent = (e) => {
if (!isMouseDown) {
window.vscodeHost.postMessage('synthetic-mouse-event', { type: e.type, screenX: e.screenX, screenY: e.screenY, clientX: e.clientX, clientY: e.clientY });
}
};
newFrame.contentWindow.addEventListener('mouseup', e => {
tryDispatchSyntheticMouseEvent(e);
isMouseDown = false;
});
newFrame.contentWindow.addEventListener('mousemove', tryDispatchSyntheticMouseEvent);
},
});
</script>
</body>
</html>

View File

@ -1,333 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FindInPageOptions, WebviewTag } from 'electron';
import { addDisposableListener } from 'vs/base/browser/dom';
import { Emitter, Event } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { IDisposable } from 'vs/base/common/lifecycle';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { webviewPartitionId } from 'vs/platform/webview/common/webviewManagerService';
import { BaseWebview, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget';
import { WebviewIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-browser/webviewIgnoreMenuShortcutsManager';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> implements Webview, WebviewFindDelegate {
private static _webviewKeyboardHandler: WebviewIgnoreMenuShortcutsManager | undefined;
public readonly checkImeCompletionState = false;
private static getWebviewKeyboardHandler(
configService: IConfigurationService,
mainProcessService: IMainProcessService,
) {
if (!this._webviewKeyboardHandler) {
this._webviewKeyboardHandler = new WebviewIgnoreMenuShortcutsManager(configService, mainProcessService);
}
return this._webviewKeyboardHandler;
}
private _webviewFindWidget: WebviewFindWidget | undefined;
private _findStarted: boolean = false;
constructor(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
private readonly _webviewThemeDataProvider: WebviewThemeDataProvider,
@IContextMenuService contextMenuService: IContextMenuService,
@ILogService private readonly _myLogService: ILogService,
@IInstantiationService instantiationService: IInstantiationService,
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@IMainProcessService mainProcessService: IMainProcessService,
@IMenuService menuService: IMenuService,
@INotificationService notificationService: INotificationService,
@IFileService fileService: IFileService,
@ITunnelService tunnelService: ITunnelService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
) {
super(id, options, contentOptions, extension, _webviewThemeDataProvider, {
contextMenuService,
notificationService,
logService: _myLogService,
telemetryService,
environmentService,
fileService,
menuService,
tunnelService,
remoteAuthorityResolverService
});
/* __GDPR__
"webview.createWebview" : {
"extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"enableFindWidget": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"webviewElementType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
telemetryService.publicLog('webview.createWebview', {
enableFindWidget: !!options.enableFindWidget,
extension: extension?.id.value,
webviewElementType: 'webview',
});
this._myLogService.debug(`Webview(${this.id}): init`);
this._register(addDisposableListener(this.element!, 'dom-ready', once(() => {
this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!));
})));
this._register(addDisposableListener(this.element!, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) {
console.log(`[Embedded Page] ${e.message}`);
}));
this._register(addDisposableListener(this.element!, 'dom-ready', () => {
this._myLogService.debug(`Webview(${this.id}): dom-ready`);
// Workaround for https://github.com/electron/electron/issues/14474
if (this.element && (this.isFocused || document.activeElement === this.element)) {
this.element.blur();
this.element.focus();
}
}));
this._register(addDisposableListener(this.element!, 'crashed', () => {
console.error('embedded page crashed');
}));
this._register(this.on('synthetic-mouse-event', (rawEvent: any) => {
if (!this.element) {
return;
}
const bounds = this.element.getBoundingClientRect();
try {
window.dispatchEvent(new MouseEvent(rawEvent.type, {
...rawEvent,
clientX: rawEvent.clientX + bounds.left,
clientY: rawEvent.clientY + bounds.top,
}));
return;
} catch {
// CustomEvent was treated as MouseEvent so don't do anything - https://github.com/microsoft/vscode/issues/78915
return;
}
}));
this._register(this.on('did-set-content', () => {
this._myLogService.debug(`Webview(${this.id}): did-set-content`);
if (this.element) {
this.element.style.flex = '';
this.element.style.width = '100%';
this.element.style.height = '100%';
}
}));
this._register(addDisposableListener(this.element!, 'devtools-opened', () => {
this._send('devtools-opened');
}));
if (options.enableFindWidget) {
this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this));
this._register(addDisposableListener(this.element!, 'found-in-page', e => {
this._hasFindResult.fire(e.result.matches > 0);
}));
this.styledFindWidget();
}
// We must ensure to put a `file:` URI as the preload attribute
// and not the `vscode-file` URI because preload scripts are loaded
// via node.js from the main side and only allow `file:` protocol
this.element!.preload = FileAccess.asFileUri('./pre/electron-index.js', require).toString(true);
this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser-index.html?platform=electron&id=${this.id}&vscode-resource-base-authority=${encodeURIComponent(this.webviewRootResourceAuthority)}&swVersion=${this._expectedServiceWorkerVersion}`;
}
protected createElement(options: WebviewOptions) {
// Do not start loading the webview yet.
// Wait the end of the ctor when all listeners have been hooked up.
const element = document.createElement('webview');
element.focus = () => {
this.doFocus();
};
element.setAttribute('partition', webviewPartitionId);
element.setAttribute('webpreferences', 'contextIsolation=yes');
element.className = `webview ${options.customClasses || ''}`;
element.style.flex = '0 1';
element.style.width = '0';
element.style.height = '0';
element.style.outline = '0';
return element;
}
protected elementFocusImpl() {
this.element?.focus();
}
public override set contentOptions(options: WebviewContentOptions) {
this._myLogService.debug(`Webview(${this.id}): will set content options`);
super.contentOptions = options;
}
protected readonly extraContentOptions = {};
public mountTo(parent: HTMLElement) {
if (!this.element) {
return;
}
if (this._webviewFindWidget) {
parent.appendChild(this._webviewFindWidget.getDomNode()!);
}
parent.appendChild(this.element);
}
protected async doPostMessage(channel: string, data?: any): Promise<void> {
this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`);
this.element?.send(channel, data);
}
protected override style(): void {
super.style();
this.styledFindWidget();
}
private styledFindWidget() {
this._webviewFindWidget?.updateTheme(this._webviewThemeDataProvider.getTheme());
}
private readonly _hasFindResult = this._register(new Emitter<boolean>());
public readonly hasFindResult: Event<boolean> = this._hasFindResult.event;
private readonly _onDidStopFind = this._register(new Emitter<void>());
public readonly onDidStopFind: Event<void> = this._onDidStopFind.event;
public startFind(value: string, options?: FindInPageOptions) {
if (!value || !this.element) {
return;
}
// ensure options is defined without modifying the original
options = options || {};
// FindNext must be false for a first request
const findOptions: FindInPageOptions = {
forward: options.forward,
findNext: true,
matchCase: options.matchCase
};
this._findStarted = true;
this.element.findInPage(value, findOptions);
}
/**
* Webviews expose a stateful find API.
* Successive calls to find will move forward or backward through onFindResults
* depending on the supplied options.
*
* @param value The string to search for. Empty strings are ignored.
*/
public find(value: string, previous: boolean): void {
if (!this.element) {
return;
}
// Searching with an empty value will throw an exception
if (!value) {
return;
}
const options = { findNext: false, forward: !previous };
if (!this._findStarted) {
this.startFind(value, options);
return;
}
this.element.findInPage(value, options);
}
public stopFind(keepSelection?: boolean): void {
this._hasFindResult.fire(false);
if (!this.element) {
return;
}
this._findStarted = false;
this.element.stopFindInPage(keepSelection ? 'keepSelection' : 'clearSelection');
this._onDidStopFind.fire();
}
public showFind() {
this._webviewFindWidget?.reveal();
}
public hideFind() {
this._webviewFindWidget?.hide();
}
public runFindAction(previous: boolean) {
this._webviewFindWidget?.find(previous);
}
public override selectAll() {
this.element?.selectAll();
}
public override copy() {
this.element?.copy();
}
public override paste() {
this.element?.paste();
}
public override cut() {
this.element?.cut();
}
public override undo() {
this.element?.undo();
}
public override redo() {
this.element?.redo();
}
protected override on<T = unknown>(channel: WebviewMessageChannels | string, handler: (data: T) => void): IDisposable {
if (!this.element) {
throw new Error('Cannot add event listener. No webview element found.');
}
return addDisposableListener(this.element, 'ipc-message', (event) => {
if (!this.element) {
return;
}
if (event.channel === channel && event.args && event.args.length) {
handler(event.args[0]);
}
});
}
}

View File

@ -1,74 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { WebviewTag } from 'electron';
import { addDisposableListener } from 'vs/base/browser/dom';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isMacintosh } from 'vs/base/common/platform';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services';
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/baseWebviewElement';
export class WebviewIgnoreMenuShortcutsManager {
private readonly _webviews = new Set<WebviewTag>();
private readonly _isUsingNativeTitleBars: boolean;
private readonly webviewMainService: IWebviewManagerService;
constructor(
configurationService: IConfigurationService,
mainProcessService: IMainProcessService,
) {
this._isUsingNativeTitleBars = configurationService.getValue<string>('window.titleBarStyle') === 'native';
this.webviewMainService = ProxyChannel.toService<IWebviewManagerService>(mainProcessService.getChannel('webview'));
}
public add(webview: WebviewTag): IDisposable {
this._webviews.add(webview);
const disposables = new DisposableStore();
if (this.shouldToggleMenuShortcutsEnablement) {
this.setIgnoreMenuShortcutsForWebview(webview, true);
}
disposables.add(addDisposableListener(webview, 'ipc-message', (event) => {
switch (event.channel) {
case WebviewMessageChannels.didFocus:
this.setIgnoreMenuShortcuts(true);
break;
case WebviewMessageChannels.didBlur:
this.setIgnoreMenuShortcuts(false);
return;
}
}));
return toDisposable(() => {
disposables.dispose();
this._webviews.delete(webview);
});
}
private get shouldToggleMenuShortcutsEnablement() {
return isMacintosh || this._isUsingNativeTitleBars;
}
private setIgnoreMenuShortcuts(value: boolean) {
for (const webview of this._webviews) {
this.setIgnoreMenuShortcutsForWebview(webview, value);
}
}
private setIgnoreMenuShortcutsForWebview(webview: WebviewTag, value: boolean) {
if (this.shouldToggleMenuShortcutsEnablement) {
this.webviewMainService.setIgnoreMenuShortcuts({ webContentsId: webview.getWebContentsId() }, value);
}
}
}

View File

@ -6,8 +6,8 @@
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
import * as webviewCommands from 'vs/workbench/contrib/webview/electron-browser/webviewCommands';
import { ElectronWebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService';
import * as webviewCommands from 'vs/workbench/contrib/webview/electron-sandbox/webviewCommands';
import { ElectronWebviewService } from 'vs/workbench/contrib/webview/electron-sandbox/webviewService';
registerSingleton(IWebviewService, ElectronWebviewService, true);

View File

@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { WebviewTag } from 'electron';
import * as nls from 'vs/nls';
import { Action2 } from 'vs/platform/actions/common/actions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@ -24,15 +23,6 @@ export class OpenWebviewDeveloperToolsAction extends Action2 {
async run(accessor: ServicesAccessor): Promise<void> {
const nativeHostService = accessor.get(INativeHostService);
const webviewElements = document.querySelectorAll('webview.ready');
for (const element of webviewElements) {
try {
(element as WebviewTag).openDevTools();
} catch (e) {
console.error(e);
}
}
const iframeWebviewElements = document.querySelectorAll('iframe.webview.ready');
if (iframeWebviewElements.length) {
console.info(nls.localize('iframeWebviewAlert', "Using standard dev tools to debug iframe based webview"));

View File

@ -3,31 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DynamicWebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay';
import { WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewService } from 'vs/workbench/contrib/webview/browser/webviewService';
import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
import { ElectronIframeWebview } from 'vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement';
export class ElectronWebviewService extends WebviewService {
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private readonly _configService: IConfigurationService,
) {
super(instantiationService);
}
override createWebviewElement(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewElement {
const useIframes = this._configService.getValue<string>('webview.experimental.useIframes') ?? !options.enableFindWidget;
const webview = this._instantiationService.createInstance(useIframes ? ElectronIframeWebview : ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
const webview = this._instantiationService.createInstance(ElectronIframeWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
this.registerNewWebview(webview);
return webview;
}

View File

@ -91,10 +91,6 @@ import 'vs/workbench/services/remote/electron-browser/tunnelServiceImpl';
//#region --- workbench contributions
// Webview
import 'vs/workbench/contrib/webview/electron-browser/webview.contribution';
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// NOTE: Please do NOT register services here. Use `registerSingleton()`

View File

@ -142,4 +142,7 @@ import 'vs/workbench/contrib/tasks/electron-sandbox/taskService';
// External terminal
import 'vs/workbench/contrib/externalTerminal/electron-sandbox/externalTerminal.contribution';
// Webview
import 'vs/workbench/contrib/webview/electron-sandbox/webview.contribution';
//#endregion