Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,383 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export const enum WebviewMessageChannels {
onmessage = 'onmessage',
didClickLink = 'did-click-link',
didScroll = 'did-scroll',
didFocus = 'did-focus',
didBlur = 'did-blur',
didLoad = 'did-load',
doUpdateState = 'do-update-state',
doReload = 'do-reload',
loadResource = 'load-resource',
loadLocalhost = 'load-localhost',
webviewReady = 'webview-ready',
wheel = 'did-scroll-wheel',
fatalError = 'fatal-error',
}
interface IKeydownEvent {
key: string;
keyCode: number;
code: string;
shiftKey: boolean;
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
repeat: boolean;
}
interface WebviewContent {
readonly html: string;
readonly options: WebviewContentOptions;
readonly state: string | undefined;
}
namespace WebviewState {
export const enum Type { Initializing, Ready }
export class Initializing {
readonly type = Type.Initializing;
constructor(
public readonly pendingMessages: Array<{ readonly channel: string, readonly data?: any }>
) { }
}
export const Ready = { type: Type.Ready } as const;
export type State = typeof Ready | Initializing;
}
export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
private _element: T | undefined;
protected get element(): T | undefined { return this._element; }
private _focused: boolean | undefined;
public get isFocused(): boolean { return !!this._focused; }
private _state: WebviewState.State = new WebviewState.Initializing([]);
protected content: WebviewContent;
constructor(
public readonly id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
public extension: WebviewExtensionDescription | undefined,
private readonly webviewThemeDataProvider: WebviewThemeDataProvider,
@INotificationService notificationService: INotificationService,
@ILogService private readonly _logService: ILogService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService
) {
super();
this.content = {
html: '',
options: contentOptions,
state: undefined
};
this._element = this.createElement(options, contentOptions);
const subscription = this._register(this.on(WebviewMessageChannels.webviewReady, () => {
this._logService.debug(`Webview(${this.id}): webview ready`);
this.element?.classList.add('ready');
if (this._state.type === WebviewState.Type.Initializing) {
this._state.pendingMessages.forEach(({ channel, data }) => this.doPostMessage(channel, data));
}
this._state = WebviewState.Ready;
subscription.dispose();
}));
this._register(this.on('no-csp-found', () => {
this.handleNoCspFound();
}));
this._register(this.on(WebviewMessageChannels.didClickLink, (uri: string) => {
this._onDidClickLink.fire(uri);
}));
this._register(this.on(WebviewMessageChannels.onmessage, (data: any) => {
this._onMessage.fire(data);
}));
this._register(this.on(WebviewMessageChannels.didScroll, (scrollYPercentage: number) => {
this._onDidScroll.fire({ scrollYPercentage: scrollYPercentage });
}));
this._register(this.on(WebviewMessageChannels.doReload, () => {
this.reload();
}));
this._register(this.on(WebviewMessageChannels.doUpdateState, (state: any) => {
this.state = state;
this._onDidUpdateState.fire(state);
}));
this._register(this.on(WebviewMessageChannels.didFocus, () => {
this.handleFocusChange(true);
}));
this._register(this.on(WebviewMessageChannels.wheel, (event: IMouseWheelEvent) => {
this._onDidWheel.fire(event);
}));
this._register(this.on(WebviewMessageChannels.didBlur, () => {
this.handleFocusChange(false);
}));
this._register(this.on<{ message: string }>(WebviewMessageChannels.fatalError, (e) => {
notificationService.error(localize('fatalErrorMessage', "Error loading webview: {0}", e.message));
}));
this._register(this.on('did-keydown', (data: KeyboardEvent) => {
// Electron: workaround for https://github.com/electron/electron/issues/14258
// We have to detect keyboard events in the <webview> and dispatch them to our
// keybinding service because these events do not bubble to the parent window anymore.
this.handleKeyDown(data);
}));
this.style();
this._register(webviewThemeDataProvider.onThemeDataChanged(this.style, this));
}
dispose(): void {
if (this.element) {
this.element.remove();
}
this._element = undefined;
this._onDidDispose.fire();
super.dispose();
}
private readonly _onMissingCsp = this._register(new Emitter<ExtensionIdentifier>());
public readonly onMissingCsp = this._onMissingCsp.event;
private readonly _onDidClickLink = this._register(new Emitter<string>());
public readonly onDidClickLink = this._onDidClickLink.event;
private readonly _onDidReload = this._register(new Emitter<void>());
public readonly onDidReload = this._onDidReload.event;
private readonly _onMessage = this._register(new Emitter<any>());
public readonly onMessage = this._onMessage.event;
private readonly _onDidScroll = this._register(new Emitter<{ readonly scrollYPercentage: number; }>());
public readonly onDidScroll = this._onDidScroll.event;
private readonly _onDidWheel = this._register(new Emitter<IMouseWheelEvent>());
public readonly onDidWheel = this._onDidWheel.event;
private readonly _onDidUpdateState = this._register(new Emitter<string | undefined>());
public readonly onDidUpdateState = this._onDidUpdateState.event;
private readonly _onDidFocus = this._register(new Emitter<void>());
public readonly onDidFocus = this._onDidFocus.event;
private readonly _onDidBlur = this._register(new Emitter<void>());
public readonly onDidBlur = this._onDidBlur.event;
private readonly _onDidDispose = this._register(new Emitter<void>());
public readonly onDidDispose = this._onDidDispose.event;
public postMessage(data: any): void {
this._send('message', data);
}
protected _send(channel: string, data?: any): void {
if (this._state.type === WebviewState.Type.Initializing) {
this._state.pendingMessages.push({ channel, data });
} else {
this.doPostMessage(channel, data);
}
}
protected abstract readonly extraContentOptions: { readonly [key: string]: string };
protected abstract createElement(options: WebviewOptions, contentOptions: WebviewContentOptions): T;
protected abstract on<T = unknown>(channel: string, handler: (data: T) => void): IDisposable;
protected abstract doPostMessage(channel: string, data?: any): void;
private _hasAlertedAboutMissingCsp = false;
private handleNoCspFound(): void {
if (this._hasAlertedAboutMissingCsp) {
return;
}
this._hasAlertedAboutMissingCsp = true;
if (this.extension && this.extension.id) {
if (this.environmentService.isExtensionDevelopment) {
this._onMissingCsp.fire(this.extension.id);
}
type TelemetryClassification = {
extension?: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; };
};
type TelemetryData = {
extension?: string,
};
this._telemetryService.publicLog2<TelemetryData, TelemetryClassification>('webviewMissingCsp', {
extension: this.extension.id.value
});
}
}
public reload(): void {
this.doUpdateContent(this.content);
const subscription = this._register(this.on(WebviewMessageChannels.didLoad, () => {
this._onDidReload.fire();
subscription.dispose();
}));
}
public set html(value: string) {
this.doUpdateContent({
html: value,
options: this.content.options,
state: this.content.state,
});
}
public set contentOptions(options: WebviewContentOptions) {
this._logService.debug(`Webview(${this.id}): will update content options`);
if (areWebviewInputOptionsEqual(options, this.content.options)) {
this._logService.debug(`Webview(${this.id}): skipping content options update`);
return;
}
this.doUpdateContent({
html: this.content.html,
options: options,
state: this.content.state,
});
}
public set localResourcesRoot(resources: URI[]) {
/** no op */
}
public set state(state: string | undefined) {
this.content = {
html: this.content.html,
options: this.content.options,
state,
};
}
public set initialScrollProgress(value: number) {
this._send('initial-scroll-position', value);
}
private doUpdateContent(newContent: WebviewContent) {
this._logService.debug(`Webview(${this.id}): will update content`);
this.content = newContent;
this._send('content', {
contents: this.content.html,
options: this.content.options,
state: this.content.state,
...this.extraContentOptions
});
}
protected style(): void {
const { styles, activeTheme, themeLabel } = this.webviewThemeDataProvider.getWebviewThemeData();
this._send('styles', { styles, activeTheme, themeName: themeLabel });
}
protected handleFocusChange(isFocused: boolean): void {
this._focused = isFocused;
if (isFocused) {
this._onDidFocus.fire();
} else {
this._onDidBlur.fire();
}
}
private handleKeyDown(event: IKeydownEvent) {
// Create a fake KeyboardEvent from the data provided
const emulatedKeyboardEvent = new KeyboardEvent('keydown', event);
// Force override the target
Object.defineProperty(emulatedKeyboardEvent, 'target', {
get: () => this.element,
});
// And re-dispatch
window.dispatchEvent(emulatedKeyboardEvent);
}
windowDidDragStart(): void {
// Webview break drag and droping around the main window (no events are generated when you are over them)
// Work around this by disabling pointer events during the drag.
// https://github.com/electron/electron/issues/18226
if (this.element) {
this.element.style.pointerEvents = 'none';
}
}
windowDidDragEnd(): void {
if (this.element) {
this.element.style.pointerEvents = '';
}
}
public selectAll() {
this.execCommand('selectAll');
}
public copy() {
this.execCommand('copy');
}
public paste() {
this.execCommand('paste');
}
public cut() {
this.execCommand('cut');
}
public undo() {
this.execCommand('undo');
}
public redo() {
this.execCommand('redo');
}
private execCommand(command: string) {
if (this.element) {
this._send('execCommand', command);
}
}
}

View File

