diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index f87f57f07dc..9519bdb5158 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -29,7 +29,6 @@ import { URI } from 'vs/base/common/uri'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { ConfigurationCache } from 'vs/workbench/services/configuration/browser/configurationCache'; -import { WebResources } from 'vs/workbench/browser/web.resources'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/browser/signService'; import { hash } from 'vs/base/common/hash'; @@ -68,9 +67,6 @@ class CodeRendererMain extends Disposable { // Layout this._register(addDisposableListener(window, EventType.RESIZE, () => this.workbench.layout())); - // Resource Loading - this._register(new WebResources(services.serviceCollection.get(IFileService))); - // Workbench Lifecycle this._register(this.workbench.onShutdown(() => this.dispose())); @@ -199,4 +195,4 @@ export function main(domElement: HTMLElement, options: IWorkbenchConstructionOpt const renderer = new CodeRendererMain(domElement, options); return renderer.open(); -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts new file mode 100644 index 00000000000..c2163194783 --- /dev/null +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorker.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { generateUuid } from 'vs/base/common/uuid'; +import { getMediaMime } from 'vs/base/common/mime'; + +const cacheName = 'vscode-resources'; + +declare const clients: { get(s: string): Promise }; + + +const _pending = new Map(); + +export function handleMessageEvent(event: MessageEvent): void { + const fn = _pending.get(event.data.token); + if (fn) { + fn(event.data.data); + _pending.delete(event.data.token); + } +} + +export async function handleFetchEvent(event: any): Promise { + + const url = URI.parse(event.request.url); + + if (url.path !== '/vscode-resources/fetch') { + return undefined; + } + + if (!event.clientId) { + return undefined; + } + + const cachedValue = await caches.open(cacheName).then(cache => cache.match(event.request)); + if (cachedValue) { + return cachedValue; + } + + // console.log('fetch', url.query); + try { + const token = generateUuid(); + return new Promise(async resolve => { + + const handle = setTimeout(() => { + resolve(new Response(undefined, { status: 500, statusText: 'timeout' })); + _pending.delete(token); + }, 5000); + + _pending.set(token, (data: ArrayBuffer) => { + clearTimeout(handle); + const res = new Response(data, { + status: 200, + headers: { 'Content-Type': getMediaMime(URI.parse(url.query).path) || 'text/plain' } + }); + caches.open(cacheName).then(cache => { + cache.put(event.request, res.clone()); + resolve(res); + }); + }); + + const client = await clients.get(event.clientId); + client.postMessage({ uri: url.query, token }); + }); + + + } catch (err) { + console.error(err); + return new Response(err, { status: 500 }); + } +} + diff --git a/src/vs/workbench/browser/web.resources.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts similarity index 51% rename from src/vs/workbench/browser/web.resources.ts rename to src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts index ca6c82ef787..592b17e9a8a 100644 --- a/src/vs/workbench/browser/web.resources.ts +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerClient.ts @@ -4,19 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService } from 'vs/platform/files/common/files'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { getMediaMime } from 'vs/base/common/mime'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -export class WebResources { +// todo@joh explore alternative, explicit approach +class ResourcesMutationObserver { - private readonly _regexp = /url\(('|")?(vscode-remote:\/\/.*?)\1\)/g; private readonly _urlCache = new Map(); - private readonly _requestCache = new Map>(); private readonly _observer: MutationObserver; - constructor(@IFileService private readonly _fileService: IFileService) { - // todo@joh add observer to more than head-element - // todo@joh explore alternative approach + private readonly _regexp = /url\(('|")?(vscode-remote:\/\/(.*?))\1\)/ig; + + constructor() { this._observer = new MutationObserver(r => this._handleMutation(r)); this._observer.observe(document, { subtree: true, @@ -24,6 +26,12 @@ export class WebResources { attributes: true, attributeFilter: ['style'] }); + this.scan(); + } + + scan(): void { + document.querySelectorAll('style').forEach(value => this._handleStyleNode(value)); + // todo@joh more! } dispose(): void { @@ -78,39 +86,60 @@ export class WebResources { } private async _rewriteUrls(textContent: string): Promise { - - const positions: number[] = []; - const promises: Promise[] = []; - - let match: RegExpMatchArray | null = null; - while (match = this._regexp.exec(textContent)) { - - const remoteUrl = match[2]; - positions.push(match.index! + 'url('.length + (typeof match[1] === 'string' ? match[1].length : 0)); - positions.push(remoteUrl.length); - - if (!this._urlCache.has(remoteUrl)) { - let request = this._requestCache.get(remoteUrl); - if (!request) { - const uri = URI.parse(remoteUrl, true); - request = this._fileService.readFile(uri).then(file => { - const blobUrl = URL.createObjectURL(new Blob([file.value.buffer], { type: getMediaMime(uri.path) })); - this._urlCache.set(remoteUrl, blobUrl); - }); - this._requestCache.set(remoteUrl, request); - } - promises.push(request); - } - } - - let content = textContent; - await Promise.all(promises); - for (let i = positions.length - 1; i >= 0; i -= 2) { - const start = positions[i - 1]; - const len = positions[i]; - const url = this._urlCache.get(content.substr(start, len)); - content = content.substring(0, start) + url + content.substring(start + len); - } - return content; + return textContent.replace(this._regexp, function (_m, quote, url) { + return `url(${quote}${location.href}vscode-resources/fetch?${encodeURIComponent(url)}${quote})`; + }); } } + +class ResourceServiceWorker { + + private readonly _disposables = new DisposableStore(); + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { + this._initServiceWorker(); + this._initFetchHandler(); + } + + dispose(): void { + this._disposables.dispose(); + } + + private _initServiceWorker(): void { + const url = './resourceServiceWorkerMain.js'; + navigator.serviceWorker.register(url).then(() => { + // console.log('registered'); + return navigator.serviceWorker.ready; + }).then(() => { + // console.log('ready'); + this._disposables.add(new ResourcesMutationObserver()); + }).catch(err => { + console.error(err); + }); + } + + private _initFetchHandler(): void { + + const fetchListener: (this: ServiceWorkerContainer, ev: MessageEvent) => void = event => { + const uri = URI.parse(event.data.uri); + this._fileService.readFile(uri).then(file => { + // todo@joh typings + (event.source).postMessage({ + token: event.data.token, + data: file.value.buffer.buffer + }, [file.value.buffer.buffer]); + }); + }; + navigator.serviceWorker.addEventListener('message', fetchListener); + this._disposables.add(toDisposable(() => navigator.serviceWorker.removeEventListener('message', fetchListener))); + } +} + +Registry.as(Extensions.Workbench).registerWorkbenchContribution( + ResourceServiceWorker, + LifecyclePhase.Starting +); + + diff --git a/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts new file mode 100644 index 00000000000..d2c9220fad5 --- /dev/null +++ b/src/vs/workbench/contrib/resources/browser/resourceServiceWorkerMain.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +(function () { + type Handler = { + handleFetchEvent(event: Event): Promise; + handleMessageEvent(event: MessageEvent): void; + }; + + const handlerPromise = new Promise((resolve, reject) => { + // load loader + const baseUrl = './out/'; + importScripts(baseUrl + 'vs/loader.js'); + require.config({ + baseUrl, + catchError: true + }); + require(['vs/workbench/contrib/resources/browser/resourceServiceWorker'], resolve, reject); + }); + + self.addEventListener('message', event => { + handlerPromise.then(handler => { + handler.handleMessageEvent(event); + }); + }); + + self.addEventListener('fetch', (event: any) => { + event.respondWith(handlerPromise.then(handler => { + return handler.handleFetchEvent(event).then(value => { + if (value instanceof Response) { + return value; + } else { + return fetch(event.request); + } + }); + })); + }); + self.addEventListener('install', event => { + //@ts-ignore + event.waitUntil(self.skipWaiting()); + }); + + self.addEventListener('activate', event => { + //@ts-ignore + event.waitUntil(self.clients.claim()); // Become available to all pages + }); +})(); diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index c28adc0ad98..bb96b82c27c 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -28,6 +28,11 @@ import 'vs/workbench/browser/parts/quickinput/quickInputActions'; //#endregion +//#region --- Remote Resource loading + +import 'vs/workbench/contrib/resources/browser/resourceServiceWorkerClient'; + +//#endregion //#region --- API Extension Points