mirror of
https://github.com/coder/code-server.git
synced 2026-05-28 16:09:35 +00:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
126
lib/vscode/src/vs/workbench/contrib/webview/browser/pre/host.js
Normal file
126
lib/vscode/src/vs/workbench/contrib/webview/browser/pre/host.js
Normal 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);
|
||||
}());
|
||||
@@ -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>
|
||||
673
lib/vscode/src/vs/workbench/contrib/webview/browser/pre/main.js
Normal file
673
lib/vscode/src/vs/workbench/contrib/webview/browser/pre/main.js
Normal 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;
|
||||
}
|
||||
}());
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
102
lib/vscode/src/vs/workbench/contrib/webview/browser/themeing.ts
Normal file
102
lib/vscode/src/vs/workbench/contrib/webview/browser/themeing.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
154
lib/vscode/src/vs/workbench/contrib/webview/browser/webview.ts
Normal file
154
lib/vscode/src/vs/workbench/contrib/webview/browser/webview.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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() { }
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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+?:/, '');
|
||||
}
|
||||
@@ -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);
|
||||
}());
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user