@@ -0,0 +1,271 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension } from 'vs/base/browser/dom';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { memoize } from 'vs/base/common/decorators';
import { Emitter, Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
/**
* Webview editor overlay that creates and destroys the underlying webview as needed.
*/
export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOverlay {
private readonly _onDidWheel = this._register(new Emitter<IMouseWheelEvent>());
public readonly onDidWheel = this._onDidWheel.event;
private readonly _pendingMessages = new Set<any>();
private readonly _webview = this._register(new MutableDisposable<WebviewElement>());
private readonly _webviewEvents = this._register(new DisposableStore());
private _html: string = '';
private _initialScrollProgress: number = 0;
private _state: string | undefined = undefined;
private _extension: WebviewExtensionDescription | undefined;
private _contentOptions: WebviewContentOptions;
private _options: WebviewOptions;
private _owner: any = undefined;
private readonly _scopedContextKeyService = this._register(new MutableDisposable<IContextKeyService>());
private _findWidgetVisible: IContextKey<boolean>;
public constructor(
public readonly id: string,
initialOptions: WebviewOptions,
initialContentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
@ILayoutService private readonly _layoutService: ILayoutService,
@IWebviewService private readonly _webviewService: IWebviewService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService
) {
super();
this._extension = extension;
this._options = initialOptions;
this._contentOptions = initialContentOptions;
this._findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(_contextKeyService);
}
public get isFocused() {
return !!this._webview.value?.isFocused;
}
private readonly _onDidDispose = this._register(new Emitter<void>());
public onDidDispose = this._onDidDispose.event;
dispose() {
this.container.remove();
this._onDidDispose.fire();
super.dispose();
}
@memoize
public get container() {
const container = document.createElement('div');
container.id = `webview-${this.id}`;
container.style.visibility = 'hidden';
// Webviews cannot be reparented in the dom as it will destory their contents.
// Mount them to a high level node to avoid this.
this._layoutService.container.appendChild(container);
return container;
}
public claim(owner: any) {
this._owner = owner;
this.show();
}
public release(owner: any) {
if (this._owner !== owner) {
return;
}
this._owner = undefined;
this.container.style.visibility = 'hidden';
if (!this._options.retainContextWhenHidden) {
this._webview.clear();
this._webviewEvents.clear();
}
}
public layoutWebviewOverElement(element: HTMLElement, dimension?: Dimension) {
if (!this.container || !this.container.parentElement) {
return;
}
const frameRect = element.getBoundingClientRect();
const containerRect = this.container.parentElement.getBoundingClientRect();
this.container.style.position = 'absolute';
this.container.style.overflow = 'hidden';
this.container.style.top = `${frameRect.top - containerRect.top}px`;
this.container.style.left = `${frameRect.left - containerRect.left}px`;
this.container.style.width = `${dimension ? dimension.width : frameRect.width}px`;
this.container.style.height = `${dimension ? dimension.height : frameRect.height}px`;
}
private show() {
if (!this._webview.value) {
const webview = this._webviewService.createWebviewElement(this.id, this._options, this._contentOptions, this.extension);
this._webview.value = webview;
webview.state = this._state;
if (this._html) {
webview.html = this._html;
}
if (this._options.tryRestoreScrollPosition) {
webview.initialScrollProgress = this._initialScrollProgress;
}
webview.mountTo(this.container);
this._scopedContextKeyService.value = this._contextKeyService.createScoped(this.container);
this._findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._scopedContextKeyService.value);
// Forward events from inner webview to outer listeners
this._webviewEvents.clear();
this._webviewEvents.add(webview.onDidFocus(() => { this._onDidFocus.fire(); }));
this._webviewEvents.add(webview.onDidBlur(() => { this._onDidBlur.fire(); }));
this._webviewEvents.add(webview.onDidClickLink(x => { this._onDidClickLink.fire(x); }));
this._webviewEvents.add(webview.onMessage(x => { this._onMessage.fire(x); }));
this._webviewEvents.add(webview.onMissingCsp(x => { this._onMissingCsp.fire(x); }));
this._webviewEvents.add(webview.onDidWheel(x => { this._onDidWheel.fire(x); }));
this._webviewEvents.add(webview.onDidReload(() => { this._onDidReload.fire(); }));
this._webviewEvents.add(webview.onDidScroll(x => {
this._initialScrollProgress = x.scrollYPercentage;
this._onDidScroll.fire(x);
}));
this._webviewEvents.add(webview.onDidUpdateState(state => {
this._state = state;
this._onDidUpdateState.fire(state);
}));
this._pendingMessages.forEach(msg => webview.postMessage(msg));
this._pendingMessages.clear();
}
this.container.style.visibility = 'visible';
}
public get html(): string { return this._html; }
public set html(value: string) {
this._html = value;
this.withWebview(webview => webview.html = value);
}
public get initialScrollProgress(): number { return this._initialScrollProgress; }
public set initialScrollProgress(value: number) {
this._initialScrollProgress = value;
this.withWebview(webview => webview.initialScrollProgress = value);
}
public get state(): string | undefined { return this._state; }
public set state(value: string | undefined) {
this._state = value;
this.withWebview(webview => webview.state = value);
}
public get extension(): WebviewExtensionDescription | undefined { return this._extension; }
public set extension(value: WebviewExtensionDescription | undefined) {
this._extension = value;
this.withWebview(webview => webview.extension = value);
}
public get options(): WebviewOptions { return this._options; }
public set options(value: WebviewOptions) { this._options = { customClasses: this._options.customClasses, ...value }; }
public get contentOptions(): WebviewContentOptions { return this._contentOptions; }
public set contentOptions(value: WebviewContentOptions) {
this._contentOptions = value;
this.withWebview(webview => webview.contentOptions = value);
}
public set localResourcesRoot(resources: URI[]) {
this.withWebview(webview => webview.localResourcesRoot = resources);
}
private readonly _onDidFocus = this._register(new Emitter<void>());
public readonly onDidFocus: Event<void> = this._onDidFocus.event;
private readonly _onDidBlur = this._register(new Emitter<void>());
public readonly onDidBlur: Event<void> = this._onDidBlur.event;
private readonly _onDidClickLink = this._register(new Emitter<string>());
public readonly onDidClickLink: Event<string> = this._onDidClickLink.event;
private readonly _onDidReload = this._register(new Emitter<void>());
public readonly onDidReload = this._onDidReload.event;
private readonly _onDidScroll = this._register(new Emitter<{ scrollYPercentage: number; }>());
public readonly onDidScroll: Event<{ scrollYPercentage: number; }> = this._onDidScroll.event;
private readonly _onDidUpdateState = this._register(new Emitter<string | undefined>());
public readonly onDidUpdateState: Event<string | undefined> = this._onDidUpdateState.event;
private readonly _onMessage = this._register(new Emitter<any>());
public readonly onMessage: Event<any> = this._onMessage.event;
private readonly _onMissingCsp = this._register(new Emitter<ExtensionIdentifier>());
public readonly onMissingCsp: Event<any> = this._onMissingCsp.event;
postMessage(data: any): void {
if (this._webview.value) {
this._webview.value.postMessage(data);
} else {
this._pendingMessages.add(data);
}
}
focus(): void { this.withWebview(webview => webview.focus()); }
reload(): void { this.withWebview(webview => webview.reload()); }
selectAll(): void { this.withWebview(webview => webview.selectAll()); }
copy(): void { this.withWebview(webview => webview.copy()); }
paste(): void { this.withWebview(webview => webview.paste()); }
cut(): void { this.withWebview(webview => webview.cut()); }
undo(): void { this.withWebview(webview => webview.undo()); }
redo(): void { this.withWebview(webview => webview.redo()); }
showFind() {
if (this._webview.value) {
this._webview.value.showFind();
this._findWidgetVisible.set(true);
}
}
hideFind() {
this._findWidgetVisible.reset();
this._webview.value?.hideFind();
}
runFindAction(previous: boolean): void { this.withWebview(webview => webview.runFindAction(previous)); }
public getInnerWebview() {
return this._webview.value;
}
private withWebview(f: (webview: Webview) => void): void {
if (this._webview.value) {
f(this._webview.value);
}
}
windowDidDragStart() {
this.withWebview(webview => webview.windowDidDragStart());
}
windowDidDragEnd() {
this.withWebview(webview => webview.windowDidDragEnd());
}
}

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Fake</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* 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 () {
const id = document.location.search.match(/\bid=([\w-]+)/)[1];
const onElectron = /platform=electron/.test(document.location.search);
const hostMessaging = new class HostMessaging {
constructor() {
this.handlers = new Map();
window.addEventListener('message', (e) => {
if (e.data && (e.data.command === 'onmessage' || e.data.command === 'do-update-state')) {
// Came from inner iframe
this.postMessage(e.data.command, e.data.data);
return;
}
const channel = e.data.channel;
const handler = this.handlers.get(channel);
if (handler) {
handler(e, e.data.args);
} else {
console.log('no handler for ', e);
}
});
}
postMessage(channel, data) {
window.parent.postMessage({ target: id, channel, data }, '*');
}
onMessage(channel, handler) {
this.handlers.set(channel, handler);
}
}();
function fatalError(/** @type {string} */ message) {
console.error(`Webview fatal error: ${message}`);
hostMessaging.postMessage('fatal-error', { message });
}
const workerReady = new Promise(async (resolveWorkerReady) => {
if (onElectron) {
return resolveWorkerReady();
}
if (!areServiceWorkersEnabled()) {
fatalError('Service Workers are not enabled in browser. Webviews will not work.');
return resolveWorkerReady();
}
const expectedWorkerVersion = 1;
navigator.serviceWorker.register('service-worker.js').then(
async registration => {
await navigator.serviceWorker.ready;
const versionHandler = (event) => {
if (event.data.channel !== 'version') {
return;
}
navigator.serviceWorker.removeEventListener('message', versionHandler);
if (event.data.version === expectedWorkerVersion) {
return resolveWorkerReady();
} else {
// If we have the wrong version, try once to unregister and re-register
return registration.update()
.then(() => navigator.serviceWorker.ready)
.finally(resolveWorkerReady);
}
};
navigator.serviceWorker.addEventListener('message', versionHandler);
registration.active.postMessage({ channel: 'version' });
},
error => {
fatalError(`Could not register service workers: ${error}.`);
resolveWorkerReady();
});
const forwardFromHostToWorker = (channel) => {
hostMessaging.onMessage(channel, event => {
navigator.serviceWorker.ready.then(registration => {
registration.active.postMessage({ channel: channel, data: event.data.args });
});
});
};
forwardFromHostToWorker('did-load-resource');
forwardFromHostToWorker('did-load-localhost');
navigator.serviceWorker.addEventListener('message', event => {
if (['load-resource', 'load-localhost'].includes(event.data.channel)) {
hostMessaging.postMessage(event.data.channel, event.data);
}
});
});
function areServiceWorkersEnabled() {
try {
return !!navigator.serviceWorker;
} catch (e) {
return false;
}
}
/** @type {import('./main').WebviewHost} */
const host = {
postMessage: hostMessaging.postMessage.bind(hostMessaging),
onMessage: hostMessaging.onMessage.bind(hostMessaging),
ready: workerReady,
fakeLoad: !onElectron,
onElectron: onElectron,
rewriteCSP: onElectron
? (csp) => {
return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:');
}
: (csp, endpoint) => {
const endpointUrl = new URL(endpoint);
return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin);
}
};
(/** @type {any} */ (window)).createWebviewManager(host);
}());

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en" style="width: 100%; height: 100%;">
<head>
<meta charset="UTF-8">
<!-- Disable pinch zooming -->
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Virtual Document</title>
</head>
<body style="margin: 0; overflow: hidden; width: 100%; height: 100%" role="document">
<script src="main.js"></script>
<script src="host.js"></script>
</body>
</html>

