Add experimental support for using iframes for webviews (#100991)
* Use non-deprecated API version * Prototype: enable offline iframe based webviews on desktop Adds a new `vscode-webview` protocol for loading the wrapper contents of a webview Still needs cleaning up * fix loading of electron webview * Cleanup and add `webview.experimental.useIframes` setting Co-authored-by: deepak1556 <hop2deep@gmail.com>
This commit is contained in:
parent
16be2c9885
commit
48c6e3979d
|
@ -32,7 +32,7 @@ interface StandardTsServerRequests {
|
||||||
'completions': [Proto.CompletionsRequestArgs, Proto.CompletionsResponse];
|
'completions': [Proto.CompletionsRequestArgs, Proto.CompletionsResponse];
|
||||||
'configure': [Proto.ConfigureRequestArguments, Proto.ConfigureResponse];
|
'configure': [Proto.ConfigureRequestArguments, Proto.ConfigureResponse];
|
||||||
'definition': [Proto.FileLocationRequestArgs, Proto.DefinitionResponse];
|
'definition': [Proto.FileLocationRequestArgs, Proto.DefinitionResponse];
|
||||||
'definitionAndBoundSpan': [Proto.FileLocationRequestArgs, Proto.DefinitionInfoAndBoundSpanReponse];
|
'definitionAndBoundSpan': [Proto.FileLocationRequestArgs, Proto.DefinitionInfoAndBoundSpanResponse];
|
||||||
'docCommentTemplate': [Proto.FileLocationRequestArgs, Proto.DocCommandTemplateResponse];
|
'docCommentTemplate': [Proto.FileLocationRequestArgs, Proto.DocCommandTemplateResponse];
|
||||||
'documentHighlights': [Proto.DocumentHighlightsRequestArgs, Proto.DocumentHighlightsResponse];
|
'documentHighlights': [Proto.DocumentHighlightsRequestArgs, Proto.DocumentHighlightsResponse];
|
||||||
'format': [Proto.FormatRequestArgs, Proto.FormatResponse];
|
'format': [Proto.FormatRequestArgs, Proto.FormatResponse];
|
||||||
|
|
|
@ -86,8 +86,9 @@ setCurrentWorkingDirectory();
|
||||||
// Register custom schemes with privileges
|
// Register custom schemes with privileges
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
{
|
{
|
||||||
scheme: 'vscode-resource',
|
scheme: 'vscode-webview',
|
||||||
privileges: {
|
privileges: {
|
||||||
|
standard: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
supportFetchAPI: true,
|
supportFetchAPI: true,
|
||||||
corsEnabled: true,
|
corsEnabled: true,
|
||||||
|
|
|
@ -62,6 +62,14 @@ export namespace Schemas {
|
||||||
|
|
||||||
export const webviewPanel = 'webview-panel';
|
export const webviewPanel = 'webview-panel';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheme used for loading the wrapper html and script in webviews.
|
||||||
|
*/
|
||||||
|
export const vscodeWebview = 'vscode-webview';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scheme used for loading resources inside of webviews.
|
||||||
|
*/
|
||||||
export const vscodeWebviewResource = 'vscode-webview-resource';
|
export const vscodeWebviewResource = 'vscode-webview-resource';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' vscode-webview: https://*.vscode-webview-test.com; object-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
|
||||||
</head>
|
</head>
|
||||||
<body aria-label="">
|
<body aria-label="">
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -174,11 +174,12 @@ export class CodeApplication extends Disposable {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%20role%3D%22document%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') {
|
const uri = URI.parse(source);
|
||||||
return true;
|
if (uri.scheme === Schemas.vscodeWebview) {
|
||||||
|
return uri.path === '/index.html' || uri.path === '/electron-browser/index.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
const srcUri = URI.parse(source).fsPath.toLowerCase();
|
const srcUri = uri.fsPath.toLowerCase();
|
||||||
const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase();
|
const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase();
|
||||||
|
|
||||||
return srcUri.startsWith(rootUri + sep);
|
return srcUri.startsWith(rootUri + sep);
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const IWebviewManagerService = createDecorator<IWebviewManagerService>('w
|
||||||
export interface IWebviewManagerService {
|
export interface IWebviewManagerService {
|
||||||
_serviceBrand: unknown;
|
_serviceBrand: unknown;
|
||||||
|
|
||||||
registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void>;
|
registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void>;
|
||||||
unregisterWebview(id: string): Promise<void>;
|
unregisterWebview(id: string): Promise<void>;
|
||||||
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;
|
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
|
||||||
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
|
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void> {
|
public async registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void> {
|
||||||
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
|
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
|
||||||
|
|
||||||
this.protocolProvider.registerWebview(id, {
|
this.protocolProvider.registerWebview(id, {
|
||||||
|
|
|
@ -20,7 +20,7 @@ interface PortMappingData {
|
||||||
export class WebviewPortMappingProvider extends Disposable {
|
export class WebviewPortMappingProvider extends Disposable {
|
||||||
|
|
||||||
private readonly _webviewData = new Map<string, {
|
private readonly _webviewData = new Map<string, {
|
||||||
readonly webContentsId: number;
|
readonly webContentsId: number | undefined;
|
||||||
readonly manager: WebviewPortMappingManager;
|
readonly manager: WebviewPortMappingManager;
|
||||||
metadata: PortMappingData;
|
metadata: PortMappingData;
|
||||||
}>();
|
}>();
|
||||||
|
@ -56,24 +56,28 @@ export class WebviewPortMappingProvider extends Disposable {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async registerWebview(id: string, webContentsId: number, metadata: PortMappingData): Promise<void> {
|
public async registerWebview(id: string, webContentsId: number | undefined, metadata: PortMappingData): Promise<void> {
|
||||||
const manager = new WebviewPortMappingManager(
|
const manager = new WebviewPortMappingManager(
|
||||||
() => this._webviewData.get(id)?.metadata.extensionLocation,
|
() => this._webviewData.get(id)?.metadata.extensionLocation,
|
||||||
() => this._webviewData.get(id)?.metadata.mappings || [],
|
() => this._webviewData.get(id)?.metadata.mappings || [],
|
||||||
this._tunnelService);
|
this._tunnelService);
|
||||||
|
|
||||||
this._webviewData.set(id, { webContentsId, metadata, manager });
|
this._webviewData.set(id, { webContentsId, metadata, manager });
|
||||||
|
if (typeof webContentsId === 'number') {
|
||||||
this._webContentsIdsToWebviewIds.set(webContentsId, id);
|
this._webContentsIdsToWebviewIds.set(webContentsId, id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public unregisterWebview(id: string): void {
|
public unregisterWebview(id: string): void {
|
||||||
const existing = this._webviewData.get(id);
|
const existing = this._webviewData.get(id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.manager.dispose();
|
existing.manager.dispose();
|
||||||
this._webviewData.delete(id);
|
this._webviewData.delete(id);
|
||||||
|
if (typeof existing.webContentsId === 'number') {
|
||||||
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
|
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async updateWebviewMetadata(id: string, metadataDelta: Partial<PortMappingData>): Promise<void> {
|
public async updateWebviewMetadata(id: string, metadataDelta: Partial<PortMappingData>): Promise<void> {
|
||||||
const entry = this._webviewData.get(id);
|
const entry = this._webviewData.get(id);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { session } from 'electron';
|
import { session, protocol } from 'electron';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
@ -23,6 +23,13 @@ interface WebviewMetadata {
|
||||||
|
|
||||||
export class WebviewProtocolProvider extends Disposable {
|
export class WebviewProtocolProvider extends Disposable {
|
||||||
|
|
||||||
|
private static validWebviewFilePaths = new Map([
|
||||||
|
['/index.html', 'index.html'],
|
||||||
|
['/electron-browser/index.html', 'index.html'],
|
||||||
|
['/main.js', 'main.js'],
|
||||||
|
['/host.js', 'host.js'],
|
||||||
|
]);
|
||||||
|
|
||||||
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
|
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -33,62 +40,22 @@ export class WebviewProtocolProvider extends Disposable {
|
||||||
|
|
||||||
const sess = session.fromPartition(webviewPartitionId);
|
const sess = session.fromPartition(webviewPartitionId);
|
||||||
|
|
||||||
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise<void> => {
|
// Register the protocol loading webview html
|
||||||
try {
|
const webviewHandler = this.handleWebviewRequest.bind(this);
|
||||||
const uri = URI.parse(request.url);
|
protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
|
||||||
|
sess.protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
|
||||||
|
|
||||||
const id = uri.authority;
|
// Register the protocol loading webview resources both inside the webview and at the top level
|
||||||
const metadata = this.webviewMetadata.get(id);
|
const webviewResourceHandler = this.handleWebviewResourceRequest.bind(this);
|
||||||
if (metadata) {
|
protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
|
||||||
|
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
|
||||||
|
|
||||||
// Try to further rewrite remote uris so that they go to the resolved server on the main thread
|
this._register(toDisposable(() => {
|
||||||
let rewriteUri: undefined | ((uri: URI) => URI);
|
protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
|
||||||
if (metadata.remoteConnectionData) {
|
sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
|
||||||
rewriteUri = (uri) => {
|
protocol.unregisterProtocol(Schemas.vscodeWebview);
|
||||||
if (metadata.remoteConnectionData) {
|
sess.protocol.unregisterProtocol(Schemas.vscodeWebview);
|
||||||
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
|
}));
|
||||||
const scheme = metadata.remoteConnectionData.host === 'localhost' || metadata.remoteConnectionData.host === '127.0.0.1' ? 'http' : 'https';
|
|
||||||
return URI.parse(`${scheme}://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
|
|
||||||
path: '/vscode-remote-resource',
|
|
||||||
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uri;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await loadLocalResource(uri, {
|
|
||||||
extensionLocation: metadata.extensionLocation,
|
|
||||||
roots: metadata.localResourceRoots,
|
|
||||||
remoteConnectionData: metadata.remoteConnectionData,
|
|
||||||
rewriteUri,
|
|
||||||
}, this.fileService, this.requestService);
|
|
||||||
|
|
||||||
if (result.type === WebviewResourceResponse.Type.Success) {
|
|
||||||
return callback({
|
|
||||||
statusCode: 200,
|
|
||||||
data: this.streamToNodeReadable(result.stream),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': result.mimeType,
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
|
||||||
console.error('Webview: Cannot load resource outside of protocol root');
|
|
||||||
return callback({ data: null, statusCode: 401 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback({ data: null, statusCode: 404 });
|
|
||||||
});
|
|
||||||
|
|
||||||
this._register(toDisposable(() => sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private streamToNodeReadable(stream: VSBufferReadableStream): Readable {
|
private streamToNodeReadable(stream: VSBufferReadableStream): Readable {
|
||||||
|
@ -152,4 +119,81 @@ export class WebviewProtocolProvider extends Disposable {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleWebviewRequest(request: Electron.Request, callback: any) {
|
||||||
|
try {
|
||||||
|
const uri = URI.parse(request.url);
|
||||||
|
const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path);
|
||||||
|
if (typeof entry === 'string') {
|
||||||
|
let url: string;
|
||||||
|
if (uri.path.startsWith('/electron-browser')) {
|
||||||
|
url = require.toUrl(`vs/workbench/contrib/webview/electron-browser/pre/${entry}`);
|
||||||
|
} else {
|
||||||
|
url = require.toUrl(`vs/workbench/contrib/webview/browser/pre/${entry}`);
|
||||||
|
}
|
||||||
|
return callback(url.replace('file://', ''));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleWebviewResourceRequest(
|
||||||
|
request: Electron.Request,
|
||||||
|
callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const uri = URI.parse(request.url);
|
||||||
|
|
||||||
|
const id = uri.authority;
|
||||||
|
const metadata = this.webviewMetadata.get(id);
|
||||||
|
if (metadata) {
|
||||||
|
|
||||||
|
// Try to further rewrite remote uris so that they go to the resolved server on the main thread
|
||||||
|
let rewriteUri: undefined | ((uri: URI) => URI);
|
||||||
|
if (metadata.remoteConnectionData) {
|
||||||
|
rewriteUri = (uri) => {
|
||||||
|
if (metadata.remoteConnectionData) {
|
||||||
|
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
|
||||||
|
const scheme = metadata.remoteConnectionData.host === 'localhost' || metadata.remoteConnectionData.host === '127.0.0.1' ? 'http' : 'https';
|
||||||
|
return URI.parse(`${scheme}://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
|
||||||
|
path: '/vscode-remote-resource',
|
||||||
|
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await loadLocalResource(uri, {
|
||||||
|
extensionLocation: metadata.extensionLocation,
|
||||||
|
roots: metadata.localResourceRoots,
|
||||||
|
remoteConnectionData: metadata.remoteConnectionData,
|
||||||
|
rewriteUri,
|
||||||
|
}, this.fileService, this.requestService);
|
||||||
|
|
||||||
|
if (result.type === WebviewResourceResponse.Type.Success) {
|
||||||
|
return callback({
|
||||||
|
statusCode: 200,
|
||||||
|
data: this.streamToNodeReadable(result.stream),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': result.mimeType,
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
||||||
|
console.error('Webview: Cannot load resource outside of protocol root');
|
||||||
|
return callback({ data: null, statusCode: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback({ data: null, statusCode: 404 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
(function () {
|
(function () {
|
||||||
const id = document.location.search.match(/\bid=([\w-]+)/)[1];
|
const id = document.location.search.match(/\bid=([\w-]+)/)[1];
|
||||||
|
const onElectron = /platform=electron/.test(document.location.search);
|
||||||
|
|
||||||
const hostMessaging = new class HostMessaging {
|
const hostMessaging = new class HostMessaging {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -36,6 +37,10 @@
|
||||||
}();
|
}();
|
||||||
|
|
||||||
const workerReady = new Promise(async (resolveWorkerReady) => {
|
const workerReady = new Promise(async (resolveWorkerReady) => {
|
||||||
|
if (onElectron) {
|
||||||
|
return resolveWorkerReady();
|
||||||
|
}
|
||||||
|
|
||||||
if (!areServiceWorkersEnabled()) {
|
if (!areServiceWorkersEnabled()) {
|
||||||
console.log('Service Workers are not enabled. Webviews will not work properly');
|
console.log('Service Workers are not enabled. Webviews will not work properly');
|
||||||
return resolveWorkerReady();
|
return resolveWorkerReady();
|
||||||
|
@ -95,8 +100,12 @@
|
||||||
postMessage: hostMessaging.postMessage.bind(hostMessaging),
|
postMessage: hostMessaging.postMessage.bind(hostMessaging),
|
||||||
onMessage: hostMessaging.onMessage.bind(hostMessaging),
|
onMessage: hostMessaging.onMessage.bind(hostMessaging),
|
||||||
ready: workerReady,
|
ready: workerReady,
|
||||||
fakeLoad: true,
|
fakeLoad: !onElectron,
|
||||||
rewriteCSP: (csp, endpoint) => {
|
rewriteCSP: onElectron
|
||||||
|
? (csp) => {
|
||||||
|
return csp.replace(/vscode-resource:(?=(\s|;|$))/g, 'vscode-webview-resource:');
|
||||||
|
}
|
||||||
|
: (csp, endpoint) => {
|
||||||
const endpointUrl = new URL(endpoint);
|
const endpointUrl = new URL(endpoint);
|
||||||
return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin);
|
return csp.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, endpointUrl.origin);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,7 @@ import { addDisposableListener } from 'vs/base/browser/dom';
|
||||||
import { streamToBuffer } from 'vs/base/common/buffer';
|
import { streamToBuffer } from 'vs/base/common/buffer';
|
||||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { Schemas } from 'vs/base/common/network';
|
import { Schemas } from 'vs/base/common/network';
|
||||||
import { isWeb } from 'vs/base/common/platform';
|
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
|
||||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||||
import { IFileService } from 'vs/platform/files/common/files';
|
import { IFileService } from 'vs/platform/files/common/files';
|
||||||
import { ILogService } from 'vs/platform/log/common/log';
|
import { ILogService } from 'vs/platform/log/common/log';
|
||||||
|
@ -37,7 +35,6 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
||||||
@ITunnelService tunnelService: ITunnelService,
|
@ITunnelService tunnelService: ITunnelService,
|
||||||
@IFileService private readonly fileService: IFileService,
|
@IFileService private readonly fileService: IFileService,
|
||||||
@IRequestService private readonly requestService: IRequestService,
|
@IRequestService private readonly requestService: IRequestService,
|
||||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
|
||||||
@ITelemetryService telemetryService: ITelemetryService,
|
@ITelemetryService telemetryService: ITelemetryService,
|
||||||
@IEnvironmentService environmentService: IEnvironmentService,
|
@IEnvironmentService environmentService: IEnvironmentService,
|
||||||
@IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService,
|
@IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService,
|
||||||
|
@ -46,10 +43,6 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
||||||
) {
|
) {
|
||||||
super(id, options, contentOptions, extension, webviewThemeDataProvider, logService, telemetryService, environmentService, _workbenchEnvironmentService);
|
super(id, options, contentOptions, extension, webviewThemeDataProvider, logService, telemetryService, environmentService, _workbenchEnvironmentService);
|
||||||
|
|
||||||
if (!this.useExternalEndpoint && (!_workbenchEnvironmentService.options || typeof _workbenchEnvironmentService.webviewExternalEndpoint !== 'string')) {
|
|
||||||
throw new Error('To use iframe based webviews, you must configure `environmentService.webviewExternalEndpoint`');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._portMappingManager = this._register(new WebviewPortMappingManager(
|
this._portMappingManager = this._register(new WebviewPortMappingManager(
|
||||||
() => this.extension?.location,
|
() => this.extension?.location,
|
||||||
() => this.content.options.portMapping || [],
|
() => this.content.options.portMapping || [],
|
||||||
|
@ -67,8 +60,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
||||||
this.localLocalhost(entry.origin);
|
this.localLocalhost(entry.origin);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// The extensionId and purpose in the URL are used for filtering in js-debug:
|
this.initElement(extension, options);
|
||||||
this.element!.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}&extensionId=${extension?.id.value ?? ''}&purpose=${options.purpose}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createElement(options: WebviewOptions, _contentOptions: WebviewContentOptions) {
|
protected createElement(options: WebviewOptions, _contentOptions: WebviewContentOptions) {
|
||||||
|
@ -83,6 +75,11 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
||||||
return element;
|
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 {
|
private get externalEndpoint(): string {
|
||||||
const endpoint = this.workbenchEnvironmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
|
const endpoint = this.workbenchEnvironmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
|
||||||
if (endpoint[endpoint.length - 1] === '/') {
|
if (endpoint[endpoint.length - 1] === '/') {
|
||||||
|
@ -91,10 +88,6 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get useExternalEndpoint(): boolean {
|
|
||||||
return isWeb || this._configurationService.getValue<boolean>('webview.experimental.useExternalEndpoint');
|
|
||||||
}
|
|
||||||
|
|
||||||
public mountTo(parent: HTMLElement) {
|
public mountTo(parent: HTMLElement) {
|
||||||
if (this.element) {
|
if (this.element) {
|
||||||
parent.appendChild(this.element);
|
parent.appendChild(this.element);
|
||||||
|
@ -105,7 +98,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
||||||
super.html = this.preprocessHtml(value);
|
super.html = this.preprocessHtml(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private preprocessHtml(value: string): string {
|
protected preprocessHtml(value: string): string {
|
||||||
return value
|
return value
|
||||||
.replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
|
.replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
|
||||||
if (scheme) {
|
if (scheme) {
|
||||||
|
@ -121,7 +114,7 @@ export class IFrameWebview extends BaseWebview<HTMLIFrameElement> implements Web
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get extraContentOptions() {
|
protected get extraContentOptions(): any {
|
||||||
return {
|
return {
|
||||||
endpoint: this.externalEndpoint,
|
endpoint: this.externalEndpoint,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { Schemas } from 'vs/base/common/network';
|
||||||
|
import { URI } from 'vs/base/common/uri';
|
||||||
|
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||||
|
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 { 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-browser/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();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: string,
|
||||||
|
options: WebviewOptions,
|
||||||
|
contentOptions: WebviewContentOptions,
|
||||||
|
extension: WebviewExtensionDescription | undefined,
|
||||||
|
webviewThemeDataProvider: WebviewThemeDataProvider,
|
||||||
|
@ITunnelService tunnelService: ITunnelService,
|
||||||
|
@IFileService fileService: IFileService,
|
||||||
|
@IRequestService requestService: IRequestService,
|
||||||
|
@ITelemetryService telemetryService: ITelemetryService,
|
||||||
|
@IEnvironmentService environmentService: IEnvironmentService,
|
||||||
|
@IWorkbenchEnvironmentService _workbenchEnvironmentService: IWorkbenchEnvironmentService,
|
||||||
|
@IRemoteAuthorityResolverService _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||||
|
@ILogService logService: ILogService,
|
||||||
|
@IInstantiationService instantiationService: IInstantiationService,
|
||||||
|
) {
|
||||||
|
super(id, options, contentOptions, extension, webviewThemeDataProvider,
|
||||||
|
tunnelService, fileService, requestService, telemetryService, environmentService, _workbenchEnvironmentService, _remoteAuthorityResolverService, logService);
|
||||||
|
|
||||||
|
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, Promise.resolve(undefined)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,134 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
import { URI } from 'vs/base/common/uri';
|
||||||
|
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||||
|
import * as modes from 'vs/editor/common/modes';
|
||||||
|
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 { 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';
|
||||||
|
import { Schemas } from 'vs/base/common/network';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
getWebContentsId: Promise<number | undefined>,
|
||||||
|
@ILogService private readonly _logService: ILogService,
|
||||||
|
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||||
|
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||||
|
@IMainProcessService mainProcessService: IMainProcessService,
|
||||||
|
) {
|
||||||
|
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.configuration.remoteAuthority;
|
||||||
|
const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
|
||||||
|
|
||||||
|
this._ready = getWebContentsId.then(async (webContentsId) => {
|
||||||
|
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`);
|
||||||
|
|
||||||
|
await this._webviewManagerService.registerWebview(this.id, webContentsId, {
|
||||||
|
extensionLocation: this.extension?.location.toJSON(),
|
||||||
|
localResourceRoots: this._localResourceRoots.map(x => x.toJSON()),
|
||||||
|
remoteConnectionData: remoteConnectionData,
|
||||||
|
portMappings: this._portMappings,
|
||||||
|
});
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,22 +5,19 @@
|
||||||
|
|
||||||
import { FindInPageOptions, WebviewTag } from 'electron';
|
import { FindInPageOptions, WebviewTag } from 'electron';
|
||||||
import { addDisposableListener } from 'vs/base/browser/dom';
|
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||||
import { equals } from 'vs/base/common/arrays';
|
|
||||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||||
import { Emitter, Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { once } from 'vs/base/common/functional';
|
import { once } from 'vs/base/common/functional';
|
||||||
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { Schemas } from 'vs/base/common/network';
|
import { Schemas } from 'vs/base/common/network';
|
||||||
import { isMacintosh } from 'vs/base/common/platform';
|
import { isMacintosh } from 'vs/base/common/platform';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
|
||||||
import * as modes from 'vs/editor/common/modes';
|
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||||
import { ILogService } from 'vs/platform/log/common/log';
|
import { ILogService } from 'vs/platform/log/common/log';
|
||||||
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
|
||||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||||
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
|
import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader';
|
||||||
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
|
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
|
||||||
|
@ -29,97 +26,7 @@ import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/t
|
||||||
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
|
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
|
||||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||||
import { WebviewFindDelegate, WebviewFindWidget } from '../browser/webviewFindWidget';
|
import { WebviewFindDelegate, WebviewFindWidget } from '../browser/webviewFindWidget';
|
||||||
|
import { WebviewResourceRequestManager, rewriteVsCodeResourceUrls } from './resourceLoading';
|
||||||
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,
|
|
||||||
webview: WebviewTag,
|
|
||||||
initialContentOptions: WebviewContentOptions,
|
|
||||||
@ILogService private readonly _logService: ILogService,
|
|
||||||
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
|
||||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
|
||||||
@IMainProcessService mainProcessService: IMainProcessService,
|
|
||||||
) {
|
|
||||||
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.configuration.remoteAuthority;
|
|
||||||
const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
|
|
||||||
|
|
||||||
this._ready = new Promise(resolve => {
|
|
||||||
this._register(addDisposableListener(webview!, 'did-start-loading', once(() => {
|
|
||||||
this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`);
|
|
||||||
|
|
||||||
const webContentsId = webview.getWebContentsId();
|
|
||||||
|
|
||||||
this._webviewManagerService.registerWebview(this.id, webContentsId, {
|
|
||||||
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`);
|
|
||||||
}).finally(() => resolve());
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
|
|
||||||
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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(options: WebviewContentOptions) {
|
|
||||||
const localResourceRoots = options.localResourceRoots || [];
|
|
||||||
const portMappings = options.portMapping || [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
equals(this._localResourceRoots, localResourceRoots, (a, b) => a.toString() === b.toString())
|
|
||||||
&& equals(this._portMappings, portMappings, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort)
|
|
||||||
) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
async synchronize(): Promise<void> {
|
|
||||||
return this._ready;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebviewKeyboardHandler {
|
class WebviewKeyboardHandler {
|
||||||
|
|
||||||
|
@ -222,7 +129,17 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
||||||
|
|
||||||
this._myLogService.debug(`Webview(${this.id}): init`);
|
this._myLogService.debug(`Webview(${this.id}): init`);
|
||||||
|
|
||||||
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.element!, this.content.options));
|
const webviewId = new Promise<number | undefined>((resolve, reject) => {
|
||||||
|
const sub = this._register(addDisposableListener(this.element!, 'did-start-loading', once(() => {
|
||||||
|
if (!this.element) {
|
||||||
|
reject();
|
||||||
|
throw new Error('No element');
|
||||||
|
}
|
||||||
|
resolve(this.element.getWebContentsId());
|
||||||
|
sub.dispose();
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, webviewId));
|
||||||
|
|
||||||
this._register(addDisposableListener(this.element!, 'did-start-loading', once(() => {
|
this._register(addDisposableListener(this.element!, 'did-start-loading', once(() => {
|
||||||
this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!));
|
this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!));
|
||||||
|
@ -289,7 +206,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
||||||
}
|
}
|
||||||
|
|
||||||
this.element!.preload = require.toUrl('./pre/electron-index.js');
|
this.element!.preload = require.toUrl('./pre/electron-index.js');
|
||||||
this.element!.src = 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%20role%3D%22document%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E';
|
this.element!.src = `${Schemas.vscodeWebview}://${this.id}/electron-browser/index.html`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createElement(options: WebviewOptions) {
|
protected createElement(options: WebviewOptions) {
|
||||||
|
@ -301,6 +218,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
||||||
element.focus = () => {
|
element.focus = () => {
|
||||||
this.doFocus();
|
this.doFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
element.setAttribute('partition', webviewPartitionId);
|
element.setAttribute('partition', webviewPartitionId);
|
||||||
element.setAttribute('webpreferences', 'contextIsolation=yes');
|
element.setAttribute('webpreferences', 'contextIsolation=yes');
|
||||||
element.className = `webview ${options.customClasses || ''}`;
|
element.className = `webview ${options.customClasses || ''}`;
|
||||||
|
@ -332,24 +250,9 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
||||||
public set html(value: string) {
|
public set html(value: string) {
|
||||||
this._myLogService.debug(`Webview(${this.id}): will set html`);
|
this._myLogService.debug(`Webview(${this.id}): will set html`);
|
||||||
|
|
||||||
super.html = this.preprocessHtml(value);
|
super.html = rewriteVsCodeResourceUrls(this.id, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private preprocessHtml(value: string): string {
|
|
||||||
return value
|
|
||||||
.replace(/(["'])vscode-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
|
|
||||||
if (scheme) {
|
|
||||||
return `${startQuote}${Schemas.vscodeWebviewResource}://${this.id}/${scheme}${path}${endQuote}`;
|
|
||||||
}
|
|
||||||
if (!path.startsWith('//')) {
|
|
||||||
// Add an empty authority if we don't already have one
|
|
||||||
path = '//' + path;
|
|
||||||
}
|
|
||||||
return `${startQuote}${Schemas.vscodeWebviewResource}://${this.id}/file${path}${endQuote}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public mountTo(parent: HTMLElement) {
|
public mountTo(parent: HTMLElement) {
|
||||||
if (!this.element) {
|
if (!this.element) {
|
||||||
return;
|
return;
|
||||||
|
@ -365,7 +268,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview<WebviewTag> impleme
|
||||||
this._myLogService.debug(`Webview(${this.id}): will post message on '${channel}'`);
|
this._myLogService.debug(`Webview(${this.id}): will post message on '${channel}'`);
|
||||||
|
|
||||||
this._messagePromise = this._messagePromise
|
this._messagePromise = this._messagePromise
|
||||||
.then(() => this._resourceRequestManager.synchronize())
|
.then(() => this._resourceRequestManager.ensureReady())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`);
|
this._myLogService.debug(`Webview(${this.id}): did post message on '${channel}'`);
|
||||||
return this.element?.send(channel, data);
|
return this.element?.send(channel, data);
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { DynamicWebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay';
|
import { DynamicWebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay';
|
||||||
import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
|
|
||||||
import { IFrameWebview } from 'vs/workbench/contrib/webview/browser/webviewElement';
|
|
||||||
import { WebviewIconManager } from 'vs/workbench/contrib/webview/browser/webviewIconManager';
|
|
||||||
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
|
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
|
||||||
|
import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
|
||||||
|
import { WebviewIconManager } from 'vs/workbench/contrib/webview/browser/webviewIconManager';
|
||||||
|
import { ElectronIframeWebview } from 'vs/workbench/contrib/webview/electron-browser/iframeWebviewElement';
|
||||||
import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
|
import { ElectronWebviewBasedWebview } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
|
||||||
|
|
||||||
export class ElectronWebviewService implements IWebviewService {
|
export class ElectronWebviewService implements IWebviewService {
|
||||||
|
@ -32,12 +32,8 @@ export class ElectronWebviewService implements IWebviewService {
|
||||||
contentOptions: WebviewContentOptions,
|
contentOptions: WebviewContentOptions,
|
||||||
extension: WebviewExtensionDescription | undefined,
|
extension: WebviewExtensionDescription | undefined,
|
||||||
): WebviewElement {
|
): WebviewElement {
|
||||||
const useExternalEndpoint = this._configService.getValue<string>('webview.experimental.useExternalEndpoint');
|
const useIframes = this._configService.getValue<string>('webview.experimental.useIframes');
|
||||||
if (useExternalEndpoint) {
|
return this._instantiationService.createInstance(useIframes ? ElectronIframeWebview : ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
|
||||||
return this._instantiationService.createInstance(IFrameWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
|
|
||||||
} else {
|
|
||||||
return this._instantiationService.createInstance(ElectronWebviewBasedWebview, id, options, contentOptions, extension, this._webviewThemeDataProvider);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createWebviewOverlay(
|
createWebviewOverlay(
|
||||||
|
|
Loading…
Reference in a new issue