View File

@@ -0,0 +1,673 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
/**
* @typedef {{
* postMessage: (channel: string, data?: any) => void,
* onMessage: (channel: string, handler: any) => void,
* focusIframeOnCreate?: boolean,
* ready?: Promise<void>,
* onIframeLoaded?: (iframe: HTMLIFrameElement) => void,
* fakeLoad?: boolean,
* rewriteCSP: (existingCSP: string, endpoint?: string) => string,
* onElectron?: boolean
* }} WebviewHost
*/
(function () {
'use strict';
const isSafari = navigator.vendor && navigator.vendor.indexOf('Apple') > -1 &&
navigator.userAgent &&
navigator.userAgent.indexOf('CriOS') === -1 &&
navigator.userAgent.indexOf('FxiOS') === -1;
/**
* Use polling to track focus of main webview and iframes within the webview
*
* @param {Object} handlers
* @param {() => void} handlers.onFocus
* @param {() => void} handlers.onBlur
*/
const trackFocus = ({ onFocus, onBlur }) => {
const interval = 50;
let isFocused = document.hasFocus();
setInterval(() => {
const isCurrentlyFocused = document.hasFocus();
if (isCurrentlyFocused === isFocused) {
return;
}
isFocused = isCurrentlyFocused;
if (isCurrentlyFocused) {
onFocus();
} else {
onBlur();
}
}, interval);
};
const getActiveFrame = () => {
return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame'));
};
const getPendingFrame = () => {
return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame'));
};
const defaultCssRules = `
body {
background-color: transparent;
color: var(--vscode-editor-foreground);
font-family: var(--vscode-font-family);
font-weight: var(--vscode-font-weight);
font-size: var(--vscode-font-size);
margin: 0;
padding: 0 20px;
}
img {
max-width: 100%;
max-height: 100%;
}
a {
color: var(--vscode-textLink-foreground);
}
a:hover {
color: var(--vscode-textLink-activeForeground);
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
code {
color: var(--vscode-textPreformat-foreground);
}
blockquote {
background: var(--vscode-textBlockQuote-background);
border-color: var(--vscode-textBlockQuote-border);
}
kbd {
color: var(--vscode-editor-foreground);
border-radius: 3px;
vertical-align: middle;
padding: 1px 3px;
background-color: hsla(0,0%,50%,.17);
border: 1px solid rgba(71,71,71,.4);
border-bottom-color: rgba(88,88,88,.4);
box-shadow: inset 0 -1px 0 rgba(88,88,88,.4);
}
.vscode-light kbd {
background-color: hsla(0,0%,87%,.5);
border: 1px solid hsla(0,0%,80%,.7);
border-bottom-color: hsla(0,0%,73%,.7);
box-shadow: inset 0 -1px 0 hsla(0,0%,73%,.7);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-corner {
background-color: var(--vscode-editor-background);
}
::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground);
}
::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground);
}`;
/**
* @param {boolean} allowMultipleAPIAcquire
* @param {*} [state]
* @return {string}
*/
function getVsCodeApiScript(allowMultipleAPIAcquire, state) {
const encodedState = state ? encodeURIComponent(state) : undefined;
return `
globalThis.acquireVsCodeApi = (function() {
const originalPostMessage = window.parent.postMessage.bind(window.parent);
const targetOrigin = '*';
let acquired = false;
let state = ${state ? `JSON.parse(decodeURIComponent("${encodedState}"))` : undefined};
return () => {
if (acquired && !${allowMultipleAPIAcquire}) {
throw new Error('An instance of the VS Code API has already been acquired');
}
acquired = true;
return Object.freeze({
postMessage: function(msg) {
return originalPostMessage({ command: 'onmessage', data: msg }, targetOrigin);
},
setState: function(newState) {
state = newState;
originalPostMessage({ command: 'do-update-state', data: JSON.stringify(newState) }, targetOrigin);
return newState;
},
getState: function() {
return state;
}
});
};
})();
delete window.parent;
delete window.top;
delete window.frameElement;
`;
}
/**
* @param {WebviewHost} host
*/
function createWebviewManager(host) {
// state
let firstLoad = true;
let loadTimeout;
let pendingMessages = [];
const initData = {
initialScrollProgress: undefined,
};
/**
* @param {HTMLDocument?} document
* @param {HTMLElement?} body
*/
const applyStyles = (document, body) => {
if (!document) {
return;
}
if (body) {
body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast');
body.classList.add(initData.activeTheme);
body.dataset.vscodeThemeKind = initData.activeTheme;
body.dataset.vscodeThemeName = initData.themeName || '';
}
if (initData.styles) {
const documentStyle = document.documentElement.style;
// Remove stale properties
for (let i = documentStyle.length - 1; i >= 0; i--) {
const property = documentStyle[i];
// Don't remove properties that the webview might have added separately
if (property && property.startsWith('--vscode-')) {
documentStyle.removeProperty(property);
}
}
// Re-add new properties
for (const variable of Object.keys(initData.styles)) {
documentStyle.setProperty(`--${variable}`, initData.styles[variable]);
}
}
};
/**
* @param {MouseEvent} event
*/
const handleInnerClick = (event) => {
if (!event || !event.view || !event.view.document) {
return;
}
let baseElement = event.view.document.getElementsByTagName('base')[0];
/** @type {any} */
let node = event.target;
while (node) {
if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) {
if (node.getAttribute('href') === '#') {
event.view.scrollTo(0, 0);
} else if (node.hash && (node.getAttribute('href') === node.hash || (baseElement && node.href.indexOf(baseElement.href) >= 0))) {
let scrollTarget = event.view.document.getElementById(node.hash.substr(1, node.hash.length - 1));
if (scrollTarget) {
scrollTarget.scrollIntoView();
}
} else {
host.postMessage('did-click-link', node.href.baseVal || node.href);
}
event.preventDefault();
break;
}
node = node.parentNode;
}
};
/**
* @param {MouseEvent} event
*/
const handleAuxClick =
(event) => {
// Prevent middle clicks opening a broken link in the browser
if (!event.view || !event.view.document) {
return;
}
if (event.button === 1) {
let node = /** @type {any} */ (event.target);
while (node) {
if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) {
event.preventDefault();
break;
}
node = node.parentNode;
}
}
};
/**
* @param {KeyboardEvent} e
*/
const handleInnerKeydown = (e) => {
// If the keypress would trigger a browser event, such as copy or paste,
// make sure we block the browser from dispatching it. Instead VS Code
// handles these events and will dispatch a copy/paste back to the webview
// if needed
if (isUndoRedo(e)) {
e.preventDefault();
} else if (isCopyPasteOrCut(e)) {
if (host.onElectron) {
e.preventDefault();
} else {
return; // let the browser handle this
}
}
host.postMessage('did-keydown', {
key: e.key,
keyCode: e.keyCode,
code: e.code,
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
repeat: e.repeat
});
};
/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isCopyPasteOrCut(e) {
const hasMeta = e.ctrlKey || e.metaKey;
return hasMeta && ['c', 'v', 'x'].includes(e.key);
}
/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isUndoRedo(e) {
const hasMeta = e.ctrlKey || e.metaKey;
return hasMeta && ['z', 'y'].includes(e.key);
}
let isHandlingScroll = false;
const handleWheel = (event) => {
if (isHandlingScroll) {
return;
}
host.postMessage('did-scroll-wheel', {
deltaMode: event.deltaMode,
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaZ: event.deltaZ,
detail: event.detail,
type: event.type
});
};
const handleInnerScroll = (event) => {
if (!event.target || !event.target.body) {
return;
}
if (isHandlingScroll) {
return;
}
const progress = event.currentTarget.scrollY / event.target.body.clientHeight;
if (isNaN(progress)) {
return;
}
isHandlingScroll = true;
window.requestAnimationFrame(() => {
try {
host.postMessage('did-scroll', progress);
} catch (e) {
// noop
}
isHandlingScroll = false;
});
};
/**
* @return {string}
*/
function toContentHtml(data) {
const options = data.options;
const text = data.contents;
const newDocument = new DOMParser().parseFromString(text, 'text/html');
newDocument.querySelectorAll('a').forEach(a => {
if (!a.title) {
a.title = a.getAttribute('href');
}
});
// apply default script
if (options.allowScripts) {
const defaultScript = newDocument.createElement('script');
defaultScript.id = '_vscodeApiScript';
defaultScript.textContent = getVsCodeApiScript(options.allowMultipleAPIAcquire, data.state);
newDocument.head.prepend(defaultScript);
}
// apply default styles
const defaultStyles = newDocument.createElement('style');
defaultStyles.id = '_defaultStyles';
defaultStyles.textContent = defaultCssRules;
newDocument.head.prepend(defaultStyles);
applyStyles(newDocument, newDocument.body);
// Check for CSP
const csp = newDocument.querySelector('meta[http-equiv="Content-Security-Policy"]');
if (!csp) {
host.postMessage('no-csp-found');
} else {
try {
csp.setAttribute('content', host.rewriteCSP(csp.getAttribute('content'), data.endpoint));
} catch (e) {
console.error(`Could not rewrite csp: ${e}`);
}
}
// set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off
// and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden
return '<!DOCTYPE html>\n' + newDocument.documentElement.outerHTML;
}
document.addEventListener('DOMContentLoaded', () => {
const idMatch = document.location.search.match(/\bid=([\w-]+)/);
const ID = idMatch ? idMatch[1] : undefined;
if (!document.body) {
return;
}
host.onMessage('styles', (_event, data) => {
initData.styles = data.styles;
initData.activeTheme = data.activeTheme;
initData.themeName = data.themeName;
const target = getActiveFrame();
if (!target) {
return;
}
if (target.contentDocument) {
applyStyles(target.contentDocument, target.contentDocument.body);
}
});
// propagate focus
host.onMessage('focus', () => {
const target = getActiveFrame();
if (target) {
target.contentWindow.focus();
}
});
// update iframe-contents
let updateId = 0;
host.onMessage('content', async (_event, data) => {
const currentUpdateId = ++updateId;
await host.ready;
if (currentUpdateId !== updateId) {
return;
}
const options = data.options;
const newDocument = toContentHtml(data);
const frame = getActiveFrame();
const wasFirstLoad = firstLoad;
// keep current scrollY around and use later
let setInitialScrollPosition;
if (firstLoad) {
firstLoad = false;
setInitialScrollPosition = (body, window) => {
if (!isNaN(initData.initialScrollProgress)) {
if (window.scrollY === 0) {
window.scroll(0, body.clientHeight * initData.initialScrollProgress);
}
}
};
} else {
const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? frame.contentWindow.scrollY : 0;
setInitialScrollPosition = (body, window) => {
if (window.scrollY === 0) {
window.scroll(0, scrollY);
}
};
}
// Clean up old pending frames and set current one as new one
const previousPendingFrame = getPendingFrame();
if (previousPendingFrame) {
previousPendingFrame.setAttribute('id', '');
document.body.removeChild(previousPendingFrame);
}
if (!wasFirstLoad) {
pendingMessages = [];
}
const newFrame = document.createElement('iframe');
newFrame.setAttribute('id', 'pending-frame');
newFrame.setAttribute('frameborder', '0');
newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin allow-pointer-lock allow-downloads' : 'allow-same-origin allow-pointer-lock');
if (host.fakeLoad) {
// We should just be able to use srcdoc, but I wasn't
// seeing the service worker applying properly.
// Fake load an empty on the correct origin and then write real html
// into it to get around this.
newFrame.src = `./fake.html?id=${ID}`;
}
newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden';
document.body.appendChild(newFrame);
if (!host.fakeLoad) {
// write new content onto iframe
newFrame.contentDocument.open();
}
/**
* @param {Document} contentDocument
*/
function onFrameLoaded(contentDocument) {
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325
setTimeout(() => {
if (host.fakeLoad) {
contentDocument.open();
contentDocument.write(newDocument);
contentDocument.close();
hookupOnLoadHandlers(newFrame);
}
if (contentDocument) {
applyStyles(contentDocument, contentDocument.body);
}
}, 0);
}
if (host.fakeLoad && !options.allowScripts && isSafari) {
// On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired.
// Use polling instead.
const interval = setInterval(() => {
// If the frame is no longer mounted, loading has stopped
if (!newFrame.parentElement) {
clearInterval(interval);
return;
}
if (newFrame.contentDocument.readyState !== 'loading') {
clearInterval(interval);
onFrameLoaded(newFrame.contentDocument);
}
}, 10);
} else {
newFrame.contentWindow.addEventListener('DOMContentLoaded', e => {
const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined;
onFrameLoaded(contentDocument);
});
}
/**
* @param {Document} contentDocument
* @param {Window} contentWindow
*/
const onLoad = (contentDocument, contentWindow) => {
if (contentDocument && contentDocument.body) {
// Workaround for https://github.com/microsoft/vscode/issues/12865
// check new scrollY and reset if necessary
setInitialScrollPosition(contentDocument.body, contentWindow);
}
const newFrame = getPendingFrame();
if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) {
const oldActiveFrame = getActiveFrame();
if (oldActiveFrame) {
document.body.removeChild(oldActiveFrame);
}
// Styles may have changed since we created the element. Make sure we re-style
applyStyles(newFrame.contentDocument, newFrame.contentDocument.body);
newFrame.setAttribute('id', 'active-frame');
newFrame.style.visibility = 'visible';
if (host.focusIframeOnCreate) {
newFrame.contentWindow.focus();
}
contentWindow.addEventListener('scroll', handleInnerScroll);
contentWindow.addEventListener('wheel', handleWheel);
pendingMessages.forEach((data) => {
contentWindow.postMessage(data, '*');
});
pendingMessages = [];
}
host.postMessage('did-load');
};
/**
* @param {HTMLIFrameElement} newFrame
*/
function hookupOnLoadHandlers(newFrame) {
clearTimeout(loadTimeout);
loadTimeout = undefined;
loadTimeout = setTimeout(() => {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(newFrame.contentDocument, newFrame.contentWindow);
}, 200);
newFrame.contentWindow.addEventListener('load', function (e) {
const contentDocument = /** @type {Document} */ (e.target);
if (loadTimeout) {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(contentDocument, this);
}
});
// Bubble out various events
newFrame.contentWindow.addEventListener('click', handleInnerClick);
newFrame.contentWindow.addEventListener('auxclick', handleAuxClick);
newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown);
newFrame.contentWindow.addEventListener('contextmenu', e => e.preventDefault());
if (host.onIframeLoaded) {
host.onIframeLoaded(newFrame);
}
}
if (!host.fakeLoad) {
hookupOnLoadHandlers(newFrame);
}
if (!host.fakeLoad) {
newFrame.contentDocument.write(newDocument);
newFrame.contentDocument.close();
}
host.postMessage('did-set-content', undefined);
});
// Forward message to the embedded iframe
host.onMessage('message', (_event, data) => {
const pending = getPendingFrame();
if (!pending) {
const target = getActiveFrame();
if (target) {
target.contentWindow.postMessage(data, '*');
return;
}
}
pendingMessages.push(data);
});
host.onMessage('initial-scroll-position', (_event, progress) => {
initData.initialScrollProgress = progress;
});
host.onMessage('execCommand', (_event, data) => {
const target = getActiveFrame();
if (!target) {
return;
}
target.contentDocument.execCommand(data);
});
trackFocus({
onFocus: () => host.postMessage('did-focus'),
onBlur: () => host.postMessage('did-blur')
});
// signal ready
host.postMessage('webview-ready', {});
});
}
if (typeof module !== 'undefined') {
module.exports = createWebviewManager;
} else {
(/** @type {any} */ (window)).createWebviewManager = createWebviewManager;
}
}());

View File

@@ -0,0 +1,278 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference lib="webworker" />
const VERSION = 1;
const rootPath = self.location.pathname.replace(/\/service-worker.js$/, '');
/**
* Root path for resources
*/
const resourceRoot = rootPath + '/vscode-resource';
const resolveTimeout = 30000;
/**
* @template T
* @typedef {{
* resolve: (x: T) => void,
* promise: Promise<T>
* }} RequestStoreEntry
*/
/**
* @template T
*/
class RequestStore {
constructor() {
/** @type {Map<string, RequestStoreEntry<T>>} */
this.map = new Map();
}
/**
* @param {string} webviewId
* @param {string} path
* @return {Promise<T> | undefined}
*/
get(webviewId, path) {
const entry = this.map.get(this._key(webviewId, path));
return entry && entry.promise;
}
/**
* @param {string} webviewId
* @param {string} path
* @returns {Promise<T>}
*/
create(webviewId, path) {
const existing = this.get(webviewId, path);
if (existing) {
return existing;
}
let resolve;
const promise = new Promise(r => resolve = r);
const entry = { resolve, promise };
const key = this._key(webviewId, path);
this.map.set(key, entry);
const dispose = () => {
clearTimeout(timeout);
const existingEntry = this.map.get(key);
if (existingEntry === entry) {
return this.map.delete(key);
}
};
const timeout = setTimeout(dispose, resolveTimeout);
return promise;
}
/**
* @param {string} webviewId
* @param {string} path
* @param {T} result
* @return {boolean}
*/
resolve(webviewId, path, result) {
const entry = this.map.get(this._key(webviewId, path));
if (!entry) {
return false;
}
entry.resolve(result);
return true;
}
/**
* @param {string} webviewId
* @param {string} path
* @return {string}
*/
_key(webviewId, path) {
return `${webviewId}@@@${path}`;
}
}
/**
* Map of requested paths to responses.
*
* @type {RequestStore<{ body: any, mime: string } | undefined>}
*/
const resourceRequestStore = new RequestStore();
/**
* Map of requested localhost origins to optional redirects.
*
* @type {RequestStore<string | undefined>}
*/
const localhostRequestStore = new RequestStore();
const notFound = () =>
new Response('Not Found', { status: 404, });
self.addEventListener('message', async (event) => {
switch (event.data.channel) {
case 'version':
{
self.clients.get(event.source.id).then(client => {
if (client) {
client.postMessage({
channel: 'version',
version: VERSION
});
}
});
return;
}
case 'did-load-resource':
{
const webviewId = getWebviewIdForClient(event.source);
const data = event.data.data;
const response = data.status === 200
? { body: data.data, mime: data.mime }
: undefined;
if (!resourceRequestStore.resolve(webviewId, data.path, response)) {
console.log('Could not resolve unknown resource', data.path);
}
return;
}
case 'did-load-localhost':
{
const webviewId = getWebviewIdForClient(event.source);
const data = event.data.data;
if (!localhostRequestStore.resolve(webviewId, data.origin, data.location)) {
console.log('Could not resolve unknown localhost', data.origin);
}
return;
}
}
console.log('Unknown message');
});
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// See if it's a resource request
if (requestUrl.origin === self.origin && requestUrl.pathname.startsWith(resourceRoot + '/')) {
return event.respondWith(processResourceRequest(event, requestUrl));
}
// See if it's a localhost request
if (requestUrl.origin !== self.origin && requestUrl.host.match(/^localhost:(\d+)$/)) {
return event.respondWith(processLocalhostRequest(event, requestUrl));
}
});
self.addEventListener('install', (event) => {
event.waitUntil(self.skipWaiting()); // Activate worker immediately
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim()); // Become available to all pages
});
async function processResourceRequest(event, requestUrl) {
const client = await self.clients.get(event.clientId);
if (!client) {
console.log('Could not find inner client for request');
return notFound();
}
const webviewId = getWebviewIdForClient(client);
const resourcePath = requestUrl.pathname.startsWith(resourceRoot + '/') ? requestUrl.pathname.slice(resourceRoot.length) : requestUrl.pathname;
function resolveResourceEntry(entry) {
if (!entry) {
return notFound();
}
return new Response(entry.body, {
status: 200,
headers: { 'Content-Type': entry.mime }
});
}
const parentClient = await getOuterIframeClient(webviewId);
if (!parentClient) {
console.log('Could not find parent client for request');
return notFound();
}
// Check if we've already resolved this request
const existing = resourceRequestStore.get(webviewId, resourcePath);
if (existing) {
return existing.then(resolveResourceEntry);
}
parentClient.postMessage({
channel: 'load-resource',
path: resourcePath
});
return resourceRequestStore.create(webviewId, resourcePath)
.then(resolveResourceEntry);
}
/**
* @param {*} event
* @param {URL} requestUrl
*/
async function processLocalhostRequest(event, requestUrl) {
const client = await self.clients.get(event.clientId);
if (!client) {
// This is expected when requesting resources on other localhost ports
// that are not spawned by vs code
return undefined;
}
const webviewId = getWebviewIdForClient(client);
const origin = requestUrl.origin;
const resolveRedirect = redirectOrigin => {
if (!redirectOrigin) {
return fetch(event.request);
}
const location = event.request.url.replace(new RegExp(`^${requestUrl.origin}(/|$)`), `${redirectOrigin}$1`);
return new Response(null, {
status: 302,
headers: {
Location: location
}
});
};
const parentClient = await getOuterIframeClient(webviewId);
if (!parentClient) {
console.log('Could not find parent client for request');
return notFound();
}
// Check if we've already resolved this request
const existing = localhostRequestStore.get(webviewId, origin);
if (existing) {
return existing.then(resolveRedirect);
}
parentClient.postMessage({
channel: 'load-localhost',
origin: origin
});
return localhostRequestStore.create(webviewId, origin)
.then(resolveRedirect);
}
function getWebviewIdForClient(client) {
const requesterClientUrl = new URL(client.url);
return requesterClientUrl.search.match(/\bid=([a-z0-9-]+)/i)[1];
}
async function getOuterIframeClient(webviewId) {
const allClients = await self.clients.matchAll({ includeUncontrolled: true });
return allClients.find(client => {
const clientUrl = new URL(client.url);
return (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html`) && clientUrl.search.match(new RegExp('\\bid=' + webviewId));
});
}

View File

@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createMemoizer } from 'vs/base/common/decorators';
import { Disposable } from 'vs/base/common/lifecycle';
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { Emitter } from 'vs/base/common/event';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { ColorScheme } from 'vs/platform/theme/common/theme';
interface WebviewThemeData {
readonly activeTheme: string;
readonly themeLabel: string;
readonly styles: { readonly [key: string]: string | number; };
}
export class WebviewThemeDataProvider extends Disposable {
private static readonly MEMOIZER = createMemoizer();
private readonly _onThemeDataChanged = this._register(new Emitter<void>());
public readonly onThemeDataChanged = this._onThemeDataChanged.event;
constructor(
@IThemeService private readonly _themeService: IThemeService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super();
this._register(this._themeService.onDidColorThemeChange(() => {
this.reset();
}));
const webviewConfigurationKeys = ['editor.fontFamily', 'editor.fontWeight', 'editor.fontSize'];
this._register(this._configurationService.onDidChangeConfiguration(e => {
if (webviewConfigurationKeys.some(key => e.affectsConfiguration(key))) {
this.reset();
}
}));
}
public getTheme(): IColorTheme {
return this._themeService.getColorTheme();
}
@WebviewThemeDataProvider.MEMOIZER
public getWebviewThemeData(): WebviewThemeData {
const configuration = this._configurationService.getValue<IEditorOptions>('editor');
const editorFontFamily = configuration.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
const editorFontWeight = configuration.fontWeight || EDITOR_FONT_DEFAULTS.fontWeight;
const editorFontSize = configuration.fontSize || EDITOR_FONT_DEFAULTS.fontSize;
const theme = this._themeService.getColorTheme();
const exportedColors = colorRegistry.getColorRegistry().getColors().reduce((colors, entry) => {
const color = theme.getColor(entry.id);
if (color) {
colors['vscode-' + entry.id.replace('.', '-')] = color.toString();
}
return colors;
}, {} as { [key: string]: string; });
const styles = {
'vscode-font-family': DEFAULT_FONT_FAMILY,
'vscode-font-weight': 'normal',
'vscode-font-size': '13px',
'vscode-editor-font-family': editorFontFamily,
'vscode-editor-font-weight': editorFontWeight,
'vscode-editor-font-size': editorFontSize + 'px',
...exportedColors
};
const activeTheme = ApiThemeClassName.fromTheme(theme);
return { styles, activeTheme, themeLabel: theme.label, };
}
private reset() {
WebviewThemeDataProvider.MEMOIZER.clear();
this._onThemeDataChanged.fire();
}
}
enum ApiThemeClassName {
light = 'vscode-light',
dark = 'vscode-dark',
highContrast = 'vscode-high-contrast'
}
namespace ApiThemeClassName {
export function fromTheme(theme: IColorTheme): ApiThemeClassName {
switch (theme.type) {
case ColorScheme.LIGHT: return ApiThemeClassName.light;
case ColorScheme.DARK: return ApiThemeClassName.dark;
default: return ApiThemeClassName.highContrast;
}
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MultiCommand, RedoCommand, SelectAllCommand, UndoCommand } from 'vs/editor/browser/editorExtensions';
import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard';
import { IWebviewService, Webview } from 'vs/workbench/contrib/webview/browser/webview';
const PRIORITY = 100;
function overrideCommandForWebview(command: MultiCommand | undefined, f: (webview: Webview) => void) {
command?.addImplementation(PRIORITY, accessor => {
const webviewService = accessor.get(IWebviewService);
const webview = webviewService.activeWebview;
if (webview?.isFocused) {
f(webview);
return true;
}
return false;
});
}
overrideCommandForWebview(UndoCommand, webview => webview.undo());
overrideCommandForWebview(RedoCommand, webview => webview.redo());
overrideCommandForWebview(SelectAllCommand, webview => webview.selectAll());
overrideCommandForWebview(CopyAction, webview => webview.copy());
overrideCommandForWebview(PasteAction, webview => webview.paste());
overrideCommandForWebview(CutAction, webview => webview.cut());

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension } from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
/**
* Set when the find widget in a webview is visible.
*/
export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey<boolean>('webviewFindWidgetVisible', false);
export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED = new RawContextKey<boolean>('webviewFindWidgetFocused', false);
export const webviewHasOwnEditFunctionsContextKey = 'webviewHasOwnEditFunctions';
export const webviewHasOwnEditFunctionsContext = new RawContextKey<boolean>(webviewHasOwnEditFunctionsContextKey, false);
export const IWebviewService = createDecorator<IWebviewService>('webviewService');
export interface WebviewIcons {
readonly light: URI;
readonly dark: URI;
}
/**
* Handles the creation of webview elements.
*/
export interface IWebviewService {
readonly _serviceBrand: undefined;
readonly activeWebview: Webview | undefined;
createWebviewElement(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewElement;
createWebviewOverlay(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewOverlay;
setIcons(id: string, value: WebviewIcons | undefined): void;
}
export const enum WebviewContentPurpose {
NotebookRenderer = 'notebookRenderer',
CustomEditor = 'customEditor',
}
export interface WebviewOptions {
// The purpose of the webview; this is (currently) only used for filtering in js-debug
readonly purpose?: WebviewContentPurpose;
readonly customClasses?: string;
readonly enableFindWidget?: boolean;
readonly tryRestoreScrollPosition?: boolean;
readonly retainContextWhenHidden?: boolean;
}
export interface WebviewContentOptions {
readonly allowMultipleAPIAcquire?: boolean;
readonly allowScripts?: boolean;
readonly localResourceRoots?: ReadonlyArray<URI>;
readonly portMapping?: ReadonlyArray<modes.IWebviewPortMapping>;
readonly enableCommandUris?: boolean;
}
export interface WebviewExtensionDescription {
readonly location: URI;
readonly id: ExtensionIdentifier;
}
export interface IDataLinkClickEvent {
dataURL: string;
downloadName?: string;
}
export interface Webview extends IDisposable {
readonly id: string;
html: string;
contentOptions: WebviewContentOptions;
localResourcesRoot: URI[];
extension: WebviewExtensionDescription | undefined;
initialScrollProgress: number;
state: string | undefined;
readonly isFocused: boolean;
readonly onDidFocus: Event<void>;
readonly onDidBlur: Event<void>;
readonly onDidDispose: Event<void>;
readonly onDidClickLink: Event<string>;
readonly onDidScroll: Event<{ scrollYPercentage: number }>;
readonly onDidWheel: Event<IMouseWheelEvent>;
readonly onDidUpdateState: Event<string | undefined>;
readonly onDidReload: Event<void>;
readonly onMessage: Event<any>;
readonly onMissingCsp: Event<ExtensionIdentifier>;
postMessage(data: any): void;
focus(): void;
reload(): void;
showFind(): void;
hideFind(): void;
runFindAction(previous: boolean): void;
selectAll(): void;
copy(): void;
paste(): void;
cut(): void;
undo(): void;
redo(): void;
windowDidDragStart(): void;
windowDidDragEnd(): void;
}
/**
* Basic webview rendered in the dom
*/
export interface WebviewElement extends Webview {
mountTo(parent: HTMLElement): void;
}
/**
* Dynamically created webview drawn over another element.
*/
export interface WebviewOverlay extends Webview {
readonly container: HTMLElement;
options: WebviewOptions;
claim(owner: any): void;
release(owner: any): void;
getInnerWebview(): Webview | undefined;
layoutWebviewOverElement(element: HTMLElement, dimension?: Dimension): void;
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
import { WebviewService } from './webviewService';
registerSingleton(IWebviewService, WebviewService, true);

View File

@@ -0,0 +1,218 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { addDisposableListener } from 'vs/base/browser/dom';
import { streamToBuffer } from 'vs/base/common/buffer';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
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 { IRequestService } from 'vs/platform/request/common/request';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping';
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 { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Webview {
private readonly _portMappingManager: WebviewPortMappingManager;
constructor(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
webviewThemeDataProvider: WebviewThemeDataProvider,
@INotificationService notificationService: INotificationService,
@ITunnelService tunnelService: ITunnelService,
@IFileService private readonly fileService: IFileService,
@IRequestService private readonly requestService: IRequestService,
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@ILogService logService: ILogService,
) {
super(id, options, contentOptions, extension, webviewThemeDataProvider, notificationService, logService, telemetryService, environmentService);
this._portMappingManager = this._register(new WebviewPortMappingManager(
() => this.extension?.location,
() => this.content.options.portMapping || [],
tunnelService
));
this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => {
const rawPath = entry.path;
const normalizedPath = decodeURIComponent(rawPath);
const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path));
this.loadResource(rawPath, uri);
}));
this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => {
this.localLocalhost(entry.origin);
}));
this.initElement(extension, options);
}
protected createElement(options: WebviewOptions, _contentOptions: WebviewContentOptions) {
// Do not start loading the webview yet.
// Wait the end of the ctor when all listeners have been hooked up.
const element = document.createElement('iframe');
element.className = `webview ${options.customClasses || ''}`;
element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms', 'allow-pointer-lock', 'allow-downloads');
element.style.border = 'none';
element.style.width = '100%';
element.style.height = '100%';
return element;
}
protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) {
// The extensionId and purpose in the URL are used for filtering in js-debug:
this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`);
}
private get externalEndpoint(): string {
const endpoint = this.environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
if (endpoint[endpoint.length - 1] === '/') {
return endpoint.slice(0, endpoint.length - 1);
}
return endpoint;
}
public mountTo(parent: HTMLElement) {
if (this.element) {
parent.appendChild(this.element);
}
}
public set html(value: string) {
super.html = this.preprocessHtml(value);
}
protected preprocessHtml(value: string): string {
return value
.replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
}
return `${startQuote}${this.externalEndpoint}/vscode-resource/file${path}${endQuote}`;
})
.replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${this.externalEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
}
return `${startQuote}${this.externalEndpoint}/vscode-resource/file${path}${endQuote}`;
});
}
protected get extraContentOptions(): any {
return {
endpoint: this.externalEndpoint,
};
}
focus(): void {
if (this.element) {
this._send('focus');
}
}
showFind(): void {
throw new Error('Method not implemented.');
}
hideFind(): void {
throw new Error('Method not implemented.');
}
runFindAction(previous: boolean): void {
throw new Error('Method not implemented.');
}
private async loadResource(requestPath: string, uri: URI) {
try {
const remoteAuthority = this.environmentService.remoteAuthority;
const remoteConnectionData = remoteAuthority ? this._remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
const extensionLocation = this.extension?.location;
// If we are loading a file resource from a remote extension, rewrite the uri to go remote
let rewriteUri: undefined | ((uri: URI) => URI);
if (extensionLocation?.scheme === Schemas.vscodeRemote) {
rewriteUri = (uri) => {
if (uri.scheme === Schemas.file && extensionLocation?.scheme === Schemas.vscodeRemote) {
return URI.from({
scheme: Schemas.vscodeRemote,
authority: extensionLocation.authority,
path: '/vscode-resource',
query: JSON.stringify({
requestResourcePath: uri.path
})
});
}
return uri;
};
}
const result = await loadLocalResource(uri, {
extensionLocation: extensionLocation,
roots: this.content.options.localResourceRoots || [],
remoteConnectionData,
rewriteUri,
}, {
readFileStream: (resource) => this.fileService.readFileStream(resource).then(x => x.value),
}, this.requestService);
if (result.type === WebviewResourceResponse.Type.Success) {
const { buffer } = await streamToBuffer(result.stream);
return this._send('did-load-resource', {
status: 200,
path: requestPath,
mime: result.mimeType,
data: buffer,
});
}
} catch {
// noop
}
return this._send('did-load-resource', {
status: 404,
path: requestPath
});
}
private async localLocalhost(origin: string) {
const authority = this.environmentService.remoteAuthority;
const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined;
const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined;
return this._send('did-load-localhost', {
origin,
location: redirect
});
}
protected doPostMessage(channel: string, data?: any): void {
if (this.element) {
this.element.contentWindow!.postMessage({ channel, args: data }, '*');
}
}
protected on<T = unknown>(channel: WebviewMessageChannels, handler: (data: T) => void): IDisposable {
return addDisposableListener(window, 'message', e => {
if (!e || !e.data || e.data.target !== this.id) {
return;
}
if (e.data.channel === channel) {
handler(e.data.data);
}
});
}
}

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview';
import { Event } from 'vs/base/common/event';
export interface WebviewFindDelegate {
readonly hasFindResult: Event<boolean>;
find(value: string, previous: boolean): void;
startFind(value: string): void;
stopFind(keepSelection?: boolean): void;
focus(): void;
}
export class WebviewFindWidget extends SimpleFindWidget {
protected _findWidgetFocused: IContextKey<boolean>;
constructor(
private readonly _delegate: WebviewFindDelegate,
@IContextViewService contextViewService: IContextViewService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(contextViewService, contextKeyService);
this._findWidgetFocused = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED.bindTo(contextKeyService);
this._register(_delegate.hasFindResult(hasResult => {
this.updateButtons(hasResult);
}));
}
public find(previous: boolean) {
const val = this.inputValue;
if (val) {
this._delegate.find(val, previous);
}
}
public hide() {
super.hide();
this._delegate.stopFind(true);
this._delegate.focus();
}
public onInputChanged() {
const val = this.inputValue;
if (val) {
this._delegate.startFind(val);
} else {
this._delegate.stopFind(false);
}
return false;
}
protected onFocusTrackerFocus() {
this._findWidgetFocused.set(true);
}
protected onFocusTrackerBlur() {
this._findWidgetFocused.reset();
}
protected onFindInputFocusTrackerFocus() { }
protected onFindInputFocusTrackerBlur() { }
protected findFirst() { }
}

View File

@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { memoize } from 'vs/base/common/decorators';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview';
export class WebviewIconManager {
private readonly _icons = new Map<string, WebviewIcons>();
constructor(
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
@IConfigurationService private readonly _configService: IConfigurationService,
) {
this._configService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('workbench.iconTheme')) {
this.updateStyleSheet();
}
});
}
@memoize
private get _styleElement(): HTMLStyleElement {
const element = dom.createStyleSheet();
element.className = 'webview-icons';
return element;
}
public setIcons(
webviewId: string,
iconPath: WebviewIcons | undefined,
) {
if (iconPath) {
this._icons.set(webviewId, iconPath);
} else {
this._icons.delete(webviewId);
}
this.updateStyleSheet();
}
private async updateStyleSheet() {
await this._lifecycleService.when(LifecyclePhase.Starting);
const cssRules: string[] = [];
if (this._configService.getValue('workbench.iconTheme') !== null) {
for (const [key, value] of this._icons) {
const webviewSelector = `.show-file-icons .webview-${key}-name-file-icon::before`;
try {
cssRules.push(
`.monaco-workbench.vs ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.light)}; }`,
`.monaco-workbench.vs-dark ${webviewSelector}, .monaco-workbench.hc-black ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.dark)}; }`
);
} catch {
// noop
}
}
}
this._styleElement.textContent = cssRules.join('\n');
}
}

View File

@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { IWebviewService, Webview, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
import { DynamicWebviewEditorOverlay } from './dynamicWebviewEditorOverlay';
import { WebviewIconManager } from './webviewIconManager';
export class WebviewService implements IWebviewService {
declare readonly _serviceBrand: undefined;
protected readonly _webviewThemeDataProvider: WebviewThemeDataProvider;
private readonly _iconManager: WebviewIconManager;
constructor(
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
) {
this._webviewThemeDataProvider = this._instantiationService.createInstance(WebviewThemeDataProvider);
this._iconManager = this._instantiationService.createInstance(WebviewIconManager);
}
private _activeWebview?: Webview;
public get activeWebview() { return this._activeWebview; }
createWebviewElement(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewElement {
const webview = this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
this.addWebviewListeners(webview);
return webview;
}
createWebviewOverlay(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewOverlay {
const webview = this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension);
this.addWebviewListeners(webview);
return webview;
}
setIcons(id: string, iconPath: WebviewIcons | undefined): void {
this._iconManager.setIcons(id, iconPath);
}
protected addWebviewListeners(webview: Webview) {
webview.onDidFocus(() => {
this._activeWebview = webview;
});
const onBlur = () => {
if (this._activeWebview === webview) {
this._activeWebview = undefined;
}
};
webview.onDidBlur(onBlur);
webview.onDidDispose(onBlur);
}
}

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export function asWebviewUri(
environmentService: IWorkbenchEnvironmentService,
uuid: string,
resource: URI,
): URI {
const uri = environmentService.webviewResourceRoot
// Make sure we preserve the scheme of the resource but convert it into a normal path segment
// The scheme is important as we need to know if we are requesting a local or a remote resource.
.replace('{{resource}}', resource.scheme + withoutScheme(resource))
.replace('{{uuid}}', uuid);
return URI.parse(uri);
}
function withoutScheme(resource: URI): string {
return resource.toString().replace(/^\S+?:/, '');
}

View File

@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* 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 registerVscodeResourceScheme = (function () {
let hasRegistered = false;
return () => {
if (hasRegistered) {
return;
}
hasRegistered = true;
};
}());
const ipcRenderer = require('electron').ipcRenderer;
let isInDevelopmentMode = false;
/**
* @type {import('../../browser/pre/main').WebviewHost}
*/
const host = {
onElectron: true,
postMessage: (channel, data) => {
ipcRenderer.sendToHost(channel, data);
},
onMessage: (channel, handler) => {
ipcRenderer.on(channel, handler);
},
focusIframeOnCreate: true,
onIframeLoaded: (newFrame) => {
newFrame.contentWindow.onbeforeunload = () => {
if (isInDevelopmentMode) { // Allow reloads while developing a webview
host.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) {
host.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);
},
rewriteCSP: (csp) => {
return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:');
},
};
host.onMessage('devtools-opened', () => {
isInDevelopmentMode = true;
});
document.addEventListener('DOMContentLoaded', () => {
registerVscodeResourceScheme();
// Forward messages from the embedded iframe
window.onmessage = (message) => {
ipcRenderer.sendToHost(message.data.command, message.data.data);
};
});
require('../../browser/pre/main')(host);
}());

View File

@@ -0,0 +1,8 @@
<!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">
</body>
</html>

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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';
registerSingleton(IWebviewService, ElectronWebviewService, true);
registerAction2(webviewCommands.OpenWebviewDeveloperToolsAction);

View File

@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* 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 { Action2 } from 'vs/platform/actions/common/actions';
import * as nls from 'vs/nls';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { CATEGORIES } from 'vs/workbench/common/actions';
export class OpenWebviewDeveloperToolsAction extends Action2 {
constructor() {
super({
id: 'workbench.action.webview.openDeveloperTools',
title: { value: nls.localize('openToolsLabel', "Open Webview Developer Tools"), original: 'Open Webview Developer Tools' },
category: CATEGORIES.Developer,
f1: true
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const elements = document.querySelectorAll('webview.ready');
for (let i = 0; i < elements.length; i++) {
try {
(elements.item(i) as WebviewTag).openDevTools();
} catch (e) {
console.error(e);
}
}
}
}

View File

@@ -0,0 +1,443 @@
/*---------------------------------------------------------------------------------------------
* 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 { ThrottledDelayer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { isMacintosh } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
import { IWebviewManagerService } 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 { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget';
import { WebviewResourceRequestManager, rewriteVsCodeResourceUrls } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading';
class WebviewKeyboardHandler {
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 = createChannelSender<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 'did-focus':
this.setIgnoreMenuShortcuts(true);
break;
case 'did-blur':
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(webview.getWebContentsId(), value);
}
}
}
export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> implements Webview, WebviewFindDelegate {
private static _webviewKeyboardHandler: WebviewKeyboardHandler | undefined;
private static getWebviewKeyboardHandler(
configService: IConfigurationService,
mainProcessService: IMainProcessService,
) {
if (!this._webviewKeyboardHandler) {
this._webviewKeyboardHandler = new WebviewKeyboardHandler(configService, mainProcessService);
}
return this._webviewKeyboardHandler;
}
private _webviewFindWidget: WebviewFindWidget | undefined;
private _findStarted: boolean = false;
private readonly _resourceRequestManager: WebviewResourceRequestManager;
private _messagePromise = Promise.resolve();
private readonly _focusDelayer = this._register(new ThrottledDelayer(10));
private _elementFocusImpl!: (options?: FocusOptions | undefined) => void;
constructor(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
private readonly _webviewThemeDataProvider: WebviewThemeDataProvider,
@ILogService private readonly _myLogService: ILogService,
@IInstantiationService instantiationService: IInstantiationService,
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@IMainProcessService mainProcessService: IMainProcessService,
@INotificationService noficationService: INotificationService,
) {
super(id, options, contentOptions, extension, _webviewThemeDataProvider, noficationService, _myLogService, telemetryService, environmentService);
/* __GDPR__
"webview.createWebview" : {
"extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"enableFindWidget": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
telemetryService.publicLog('webview.createWebview', {
enableFindWidget: !!options.enableFindWidget,
extension: extension?.id.value,
});
this._myLogService.debug(`Webview(${this.id}): init`);
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options));
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`;
}
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');
this._elementFocusImpl = element.focus.bind(element);
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;
}
public set contentOptions(options: WebviewContentOptions) {
this._myLogService.debug(`Webview(${this.id}): will set content options`);
this._resourceRequestManager.update(options);
super.contentOptions = options;
}
public set localResourcesRoot(resources: URI[]) {
this._resourceRequestManager.update({
...this.contentOptions,
localResourceRoots: resources,
});
super.localResourcesRoot = resources;
}
protected readonly extraContentOptions = {};
public set html(value: string) {
this._myLogService.debug(`Webview(${this.id}): will set html`);
super.html = rewriteVsCodeResourceUrls(this.id, value);
}
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}): will post message on '${channel}'`);
this._messagePromise = this._messagePromise
.then(() => this._resourceRequestManager.ensureReady())
.then(() => {
this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`);
return this.element?.send(channel, data);
});
}
public focus(): void {
this.doFocus();
// Handle focus change programmatically (do not rely on event from <webview>)
this.handleFocusChange(true);
}
private doFocus() {
if (!this.element) {
return;
}
// Clear the existing focus first.
// This is required because the next part where we set the focus is async.
if (document.activeElement && document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
// Workaround for https://github.com/microsoft/vscode/issues/75209
// Electron's webview.focus is async so for a sequence of actions such as:
//
// 1. Open webview
// 1. Show quick pick from command palette
//
// We end up focusing the webview after showing the quick pick, which causes
// the quick pick to instantly dismiss.
//
// Workaround this by debouncing the focus and making sure we are not focused on an input
// when we try to re-focus.
this._focusDelayer.trigger(async () => {
if (!this.isFocused || !this.element) {
return;
}
if (document.activeElement && document.activeElement?.tagName !== 'BODY') {
return;
}
try {
this._elementFocusImpl();
} catch {
// noop
}
this._send('focus');
});
}
protected 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;
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: false,
matchCase: options.matchCase,
medialCapitalAsWordStart: options.medialCapitalAsWordStart
};
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: true, 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');
}
public showFind() {
this._webviewFindWidget?.reveal();
}
public hideFind() {
this._webviewFindWidget?.hide();
}
public runFindAction(previous: boolean) {
this._webviewFindWidget?.find(previous);
}
public selectAll() {
this.element?.selectAll();
}
public copy() {
this.element?.copy();
}
public paste() {
this.element?.paste();
}
public cut() {
this.element?.cut();
}
public undo() {
this.element?.undo();
}
public redo() {
this.element?.redo();
}
protected 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

@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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 { ElectronIframeWebview } from 'vs/workbench/contrib/webview/electron-sandbox/iframeWebviewElement';
import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
export class ElectronWebviewService extends WebviewService {
declare readonly _serviceBrand: undefined;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private readonly _configService: IConfigurationService,
) {
super(instantiationService);
}
createWebviewElement(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewElement {
const useIframes = this._configService.getValue<string>('webview.experimental.useIframes');
const webview = this._instantiationService.createInstance(useIframes ? ElectronIframeWebview : ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
this.addWebviewListeners(webview);
return webview;
}
createWebviewOverlay(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
): WebviewOverlay {
const webview = this._instantiationService.createInstance(DynamicWebviewEditorOverlay, id, options, contentOptions, extension);
this.addWebviewListeners(webview);
return webview;
}
}

View File

@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ThrottledDelayer } from 'vs/base/common/async';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
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 { IRequestService } from 'vs/platform/request/common/request';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
import { rewriteVsCodeResourceUrls, WebviewResourceRequestManager } from 'vs/workbench/contrib/webview/electron-sandbox/resourceLoading';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
/**
* Webview backed by an iframe but that uses Electron APIs to power the webview.
*/
export class ElectronIframeWebview extends IFrameWebview {
private readonly _resourceRequestManager: WebviewResourceRequestManager;
private _messagePromise = Promise.resolve();
private readonly _focusDelayer = this._register(new ThrottledDelayer(10));
private _elementFocusImpl!: (options?: FocusOptions | undefined) => void;
constructor(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
extension: WebviewExtensionDescription | undefined,
webviewThemeDataProvider: WebviewThemeDataProvider,
@ITunnelService tunnelService: ITunnelService,
@IFileService fileService: IFileService,
@IRequestService requestService: IRequestService,
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@ILogService logService: ILogService,
@IInstantiationService instantiationService: IInstantiationService,
@INotificationService noficationService: INotificationService,
) {
super(id, options, contentOptions, extension, webviewThemeDataProvider,
noficationService, tunnelService, fileService, requestService, telemetryService, environmentService, _remoteAuthorityResolverService, logService);
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options));
}
protected createElement(options: WebviewOptions, contentOptions: WebviewContentOptions) {
const element = super.createElement(options, contentOptions);
this._elementFocusImpl = element.focus.bind(element);
element.focus = () => {
this.doFocus();
};
return element;
}
protected initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) {
// The extensionId and purpose in the URL are used for filtering in js-debug:
this.element!.setAttribute('src', `${Schemas.vscodeWebview}://${this.id}/index.html?id=${this.id}&platform=electron&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`);
}
public set contentOptions(options: WebviewContentOptions) {
this._resourceRequestManager.update(options);
super.contentOptions = options;
}
public set localResourcesRoot(resources: URI[]) {
this._resourceRequestManager.update({
...this.contentOptions,
localResourceRoots: resources,
});
super.localResourcesRoot = resources;
}
protected get extraContentOptions() {
return {};
}
protected async doPostMessage(channel: string, data?: any): Promise<void> {
this._messagePromise = this._messagePromise
.then(() => this._resourceRequestManager.ensureReady())
.then(() => {
this.element?.contentWindow!.postMessage({ channel, args: data }, '*');
});
}
protected preprocessHtml(value: string): string {
return rewriteVsCodeResourceUrls(this.id, value);
}
public focus(): void {
this.doFocus();
// Handle focus change programmatically (do not rely on event from <webview>)
this.handleFocusChange(true);
}
private doFocus() {
if (!this.element) {
return;
}
// Workaround for https://github.com/microsoft/vscode/issues/75209
// .focus is async for imframes so for a sequence of actions such as:
//
// 1. Open webview
// 1. Show quick pick from command palette
//
// We end up focusing the webview after showing the quick pick, which causes
// the quick pick to instantly dismiss.
//
// Workaround this by debouncing the focus and making sure we are not focused on an input
// when we try to re-focus.
this._focusDelayer.trigger(async () => {
if (!this.isFocused || !this.element) {
return;
}
if (document.activeElement?.tagName === 'INPUT') {
return;
}
try {
this._elementFocusImpl();
} catch {
// noop
}
this._send('focus');
});
}
}

View File

@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals } from 'vs/base/common/arrays';
import { streamToBuffer } from 'vs/base/common/buffer';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI, UriComponents } from 'vs/base/common/uri';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import * as modes from 'vs/editor/common/modes';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { IFileService } from 'vs/platform/files/common/files';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { ILogService } from 'vs/platform/log/common/log';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRequestService } from 'vs/platform/request/common/request';
import { loadLocalResource, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewContentOptions, WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
/**
* Try to rewrite `vscode-resource:` urls in html
*/
export function rewriteVsCodeResourceUrls(
id: string,
html: string,
): string {
return html
.replace(/(["'])vscode-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${Schemas.vscodeWebviewResource}://${id}/${scheme}${path}${endQuote}`;
}
if (!path.startsWith('//')) {
// Add an empty authority if we don't already have one
path = '//' + path;
}
return `${startQuote}${Schemas.vscodeWebviewResource}://${id}/file${path}${endQuote}`;
});
}
/**
* Manages the loading of resources inside of a webview.
*/
export class WebviewResourceRequestManager extends Disposable {
private readonly _webviewManagerService: IWebviewManagerService;
private _localResourceRoots: ReadonlyArray<URI>;
private _portMappings: ReadonlyArray<modes.IWebviewPortMapping>;
private _ready: Promise<void>;
constructor(
private readonly id: string,
private readonly extension: WebviewExtensionDescription | undefined,
initialContentOptions: WebviewContentOptions,
@ILogService private readonly _logService: ILogService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IMainProcessService mainProcessService: IMainProcessService,
@INativeHostService nativeHostService: INativeHostService,
@IFileService fileService: IFileService,
@IRequestService requestService: IRequestService,
) {
super();
this._logService.debug(`WebviewResourceRequestManager(${this.id}): init`);
this._webviewManagerService = createChannelSender<IWebviewManagerService>(mainProcessService.getChannel('webview'));
this._localResourceRoots = initialContentOptions.localResourceRoots || [];
this._portMappings = initialContentOptions.portMapping || [];
const remoteAuthority = environmentService.remoteAuthority;
const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`);
this._ready = this._webviewManagerService.registerWebview(this.id, nativeHostService.windowId, {
extensionLocation: this.extension?.location.toJSON(),
localResourceRoots: this._localResourceRoots.map(x => x.toJSON()),
remoteConnectionData: remoteConnectionData,
portMappings: this._portMappings,
}).then(() => {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did register`);
});
if (remoteAuthority) {
this._register(remoteAuthorityResolverService.onDidChangeConnectionData(() => {
const update = this._webviewManagerService.updateWebviewMetadata(this.id, {
remoteConnectionData: remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null,
});
this._ready = this._ready.then(() => update);
}));
}
this._register(toDisposable(() => this._webviewManagerService.unregisterWebview(this.id)));
const loadResourceChannel = `vscode:loadWebviewResource-${id}`;
const loadResourceListener = async (_event: any, requestId: number, resource: UriComponents) => {
try {
const response = await loadLocalResource(URI.revive(resource), {
extensionLocation: this.extension?.location,
roots: this._localResourceRoots,
remoteConnectionData: remoteConnectionData,
}, {
readFileStream: (resource) => fileService.readFileStream(resource).then(x => x.value),
}, requestService);
if (response.type === WebviewResourceResponse.Type.Success) {
const buffer = await streamToBuffer(response.stream);
return this._webviewManagerService.didLoadResource(requestId, buffer);
}
} catch {
// Noop
}
this._webviewManagerService.didLoadResource(requestId, undefined);
};
ipcRenderer.on(loadResourceChannel, loadResourceListener);
this._register(toDisposable(() => ipcRenderer.removeListener(loadResourceChannel, loadResourceListener)));
}
public update(options: WebviewContentOptions) {
const localResourceRoots = options.localResourceRoots || [];
const portMappings = options.portMapping || [];
if (!this.needsUpdate(localResourceRoots, portMappings)) {
return;
}
this._localResourceRoots = localResourceRoots;
this._portMappings = portMappings;
this._logService.debug(`WebviewResourceRequestManager(${this.id}): will update`);
const update = this._webviewManagerService.updateWebviewMetadata(this.id, {
localResourceRoots: localResourceRoots.map(x => x.toJSON()),
portMappings: portMappings,
}).then(() => {
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did update`);
});
this._ready = this._ready.then(() => update);
}
private needsUpdate(
localResourceRoots: readonly URI[],
portMappings: readonly modes.IWebviewPortMapping[],
): boolean {
return !(
equals(this._localResourceRoots, localResourceRoots, (a, b) => a.toString() === b.toString())
&& equals(this._portMappings, portMappings, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort)
);
}
public ensureReady(): Promise<void> {
return this._ready;
}
}