diff --git a/.gitpod.yml b/.gitpod.yml
index 26a74f0df7af5..e334dfcd62823 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -1,7 +1,10 @@
 image:
   file: .gitpod.dockerfile
 ports:
-- port: 3000
+- port: 3000 # Theia
+- port: 3030 # VS Code extension tests
+- port: 9339 # Node.js debug port
+  onOpen: ignore
 - port: 6080
   onOpen: ignore
 - port: 5900
@@ -10,7 +13,7 @@ tasks:
 - init: yarn
   command: >
     jwm &
-    yarn --cwd examples/browser start ../..
+    yarn --cwd examples/browser start ../.. --hostname=0.0.0.0
 github:
   prebuilds:
     pullRequestsFromForks: true
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 80a9d380a0b9b..825f002eca906 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -35,7 +35,8 @@
         "--no-app-auto-install"
       ],
       "env": {
-        "NODE_ENV": "development"
+        "NODE_ENV": "development",
+        "THEIA_WEBVIEW_EXTERNAL_ENDPOINT": "${env:THEIA_WEBVIEW_EXTERNAL_ENDPOINT}"
       },
       "sourceMaps": true,
       "outFiles": [
@@ -63,7 +64,8 @@
         "--hosted-plugin-inspect=9339"
       ],
       "env": {
-        "NODE_ENV": "development"
+        "NODE_ENV": "development",
+        "THEIA_WEBVIEW_EXTERNAL_ENDPOINT": "${env:THEIA_WEBVIEW_EXTERNAL_ENDPOINT}"
       },
       "sourceMaps": true,
       "outFiles": [
@@ -104,7 +106,8 @@
         "--no-app-auto-install"
       ],
       "env": {
-        "NODE_ENV": "development"
+        "NODE_ENV": "development",
+        "THEIA_WEBVIEW_EXTERNAL_ENDPOINT": "${env:THEIA_WEBVIEW_EXTERNAL_ENDPOINT}"
       },
       "sourceMaps": true,
       "outFiles": [
@@ -166,7 +169,8 @@
         "--hosted-plugin-inspect=9339"
       ],
       "env": {
-        "THEIA_DEFAULT_PLUGINS": "local-dir:${workspaceFolder}/plugins"
+        "THEIA_DEFAULT_PLUGINS": "local-dir:${workspaceFolder}/plugins",
+        "THEIA_WEBVIEW_EXTERNAL_ENDPOINT": "${env:THEIA_WEBVIEW_EXTERNAL_ENDPOINT}"
       },
       "stopOnEntry": false,
       "sourceMaps": true,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46769e1fa9dcb..e8e3ec689b3c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,29 @@ Breaking changes:
 - [core] renamed preference `list.openMode` to `workbench.list.openMode` [#6481](https://github.com/eclipse-theia/theia/pull/6481)
 - [task] changed `TaskSchemaUpdater.update()` from asynchronous to synchronous [#6483](https://github.com/eclipse-theia/theia/pull/6483)
 - [monaco] monaco prefix has been removed from commands [#5590](https://github.com/eclipse-theia/theia/pull/5590)
+- [plugin] webviews are reimplemented to align with [VS Code browser implementation](https://blog.mattbierner.com/vscode-webview-web-learnings/) [#6465](https://github.com/eclipse-theia/theia/pull/6465)
+  - Security: `vscode.previewHTML` is removed, see https://code.visualstudio.com/updates/v1_33#_removing-the-vscodepreviewhtml-command
+  - Security: Before all webviews were deployed on [the same origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)
+  allowing them to break out and manipulate shared data as cookies, local storage or even start service workers
+  for the main window as well as for each other. Now each webview will be deployed on own origin by default.
+    - Webview origin pattern can be configured with `THEIA_WEBVIEW_EXTERNAL_ENDPOINT` env variable. The default value is `{{uuid}}.webview.{{hostname}}`.
+  Here `{{uuid}}` and `{{hostname}}` are placeholders which get replaced at runtime with proper webview uuid 
+  and [hostname](https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/hostname) correspondingly.
+    - To switch to unsecure mode as before configure `THEIA_WEBVIEW_EXTERNAL_ENDPOINT` with `{{hostname}}` as a value.
+    You can also drop `{{uuid}}.` prefix, in this case, webviews still will be able to access each other but not the main window.
+  - Remote: Local URIs are resolved by default to the host serving Theia.
+  If you want to resolve to another host or change how remote URIs are constructed then
+  implement [ExternalUriService.resolve](./packages/core/src/browser/external-uri-service.ts) in a frontend module.
+  - Content loading: Webview HTTP endpoint is removed. Content loaded via [WebviewResourceLoader](./packages/plugin-ext/src/main/common/webview-protocol.ts) JSON-RPC service
+  with properly preserved resource URIs. Content is only loaded if it's allowed by WebviewOptions.localResourceRoots, otherwise, the service won't be called.
+  If you want to customize content loading then implement [WebviewResourceLoaderImpl](packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts) in a backend module.
+  - Theming: Theia styles are not applied to webviews anymore
+   instead [VS Code way of styling](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content) should be used.
+   VS Code color variables also available with `--theia` prefix.
+  - Testing: Webview can work only in secure context because they rely on service workers to load local content and redirect local to remote requests.
+  Most browsers define a page as served from secure context if its url has `https` scheme. For local testing `localhost` is treated as a secure context as well.
+  Unfortunately, it does not work nicely in FireFox, since it does not treat subdomains of localhost as secure as well, compare to Chrome.
+  If you want to test with FireFox you can configure it as described [here](https://github.com/eclipse-theia/theia/pull/6465#issuecomment-556443218).
 
 ## v0.12.0
 
diff --git a/packages/core/package.json b/packages/core/package.json
index 03ed5acae678e..37e9fb5acc091 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -6,7 +6,7 @@
   "typings": "lib/common/index.d.ts",
   "dependencies": {
     "@babel/runtime": "^7.5.5",
-    "@phosphor/widgets": "^1.5.0",
+    "@phosphor/widgets": "^1.9.3",
     "@primer/octicons-react": "^9.0.0",
     "@theia/application-package": "^0.12.0",
     "@types/body-parser": "^1.16.4",
diff --git a/packages/core/src/browser/color-application-contribution.ts b/packages/core/src/browser/color-application-contribution.ts
new file mode 100644
index 0000000000000..9f03df91e57a1
--- /dev/null
+++ b/packages/core/src/browser/color-application-contribution.ts
@@ -0,0 +1,75 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { injectable, inject, named } from 'inversify';
+import { ColorRegistry } from './color-registry';
+import { Emitter } from '../common/event';
+import { ThemeService } from './theming';
+import { FrontendApplicationContribution } from './frontend-application';
+import { ContributionProvider } from '../common/contribution-provider';
+import { Disposable, DisposableCollection } from '../common/disposable';
+
+export const ColorContribution = Symbol('ColorContribution');
+export interface ColorContribution {
+    registerColors(colors: ColorRegistry): void;
+}
+
+@injectable()
+export class ColorApplicationContribution implements FrontendApplicationContribution {
+
+    protected readonly onDidChangeEmitter = new Emitter<void>();
+    readonly onDidChange = this.onDidChangeEmitter.event;
+
+    @inject(ColorRegistry)
+    protected readonly colors: ColorRegistry;
+
+    @inject(ContributionProvider) @named(ColorContribution)
+    protected readonly colorContributions: ContributionProvider<ColorContribution>;
+
+    onStart(): void {
+        for (const contribution of this.colorContributions.getContributions()) {
+            contribution.registerColors(this.colors);
+        }
+
+        this.update();
+        ThemeService.get().onThemeChange(() => this.update());
+    }
+
+    protected readonly toUpdate = new DisposableCollection();
+    protected update(): void {
+        if (!document) {
+            return;
+        }
+        this.toUpdate.dispose();
+        const theme = 'theia-' + ThemeService.get().getCurrentTheme().type;
+        document.body.classList.add(theme);
+        this.toUpdate.push(Disposable.create(() => document.body.classList.remove(theme)));
+
+        const documentElement = document.documentElement;
+        if (documentElement) {
+            for (const id of this.colors.getColors()) {
+                const color = this.colors.getCurrentColor(id);
+                if (color) {
+                    const propertyName = `--theia-${id.replace('.', '-')}`;
+                    documentElement.style.setProperty(propertyName, color);
+                    this.toUpdate.push(Disposable.create(() => documentElement.style.removeProperty(propertyName)));
+                }
+            }
+        }
+        this.onDidChangeEmitter.fire(undefined);
+    }
+
+}
diff --git a/packages/core/src/browser/color-registry.ts b/packages/core/src/browser/color-registry.ts
new file mode 100644
index 0000000000000..03fcc2ec19530
--- /dev/null
+++ b/packages/core/src/browser/color-registry.ts
@@ -0,0 +1,47 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { injectable } from 'inversify';
+import { Disposable } from '../common/disposable';
+
+export interface ColorDefaults {
+    light?: string
+    dark?: string
+    hc?: string
+}
+
+export interface ColorOptions {
+    defaults?: ColorDefaults
+    description: string
+}
+
+/**
+ * It should be implemented by an extension, e.g. by the monaco extension.
+ */
+@injectable()
+export class ColorRegistry {
+
+    *getColors(): IterableIterator<string> { }
+
+    getCurrentColor(id: string): string | undefined {
+        return undefined;
+    }
+
+    register(id: string, options: ColorOptions): Disposable {
+        return Disposable.NULL;
+    }
+
+}
diff --git a/packages/core/src/browser/endpoint.ts b/packages/core/src/browser/endpoint.ts
index ac26658255ec2..8d2f95da51aef 100644
--- a/packages/core/src/browser/endpoint.ts
+++ b/packages/core/src/browser/endpoint.ts
@@ -53,6 +53,9 @@ export class Endpoint {
     }
 
     protected get host(): string {
+        if (this.options.host) {
+            return this.options.host;
+        }
         if (this.location.host) {
             return this.location.host;
         }
@@ -77,6 +80,9 @@ export class Endpoint {
     }
 
     protected get wsScheme(): string {
+        if (this.options.wsScheme) {
+            return this.options.wsScheme;
+        }
         return this.httpScheme === Endpoint.PROTO_HTTPS ? Endpoint.PROTO_WSS : Endpoint.PROTO_WS;
     }
 
diff --git a/packages/core/src/browser/external-uri-service.ts b/packages/core/src/browser/external-uri-service.ts
new file mode 100644
index 0000000000000..8ccf66fd69db6
--- /dev/null
+++ b/packages/core/src/browser/external-uri-service.ts
@@ -0,0 +1,64 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { injectable } from 'inversify';
+import URI from '../common/uri';
+import { MaybePromise } from '../common/types';
+import { Endpoint } from './endpoint';
+
+@injectable()
+export class ExternalUriService {
+
+    /**
+     * Maps local to remote URLs.
+     * Should be no-op if the given URL is not a localhost URL.
+     *
+     * By default maps to an origin serving Theia.
+     *
+     * Use `parseLocalhost` to retrive localhost address and port information.
+     */
+    resolve(uri: URI): MaybePromise<URI> {
+        const localhost = this.parseLocalhost(uri);
+        if (localhost) {
+            return this.toRemoteUrl(uri, localhost);
+        }
+        return uri;
+    }
+
+    protected toRemoteUrl(uri: URI, localhost: { address: string, port: number }): URI {
+        const host = this.toRemoteHost(localhost);
+        return new Endpoint({ host }).getRestUrl().withPath(uri.path).withFragment(uri.fragment).withQuery(uri.query);
+    }
+
+    protected toRemoteHost(localhost: { address: string, port: number }): string {
+        return `${window.location.hostname}:${localhost.port}`;
+    }
+
+    parseLocalhost(uri: URI): { address: string, port: number } | undefined {
+        if (uri.scheme !== 'http' && uri.scheme !== 'https') {
+            return undefined;
+        }
+        const localhostMatch = /^(localhost|127\.0\.0\.1|0\.0\.0\.0):(\d+)$/.exec(uri.authority);
+        if (!localhostMatch) {
+            return undefined;
+        }
+        return {
+            address: localhostMatch[1],
+            port: +localhostMatch[2],
+        };
+    }
+
+}
diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts
index 20c91dbc4747d..676130f3ffd9e 100644
--- a/packages/core/src/browser/frontend-application-module.ts
+++ b/packages/core/src/browser/frontend-application-module.ts
@@ -83,6 +83,9 @@ import { ProgressStatusBarItem } from './progress-status-bar-item';
 import { TabBarDecoratorService, TabBarDecorator } from './shell/tab-bar-decorator';
 import { ContextMenuContext } from './menu/context-menu-context';
 import { bindResourceProvider, bindMessageService, bindPreferenceService } from './frontend-application-bindings';
+import { ColorRegistry } from './color-registry';
+import { ColorContribution, ColorApplicationContribution } from './color-application-contribution';
+import { ExternalUriService } from './external-uri-service';
 
 export { bindResourceProvider, bindMessageService, bindPreferenceService };
 
@@ -91,6 +94,11 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
     themeService.register(...BuiltinThemeProvider.themes);
     themeService.startupTheme();
 
+    bind(ColorRegistry).toSelf().inSingletonScope();
+    bindContributionProvider(bind, ColorContribution);
+    bind(ColorApplicationContribution).toSelf().inSingletonScope();
+    bind(FrontendApplicationContribution).toService(ColorApplicationContribution);
+
     bind(FrontendApplication).toSelf().inSingletonScope();
     bind(FrontendApplicationStateService).toSelf().inSingletonScope();
     bind(DefaultFrontendApplicationContribution).toSelf();
@@ -131,6 +139,8 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
     bindContributionProvider(bind, OpenHandler);
     bind(DefaultOpenerService).toSelf().inSingletonScope();
     bind(OpenerService).toService(DefaultOpenerService);
+
+    bind(ExternalUriService).toSelf().inSingletonScope();
     bind(HttpOpenHandler).toSelf().inSingletonScope();
     bind(OpenHandler).toService(HttpOpenHandler);
 
diff --git a/packages/core/src/browser/http-open-handler.ts b/packages/core/src/browser/http-open-handler.ts
index bf23712663c6a..875c64b047aeb 100644
--- a/packages/core/src/browser/http-open-handler.ts
+++ b/packages/core/src/browser/http-open-handler.ts
@@ -18,6 +18,7 @@ import { injectable, inject } from 'inversify';
 import URI from '../common/uri';
 import { OpenHandler } from './opener-service';
 import { WindowService } from './window/window-service';
+import { ExternalUriService } from './external-uri-service';
 
 @injectable()
 export class HttpOpenHandler implements OpenHandler {
@@ -27,12 +28,16 @@ export class HttpOpenHandler implements OpenHandler {
     @inject(WindowService)
     protected readonly windowService: WindowService;
 
+    @inject(ExternalUriService)
+    protected readonly externalUriService: ExternalUriService;
+
     canHandle(uri: URI): number {
-        return uri.scheme.startsWith('http') ? 500 : 0;
+        return (uri.scheme.startsWith('http') || uri.scheme.startsWith('mailto')) ? 500 : 0;
     }
 
-    open(uri: URI): Window | undefined {
-        return this.windowService.openNewWindow(uri.toString(true), { external: true });
+    async open(uri: URI): Promise<Window | undefined> {
+        const resolvedUri = await this.externalUriService.resolve(uri);
+        return this.windowService.openNewWindow(resolvedUri.toString(true), { external: true });
     }
 
 }
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index a391ed3da3598..27f63b3034c98 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -1005,6 +1005,11 @@ export class ApplicationShell extends Widget {
     private readonly toDisposeOnActivationCheck = new DisposableCollection();
     private assertActivated(widget: Widget): void {
         this.toDisposeOnActivationCheck.dispose();
+
+        const onDispose = () => this.toDisposeOnActivationCheck.dispose();
+        widget.disposed.connect(onDispose);
+        this.toDisposeOnActivationCheck.push(Disposable.create(() => widget.disposed.disconnect(onDispose)));
+
         let start = 0;
         const step: FrameRequestCallback = timestamp => {
             if (document.activeElement && widget.node.contains(document.activeElement)) {
diff --git a/packages/core/src/browser/shell/shell-layout-restorer.ts b/packages/core/src/browser/shell/shell-layout-restorer.ts
index 1e348a27fdc2c..dbd7f64761ed3 100644
--- a/packages/core/src/browser/shell/shell-layout-restorer.ts
+++ b/packages/core/src/browser/shell/shell-layout-restorer.ts
@@ -333,6 +333,9 @@ export class ShellLayoutRestorer implements CommandContribution {
                     this.logger.warn(`Couldn't restore widget state for ${widget.id}. Error: ${e} `);
                 }
             }
+            if (widget.isDisposed) {
+                return undefined;
+            }
             return widget;
         } catch (e) {
             if (ApplicationShellLayoutMigrationError.is(e)) {
diff --git a/packages/core/src/browser/shell/theia-dock-panel.ts b/packages/core/src/browser/shell/theia-dock-panel.ts
index 37f696cbdf54f..efbef03ba8258 100644
--- a/packages/core/src/browser/shell/theia-dock-panel.ts
+++ b/packages/core/src/browser/shell/theia-dock-panel.ts
@@ -18,6 +18,7 @@ import { find, toArray, ArrayExt } from '@phosphor/algorithm';
 import { TabBar, Widget, DockPanel, Title, DockLayout } from '@phosphor/widgets';
 import { Signal } from '@phosphor/signaling';
 import { Disposable, DisposableCollection } from '../../common/disposable';
+import { MessageLoop } from '../widgets';
 
 const MAXIMIZED_CLASS = 'theia-maximized';
 
@@ -140,14 +141,28 @@ export class TheiaDockPanel extends DockPanel {
             this.toDisposeOnToggleMaximized.dispose();
             return;
         }
+        if (this.isAttached) {
+            MessageLoop.sendMessage(this, Widget.Msg.BeforeDetach);
+            this.node.remove();
+            MessageLoop.sendMessage(this, Widget.Msg.AfterDetach);
+        }
         maximizedElement.style.display = 'block';
         this.addClass(MAXIMIZED_CLASS);
+        MessageLoop.sendMessage(this, Widget.Msg.BeforeAttach);
         maximizedElement.appendChild(this.node);
+        MessageLoop.sendMessage(this, Widget.Msg.AfterAttach);
         this.fit();
         this.toDisposeOnToggleMaximized.push(Disposable.create(() => {
             maximizedElement.style.display = 'none';
             this.removeClass(MAXIMIZED_CLASS);
+            if (this.isAttached) {
+                MessageLoop.sendMessage(this, Widget.Msg.BeforeDetach);
+                this.node.remove();
+                MessageLoop.sendMessage(this, Widget.Msg.AfterDetach);
+            }
+            MessageLoop.sendMessage(this, Widget.Msg.BeforeAttach);
             areaContainer.appendChild(this.node);
+            MessageLoop.sendMessage(this, Widget.Msg.AfterAttach);
             this.fit();
         }));
 
diff --git a/packages/core/src/browser/theming.ts b/packages/core/src/browser/theming.ts
index a5c4c51473901..72949d198c16f 100644
--- a/packages/core/src/browser/theming.ts
+++ b/packages/core/src/browser/theming.ts
@@ -25,8 +25,11 @@ import { CommonMenus } from './common-frontend-contribution';
 
 export const ThemeServiceSymbol = Symbol('ThemeService');
 
+export type ThemeType = 'light' | 'dark' | 'hc';
+
 export interface Theme {
     readonly id: string;
+    readonly type: ThemeType;
     readonly label: string;
     readonly description?: string;
     readonly editorTheme?: string;
@@ -197,8 +200,9 @@ export class BuiltinThemeProvider {
     static readonly darkCss = require('../../src/browser/style/variables-dark.useable.css');
     static readonly lightCss = require('../../src/browser/style/variables-bright.useable.css');
 
-    static readonly darkTheme = {
+    static readonly darkTheme: Theme = {
         id: 'dark',
+        type: 'dark',
         label: 'Dark Theme',
         description: 'Bright fonts on dark backgrounds.',
         editorTheme: 'dark-plus', // loaded in /packages/monaco/src/browser/textmate/monaco-theme-registry.ts
@@ -210,8 +214,9 @@ export class BuiltinThemeProvider {
         }
     };
 
-    static readonly lightTheme = {
+    static readonly lightTheme: Theme = {
         id: 'light',
+        type: 'light',
         label: 'Light Theme',
         description: 'Dark fonts on light backgrounds.',
         editorTheme: 'light-plus', // loaded in /packages/monaco/src/browser/textmate/monaco-theme-registry.ts
diff --git a/packages/monaco/src/browser/monaco-color-registry.ts b/packages/monaco/src/browser/monaco-color-registry.ts
new file mode 100644
index 0000000000000..f776ae4d9a721
--- /dev/null
+++ b/packages/monaco/src/browser/monaco-color-registry.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { injectable } from 'inversify';
+import { ColorRegistry, ColorOptions } from '@theia/core/lib/browser/color-registry';
+import { Disposable } from '@theia/core/lib/common/disposable';
+
+@injectable()
+export class MonacoColorRegistry implements ColorRegistry {
+
+    protected readonly monacoThemeService = monaco.services.StaticServices.standaloneThemeService.get();
+    protected readonly monacoColorRegistry = monaco.color.getColorRegistry();
+
+    *getColors(): IterableIterator<string> {
+        for (const { id } of this.monacoColorRegistry.getColors()) {
+            yield id;
+        }
+    }
+
+    getCurrentColor(id: string): string | undefined {
+        const color = this.monacoThemeService.getTheme().getColor(id);
+        return color && color.toString();
+    }
+
+    register(id: string, options: ColorOptions): Disposable {
+        const identifier = this.monacoColorRegistry.registerColor(id, options.defaults, options.description);
+        return Disposable.create(() => this.monacoColorRegistry.deregisterColor(identifier));
+    }
+
+}
diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts
index 8d205dace3dee..91f1967f016a4 100644
--- a/packages/monaco/src/browser/monaco-frontend-module.ts
+++ b/packages/monaco/src/browser/monaco-frontend-module.ts
@@ -18,6 +18,7 @@ import '../../src/browser/style/index.css';
 import '../../src/browser/style/symbol-sprite.svg';
 import '../../src/browser/style/symbol-icons.css';
 
+import debounce = require('lodash.debounce');
 import { ContainerModule, decorate, injectable, interfaces } from 'inversify';
 import { MenuContribution, CommandContribution } from '@theia/core/lib/common';
 import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope';
@@ -58,9 +59,9 @@ import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
 import { MonacoContextKeyService } from './monaco-context-key-service';
 import { MonacoMimeService } from './monaco-mime-service';
 import { MimeService } from '@theia/core/lib/browser/mime-service';
-
-import debounce = require('lodash.debounce');
 import { MonacoEditorServices } from './monaco-editor';
+import { MonacoColorRegistry } from './monaco-color-registry';
+import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
 
 decorate(injectable(), MonacoToProtocolConverter);
 decorate(injectable(), ProtocolToMonacoConverter);
@@ -130,6 +131,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
 
     bind(MonacoMimeService).toSelf().inSingletonScope();
     rebind(MimeService).toService(MonacoMimeService);
+
+    bind(MonacoColorRegistry).toSelf().inSingletonScope();
+    rebind(ColorRegistry).toService(MonacoColorRegistry);
 });
 
 export const MonacoConfigurationService = Symbol('MonacoConfigurationService');
diff --git a/packages/monaco/src/browser/monaco-loader.ts b/packages/monaco/src/browser/monaco-loader.ts
index 5a3a7b9b8cd3d..b7455839b92c7 100644
--- a/packages/monaco/src/browser/monaco-loader.ts
+++ b/packages/monaco/src/browser/monaco-loader.ts
@@ -61,6 +61,7 @@ export function loadMonaco(vsRequire: any): Promise<void> {
                 'vs/base/parts/quickopen/browser/quickOpenModel',
                 'vs/base/common/filters',
                 'vs/platform/theme/common/styler',
+                'vs/platform/theme/common/colorRegistry',
                 'vs/base/common/platform',
                 'vs/editor/common/modes',
                 'vs/editor/contrib/suggest/suggest',
@@ -76,7 +77,8 @@ export function loadMonaco(vsRequire: any): Promise<void> {
             ], (css: any, html: any, commands: any, actions: any,
                 keybindingsRegistry: any, keybindingResolver: any, resolvedKeybinding: any, keybindingLabels: any,
                 keyCodes: any, mime: any, editorExtensions: any, simpleServices: any, standaloneServices: any, quickOpenWidget: any, quickOpenModel: any,
-                filters: any, styler: any, platform: any, modes: any, suggest: any, snippetParser: any,
+                filters: any, styler: any, colorRegistry: any,
+                platform: any, modes: any, suggest: any, snippetParser: any,
                 configuration: any, configurationModels: any,
                 codeEditorService: any, codeEditorServiceImpl: any,
                 markerService: any,
@@ -91,6 +93,7 @@ export function loadMonaco(vsRequire: any): Promise<void> {
                     global.monaco.quickOpen = Object.assign({}, quickOpenWidget, quickOpenModel);
                     global.monaco.filters = filters;
                     global.monaco.theme = styler;
+                    global.monaco.color = colorRegistry;
                     global.monaco.platform = platform;
                     global.monaco.editorExtensions = editorExtensions;
                     global.monaco.modes = modes;
diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts
index 9a73d4e767b87..c71fe10fa5f73 100644
--- a/packages/monaco/src/typings/monaco/index.d.ts
+++ b/packages/monaco/src/typings/monaco/index.d.ts
@@ -477,6 +477,7 @@ declare module monaco.services {
 
     export interface IStandaloneTheme {
         tokenTheme: TokenTheme;
+        getColor(color: string): Color | undefined;
     }
 
     export interface TokenTheme {
@@ -537,6 +538,23 @@ declare module monaco.theme {
     export function attachQuickOpenStyler(widget: IThemable, themeService: IThemeService): monaco.IDisposable;
 }
 
+declare module monaco.color {
+    export interface ColorContribution {
+        readonly id: string;
+    }
+    export interface ColorDefaults {
+        ligh?: string;
+        dark?: string;
+        hc?: string;
+    }
+    export interface IColorRegistry {
+        getColors(): ColorContribution[];
+        registerColor(id: string, defaults: ColorDefaults | undefined, description: string): string;
+        deregisterColor(id: string): void;
+    }
+    export function getColorRegistry(): IColorRegistry;
+}
+
 declare module monaco.referenceSearch {
 
     export interface Location {
diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
index 94661977e454e..865f3b1c41975 100644
--- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
+++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
@@ -24,7 +24,6 @@ import { EditorManager } from '@theia/editor/lib/browser';
 import { TextDocumentShowOptions } from '@theia/plugin-ext/lib/common/plugin-api-rpc-model';
 import { DocumentsMainImpl } from '@theia/plugin-ext/lib/main/browser/documents-main';
 import { createUntitledResource } from '@theia/plugin-ext/lib/main/browser/editor/untitled-resource';
-import { WebviewWidget } from '@theia/plugin-ext/lib/main/browser/webview/webview';
 import { fromViewColumn, toDocumentSymbol } from '@theia/plugin-ext/lib/plugin/type-converters';
 import { ViewColumn } from '@theia/plugin-ext/lib/plugin/types-impl';
 import { WorkspaceCommands } from '@theia/workspace/lib/browser';
@@ -44,10 +43,6 @@ export namespace VscodeCommands {
     export const SET_CONTEXT: Command = {
         id: 'setContext'
     };
-
-    export const PREVIEW_HTML: Command = {
-        id: 'vscode.previewHtml'
-    };
 }
 
 @injectable()
@@ -123,26 +118,6 @@ export class PluginVscodeCommandsContribution implements CommandContribution {
                 this.contextKeyService.createKey(String(contextKey), contextValue);
             }
         });
-        commands.registerCommand(VscodeCommands.PREVIEW_HTML, {
-            isVisible: () => false,
-            // tslint:disable-next-line: no-any
-            execute: async (resource: URI, position?: any, label?: string, options?: any) => {
-                label = label || resource.fsPath;
-                const view = new WebviewWidget(label, { allowScripts: true }, {}, this.mouseTracker);
-                const res = await this.resources(new TheiaURI(resource));
-                const str = await res.readContents();
-                const html = this.getHtml(str);
-                this.shell.addWidget(view, { area: 'main', mode: 'split-right' });
-                this.shell.activateWidget(view.id);
-                view.setHTML(html);
-
-                const editorWidget = await this.editorManager.getOrCreateByUri(new TheiaURI(resource));
-                editorWidget.editor.onDocumentContentChanged(listener => {
-                    view.setHTML(this.getHtml(editorWidget.editor.document.getText()));
-                });
-
-            }
-        });
 
         // https://code.visualstudio.com/docs/getstarted/keybindings#_navigation
         /*
@@ -337,8 +312,4 @@ export class PluginVscodeCommandsContribution implements CommandContribution {
         // see https://github.com/microsoft/vscode/blob/master/src/vs/workbench/api/common/extHostApiCommands.ts
     }
 
-    private getHtml(body: String): string {
-        return `<!DOCTYPE html><html><head></head>${body}</html>`;
-    }
-
 }
diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
index 97cb8684b9be1..601607dca3d85 100644
--- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
+++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts
@@ -54,33 +54,6 @@ export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIF
         return registerCommand(command, handler, thisArg);
     };
 
-    // replace createWebviewPanel API for override html setter
-    const createWebviewPanel = vscode.window.createWebviewPanel;
-    vscode.window.createWebviewPanel = function (viewType: string, title: string, showOptions: any, options: any | undefined): any {
-        const panel = createWebviewPanel(viewType, title, showOptions, options);
-        // redefine property
-        Object.defineProperty(panel.webview, 'html', {
-            set: function (html: string): void {
-                const newHtml = html.replace(new RegExp('vscode-resource:/', 'g'), '/webview/');
-                this.checkIsDisposed();
-                if (this._html !== newHtml) {
-                    this._html = newHtml;
-                    this.proxy.$setHtml(this.viewId, newHtml);
-                }
-            }
-        });
-
-        // override postMessage method to replace vscode-resource:
-        const originalPostMessage = panel.webview.postMessage;
-        panel.webview.postMessage = (message: any): PromiseLike<boolean> => {
-            const decoded = JSON.stringify(message);
-            const newMessage = decoded.replace(new RegExp('vscode-resource:/', 'g'), '/webview/');
-            return originalPostMessage.call(panel.webview, JSON.parse(newMessage));
-        };
-
-        return panel;
-    };
-
     // use Theia plugin api instead vscode extensions
     (<any>vscode).extensions = {
         get all(): any[] {
diff --git a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts
index 82407d75674ee..ad47e1eabb6bc 100644
--- a/packages/plugin-ext-vscode/src/node/scanner-vscode.ts
+++ b/packages/plugin-ext-vscode/src/node/scanner-vscode.ts
@@ -28,11 +28,6 @@ export class VsCodePluginScanner extends TheiaPluginScanner implements PluginSca
     }
 
     getModel(plugin: PluginPackage): PluginModel {
-        // translate vscode builtins, as they are published with a prefix. See https://github.com/theia-ide/vscode-builtin-extensions/blob/master/src/republish.js#L50
-        const built_prefix = '@theia/vscode-builtin-';
-        if (plugin && plugin.name && plugin.name.startsWith(built_prefix)) {
-            plugin.name = plugin.name.substr(built_prefix.length);
-        }
         const result: PluginModel = {
             packagePath: plugin.packagePath,
             // see id definition: https://github.com/microsoft/vscode/blob/15916055fe0cb9411a5f36119b3b012458fe0a1d/src/vs/platform/extensions/common/extensions.ts#L167-L169
diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json
index 52b63f9b21154..1f42bceaf3039 100644
--- a/packages/plugin-ext/package.json
+++ b/packages/plugin-ext/package.json
@@ -24,14 +24,21 @@
     "@theia/task": "^0.12.0",
     "@theia/terminal": "^0.12.0",
     "@theia/workspace": "^0.12.0",
+    "@types/connect": "^3.4.32",
+    "@types/mime": "^2.0.1",
+    "@types/serve-static": "^1.13.3",
+    "connect": "^3.7.0",
     "decompress": "^4.2.0",
     "escape-html": "^1.0.3",
     "jsonc-parser": "^2.0.2",
     "lodash.clonedeep": "^4.5.0",
     "macaddress": "^0.2.9",
+    "mime": "^2.4.4",
     "ps-tree": "^1.2.0",
     "request": "^2.82.0",
+    "serve-static": "^1.14.1",
     "uuid": "^3.2.1",
+    "vhost": "^3.0.2",
     "vscode-debugprotocol": "^1.32.0",
     "vscode-textmate": "^4.0.1"
   },
diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts
index a958e0fa5db9b..62d99bbc3cb7c 100644
--- a/packages/plugin-ext/src/common/plugin-api-rpc.ts
+++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts
@@ -162,6 +162,7 @@ export interface PluginManagerInitializeParams {
     workspaceState: KeysToKeysToAnyValue
     env: EnvInit
     extApi?: ExtPluginApi[]
+    webview: WebviewInitData
 }
 
 export interface PluginManagerStartParams {
@@ -560,6 +561,7 @@ export enum TreeViewItemCollapsibleState {
 
 export interface WindowMain {
     $openUri(uri: UriComponents): Promise<boolean>;
+    $asExternalUri(uri: UriComponents): Promise<UriComponents>;
 }
 
 export interface WindowStateExt {
@@ -1218,6 +1220,11 @@ export interface LanguagesMain {
     $registerRenameProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], supportsResoveInitialValues: boolean): void;
 }
 
+export interface WebviewInitData {
+    webviewResourceRoot: string
+    webviewCspSource: string
+}
+
 export interface WebviewPanelViewState {
     readonly active: boolean;
     readonly visible: boolean;
@@ -1232,7 +1239,7 @@ export interface WebviewsExt {
         viewType: string,
         title: string,
         state: any,
-        position: number,
+        viewState: WebviewPanelViewState,
         options: theia.WebviewOptions & theia.WebviewPanelOptions): PromiseLike<void>;
 }
 
@@ -1241,12 +1248,11 @@ export interface WebviewsMain {
         viewType: string,
         title: string,
         showOptions: theia.WebviewPanelShowOptions,
-        options: theia.WebviewPanelOptions & theia.WebviewOptions | undefined,
-        pluginLocation: UriComponents): void;
+        options: theia.WebviewPanelOptions & theia.WebviewOptions): void;
     $disposeWebview(handle: string): void;
     $reveal(handle: string, showOptions: theia.WebviewPanelShowOptions): void;
     $setTitle(handle: string, value: string): void;
-    $setIconPath(handle: string, value: { light: string, dark: string } | string | undefined): void;
+    $setIconPath(handle: string, value: IconUrl | undefined): void;
     $setHtml(handle: string, value: string): void;
     $setOptions(handle: string, options: theia.WebviewOptions): void;
     $postMessage(handle: string, value: any): Thenable<boolean>;
diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
index e9c554f23cc88..b30e6286ee8a9 100644
--- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
+++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
@@ -53,6 +53,9 @@ import { Emitter, isCancelled } from '@theia/core';
 import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
 import { PluginViewRegistry } from '../../main/browser/view/plugin-view-registry';
 import { TaskProviderRegistry, TaskResolverRegistry } from '@theia/task/lib/browser/task-contribution';
+import { WebviewEnvironment } from '../../main/browser/webview/webview-environment';
+import { WebviewWidget } from '../../main/browser/webview/webview';
+import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
 
 export type PluginHost = 'frontend' | string;
 export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker';
@@ -127,6 +130,12 @@ export class HostedPluginSupport {
     @inject(ProgressService)
     protected readonly progressService: ProgressService;
 
+    @inject(WebviewEnvironment)
+    protected readonly webviewEnvironment: WebviewEnvironment;
+
+    @inject(WidgetManager)
+    protected readonly widgets: WidgetManager;
+
     private theiaReadyPromise: Promise<any>;
 
     protected readonly managers = new Map<string, PluginManagerExt>();
@@ -154,6 +163,26 @@ export class HostedPluginSupport {
         this.viewRegistry.onDidExpandView(id => this.activateByView(id));
         this.taskProviderRegistry.onWillProvideTaskProvider(event => this.ensureTaskActivation(event));
         this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event));
+        this.widgets.onDidCreateWidget(({ factoryId, widget }) => {
+            if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) {
+                const storeState = widget.storeState.bind(widget);
+                const restoreState = widget.restoreState.bind(widget);
+                widget.storeState = () => {
+                    if (this.webviewRevivers.has(widget.viewType)) {
+                        return storeState();
+                    }
+                    return {};
+                };
+                widget.restoreState = oldState => {
+                    if (oldState.viewType) {
+                        restoreState(oldState);
+                        this.preserveWebview(widget);
+                    } else {
+                        widget.dispose();
+                    }
+                };
+            }
+        });
     }
 
     get plugins(): PluginMetadata[] {
@@ -181,6 +210,7 @@ export class HostedPluginSupport {
 
     protected async doLoad(): Promise<void> {
         const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ }));
+        toDisconnect.push(Disposable.create(() => this.preserveWebviews()));
         this.server.onDidCloseConnection(() => toDisconnect.dispose());
 
         // process empty plugins as well in order to properly remove stale plugin widgets
@@ -207,6 +237,7 @@ export class HostedPluginSupport {
             return;
         }
         await this.startPlugins(contributionsByHost, toDisconnect);
+        this.restoreWebviews();
     }
 
     /**
@@ -355,13 +386,15 @@ export class HostedPluginSupport {
             this.managers.set(host, manager);
             toDisconnect.push(Disposable.create(() => this.managers.delete(host)));
 
-            const [extApi, globalState, workspaceState] = await Promise.all([
+            const [extApi, globalState, workspaceState, webviewResourceRoot, webviewCspSource] = await Promise.all([
                 this.server.getExtPluginAPI(),
                 this.pluginServer.getAllStorageValues(undefined),
                 this.pluginServer.getAllStorageValues({
                     workspace: this.workspaceService.workspace,
                     roots: this.workspaceService.tryGetRoots()
-                })
+                }),
+                this.webviewEnvironment.resourceRoot(),
+                this.webviewEnvironment.cspSource()
             ]);
             if (toDisconnect.disposed) {
                 return undefined;
@@ -372,7 +405,11 @@ export class HostedPluginSupport {
                 globalState,
                 workspaceState,
                 env: { queryParams: getQueryParameters(), language: navigator.language },
-                extApi
+                extApi,
+                webview: {
+                    webviewResourceRoot,
+                    webviewCspSource
+                }
             });
             if (toDisconnect.disposed) {
                 return undefined;
@@ -554,6 +591,71 @@ export class HostedPluginSupport {
         console.log(`[${this.clientId}] ${prefix} of ${pluginCount} took: ${measurement()} ms`);
     }
 
+    protected readonly webviewsToRestore = new Set<WebviewWidget>();
+    protected readonly webviewRevivers = new Map<string, (webview: WebviewWidget) => Promise<void>>();
+
+    registerWebviewReviver(viewType: string, reviver: (webview: WebviewWidget) => Promise<void>): void {
+        if (this.webviewRevivers.has(viewType)) {
+            throw new Error(`Reviver for ${viewType} already registered`);
+        }
+        this.webviewRevivers.set(viewType, reviver);
+    }
+
+    unregisterWebviewReviver(viewType: string): void {
+        this.webviewRevivers.delete(viewType);
+    }
+
+    protected preserveWebviews(): void {
+        for (const webview of this.widgets.getWidgets(WebviewWidget.FACTORY_ID)) {
+            this.preserveWebview(webview as WebviewWidget);
+        }
+    }
+
+    protected preserveWebview(webview: WebviewWidget): void {
+        if (!this.webviewsToRestore.has(webview)) {
+            this.webviewsToRestore.add(webview);
+            webview.disposed.connect(() => this.webviewsToRestore.delete(webview));
+        }
+    }
+
+    protected restoreWebviews(): void {
+        for (const webview of this.webviewsToRestore) {
+            this.restoreWebview(webview);
+        }
+        this.webviewsToRestore.clear();
+    }
+
+    protected async restoreWebview(webview: WebviewWidget): Promise<void> {
+        await this.activateByEvent(`onWebviewPanel:${webview.viewType}`);
+        const restore = this.webviewRevivers.get(webview.viewType);
+        if (!restore) {
+            webview.setHTML(this.getDeserializationFailedContents(`
+            <p>The extension providing '${webview.viewType}' view is not capable of restoring it.</p>
+            <p>Want to help fix this? Please inform the extension developer to register a <a href="https://code.visualstudio.com/api/extension-guides/webview#serialization">reviver</a>.</p>
+            `));
+            return;
+        }
+        try {
+            await restore(webview);
+        } catch (e) {
+            webview.setHTML(this.getDeserializationFailedContents(`
+            An error occurred while restoring '${webview.viewType}' view. Please check logs.
+            `));
+            console.error('Failed to restore the webview', e);
+        }
+    }
+
+    protected getDeserializationFailedContents(message: string): string {
+        return `<!DOCTYPE html>
+		<html>
+			<head>
+				<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+				<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
+			</head>
+			<body>${message}</body>
+		</html>`;
+    }
+
 }
 
 export class PluginContributions extends DisposableCollection {
diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
index d582cbaf27329..3271f319cbb60 100644
--- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
+++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
@@ -30,6 +30,7 @@ import { MessageRegistryExt } from '../../../plugin/message-registry';
 import { WorkerEnvExtImpl } from './worker-env-ext';
 import { ClipboardExt } from '../../../plugin/clipboard-ext';
 import { KeyValueStorageProxy } from '../../../plugin/plugin-storage';
+import { WebviewsExtImpl } from '../../../plugin/webviews';
 
 // tslint:disable-next-line:no-any
 const ctx = self as any;
@@ -59,6 +60,7 @@ const workspaceExt = new WorkspaceExtImpl(rpc, editorsAndDocuments, messageRegis
 const preferenceRegistryExt = new PreferenceRegistryExtImpl(rpc, workspaceExt);
 const debugExt = createDebugExtStub(rpc);
 const clipboardExt = new ClipboardExt(rpc);
+const webviewExt = new WebviewsExtImpl(rpc, workspaceExt);
 
 const pluginManager = new PluginManagerExtImpl({
     // tslint:disable-next-line:no-any
@@ -131,7 +133,7 @@ const pluginManager = new PluginManagerExtImpl({
             }
         }
     }
-}, envExt, storageProxy, preferenceRegistryExt, rpc);
+}, envExt, storageProxy, preferenceRegistryExt, webviewExt, rpc);
 
 const apiFactory = createAPIFactory(
     rpc,
@@ -142,7 +144,8 @@ const apiFactory = createAPIFactory(
     editorsAndDocuments,
     workspaceExt,
     messageRegistryExt,
-    clipboardExt
+    clipboardExt,
+    webviewExt
 );
 let defaultApi: typeof theia;
 
@@ -169,6 +172,7 @@ rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, pluginManager);
 rpc.set(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, editorsAndDocuments);
 rpc.set(MAIN_RPC_CONTEXT.WORKSPACE_EXT, workspaceExt);
 rpc.set(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT, preferenceRegistryExt);
+rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, webviewExt);
 
 function isElectron(): boolean {
     if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) {
diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
index 94d68e15acbcd..e0c05570c856f 100644
--- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
+++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
@@ -29,6 +29,7 @@ import { EnvNodeExtImpl } from '../../plugin/node/env-node-ext';
 import { ClipboardExt } from '../../plugin/clipboard-ext';
 import { loadManifest } from './plugin-manifest-loader';
 import { KeyValueStorageProxy } from '../../plugin/plugin-storage';
+import { WebviewsExtImpl } from '../../plugin/webviews';
 
 /**
  * Handle the RPC calls.
@@ -52,12 +53,14 @@ export class PluginHostRPC {
         const workspaceExt = new WorkspaceExtImpl(this.rpc, editorsAndDocumentsExt, messageRegistryExt);
         const preferenceRegistryExt = new PreferenceRegistryExtImpl(this.rpc, workspaceExt);
         const clipboardExt = new ClipboardExt(this.rpc);
-        this.pluginManager = this.createPluginManager(envExt, storageProxy, preferenceRegistryExt, this.rpc);
+        const webviewExt = new WebviewsExtImpl(this.rpc, workspaceExt);
+        this.pluginManager = this.createPluginManager(envExt, storageProxy, preferenceRegistryExt, webviewExt, this.rpc);
         this.rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, this.pluginManager);
         this.rpc.set(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, editorsAndDocumentsExt);
         this.rpc.set(MAIN_RPC_CONTEXT.WORKSPACE_EXT, workspaceExt);
         this.rpc.set(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT, preferenceRegistryExt);
         this.rpc.set(MAIN_RPC_CONTEXT.STORAGE_EXT, storageProxy);
+        this.rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, webviewExt);
 
         this.apiFactory = createAPIFactory(
             this.rpc,
@@ -68,7 +71,8 @@ export class PluginHostRPC {
             editorsAndDocumentsExt,
             workspaceExt,
             messageRegistryExt,
-            clipboardExt
+            clipboardExt,
+            webviewExt
         );
     }
 
@@ -84,8 +88,10 @@ export class PluginHostRPC {
         }
     }
 
-    // tslint:disable-next-line:no-any
-    createPluginManager(envExt: EnvExtImpl, storageProxy: KeyValueStorageProxy, preferencesManager: PreferenceRegistryExtImpl, rpc: any): PluginManagerExtImpl {
+    createPluginManager(
+        envExt: EnvExtImpl, storageProxy: KeyValueStorageProxy, preferencesManager: PreferenceRegistryExtImpl, webview: WebviewsExtImpl,
+        // tslint:disable-next-line:no-any
+        rpc: any): PluginManagerExtImpl {
         const { extensionTestsPath } = process.env;
         const self = this;
         const pluginManager = new PluginManagerExtImpl({
@@ -216,7 +222,7 @@ export class PluginHostRPC {
                     `Path ${extensionTestsPath} does not point to a valid extension test runner.`
                 );
             } : undefined
-        }, envExt, storageProxy, preferencesManager, rpc);
+        }, envExt, storageProxy, preferencesManager, webview, rpc);
         return pluginManager;
     }
 }
diff --git a/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts b/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts
index 6b64f6f8b0ae2..46feb067dffac 100644
--- a/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts
+++ b/packages/plugin-ext/src/hosted/node/plugin-manifest-loader.ts
@@ -26,6 +26,11 @@ export async function loadManifest(pluginPath: string): Promise<any> {
         fs.readJson(path.join(pluginPath, 'package.json')),
         loadTranslations(pluginPath)
     ]);
+    // translate vscode builtins, as they are published with a prefix. See https://github.com/theia-ide/vscode-builtin-extensions/blob/master/src/republish.js#L50
+    const built_prefix = '@theia/vscode-builtin-';
+    if (manifest && manifest.name && manifest.name.startsWith(built_prefix)) {
+        manifest.name = manifest.name.substr(built_prefix.length);
+    }
     return manifest && translations && Object.keys(translations).length ?
         localize(manifest, translations) :
         manifest;
diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts
index fcd92dda94edd..108ee70b2f5a2 100644
--- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts
+++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts
@@ -21,6 +21,7 @@ import { injectable, inject } from 'inversify';
 import { MenuPath, ILogger, CommandRegistry, Command, Mutable, MenuAction, SelectionService, CommandHandler, Disposable, DisposableCollection } from '@theia/core';
 import { EDITOR_CONTEXT_MENU, EditorWidget } from '@theia/editor/lib/browser';
 import { MenuModelRegistry } from '@theia/core/lib/common';
+import { Emitter } from '@theia/core/lib/common/event';
 import { TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
 import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution';
 import { QuickCommandService } from '@theia/core/lib/browser/quick-open/quick-command-service';
@@ -38,6 +39,7 @@ import { PluginViewWidget } from '../view/plugin-view-widget';
 import { ViewContextKeyService } from '../view/view-context-key-service';
 import { WebviewWidget } from '../webview/webview';
 import { Navigatable } from '@theia/core/lib/browser/navigatable';
+import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
 
 type CodeEditorWidget = EditorWidget | WebviewWidget;
 export namespace CodeEditorWidget {
@@ -80,6 +82,9 @@ export class MenusContributionPointHandler {
     @inject(ViewContextKeyService)
     protected readonly viewContextKeys: ViewContextKeyService;
 
+    @inject(ContextKeyService)
+    protected readonly contextKeyService: ContextKeyService;
+
     handle(contributions: PluginContribution): Disposable {
         const allMenus = contributions.menus;
         if (!allMenus) {
@@ -194,6 +199,23 @@ export class MenusContributionPointHandler {
         toDispose.push(this.commands.registerCommand(command, handler));
 
         const { when } = action;
+        const whenKeys = when && this.contextKeyService.parseKeys(when);
+        let onDidChange;
+        if (whenKeys && whenKeys.size) {
+            const onDidChangeEmitter = new Emitter<void>();
+            toDispose.push(onDidChangeEmitter);
+            onDidChange = onDidChangeEmitter.event;
+            this.contextKeyService.onDidChange.maxListeners = this.contextKeyService.onDidChange.maxListeners + 1;
+            toDispose.push(this.contextKeyService.onDidChange(event => {
+                if (event.affects(whenKeys)) {
+                    onDidChangeEmitter.fire(undefined);
+                }
+            }));
+            toDispose.push(Disposable.create(() => {
+                this.contextKeyService.onDidChange.maxListeners = this.contextKeyService.onDidChange.maxListeners - 1;
+            }));
+        }
+
         // handle group and priority
         // if group is empty or white space is will be set to navigation
         // ' ' => ['navigation', 0]
@@ -202,7 +224,7 @@ export class MenusContributionPointHandler {
         // if priority is not a number it will be set to 0
         // navigation@test => ['navigation', 0]
         const [group, sort] = (action.group || 'navigation').split('@');
-        const item: Mutable<TabBarToolbarItem> = { id, command: id, group: group.trim() || 'navigation', priority: ~~sort || undefined, when };
+        const item: Mutable<TabBarToolbarItem> = { id, command: id, group: group.trim() || 'navigation', priority: ~~sort || undefined, when, onDidChange };
         toDispose.push(this.tabBarToolbar.registerItem(item));
 
         toDispose.push(this.onDidRegisterCommand(action.command, pluginCommand => {
diff --git a/packages/plugin-ext/src/main/browser/plugin-command-open-handler.ts b/packages/plugin-ext/src/main/browser/plugin-command-open-handler.ts
new file mode 100644
index 0000000000000..4d685ab30a446
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/plugin-command-open-handler.ts
@@ -0,0 +1,50 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { injectable, inject } from 'inversify';
+import URI from '@theia/core/lib/common/uri';
+import { OpenHandler } from '@theia/core/lib/browser/opener-service';
+import { Schemes } from '../../common/uri-components';
+import { CommandService } from '@theia/core/lib/common/command';
+
+@injectable()
+export class PluginCommandOpenHandler implements OpenHandler {
+
+    readonly id = 'plugin-command';
+
+    @inject(CommandService)
+    protected readonly commands: CommandService;
+
+    canHandle(uri: URI): number {
+        return uri.scheme === Schemes.COMMAND ? 500 : -1;
+    }
+
+    async open(uri: URI): Promise<boolean> {
+        // tslint:disable-next-line:no-any
+        let args: any = [];
+        try {
+            args = JSON.parse(uri.query);
+            if (!Array.isArray(args)) {
+                args = [args];
+            }
+        } catch (e) {
+            // ignore error
+        }
+        await this.commands.executeCommand(uri.path.toString(), ...args);
+        return true;
+    }
+
+}
diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
index 5c6dfc110ea9d..ca0daed42b0e6 100644
--- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
+++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts
@@ -20,7 +20,7 @@ import '../../../src/main/browser/style/index.css';
 import { ContainerModule } from 'inversify';
 import {
     FrontendApplicationContribution, FrontendApplication, WidgetFactory, bindViewContribution,
-    ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeImpl, TreeWidget, TreeModelImpl
+    ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeImpl, TreeWidget, TreeModelImpl, OpenHandler
 } from '@theia/core/lib/browser';
 import { MaybePromise, CommandContribution, ResourceResolver, bindContributionProvider } from '@theia/core/lib/common';
 import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging';
@@ -63,6 +63,13 @@ import { LanguagesMainFactory, OutputChannelRegistryFactory } from '../../common
 import { LanguagesMainImpl } from './languages-main';
 import { OutputChannelRegistryMainImpl } from './output-channel-registry-main';
 import { InPluginFileSystemWatcherManager } from './in-plugin-filesystem-watcher-manager';
+import { WebviewWidget, WebviewWidgetIdentifier, WebviewWidgetExternalEndpoint } from './webview/webview';
+import { WebviewEnvironment } from './webview/webview-environment';
+import { WebviewThemeDataProvider } from './webview/webview-theme-data-provider';
+import { PluginCommandOpenHandler } from './plugin-command-open-handler';
+import { bindWebviewPreferences } from './webview/webview-preferences';
+import { WebviewResourceLoader, WebviewResourceLoaderPath } from '../common/webview-protocol';
+import { WebviewResourceCache } from './webview/webview-resource-cache';
 
 export default new ContainerModule((bind, unbind, isBound, rebind) => {
 
@@ -146,6 +153,33 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
         }
     })).inSingletonScope();
 
+    bind(PluginCommandOpenHandler).toSelf().inSingletonScope();
+    bind(OpenHandler).toService(PluginCommandOpenHandler);
+
+    bindWebviewPreferences(bind);
+    bind(WebviewEnvironment).toSelf().inSingletonScope();
+    bind(WebviewThemeDataProvider).toSelf().inSingletonScope();
+    bind(WebviewResourceLoader).toDynamicValue(ctx =>
+        WebSocketConnectionProvider.createProxy(ctx.container, WebviewResourceLoaderPath)
+    ).inSingletonScope();
+    bind(WebviewResourceCache).toSelf().inSingletonScope();
+    bind(WebviewWidget).toSelf();
+    bind(WidgetFactory).toDynamicValue(({ container }) => ({
+        id: WebviewWidget.FACTORY_ID,
+        createWidget: async (identifier: WebviewWidgetIdentifier) => {
+            const externalEndpoint = await container.get(WebviewEnvironment).externalEndpoint();
+            let endpoint = externalEndpoint.replace('{{uuid}}', identifier.id);
+            if (endpoint[endpoint.length - 1] === '/') {
+                endpoint = endpoint.slice(0, endpoint.length - 1);
+            }
+
+            const child = container.createChild();
+            child.bind(WebviewWidgetIdentifier).toConstantValue(identifier);
+            child.bind(WebviewWidgetExternalEndpoint).toConstantValue(endpoint);
+            return child.get(WebviewWidget);
+        }
+    })).inSingletonScope();
+
     bind(PluginViewWidget).toSelf();
     bind(WidgetFactory).toDynamicValue(({ container }) => ({
         id: PLUGIN_VIEW_FACTORY_ID,
diff --git a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts
index be303d75aaa9b..42c7c5a3419a0 100644
--- a/packages/plugin-ext/src/main/browser/plugin-shared-style.ts
+++ b/packages/plugin-ext/src/main/browser/plugin-shared-style.ts
@@ -78,9 +78,8 @@ export class PluginSharedStyle {
     }): void {
         const sheet = (<CSSStyleSheet>this.style.sheet);
         const cssBody = body(ThemeService.get().getCurrentTheme());
-        sheet.insertRule(selector + ' { ' + cssBody + ' }', 0);
+        sheet.insertRule(selector + ' {\n' + cssBody + '\n}', 0);
     }
-
     deleteRule(selector: string): void {
         const sheet = (<CSSStyleSheet>this.style.sheet);
         const rules = sheet.rules || sheet.cssRules || [];
diff --git a/packages/plugin-ext/src/main/browser/style/webview.css b/packages/plugin-ext/src/main/browser/style/webview.css
index 4484abee04005..61a449d613eb9 100644
--- a/packages/plugin-ext/src/main/browser/style/webview.css
+++ b/packages/plugin-ext/src/main/browser/style/webview.css
@@ -14,6 +14,11 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
 
+.theia-webview.p-mod-hidden {
+    visibility: hidden;
+    display: flex !important;
+}
+
 .theia-webview {
     display: flex;
     flex-direction: column;
@@ -25,12 +30,12 @@
     border: none; margin: 0; padding: 0;
 }
 
-.webview-icon {
+.theia-webview-icon {
     background: none !important;
     min-height: 20px;
 }
 
-.webview-icon::before {
+.theia-webview-icon::before {
     background-size: 13px;
     background-repeat: no-repeat;
     vertical-align: middle;
@@ -41,7 +46,7 @@
     content: "";
 }
 
-.p-TabBar.theia-app-sides .webview-icon::before {
+.p-TabBar.theia-app-sides .theia-webview-icon::before {
     width: var(--theia-private-sidebar-icon-size);
     height: var(--theia-private-sidebar-icon-size);
     background-size: contain;
diff --git a/packages/plugin-ext/src/main/browser/view-column-service.ts b/packages/plugin-ext/src/main/browser/view-column-service.ts
index 4c0384e2b7399..aad11a196eb6e 100644
--- a/packages/plugin-ext/src/main/browser/view-column-service.ts
+++ b/packages/plugin-ext/src/main/browser/view-column-service.ts
@@ -18,6 +18,7 @@ import { injectable, inject } from 'inversify';
 import { Emitter, Event } from '@theia/core/lib/common/event';
 import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
 import { toArray } from '@phosphor/algorithm';
+import { TabBar, Widget } from '@phosphor/widgets';
 
 @injectable()
 export class ViewColumnService {
@@ -51,26 +52,59 @@ export class ViewColumnService {
     }
 
     updateViewColumns(): void {
-        const positionIds = new Map<number, string[]>();
-        toArray(this.shell.mainPanel.tabBars()).forEach(tabBar => {
-            if (!tabBar.node.style.left) {
-                return;
-            }
-            const position = parseInt(tabBar.node.style.left);
-            const viewColumnIds = tabBar.titles.map(title => title.owner.id);
-            positionIds.set(position, viewColumnIds);
-        });
         this.columnValues.clear();
         this.viewColumnIds.clear();
-        [...positionIds.keys()].sort((a, b) => a - b).forEach((key: number, viewColumn: number) => {
-            positionIds.get(key)!.forEach((id: string) => {
-                this.columnValues.set(id, viewColumn);
-                if (!this.viewColumnIds.has(viewColumn)) {
-                    this.viewColumnIds.set(viewColumn, []);
+
+        const rows = new Map<number, Set<number>>();
+        const columns = new Map<number, Map<number, TabBar<Widget>>>();
+        for (const tabBar of toArray(this.shell.mainPanel.tabBars())) {
+            if (!tabBar.node.style.top || !tabBar.node.style.left) {
+                continue;
+            }
+            const top = parseInt(tabBar.node.style.top);
+            const left = parseInt(tabBar.node.style.left);
+
+            const row = rows.get(top) || new Set<number>();
+            row.add(left);
+            rows.set(top, row);
+
+            const column = columns.get(left) || new Map<number, TabBar<Widget>>();
+            column.set(top, tabBar);
+            columns.set(left, column);
+        }
+        const firstRow = rows.get([...rows.keys()].sort()[0]);
+        if (!firstRow) {
+            return;
+        }
+        const lefts = [...firstRow.keys()].sort();
+        for (let i = 0; i < lefts.length; i++) {
+            const column = columns.get(lefts[i]);
+            if (!column) {
+                break;
+            }
+            const cellIndexes = [...column.keys()].sort();
+            let viewColumn = Math.min(i, 2);
+            for (let j = 0; j < cellIndexes.length; j++) {
+                const cell = column.get(cellIndexes[j]);
+                if (!cell) {
+                    break;
                 }
-                this.viewColumnIds.get(viewColumn)!.push(id);
-            });
-        });
+                this.setViewColumn(cell, viewColumn);
+                if (viewColumn < 7) {
+                    viewColumn += 3;
+                }
+            }
+        }
+    }
+
+    protected setViewColumn(tabBar: TabBar<Widget>, viewColumn: number): void {
+        const ids = [];
+        for (const title of tabBar.titles) {
+            const id = title.owner.id;
+            ids.push(id);
+            this.columnValues.set(id, viewColumn);
+        }
+        this.viewColumnIds.set(viewColumn, ids);
     }
 
     getViewColumnIds(viewColumn: number): string[] {
diff --git a/packages/plugin-ext/src/main/browser/webview/pre/fake.html b/packages/plugin-ext/src/main/browser/webview/pre/fake.html
new file mode 100644
index 0000000000000..18c40421e34bc
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/pre/fake.html
@@ -0,0 +1,14 @@
+<!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>
diff --git a/packages/plugin-ext/src/main/browser/webview/pre/host.js b/packages/plugin-ext/src/main/browser/webview/pre/host.js
new file mode 100644
index 0000000000000..b5fa2e9cd01b3
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/pre/host.js
@@ -0,0 +1,115 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+/*---------------------------------------------------------------------------------------------
+*  Copyright (c) Microsoft Corporation. All rights reserved.
+*  Licensed under the MIT License. See License.txt in the project root for license information.
+*--------------------------------------------------------------------------------------------*/
+// copied and modified from https://github.com/microsoft/vscode/blob/ba40bd16433d5a817bfae15f3b4350e18f144af4/src/vs/workbench/contrib/webview/browser/pre/host.js
+// @ts-check
+(function () {
+    const id = document.location.search.match(/\bid=([\w-]+)/)[1];
+
+    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.error('no handler for ', e);
+                }
+            });
+        }
+
+        postMessage(channel, data) {
+            window.parent.postMessage({ target: id, channel, data }, '*');
+        }
+
+        onMessage(channel, handler) {
+            this.handlers.set(channel, handler);
+        }
+    }();
+
+    const workerReady = new Promise(async (resolveWorkerReady) => {
+        if (!areServiceWorkersEnabled()) {
+            console.error('Service Workers are not enabled. Webviews will not work properly');
+            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' });
+        });
+
+        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;
+        }
+    }
+
+    window.createWebviewManager({
+        postMessage: hostMessaging.postMessage.bind(hostMessaging),
+        onMessage: hostMessaging.onMessage.bind(hostMessaging),
+        ready: workerReady,
+        fakeLoad: true
+    });
+}());
diff --git a/packages/plugin-ext/src/main/browser/webview/pre/index.html b/packages/plugin-ext/src/main/browser/webview/pre/index.html
new file mode 100644
index 0000000000000..f4ed42759569d
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/pre/index.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en" style="width: 100%; height: 100%;">
+
+<head>
+	<meta charset="UTF-8">
+	<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%">
+	<script src="main.js"></script>
+	<script src="host.js"></script>
+</body>
+
+</html>
diff --git a/packages/plugin-ext/src/main/browser/webview/pre/main.js b/packages/plugin-ext/src/main/browser/webview/pre/main.js
new file mode 100644
index 0000000000000..b667df984dd0d
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/pre/main.js
@@ -0,0 +1,577 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+// copied and modified from https://github.com/microsoft/vscode/blob/ba40bd16433d5a817bfae15f3b4350e18f144af4/src/vs/workbench/contrib/webview/browser/pre/main.js
+// @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
+ * }} WebviewHost
+ */
+
+(function () {
+    'use strict';
+
+	/**
+	 * 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: var(--vscode-editor-background);
+		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-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 {*} [state]
+	 * @return {string}
+	 */
+    function getVsCodeApiScript(state) {
+        return `
+			const acquireVsCodeApi = (function() {
+				const originalPostMessage = window.parent.postMessage.bind(window.parent);
+				const targetOrigin = '*';
+				let acquired = false;
+
+				let state = ${state ? `JSON.parse(${JSON.stringify(state)})` : undefined};
+
+				return () => {
+					if (acquired) {
+						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;
+						}
+					});
+				};
+			})();
+            const acquireTheiaApi = acquireVsCodeApi;
+			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);
+            }
+
+            if (initData.styles) {
+                for (const variable of Object.keys(initData.styles)) {
+                    document.documentElement.style.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) => {
+            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
+            });
+        };
+
+        let isHandlingScroll = false;
+        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.textContent = getVsCodeApiScript(data.state);
+                newDocument.head.prepend(defaultScript);
+            }
+
+            // apply default styles
+            const defaultStyles = newDocument.createElement('style');
+            defaultStyles.id = '_defaultStyles';
+            defaultStyles.innerHTML = 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 {
+                // Rewrite vscode-resource in csp
+                if (data.endpoint) {
+                    try {
+                        const endpointUrl = new URL(data.endpoint);
+                        csp.setAttribute('content', csp.getAttribute('content').replace(/(?:vscode|theia)-resource:(?=(\s|;|$))/g, endpointUrl.origin));
+                    } catch (e) {
+                        console.error('Could not rewrite csp');
+                    }
+                }
+            }
+
+            // 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;
+
+                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-same-origin');
+                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();
+                }
+
+                newFrame.contentWindow.addEventListener('DOMContentLoaded', e => {
+                    // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325
+                    setTimeout(() => {
+                        if (host.fakeLoad) {
+                            newFrame.contentDocument.open();
+                            newFrame.contentDocument.write(newDocument);
+                            newFrame.contentDocument.close();
+                            hookupOnLoadHandlers(newFrame);
+                        }
+                        const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined;
+                        if (contentDocument) {
+                            applyStyles(contentDocument, contentDocument.body);
+                        }
+                    }, 0);
+                });
+
+                const onLoad = (contentDocument, contentWindow) => {
+                    if (contentDocument && contentDocument.body) {
+                        // Workaround for https://github.com/Microsoft/vscode/issues/12865
+                        // check new scrollY and reset if neccessary
+                        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);
+
+                        pendingMessages.forEach((data) => {
+                            contentWindow.postMessage(data, '*');
+                        });
+                        pendingMessages = [];
+                    }
+                };
+
+				/**
+				 * @param {HTMLIFrameElement} newFrame
+				 */
+                function hookupOnLoadHandlers(newFrame) {
+                    const timeoutDelay = 5000;
+                    clearTimeout(loadTimeout);
+                    loadTimeout = undefined;
+                    loadTimeout = setTimeout(() => {
+                        clearTimeout(loadTimeout);
+                        loadTimeout = undefined;
+                        console.warn('Loading webview is slow, took: ' + timeoutDelay + 'ms');
+                        onLoad(newFrame.contentDocument, newFrame.contentWindow);
+                    }, timeoutDelay);
+
+                    newFrame.contentWindow.addEventListener('load', function (e) {
+                        if (loadTimeout) {
+                            clearTimeout(loadTimeout);
+                            loadTimeout = undefined;
+                            onLoad(e.target, 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;
+            });
+
+
+            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 {
+        window.createWebviewManager = createWebviewManager;
+    }
+}());
diff --git a/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js b/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js
new file mode 100644
index 0000000000000..7a9d8675dedab
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/pre/service-worker.js
@@ -0,0 +1,295 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+/*---------------------------------------------------------------------------------------------
+*  Copyright (c) Microsoft Corporation. All rights reserved.
+*  Licensed under the MIT License. See License.txt in the project root for license information.
+*--------------------------------------------------------------------------------------------*/
+// copied and modified from https://github.com/microsoft/vscode/blob/ba40bd16433d5a817bfae15f3b4350e18f144af4/src/vs/workbench/contrib/webview/browser/pre/service-worker.js
+// @ts-check
+const VERSION = 1;
+
+const rootPath = self.location.pathname.replace(/\/service-worker.js$/, '');
+
+/**
+ * Root path for resources
+ */
+const resourceRoots = [rootPath + '/theia-resource', 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.error('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.error('Could not resolve unknown localhost', data.origin);
+                }
+                return;
+            }
+    }
+
+    console.error('Unknown message');
+});
+
+self.addEventListener('fetch', (event) => {
+    const requestUrl = new URL(event.request.url);
+
+    for (const resourceRoot of resourceRoots) {
+        // See if it's a resource request
+        if (requestUrl.origin === self.origin && requestUrl.pathname.startsWith(resourceRoot + '/')) {
+            return event.respondWith(processResourceRequest(event, requestUrl, resourceRoot));
+        }
+    }
+
+    // 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, resourceRoot) {
+    const client = await self.clients.get(event.clientId);
+    if (!client) {
+        console.error('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.error('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.error('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));
+    });
+}
diff --git a/packages/plugin-ext/src/main/browser/webview/theme-rules-service.ts b/packages/plugin-ext/src/main/browser/webview/theme-rules-service.ts
deleted file mode 100644
index 23eba55151663..0000000000000
--- a/packages/plugin-ext/src/main/browser/webview/theme-rules-service.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-/********************************************************************************
- * Copyright (C) 2018 Red Hat, Inc. and others.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v. 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * This Source Code may also be made available under the following Secondary
- * Licenses when the conditions for such availability set forth in the Eclipse
- * Public License v. 2.0 are satisfied: GNU General Public License, version 2
- * with the GNU Classpath Exception which is available at
- * https://www.gnu.org/software/classpath/license.html.
- *
- * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
- ********************************************************************************/
-
-import { ThemeService } from '@theia/core/lib/browser/theming';
-
-export const ThemeRulesServiceSymbol = Symbol('ThemeRulesService');
-
-interface IconPath {
-    light: string,
-    dark: string
-}
-
-const DEFAULT_RULE = 'body { font-size: var(--theia-ui-font-size1); color: var(--theia-ui-font-color1); }';
-
-export class ThemeRulesService {
-    private styleElement?: HTMLStyleElement;
-    private icons = new Map<string, IconPath | string>();
-    protected readonly themeService = ThemeService.get();
-    protected readonly themeRules = new Map<string, string[]>();
-
-    static get(): ThemeRulesService {
-        const global = window as any; // tslint:disable-line
-        return global[ThemeRulesServiceSymbol] || new ThemeRulesService();
-    }
-
-    protected constructor() {
-        const global = window as any; // tslint:disable-line
-        global[ThemeRulesServiceSymbol] = this;
-
-        this.themeService.onThemeChange(() => {
-            this.updateIconStyleElement();
-        });
-    }
-
-    createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0]): HTMLStyleElement {
-        const style = document.createElement('style');
-        style.type = 'text/css';
-        style.media = 'screen';
-        container.appendChild(style);
-        return style;
-    }
-
-    getCurrentThemeRules(): string[] {
-        const cssText: string[] = [];
-        const themeId = this.themeService.getCurrentTheme().id;
-        if (this.themeRules.has(themeId)) {
-            return <string[]>this.themeRules.get(themeId);
-        }
-        // tslint:disable-next-line:no-any
-        const styleElement = document.getElementById('theia-theme') as any;
-        if (!styleElement) {
-            return cssText;
-        }
-
-        const sheet: {
-            insertRule: (rule: string, index: number) => void,
-            removeRule: (index: number) => void,
-            rules: CSSRuleList
-        // tslint:disable-next-line:no-any
-        } | undefined = (<any>styleElement).sheet;
-        if (!sheet || !sheet.rules || !sheet.rules.length) {
-            return cssText;
-        }
-
-        const ruleList = sheet.rules;
-        for (let index = 0; index < ruleList.length; index++) {
-            if (ruleList[index] && ruleList[index].cssText) {
-                cssText.push(ruleList[index].cssText.toString());
-            }
-        }
-
-        if (cssText.length) {
-            cssText.push(DEFAULT_RULE);
-        }
-
-        return cssText;
-    }
-
-    setRules(styleSheet: HTMLElement, newRules: string[]): boolean {
-        const sheet: {
-            insertRule: (rule: string, index: number) => void;
-            removeRule: (index: number) => void;
-            rules: CSSRuleList;
-        // tslint:disable-next-line:no-any
-        } | undefined = (<any>styleSheet).sheet;
-
-        if (!sheet) {
-            return false;
-        }
-        for (let index = sheet.rules!.length; index > 0; index--) {
-            sheet.removeRule(0);
-        }
-        newRules.forEach((rule: string, index: number) => {
-            sheet.insertRule(rule, index);
-        });
-        return true;
-    }
-
-    setIconPath(webviewId: string, iconPath: IconPath | string | undefined): void {
-        if (!iconPath) {
-            this.icons.delete(webviewId);
-        } else {
-            this.icons.set(webviewId, <IconPath | string>iconPath);
-        }
-        if (!this.styleElement) {
-            this.styleElement = this.createStyleSheet();
-            this.styleElement.id = 'webview-icons';
-        }
-        this.updateIconStyleElement();
-    }
-
-    private updateIconStyleElement(): void {
-        if (!this.styleElement) {
-            return;
-        }
-        const cssRules: string[] = [];
-        this.icons.forEach((value, key) => {
-            let path: string;
-            if (typeof value === 'string') {
-                path = value;
-            } else {
-                path = this.isDark() ? value.dark : value.light;
-            }
-            if (path.startsWith('/')) {
-                path = `/webview${path}`;
-            }
-            cssRules.push(`.webview-icon.${key}-file-icon::before { background-image: url(${path}); }`);
-        });
-        this.setRules(this.styleElement, cssRules);
-    }
-
-    private isDark(): boolean {
-        const currentThemeId: string = this.themeService.getCurrentTheme().id;
-        return !currentThemeId.includes('light');
-    }
-}
diff --git a/packages/plugin-ext/src/main/browser/webview/webview-environment.ts b/packages/plugin-ext/src/main/browser/webview/webview-environment.ts
new file mode 100644
index 0000000000000..520134bdf5f48
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/webview-environment.ts
@@ -0,0 +1,63 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { injectable, inject, postConstruct } from 'inversify';
+import { Endpoint } from '@theia/core/lib/browser/endpoint';
+import { Deferred } from '@theia/core/lib/common/promise-util';
+import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
+import URI from '@theia/core/lib/common/uri';
+import { WebviewExternalEndpoint } from '../../common/webview-protocol';
+
+@injectable()
+export class WebviewEnvironment {
+
+    @inject(EnvVariablesServer)
+    protected readonly environments: EnvVariablesServer;
+
+    protected readonly externalEndpointHost = new Deferred<string>();
+
+    @postConstruct()
+    protected async init(): Promise<void> {
+        try {
+            const variable = await this.environments.getValue(WebviewExternalEndpoint.pattern);
+            const value = variable && variable.value || WebviewExternalEndpoint.defaultPattern;
+            this.externalEndpointHost.resolve(value.replace('{{hostname}}', window.location.host || 'localhost'));
+        } catch (e) {
+            this.externalEndpointHost.reject(e);
+        }
+    }
+
+    async externalEndpointUrl(): Promise<URI> {
+        const host = await this.externalEndpointHost.promise;
+        return new Endpoint({
+            host,
+            path: '/webview'
+        }).getRestUrl();
+    }
+
+    async externalEndpoint(): Promise<string> {
+        return (await this.externalEndpointUrl()).toString(true);
+    }
+
+    async resourceRoot(): Promise<string> {
+        return (await this.externalEndpointUrl()).resolve('theia-resource/{{resource}}').toString(true);
+    }
+
+    async cspSource(): Promise<string> {
+        return (await this.externalEndpointUrl()).withPath('').withQuery('').withFragment('').toString(true).replace('{{uuid}}', '*');
+    }
+
+}
diff --git a/packages/plugin-ext/src/main/browser/webview/webview-preferences.ts b/packages/plugin-ext/src/main/browser/webview/webview-preferences.ts
new file mode 100644
index 0000000000000..9935db33803d6
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/webview-preferences.ts
@@ -0,0 +1,56 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { interfaces } from 'inversify';
+import {
+    createPreferenceProxy,
+    PreferenceProxy,
+    PreferenceService,
+    PreferenceContribution,
+    PreferenceSchema
+} from '@theia/core/lib/browser/preferences';
+
+export const WebviewConfigSchema: PreferenceSchema = {
+    'type': 'object',
+    'properties': {
+        'webview.trace': {
+            'type': 'string',
+            'enum': ['off', 'on', 'verbose'],
+            'description': 'Controls communication tracing with webviews.',
+            'default': 'off'
+        }
+    }
+};
+
+export interface WebviewConfiguration {
+    'webview.trace': 'off' | 'on' | 'verbose'
+}
+
+export const WebviewPreferences = Symbol('WebviewPreferences');
+export type WebviewPreferences = PreferenceProxy<WebviewConfiguration>;
+
+export function createWebviewPreferences(preferences: PreferenceService): WebviewPreferences {
+    return createPreferenceProxy(preferences, WebviewConfigSchema);
+}
+
+export function bindWebviewPreferences(bind: interfaces.Bind): void {
+    bind(WebviewPreferences).toDynamicValue(ctx => {
+        const preferences = ctx.container.get<PreferenceService>(PreferenceService);
+        return createWebviewPreferences(preferences);
+    });
+
+    bind(PreferenceContribution).toConstantValue({ schema: WebviewConfigSchema });
+}
diff --git a/packages/plugin-ext/src/main/browser/webview/webview-resource-cache.ts b/packages/plugin-ext/src/main/browser/webview/webview-resource-cache.ts
new file mode 100644
index 0000000000000..816dd219333dd
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/webview-resource-cache.ts
@@ -0,0 +1,88 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { injectable } from 'inversify';
+import { Deferred } from '@theia/core/lib/common/promise-util';
+import { MaybePromise } from '@theia/core/lib/common/types';
+
+export interface WebviewResourceResponse {
+    eTag: string | undefined,
+    body(): MaybePromise<Uint8Array>
+}
+
+/**
+ * Browser based cache of webview resources across all instances.
+ */
+@injectable()
+export class WebviewResourceCache {
+
+    protected readonly cache = new Deferred<Cache | undefined>();
+
+    constructor() {
+        this.resolveCache();
+    }
+
+    protected async resolveCache(): Promise<void> {
+        try {
+            this.cache.resolve(await caches.open('webview:v1'));
+        } catch (e) {
+            console.error('Failed to enable webview caching: ', e);
+            this.cache.resolve(undefined);
+        }
+    }
+
+    async match(url: string): Promise<WebviewResourceResponse | undefined> {
+        const cache = await this.cache.promise;
+        if (!cache) {
+            return undefined;
+        }
+        const response = await cache.match(url);
+        if (!response) {
+            return undefined;
+        }
+        return {
+            eTag: response.headers.get('ETag') || undefined,
+            body: async () => {
+                const buffer = await response.arrayBuffer();
+                return new Uint8Array(buffer);
+            }
+        };
+    }
+
+    async delete(url: string): Promise<boolean> {
+        const cache = await this.cache.promise;
+        if (!cache) {
+            return false;
+        }
+        return cache.delete(url);
+    }
+
+    async put(url: string, response: WebviewResourceResponse): Promise<void> {
+        if (!response.eTag) {
+            return;
+        }
+        const cache = await this.cache.promise;
+        if (!cache) {
+            return;
+        }
+        const body = await response.body();
+        await cache.put(url, new Response(body, {
+            status: 200,
+            headers: { 'ETag': response.eTag }
+        }));
+    }
+
+}
diff --git a/packages/plugin-ext/src/main/browser/webview/webview-theme-data-provider.ts b/packages/plugin-ext/src/main/browser/webview/webview-theme-data-provider.ts
new file mode 100644
index 0000000000000..081cad8ec4066
--- /dev/null
+++ b/packages/plugin-ext/src/main/browser/webview/webview-theme-data-provider.ts
@@ -0,0 +1,119 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+/*---------------------------------------------------------------------------------------------
+ *  Copyright (c) Microsoft Corporation. All rights reserved.
+ *  Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+// copied and modified from https://github.com/microsoft/vscode/blob/ba40bd16433d5a817bfae15f3b4350e18f144af4/src/vs/workbench/contrib/webview/common/themeing.ts
+
+import { inject, postConstruct, injectable } from 'inversify';
+import { Emitter } from '@theia/core/lib/common/event';
+import { EditorPreferences, EditorConfiguration } from '@theia/editor/lib/browser/editor-preferences';
+import { ThemeService } from '@theia/core/lib/browser/theming';
+import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
+import { ColorApplicationContribution } from '@theia/core/lib/browser/color-application-contribution';
+
+export type WebviewThemeType = 'vscode-light' | 'vscode-dark' | 'vscode-high-contrast';
+export interface WebviewThemeData {
+    readonly activeTheme: WebviewThemeType;
+    readonly styles: { readonly [key: string]: string | number; };
+}
+
+@injectable()
+export class WebviewThemeDataProvider {
+
+    protected readonly onDidChangeThemeDataEmitter = new Emitter<void>();
+    readonly onDidChangeThemeData = this.onDidChangeThemeDataEmitter.event;
+
+    @inject(EditorPreferences)
+    protected readonly editorPreferences: EditorPreferences;
+
+    @inject(ColorRegistry)
+    protected readonly colorRegistry: ColorRegistry;
+
+    @inject(ColorApplicationContribution)
+    protected readonly colorContribution: ColorApplicationContribution;
+
+    protected themeData: WebviewThemeData | undefined;
+
+    protected readonly editorStyles = new Map<keyof EditorConfiguration, string>([
+        ['editor.fontFamily', 'editor-font-family'],
+        ['editor.fontWeight', 'editor-font-weight'],
+        ['editor.fontSize', 'editor-font-size']
+    ]);
+
+    @postConstruct()
+    protected init(): void {
+        this.colorContribution.onDidChange(() => this.reset());
+
+        this.editorPreferences.onPreferenceChanged(e => {
+            if (this.editorStyles.has(e.preferenceName)) {
+                this.reset();
+            }
+        });
+    }
+
+    protected reset(): void {
+        if (this.themeData) {
+            this.themeData = undefined;
+            this.onDidChangeThemeDataEmitter.fire(undefined);
+        }
+    }
+
+    getThemeData(): WebviewThemeData {
+        if (!this.themeData) {
+            this.themeData = this.computeThemeData();
+        }
+        return this.themeData;
+    }
+
+    protected computeThemeData(): WebviewThemeData {
+        const styles: { [key: string]: string | number; } = {};
+        // tslint:disable-next-line:no-any
+        const addStyle = (id: string, rawValue: any) => {
+            if (rawValue) {
+                const value = typeof rawValue === 'number' || typeof rawValue === 'string' ? rawValue : String(rawValue);
+                styles['vscode-' + id.replace('.', '-')] = value;
+                styles['theia-' + id.replace('.', '-')] = value;
+            }
+        };
+
+        addStyle('font-family', '-apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif');
+        addStyle('font-weight', 'normal');
+        addStyle('font-size', '13px');
+        this.editorStyles.forEach((value, key) => addStyle(value, this.editorPreferences[key]));
+
+        for (const id of this.colorRegistry.getColors()) {
+            const color = this.colorRegistry.getCurrentColor(id);
+            if (color) {
+                addStyle(id, color.toString());
+            }
+        }
+
+        const activeTheme = this.getActiveTheme();
+        return { styles, activeTheme };
+    }
+
+    protected getActiveTheme(): WebviewThemeType {
+        const theme = ThemeService.get().getCurrentTheme();
+        switch (theme.type) {
+            case 'light': return 'vscode-light';
+            case 'dark': return 'vscode-dark';
+            default: return 'vscode-high-contrast';
+        }
+    }
+
+}
diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts
index 704b9e5d4c682..61c1852cd908a 100644
--- a/packages/plugin-ext/src/main/browser/webview/webview.ts
+++ b/packages/plugin-ext/src/main/browser/webview/webview.ts
@@ -13,316 +13,578 @@
  *
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
+/*---------------------------------------------------------------------------------------------
+*  Copyright (c) Microsoft Corporation. All rights reserved.
+*  Licensed under the MIT License. See License.txt in the project root for license information.
+*--------------------------------------------------------------------------------------------*/
+// copied and modified from https://github.com/microsoft/vscode/blob/ba40bd16433d5a817bfae15f3b4350e18f144af4/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts
+// copied and modified from https://github.com/microsoft/vscode/blob/ba40bd16433d5a817bfae15f3b4350e18f144af4/src/vs/workbench/contrib/webview/browser/webviewElement.ts#
+
+import * as mime from 'mime';
+import { JSONExt } from '@phosphor/coreutils/lib/json';
+import { injectable, inject, postConstruct } from 'inversify';
+import { WebviewPanelOptions, WebviewPortMapping } from '@theia/plugin';
 import { BaseWidget, Message } from '@theia/core/lib/browser/widgets/widget';
-import { IdGenerator } from '../../../common/id-generator';
 import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
+// TODO: get rid of dependencies to the mini browser
 import { MiniBrowserContentStyle } from '@theia/mini-browser/lib/browser/mini-browser-content-style';
 import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';
+import { StatefulWidget } from '@theia/core/lib/browser/shell/shell-layout-restorer';
+import { WebviewPanelViewState } from '../../../common/plugin-api-rpc';
+import { IconUrl } from '../../../common/plugin-protocol';
+import { Deferred } from '@theia/core/lib/common/promise-util';
+import { WebviewEnvironment } from './webview-environment';
+import URI from '@theia/core/lib/common/uri';
+import { Emitter } from '@theia/core/lib/common/event';
+import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
+import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
+import { Schemes } from '../../../common/uri-components';
+import { PluginSharedStyle } from '../plugin-shared-style';
+import { BuiltinThemeProvider } from '@theia/core/lib/browser/theming';
+import { WebviewThemeDataProvider } from './webview-theme-data-provider';
+import { ExternalUriService } from '@theia/core/lib/browser/external-uri-service';
+import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
+import { WebviewPreferences } from './webview-preferences';
+import { WebviewResourceLoader } from '../../common/webview-protocol';
+import { WebviewResourceCache } from './webview-resource-cache';
+import { Endpoint } from '@theia/core/lib/browser/endpoint';
 
 // tslint:disable:no-any
 
-export interface WebviewWidgetOptions {
+export const enum WebviewMessageChannels {
+    onmessage = 'onmessage',
+    didClickLink = 'did-click-link',
+    didFocus = 'did-focus',
+    didBlur = 'did-blur',
+    doUpdateState = 'do-update-state',
+    doReload = 'do-reload',
+    loadResource = 'load-resource',
+    loadLocalhost = 'load-localhost',
+    webviewReady = 'webview-ready',
+    didKeydown = 'did-keydown'
+}
+
+export interface WebviewContentOptions {
     readonly allowScripts?: boolean;
+    readonly localResourceRoots?: ReadonlyArray<string>;
+    readonly portMapping?: ReadonlyArray<WebviewPortMapping>;
+    readonly enableCommandUris?: boolean;
 }
 
-export interface WebviewEvents {
-    onMessage?(message: any): void;
-    onKeyboardEvent?(e: KeyboardEvent): void;
-    onLoad?(contentDocument: Document): void;
+@injectable()
+export class WebviewWidgetIdentifier {
+    id: string;
 }
 
-export class WebviewWidget extends BaseWidget {
-    private static readonly ID = new IdGenerator('webview-widget-');
-    private iframe: HTMLIFrameElement;
-    private state: { [key: string]: any } | undefined = undefined;
-    private loadTimeout: number | undefined;
-    private scrollY: number;
-    private readyToReceiveMessage: boolean = false;
+export const WebviewWidgetExternalEndpoint = Symbol('WebviewWidgetExternalEndpoint');
+
+@injectable()
+export class WebviewWidget extends BaseWidget implements StatefulWidget {
+
+    private static readonly standardSupportedLinkSchemes = new Set([
+        Schemes.HTTP,
+        Schemes.HTTPS,
+        Schemes.MAILTO,
+        Schemes.VSCODE
+    ]);
+
+    static FACTORY_ID = 'plugin-webview';
+
+    protected element: HTMLIFrameElement | undefined;
+
     // tslint:disable-next-line:max-line-length
-    // XXX This is a hack to be able to tack the mouse events when drag and dropping the widgets. On `mousedown` we put a transparent div over the `iframe` to avoid losing the mouse tacking.
-    protected readonly transparentOverlay: HTMLElement;
-
-    constructor(title: string,
-        private options: WebviewWidgetOptions,
-        private eventDelegate: WebviewEvents,
-        protected readonly mouseTracker: ApplicationShellMouseTracker) {
-        super();
+    // XXX This is a hack to be able to tack the mouse events when drag and dropping the widgets.
+    // On `mousedown` we put a transparent div over the `iframe` to avoid losing the mouse tacking.
+    protected transparentOverlay: HTMLElement;
+
+    @inject(WebviewWidgetIdentifier)
+    readonly identifier: WebviewWidgetIdentifier;
+
+    @inject(WebviewWidgetExternalEndpoint)
+    readonly externalEndpoint: string;
+
+    @inject(ApplicationShellMouseTracker)
+    protected readonly mouseTracker: ApplicationShellMouseTracker;
+
+    @inject(WebviewEnvironment)
+    protected readonly environment: WebviewEnvironment;
+
+    @inject(OpenerService)
+    protected readonly openerService: OpenerService;
+
+    @inject(KeybindingRegistry)
+    protected readonly keybindings: KeybindingRegistry;
+
+    @inject(PluginSharedStyle)
+    protected readonly sharedStyle: PluginSharedStyle;
+
+    @inject(WebviewThemeDataProvider)
+    protected readonly themeDataProvider: WebviewThemeDataProvider;
+
+    @inject(ExternalUriService)
+    protected readonly externalUriService: ExternalUriService;
+
+    @inject(OutputChannelManager)
+    protected readonly outputManager: OutputChannelManager;
+
+    @inject(WebviewPreferences)
+    protected readonly preferences: WebviewPreferences;
+
+    @inject(WebviewResourceLoader)
+    protected readonly resourceLoader: WebviewResourceLoader;
+
+    @inject(WebviewResourceCache)
+    protected readonly resourceCache: WebviewResourceCache;
+
+    viewState: WebviewPanelViewState = {
+        visible: false,
+        active: false,
+        position: 0
+    };
+
+    protected html = '';
+
+    protected _contentOptions: WebviewContentOptions = {};
+    get contentOptions(): WebviewContentOptions {
+        return this._contentOptions;
+    }
+
+    protected _state: string | undefined;
+    get state(): string | undefined {
+        return this._state;
+    }
+
+    viewType: string;
+    options: WebviewPanelOptions = {};
+
+    protected ready = new Deferred<void>();
+
+    protected readonly onMessageEmitter = new Emitter<any>();
+    readonly onMessage = this.onMessageEmitter.event;
+    protected readonly pendingMessages: any[] = [];
+
+    protected readonly toHide = new DisposableCollection();
+    protected hideTimeout: any | number | undefined;
+
+    @postConstruct()
+    protected init(): void {
         this.node.tabIndex = 0;
-        this.id = WebviewWidget.ID.nextId();
+        this.id = WebviewWidget.FACTORY_ID + ':' + this.identifier.id;
         this.title.closable = true;
-        this.title.label = title;
         this.addClass(WebviewWidget.Styles.WEBVIEW);
-        this.scrollY = 0;
+
+        this.toDispose.push(this.onMessageEmitter);
 
         this.transparentOverlay = document.createElement('div');
         this.transparentOverlay.classList.add(MiniBrowserContentStyle.TRANSPARENT_OVERLAY);
         this.transparentOverlay.style.display = 'none';
         this.node.appendChild(this.transparentOverlay);
 
-        this.toDispose.push(this.mouseTracker.onMousedown(e => {
-            if (this.iframe.style.display !== 'none') {
+        this.toDispose.push(this.mouseTracker.onMousedown(() => {
+            if (this.element && this.element.style.display !== 'none') {
                 this.transparentOverlay.style.display = 'block';
             }
         }));
-        this.toDispose.push(this.mouseTracker.onMouseup(e => {
-            if (this.iframe.style.display !== 'none') {
+        this.toDispose.push(this.mouseTracker.onMouseup(() => {
+            if (this.element && this.element.style.display !== 'none') {
                 this.transparentOverlay.style.display = 'none';
             }
         }));
     }
 
-    protected handleMessage(message: any): void {
-        switch (message.command) {
-            case 'onmessage':
-                this.eventDelegate.onMessage!(message.data);
-                break;
-            case 'do-update-state':
-                this.state = message.data;
-        }
+    protected onBeforeAttach(msg: Message): void {
+        super.onBeforeAttach(msg);
+        this.doShow();
+        // iframe has to be reloaded when moved to another DOM element
+        this.toDisposeOnDetach.push(Disposable.create(() => this.forceHide()));
     }
 
-    async postMessage(message: any): Promise<void> {
-        // wait message can be delivered
-        await this.waitReadyToReceiveMessage();
-        this.iframe.contentWindow!.postMessage(message, '*');
+    protected onBeforeShow(msg: Message): void {
+        super.onBeforeShow(msg);
+        this.doShow();
     }
 
-    setOptions(options: WebviewWidgetOptions): void {
-        if (!this.iframe || this.options.allowScripts === options.allowScripts) {
-            return;
-        }
-        this.updateSandboxAttribute(this.iframe, options.allowScripts);
-        this.options = options;
-        this.reloadFrame();
+    protected onAfterHide(msg: Message): void {
+        super.onAfterHide(msg);
+        this.doHide();
     }
 
-    setIconClass(iconClass: string): void {
-        this.title.iconClass = iconClass;
+    protected doHide(): void {
+        if (this.options.retainContextWhenHidden !== true) {
+            if (this.hideTimeout === undefined) {
+                // avoid removing iframe if a widget moved quickly
+                this.hideTimeout = setTimeout(() => this.forceHide(), 50);
+            }
+        }
     }
 
-    protected readonly toDisposeOnHTML = new DisposableCollection();
+    protected forceHide(): void {
+        clearTimeout(this.hideTimeout);
+        this.hideTimeout = undefined;
+        this.toHide.dispose();
+    }
 
-    setHTML(html: string): void {
-        const newDocument = new DOMParser().parseFromString(html, 'text/html');
-        if (!newDocument || !newDocument.body) {
+    protected doShow(): void {
+        clearTimeout(this.hideTimeout);
+        this.hideTimeout = undefined;
+        if (!this.toHide.disposed) {
             return;
         }
-
-        this.toDisposeOnHTML.dispose();
-        this.toDispose.push(this.toDisposeOnHTML);
-
-        (<any>newDocument.querySelectorAll('a')).forEach((a: any) => {
-            if (!a.title) {
-                a.title = a.href;
+        this.toDispose.push(this.toHide);
+
+        const element = document.createElement('iframe');
+        element.className = 'webview';
+        element.sandbox.add('allow-scripts', 'allow-same-origin');
+        element.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.identifier.id}`);
+        element.style.border = 'none';
+        element.style.width = '100%';
+        element.style.height = '100%';
+        this.element = element;
+        this.node.appendChild(this.element);
+        this.toHide.push(Disposable.create(() => {
+            if (this.element) {
+                this.element.remove();
+                this.element = undefined;
             }
+        }));
+
+        const oldReady = this.ready;
+        const ready = new Deferred<void>();
+        ready.promise.then(() => oldReady.resolve());
+        this.ready = ready;
+        this.toHide.push(Disposable.create(() => this.ready = new Deferred<void>()));
+        const subscription = this.on(WebviewMessageChannels.webviewReady, () => {
+            subscription.dispose();
+            ready.resolve();
         });
+        this.toHide.push(subscription);
 
-        (window as any)[`postMessageExt${this.id}`] = (e: any) => {
-            this.handleMessage(e);
-        };
-        this.toDisposeOnHTML.push(Disposable.create(() =>
-            delete (window as any)[`postMessageExt${this.id}`]
+        this.toHide.push(this.on(WebviewMessageChannels.onmessage, (data: any) => this.onMessageEmitter.fire(data)));
+        this.toHide.push(this.on(WebviewMessageChannels.didClickLink, (uri: string) => this.openLink(new URI(uri))));
+        this.toHide.push(this.on(WebviewMessageChannels.doUpdateState, (state: any) => {
+            this._state = state;
+        }));
+        this.toHide.push(this.on(WebviewMessageChannels.didFocus, () =>
+            // emulate the webview focus without actually changing focus
+            this.node.dispatchEvent(new FocusEvent('focus'))
         ));
-        this.updateApiScript(newDocument);
-
-        const newFrame = document.createElement('iframe');
-        newFrame.setAttribute('id', 'pending-frame');
-        newFrame.setAttribute('frameborder', '0');
-        newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden';
-        this.node.appendChild(newFrame);
-        this.iframe = newFrame;
-        this.toDisposeOnHTML.push(Disposable.create(() => {
-            newFrame.setAttribute('id', '');
-            this.node.removeChild(newFrame);
+        this.toHide.push(this.on(WebviewMessageChannels.didBlur, () => {
+            /* no-op: webview loses focus only if another element gains focus in the main window */
+        }));
+        this.toHide.push(this.on(WebviewMessageChannels.doReload, () => this.reload()));
+        this.toHide.push(this.on(WebviewMessageChannels.loadResource, (entry: any) => this.loadResource(entry.path)));
+        this.toHide.push(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) =>
+            this.loadLocalhost(entry.origin)
+        ));
+        this.toHide.push(this.on(WebviewMessageChannels.didKeydown, (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.dispatchKeyDown(data);
         }));
 
-        newFrame.contentDocument!.open('text/html', 'replace');
+        this.style();
+        this.toHide.push(this.themeDataProvider.onDidChangeThemeData(() => this.style()));
 
-        const onLoad = (contentDocument: any, contentWindow: any) => {
-            if (newFrame && newFrame.contentDocument === contentDocument) {
-                newFrame.style.visibility = 'visible';
-            }
-            if (contentDocument.body) {
-                if (this.eventDelegate && this.eventDelegate.onKeyboardEvent) {
-                    const eventNames = ['keydown', 'keypress', 'click'];
-                    // Delegate events from the `iframe` to the application.
-                    eventNames.forEach((eventName: string) => {
-                        contentDocument.addEventListener(eventName, this.eventDelegate.onKeyboardEvent!, true);
-                        this.toDispose.push(Disposable.create(() => contentDocument.removeEventListener(eventName, this.eventDelegate.onKeyboardEvent!)));
-                    });
-                }
-                if (this.eventDelegate && this.eventDelegate.onLoad) {
-                    this.eventDelegate.onLoad(<Document>contentDocument);
+        this.doUpdateContent();
+        while (this.pendingMessages.length) {
+            this.sendMessage(this.pendingMessages.shift());
+        }
+    }
+
+    protected async loadLocalhost(origin: string): Promise<void> {
+        const redirect = await this.getRedirect(origin);
+        return this.doSend('did-load-localhost', { origin, location: redirect });
+    }
+
+    protected async getRedirect(url: string): Promise<string | undefined> {
+        const uri = new URI(url);
+        const localhost = this.externalUriService.parseLocalhost(uri);
+        if (!localhost) {
+            return undefined;
+        }
+
+        if (this._contentOptions.portMapping) {
+            for (const mapping of this._contentOptions.portMapping) {
+                if (mapping.webviewPort === localhost.port) {
+                    if (mapping.webviewPort !== mapping.extensionHostPort) {
+                        return this.toRemoteUrl(
+                            uri.withAuthority(`${localhost.address}:${mapping.extensionHostPort}`)
+                        );
+                    }
                 }
             }
-        };
+        }
 
-        this.loadTimeout = window.setTimeout(() => {
-            clearTimeout(this.loadTimeout);
-            this.loadTimeout = undefined;
-            onLoad(newFrame.contentDocument, newFrame.contentWindow);
-        }, 200);
-        this.toDisposeOnHTML.push(Disposable.create(() => {
-            if (typeof this.loadTimeout === 'number') {
-                clearTimeout(this.loadTimeout);
-                this.loadTimeout = undefined;
-            }
-        }));
+        return this.toRemoteUrl(uri);
+    }
 
-        newFrame.contentWindow!.addEventListener('load', e => {
-            if (this.loadTimeout) {
-                clearTimeout(this.loadTimeout);
-                this.loadTimeout = undefined;
-                onLoad(e.target, newFrame.contentWindow);
-            }
-        }, { once: true });
-        newFrame.contentDocument!.write(newDocument!.documentElement!.innerHTML);
-        newFrame.contentDocument!.close();
+    protected async toRemoteUrl(localUri: URI): Promise<string> {
+        const remoteUri = await this.externalUriService.resolve(localUri);
+        const remoteUrl = remoteUri.toString();
+        if (remoteUrl[remoteUrl.length - 1] === '/') {
+            return remoteUrl.slice(0, remoteUrl.length - 1);
+        }
+        return remoteUrl;
+    }
+
+    setContentOptions(contentOptions: WebviewContentOptions): void {
+        if (JSONExt.deepEqual(<any>this.contentOptions, <any>contentOptions)) {
+            return;
+        }
+        this._contentOptions = contentOptions;
+        this.doUpdateContent();
+    }
 
-        this.updateSandboxAttribute(newFrame);
+    protected iconUrl: IconUrl | undefined;
+    protected readonly toDisposeOnIcon = new DisposableCollection();
+    setIconUrl(iconUrl: IconUrl | undefined): void {
+        if ((this.iconUrl && iconUrl && JSONExt.deepEqual(this.iconUrl, iconUrl)) || (this.iconUrl === iconUrl)) {
+            return;
+        }
+        this.toDisposeOnIcon.dispose();
+        this.toDispose.push(this.toDisposeOnIcon);
+        this.iconUrl = iconUrl;
+        if (iconUrl) {
+            const darkIconUrl = typeof iconUrl === 'object' ? iconUrl.dark : iconUrl;
+            const lightIconUrl = typeof iconUrl === 'object' ? iconUrl.light : iconUrl;
+            const iconClass = `webview-${this.identifier.id}-file-icon`;
+            this.toDisposeOnIcon.push(this.sharedStyle.insertRule(
+                `.theia-webview-icon.${iconClass}::before`,
+                theme => `background-image: url(${theme.id === BuiltinThemeProvider.lightTheme.id ? lightIconUrl : darkIconUrl});`
+            ));
+            this.title.iconClass = `theia-webview-icon ${iconClass}`;
+        } else {
+            this.title.iconClass = '';
+        }
+    }
+
+    setHTML(value: string): void {
+        this.html = this.preprocessHtml(value);
+        this.doUpdateContent();
+    }
+
+    protected preprocessHtml(value: string): string {
+        return value
+            .replace(/(["'])(?:vscode|theia)-resource:(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_, startQuote, _1, scheme, path, endQuote) => {
+                if (scheme) {
+                    return `${startQuote}${this.externalEndpoint}/theia-resource/${scheme}${path}${endQuote}`;
+                }
+                return `${startQuote}${this.externalEndpoint}/theia-resource/file${path}${endQuote}`;
+            });
     }
 
     protected onActivateRequest(msg: Message): void {
         super.onActivateRequest(msg);
-        // restore scrolling if there was one
-        if (this.scrollY > 0) {
-            this.iframe.contentWindow!.scrollTo({ top: this.scrollY });
-        }
+        this.focus();
+    }
+
+    focus(): void {
         this.node.focus();
-        // unblock messages
-        this.readyToReceiveMessage = true;
+        if (this.element) {
+            this.doSend('focus');
+        }
     }
 
-    // block messages
-    protected onBeforeShow(msg: Message): void {
-        this.readyToReceiveMessage = false;
+    reload(): void {
+        this.doUpdateContent();
     }
 
-    protected onBeforeHide(msg: Message): void {
-        // persist scrolling
-        if (this.iframe.contentWindow) {
-            this.scrollY = this.iframe.contentWindow.scrollY;
-        }
-        super.onBeforeHide(msg);
+    protected style(): void {
+        const { styles, activeTheme } = this.themeDataProvider.getThemeData();
+        this.doSend('styles', { styles, activeTheme });
     }
 
-    public reloadFrame(): void {
-        if (!this.iframe || !this.iframe.contentDocument || !this.iframe.contentDocument.documentElement) {
-            return;
-        }
-        this.setHTML(this.iframe.contentDocument.documentElement.innerHTML);
+    protected dispatchKeyDown(event: KeyboardEventInit): void {
+        // 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
+        this.keybindings.run(emulatedKeyboardEvent);
     }
 
-    private updateSandboxAttribute(element: HTMLElement, isAllowScript?: boolean): void {
-        if (!element) {
-            return;
+    protected openLink(link: URI): void {
+        const supported = this.toSupportedLink(link);
+        if (supported) {
+            open(this.openerService, supported);
         }
-        const allowScripts = isAllowScript !== undefined ? isAllowScript : this.options.allowScripts;
-        element.setAttribute('sandbox', allowScripts ? 'allow-scripts allow-forms allow-same-origin' : 'allow-same-origin');
     }
 
-    private updateApiScript(contentDocument: Document, isAllowScript?: boolean): void {
-        if (!contentDocument) {
-            return;
-        }
-        const allowScripts = isAllowScript !== undefined ? isAllowScript : this.options.allowScripts;
-        const scriptId = 'webview-widget-codeApi';
-        if (!allowScripts) {
-            const script = contentDocument.getElementById(scriptId);
-            if (!script) {
-                return;
+    protected toSupportedLink(link: URI): URI | undefined {
+        if (WebviewWidget.standardSupportedLinkSchemes.has(link.scheme)) {
+            const linkAsString = link.toString();
+            for (const resourceRoot of [this.externalEndpoint + '/theia-resource', this.externalEndpoint + '/vscode-resource']) {
+                if (linkAsString.startsWith(resourceRoot + '/')) {
+                    return this.normalizeRequestUri(linkAsString.substr(resourceRoot.length));
+                }
             }
-            script!.parentElement!.removeChild(script!);
-            return;
+            return link;
+        }
+        if (!!this.contentOptions.enableCommandUris && link.scheme === Schemes.COMMAND) {
+            return link;
         }
+        return undefined;
+    }
+
+    protected async loadResource(requestPath: string): Promise<void> {
+        const normalizedUri = this.normalizeRequestUri(requestPath);
+        // browser cache does not suppot file scheme, normalize to current endpoint scheme and host
+        const cacheUrl = new Endpoint({ path: normalizedUri.path.toString() }).getRestUrl().toString();
 
-        const codeApiScript = contentDocument.createElement('script');
-        codeApiScript.id = scriptId;
-        codeApiScript.textContent = `
-        window.postMessageExt = window.parent['postMessageExt${this.id}'];
-        const acquireVsCodeApi = (function() {
-                let acquired = false;
-                let state = ${this.state ? `JSON.parse(${JSON.stringify(this.state)})` : undefined};
-                return () => {
-                    if (acquired) {
-                        throw new Error('An instance of the VS Code API has already been acquired');
+        try {
+            if (this.contentOptions.localResourceRoots) {
+                for (const root of this.contentOptions.localResourceRoots) {
+                    if (!new URI(root).path.isEqualOrParent(normalizedUri.path)) {
+                        continue;
                     }
-                    acquired = true;
-                    return Object.freeze({
-                        postMessage: function(msg) {
-                            return window.postMessageExt({ command: 'onmessage', data: msg }, '*');
-                        },
-                        setState: function(newState) {
-                            state = newState;
-                            window.postMessageExt({ command: 'do-update-state', data: JSON.stringify(newState) }, '*');
-                            return newState;
-                        },
-                        getState: function() {
-                            return state;
-                        }
-                    });
-                };
-            })();
-            const acquireTheiaApi = (function() {
-                let acquired = false;
-                let state = ${this.state ? `JSON.parse(${JSON.stringify(this.state)})` : undefined};
-                return () => {
-                    if (acquired) {
-                        throw new Error('An instance of the VS Code API has already been acquired');
+                    let cached = await this.resourceCache.match(cacheUrl);
+                    const response = await this.resourceLoader.load({ uri: normalizedUri.toString(), eTag: cached && cached.eTag });
+                    if (response) {
+                        const { buffer, eTag } = response;
+                        cached = { body: () => new Uint8Array(buffer), eTag: eTag };
+                        this.resourceCache.put(cacheUrl, cached);
                     }
-                    acquired = true;
-                    return Object.freeze({
-                        postMessage: function(msg) {
-                            return window.postMessageExt({ command: 'onmessage', data: msg }, '*');
-                        },
-                        setState: function(newState) {
-                            state = newState;
-                            window.postMessageExt({ command: 'do-update-state', data: JSON.stringify(newState) }, '*');
-                            return newState;
-                        },
-                        getState: function() {
-                            return state;
-                        }
-                    });
-                };
-            })();
-            delete window.parent;
-            delete window.top;
-            delete window.frameElement;
-         `;
-        const parent = contentDocument.head ? contentDocument.head : contentDocument.body;
-        if (parent.hasChildNodes()) {
-            parent.insertBefore(codeApiScript, parent.firstChild);
-        } else {
-            parent.appendChild(codeApiScript);
+                    if (cached) {
+                        const data = await cached.body();
+                        return this.doSend('did-load-resource', {
+                            status: 200,
+                            path: requestPath,
+                            mime: mime.getType(normalizedUri.path.toString()) || 'application/octet-stream',
+                            data
+                        });
+                    }
+                }
+            }
+        } catch {
+            // no-op
         }
+
+        this.resourceCache.delete(cacheUrl);
+        return this.doSend('did-load-resource', {
+            status: 404,
+            path: requestPath
+        });
     }
 
-    /**
-     * Check if given object is ready to receive message and if it is ready, resolve promise
-     */
-    waitReceiveMessage(object: WebviewWidget, resolve: any): void {
-        if (object.readyToReceiveMessage) {
-            resolve(true);
+    protected normalizeRequestUri(requestPath: string): URI {
+        const normalizedPath = decodeURIComponent(requestPath);
+        const requestUri = new URI(normalizedPath.replace(/^\/(\w+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path));
+        if (requestUri.scheme !== 'theia-resource' && requestUri.scheme !== 'vscode-resource') {
+            return requestUri;
+        }
+
+        // Modern vscode-resources uris put the scheme of the requested resource as the authority
+        if (requestUri.authority) {
+            return new URI(requestUri.authority + ':' + requestUri.path);
+        }
+
+        // Old style vscode-resource uris lose the scheme of the resource which means they are unable to
+        // load a mix of local and remote content properly.
+        return requestUri.withScheme('file');
+    }
+
+    sendMessage(data: any): void {
+        if (this.element) {
+            this.doSend('message', data);
         } else {
-            setTimeout(this.waitReceiveMessage, 100, object, resolve);
+            this.pendingMessages.push(data);
         }
     }
 
-    /**
-     * Block until we're able to receive message
-     */
-    public async waitReadyToReceiveMessage(): Promise<boolean> {
-        return new Promise<boolean>((resolve, reject) => {
-            this.waitReceiveMessage(this, resolve);
+    protected doUpdateContent(): void {
+        this.doSend('content', {
+            contents: this.html,
+            options: this.contentOptions,
+            state: this.state
         });
     }
-}
 
+    storeState(): WebviewWidget.State {
+        return {
+            viewType: this.viewType,
+            title: this.title.label,
+            iconUrl: this.iconUrl,
+            options: this.options,
+            contentOptions: this.contentOptions,
+            state: this.state
+        };
+    }
+
+    restoreState(oldState: WebviewWidget.State): void {
+        const { viewType, title, iconUrl, options, contentOptions, state } = oldState;
+        this.viewType = viewType;
+        this.title.label = title;
+        this.setIconUrl(iconUrl);
+        this.options = options;
+        this._contentOptions = contentOptions;
+        this._state = state;
+    }
+
+    protected async doSend(channel: string, data?: any): Promise<void> {
+        if (!this.element) {
+            return;
+        }
+        try {
+            await this.ready.promise;
+            this.postMessage(channel, data);
+        } catch (e) {
+            console.error(e);
+        }
+    }
+
+    protected postMessage(channel: string, data?: any): void {
+        if (this.element) {
+            this.trace('out', channel, data);
+            this.element.contentWindow!.postMessage({ channel, args: data }, '*');
+        }
+    }
+
+    protected on<T = unknown>(channel: WebviewMessageChannels, handler: (data: T) => void): Disposable {
+        const listener = (e: any) => {
+            if (!e || !e.data || e.data.target !== this.identifier.id) {
+                return;
+            }
+            if (e.data.channel === channel) {
+                this.trace('in', e.data.channel, e.data.data);
+                handler(e.data.data);
+            }
+        };
+        window.addEventListener('message', listener);
+        return Disposable.create(() =>
+            window.removeEventListener('message', listener)
+        );
+    }
+
+    protected trace(kind: 'in' | 'out', channel: string, data?: any): void {
+        const value = this.preferences['webview.trace'];
+        if (value === 'off') {
+            return;
+        }
+        const output = this.outputManager.getChannel('webviews');
+        output.append('\n' + this.identifier.id);
+        output.append(kind === 'out' ? ' => ' : ' <= ');
+        output.append(channel);
+        if (value === 'verbose') {
+            if (data) {
+                output.append('\n' + JSON.stringify(data, undefined, 2));
+            }
+        }
+    }
+
+}
 export namespace WebviewWidget {
     export namespace Styles {
-
         export const WEBVIEW = 'theia-webview';
-
+    }
+    export interface State {
+        viewType: string
+        title: string
+        iconUrl?: IconUrl
+        options: WebviewPanelOptions
+        contentOptions: WebviewContentOptions
+        state?: string
     }
 }
diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts
index 8c0909b17f28d..06185115bcd8b 100644
--- a/packages/plugin-ext/src/main/browser/webviews-main.ts
+++ b/packages/plugin-ext/src/main/browser/webviews-main.ts
@@ -14,131 +14,80 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
 
-import { WebviewsMain, MAIN_RPC_CONTEXT, WebviewsExt } from '../../common/plugin-api-rpc';
+import debounce = require('lodash.debounce');
+import URI from 'vscode-uri';
 import { interfaces } from 'inversify';
+import { WebviewsMain, MAIN_RPC_CONTEXT, WebviewsExt, WebviewPanelViewState } from '../../common/plugin-api-rpc';
 import { RPCProtocol } from '../../common/rpc-protocol';
-import { UriComponents } from '../../common/uri-components';
 import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin';
 import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
-import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
-import { WebviewWidget } from './webview/webview';
-import { ThemeService } from '@theia/core/lib/browser/theming';
-import { ThemeRulesService } from './webview/theme-rules-service';
+import { WebviewWidget, WebviewWidgetIdentifier } from './webview/webview';
 import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
 import { ViewColumnService } from './view-column-service';
-import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';
-
-import debounce = require('lodash.debounce');
+import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
+import { JSONExt } from '@phosphor/coreutils/lib/json';
+import { Mutable } from '@theia/core/lib/common/types';
+import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';
+import { IconUrl } from '../../common/plugin-protocol';
 
 export class WebviewsMainImpl implements WebviewsMain, Disposable {
-    private readonly revivers = new Set<string>();
+
     private readonly proxy: WebviewsExt;
     protected readonly shell: ApplicationShell;
+    protected readonly widgets: WidgetManager;
+    protected readonly pluginService: HostedPluginSupport;
     protected readonly viewColumnService: ViewColumnService;
-    protected readonly keybindingRegistry: KeybindingRegistry;
-    protected readonly themeService = ThemeService.get();
-    protected readonly themeRulesService = ThemeRulesService.get();
-    protected readonly updateViewOptions: () => void;
-
-    private readonly views = new Map<string, WebviewWidget>();
-    private readonly viewsOptions = new Map<string, {
-        panelOptions: WebviewPanelShowOptions;
-        options: (WebviewPanelOptions & WebviewOptions) | undefined;
-        panelId: string;
-        active: boolean;
-        visible: boolean;
-    }>();
-
-    protected readonly mouseTracker: ApplicationShellMouseTracker;
-
     private readonly toDispose = new DisposableCollection();
 
     constructor(rpc: RPCProtocol, container: interfaces.Container) {
         this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WEBVIEWS_EXT);
         this.shell = container.get(ApplicationShell);
-        this.mouseTracker = container.get(ApplicationShellMouseTracker);
-        this.keybindingRegistry = container.get(KeybindingRegistry);
         this.viewColumnService = container.get(ViewColumnService);
-        this.updateViewOptions = debounce<() => void>(() => {
-            for (const key of this.viewsOptions.keys()) {
-                this.checkViewOptions(key);
-            }
-        }, 100);
-        this.toDispose.push(this.shell.onDidChangeActiveWidget(() => this.updateViewOptions()));
-        this.toDispose.push(this.shell.onDidChangeCurrentWidget(() => this.updateViewOptions()));
-        this.toDispose.push(this.viewColumnService.onViewColumnChanged(() => this.updateViewOptions()));
+        this.widgets = container.get(WidgetManager);
+        this.pluginService = container.get(HostedPluginSupport);
+        this.toDispose.push(this.shell.onDidChangeActiveWidget(() => this.updateViewStates()));
+        this.toDispose.push(this.shell.onDidChangeCurrentWidget(() => this.updateViewStates()));
+        this.toDispose.push(this.viewColumnService.onViewColumnChanged(() => this.updateViewStates()));
     }
 
     dispose(): void {
         this.toDispose.dispose();
     }
 
-    $createWebviewPanel(
+    async $createWebviewPanel(
         panelId: string,
         viewType: string,
         title: string,
         showOptions: WebviewPanelShowOptions,
-        options: (WebviewPanelOptions & WebviewOptions) | undefined,
-        extensionLocation: UriComponents
-    ): void {
-        const toDisposeOnClose = new DisposableCollection();
-        const toDisposeOnLoad = new DisposableCollection();
-        const view = new WebviewWidget(title, {
-            allowScripts: options ? options.enableScripts : false
-        }, {
-                onMessage: m => {
-                    this.proxy.$onMessage(panelId, m);
-                },
-                onKeyboardEvent: e => {
-                    this.keybindingRegistry.run(e);
-                },
-                onLoad: contentDocument => {
-                    const styleId = 'webview-widget-theme';
-                    let styleElement: HTMLStyleElement | null | undefined;
-                    if (!toDisposeOnLoad.disposed) {
-                        // if reload the frame
-                        toDisposeOnLoad.dispose();
-                        styleElement = <HTMLStyleElement>contentDocument.getElementById(styleId);
-                    }
-                    toDisposeOnClose.push(toDisposeOnLoad);
-                    if (!styleElement) {
-                        const parent = contentDocument.head ? contentDocument.head : contentDocument.body;
-                        styleElement = this.themeRulesService.createStyleSheet(parent);
-                        styleElement.id = styleId;
-                        parent.appendChild(styleElement);
-                    }
+        options: WebviewPanelOptions & WebviewOptions
+    ): Promise<void> {
+        const view = await this.widgets.getOrCreateWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id: panelId });
+        this.hookWebview(view);
+        view.viewType = viewType;
+        view.title.label = title;
+        const { enableFindWidget, retainContextWhenHidden, enableScripts, localResourceRoots, ...contentOptions } = options;
+        view.options = { enableFindWidget, retainContextWhenHidden };
+        view.setContentOptions({
+            allowScripts: enableScripts,
+            localResourceRoots: localResourceRoots && localResourceRoots.map(root => root.toString()),
+            ...contentOptions
+        });
+        this.addOrReattachWidget(view, showOptions);
+    }
 
-                    this.themeRulesService.setRules(styleElement, this.themeRulesService.getCurrentThemeRules());
-                    contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`;
-                    toDisposeOnLoad.push(this.themeService.onThemeChange(() => {
-                        this.themeRulesService.setRules(<HTMLElement>styleElement, this.themeRulesService.getCurrentThemeRules());
-                        contentDocument.body.className = `vscode-${ThemeService.get().getCurrentTheme().id}`;
-                    }));
-                }
-            },
-            this.mouseTracker);
+    protected hookWebview(view: WebviewWidget): void {
+        const handle = view.identifier.id;
+        this.toDispose.push(view.onDidChangeVisibility(() => this.updateViewState(view)));
+        this.toDispose.push(view.onMessage(data => this.proxy.$onMessage(handle, data)));
         view.disposed.connect(() => {
-            toDisposeOnClose.dispose();
-            this.proxy.$onDidDisposeWebviewPanel(panelId);
+            if (this.toDispose.disposed) {
+                return;
+            }
+            this.proxy.$onDidDisposeWebviewPanel(handle);
         });
-        this.toDispose.push(view);
-
-        const viewId = view.id;
-        toDisposeOnClose.push(Disposable.create(() => this.themeRulesService.setIconPath(viewId, undefined)));
-
-        this.views.set(panelId, view);
-        toDisposeOnClose.push(Disposable.create(() => this.views.delete(panelId)));
-
-        this.viewsOptions.set(viewId, { panelOptions: showOptions, options: options, panelId, visible: false, active: false });
-        toDisposeOnClose.push(Disposable.create(() => this.viewsOptions.delete(viewId)));
-
-        this.addOrReattachWidget(panelId, showOptions);
     }
-    private addOrReattachWidget(handler: string, showOptions: WebviewPanelShowOptions): void {
-        const view = this.views.get(handler);
-        if (!view) {
-            return;
-        }
+
+    private addOrReattachWidget(widget: WebviewWidget, showOptions: WebviewPanelShowOptions): void {
         const widgetOptions: ApplicationShell.WidgetOptions = { area: showOptions.area ? showOptions.area : 'main' };
 
         let mode = 'open-to-right';
@@ -159,133 +108,150 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
                     widgetIds = this.viewColumnService.getViewColumnIds(showOptions.viewColumn);
                 }
             }
-            const ref = this.shell.getWidgets(widgetOptions.area).find(widget => widget.isVisible && widgetIds.indexOf(widget.id) !== -1);
+            const ref = this.shell.getWidgets(widgetOptions.area).find(w => !w.isHidden && widgetIds.indexOf(w.id) !== -1);
             if (ref) {
                 Object.assign(widgetOptions, { ref, mode });
             }
         }
 
-        this.shell.addWidget(view, widgetOptions);
-        const visible = true;
-        let active: boolean;
+        this.shell.addWidget(widget, widgetOptions);
         if (showOptions.preserveFocus) {
-            this.shell.revealWidget(view.id);
-            active = false;
+            this.shell.revealWidget(widget.id);
         } else {
-            this.shell.activateWidget(view.id);
-            active = true;
+            this.shell.activateWidget(widget.id);
         }
-        const options = this.viewsOptions.get(view.id);
-        if (!options) {
-            return;
-        }
-        options.panelOptions = showOptions;
-        options.visible = visible;
-        options.active = active;
     }
-    $disposeWebview(handle: string): void {
-        const view = this.views.get(handle);
+
+    async $disposeWebview(handle: string): Promise<void> {
+        const view = await this.tryGetWebview(handle);
         if (view) {
             view.dispose();
         }
     }
-    $reveal(handle: string, showOptions: WebviewPanelShowOptions): void {
-        const view = this.getWebview(handle);
-        if (view.isDisposed) {
+
+    async $reveal(handle: string, showOptions: WebviewPanelShowOptions): Promise<void> {
+        const widget = await this.getWebview(handle);
+        if (widget.isDisposed) {
             return;
         }
-        const options = this.viewsOptions.get(view.id);
-        let retain = false;
-        if (options && options.options && options.options.retainContextWhenHidden) {
-            retain = options.options.retainContextWhenHidden;
-        }
-        if ((showOptions.viewColumn !== undefined && showOptions.viewColumn !== options!.panelOptions.viewColumn) || showOptions.area !== undefined) {
+        if ((showOptions.viewColumn !== undefined && showOptions.viewColumn !== widget.viewState.position) || showOptions.area !== undefined) {
             this.viewColumnService.updateViewColumns();
-            if (!options) {
-                return;
-            }
             const columnIds = showOptions.viewColumn ? this.viewColumnService.getViewColumnIds(showOptions.viewColumn) : [];
-            if (columnIds.indexOf(view.id) === -1 || options.panelOptions.area !== showOptions.area) {
-                this.addOrReattachWidget(options.panelId, showOptions);
-                options.panelOptions = showOptions;
-                this.checkViewOptions(view.id, options.panelOptions.viewColumn);
-                this.updateViewOptions();
+            const area = this.shell.getAreaFor(widget);
+            if (columnIds.indexOf(widget.id) === -1 || area !== showOptions.area) {
+                this.addOrReattachWidget(widget, showOptions);
                 return;
             }
-        } else if (!retain) {
-            // reload content when revealing
-            view.reloadFrame();
         }
-
         if (showOptions.preserveFocus) {
-            this.shell.revealWidget(view.id);
+            this.shell.revealWidget(widget.id);
         } else {
-            this.shell.activateWidget(view.id);
+            this.shell.activateWidget(widget.id);
         }
     }
-    $setTitle(handle: string, value: string): void {
-        const webview = this.getWebview(handle);
+
+    async $setTitle(handle: string, value: string): Promise<void> {
+        const webview = await this.getWebview(handle);
         webview.title.label = value;
     }
-    $setIconPath(handle: string, iconPath: { light: string; dark: string; } | string | undefined): void {
-        const webview = this.getWebview(handle);
-        webview.setIconClass(iconPath ? `webview-icon ${webview.id}-file-icon` : '');
-        this.themeRulesService.setIconPath(webview.id, iconPath);
+
+    async $setIconPath(handle: string, iconUrl: IconUrl | undefined): Promise<void> {
+        const webview = await this.getWebview(handle);
+        webview.setIconUrl(iconUrl);
     }
-    $setHtml(handle: string, value: string): void {
-        const webview = this.getWebview(handle);
+
+    async $setHtml(handle: string, value: string): Promise<void> {
+        const webview = await this.getWebview(handle);
         webview.setHTML(value);
     }
-    $setOptions(handle: string, options: WebviewOptions): void {
-        const webview = this.getWebview(handle);
-        webview.setOptions({ allowScripts: options ? options.enableScripts : false });
+
+    async $setOptions(handle: string, options: WebviewOptions): Promise<void> {
+        const webview = await this.getWebview(handle);
+        const { enableScripts, localResourceRoots, ...contentOptions } = options;
+        webview.setContentOptions({
+            allowScripts: enableScripts,
+            localResourceRoots: localResourceRoots && localResourceRoots.map(root => root.toString()),
+            ...contentOptions
+        });
     }
+
     // tslint:disable-next-line:no-any
-    $postMessage(handle: string, value: any): Thenable<boolean> {
-        const webview = this.getWebview(handle);
-        if (webview) {
-            webview.postMessage(value);
-        }
-        return Promise.resolve(webview !== undefined);
+    async $postMessage(handle: string, value: any): Promise<boolean> {
+        const webview = await this.getWebview(handle);
+        webview.sendMessage(value);
+        return true;
     }
+
     $registerSerializer(viewType: string): void {
-        this.revivers.add(viewType);
+        this.pluginService.registerWebviewReviver(viewType, widget => this.restoreWidget(widget));
         this.toDispose.push(Disposable.create(() => this.$unregisterSerializer(viewType)));
     }
+
     $unregisterSerializer(viewType: string): void {
-        this.revivers.delete(viewType);
+        this.pluginService.unregisterWebviewReviver(viewType);
     }
 
-    private async checkViewOptions(handler: string, viewColumn?: number | undefined): Promise<void> {
-        const options = this.viewsOptions.get(handler);
-        if (!options || !options.panelOptions) {
-            return;
+    protected async restoreWidget(widget: WebviewWidget): Promise<void> {
+        this.hookWebview(widget);
+        const handle = widget.identifier.id;
+        const title = widget.title.label;
+
+        let state = undefined;
+        if (widget.state) {
+            try {
+                state = JSON.parse(widget.state);
+            } catch {
+                // noop
+            }
         }
-        const view = this.views.get(options.panelId);
-        if (!view) {
-            return;
+
+        const options = widget.options;
+        const { allowScripts, localResourceRoots, ...contentOptions } = widget.contentOptions;
+        this.updateViewState(widget);
+        await this.proxy.$deserializeWebviewPanel(handle, widget.viewType, title, state, widget.viewState, {
+            enableScripts: allowScripts,
+            localResourceRoots: localResourceRoots && localResourceRoots.map(root => URI.parse(root)),
+            ...contentOptions,
+            ...options
+        });
+    }
+
+    protected readonly updateViewStates = debounce(() => {
+        for (const widget of this.widgets.getWidgets(WebviewWidget.FACTORY_ID)) {
+            if (widget instanceof WebviewWidget) {
+                this.updateViewState(widget);
+            }
         }
-        const active = !!this.shell.activeWidget ? this.shell.activeWidget.id === view!.id : false;
-        const visible = view!.isVisible;
-        if (viewColumn === undefined) {
+    }, 100);
+
+    private updateViewState(widget: WebviewWidget, viewColumn?: number | undefined): void {
+        const viewState: Mutable<WebviewPanelViewState> = {
+            active: this.shell.activeWidget === widget,
+            visible: !widget.isHidden,
+            position: viewColumn || 0
+        };
+        if (typeof viewColumn !== 'number') {
             this.viewColumnService.updateViewColumns();
-            viewColumn = this.viewColumnService.hasViewColumn(view.id) ? this.viewColumnService.getViewColumn(view.id)! : 0;
-            if (options.panelOptions.viewColumn === viewColumn && options.visible === visible && options.active === active) {
-                return;
-            }
+            viewState.position = this.viewColumnService.getViewColumn(widget.id) || 0;
+        }
+        // tslint:disable-next-line:no-any
+        if (JSONExt.deepEqual(<any>viewState, <any>widget.viewState)) {
+            return;
         }
-        options.active = active;
-        options.visible = visible;
-        options.panelOptions.viewColumn = viewColumn;
-        this.proxy.$onDidChangeWebviewPanelViewState(options.panelId, { active, visible, position: options.panelOptions.viewColumn! });
+        widget.viewState = viewState;
+        this.proxy.$onDidChangeWebviewPanelViewState(widget.identifier.id, widget.viewState);
     }
 
-    private getWebview(viewId: string): WebviewWidget {
-        const webview = this.views.get(viewId);
+    private async getWebview(viewId: string): Promise<WebviewWidget> {
+        const webview = await this.tryGetWebview(viewId);
         if (!webview) {
             throw new Error(`Unknown Webview: ${viewId}`);
         }
         return webview;
     }
 
+    private async tryGetWebview(id: string): Promise<WebviewWidget | undefined> {
+        return this.widgets.getWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id });
+    }
+
 }
diff --git a/packages/plugin-ext/src/main/browser/window-state-main.ts b/packages/plugin-ext/src/main/browser/window-state-main.ts
index 7d199850b0d36..8d0113df33517 100644
--- a/packages/plugin-ext/src/main/browser/window-state-main.ts
+++ b/packages/plugin-ext/src/main/browser/window-state-main.ts
@@ -15,24 +15,29 @@
  ********************************************************************************/
 
 import URI from 'vscode-uri';
+import CoreURI from '@theia/core/lib/common/uri';
 import { interfaces } from 'inversify';
 import { WindowStateExt, MAIN_RPC_CONTEXT, WindowMain } from '../../common/plugin-api-rpc';
 import { RPCProtocol } from '../../common/rpc-protocol';
 import { UriComponents } from '../../common/uri-components';
 import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
-import { WindowService } from '@theia/core/lib/browser/window/window-service';
+import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
+import { ExternalUriService } from '@theia/core/lib/browser/external-uri-service';
 
 export class WindowStateMain implements WindowMain, Disposable {
 
     private readonly proxy: WindowStateExt;
 
-    private readonly windowService: WindowService;
+    private readonly openerService: OpenerService;
+
+    private readonly externalUriService: ExternalUriService;
 
     private readonly toDispose = new DisposableCollection();
 
     constructor(rpc: RPCProtocol, container: interfaces.Container) {
         this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.WINDOW_STATE_EXT);
-        this.windowService = container.get(WindowService);
+        this.openerService = container.get(OpenerService);
+        this.externalUriService = container.get(ExternalUriService);
 
         const fireDidFocus = () => this.onFocusChanged(true);
         window.addEventListener('focus', fireDidFocus);
@@ -53,13 +58,19 @@ export class WindowStateMain implements WindowMain, Disposable {
 
     async $openUri(uriComponent: UriComponents): Promise<boolean> {
         const uri = URI.revive(uriComponent);
-        const url = encodeURI(uri.toString(true));
+        const url = new CoreURI(encodeURI(uri.toString(true)));
         try {
-            this.windowService.openNewWindow(url, { external: true });
+            await open(this.openerService, url);
             return true;
         } catch (e) {
             return false;
         }
     }
 
+    async $asExternalUri(uriComponents: UriComponents): Promise<UriComponents> {
+        const uri = URI.revive(uriComponents);
+        const resolved = await this.externalUriService.resolve(new CoreURI(uri));
+        return URI.parse(resolved.toString());
+    }
+
 }
diff --git a/packages/plugin-ext/src/main/common/webview-protocol.ts b/packages/plugin-ext/src/main/common/webview-protocol.ts
new file mode 100644
index 0000000000000..58a06a59fccb7
--- /dev/null
+++ b/packages/plugin-ext/src/main/common/webview-protocol.ts
@@ -0,0 +1,48 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+/**
+ * Each webview should be deployed on a unique origin (https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)
+ * to ensure isolation from browser shared state as cookies, local storage and so on.
+ *
+ * Use `THEIA_WEBVIEW_EXTERNAL_ENDPOINT` to customize the hostname pattern of a origin.
+ * By default is `{{uuid}}.webview.{{hostname}}`. Where `{{uuid}}` is a placeholder for a webview global id.
+ */
+export namespace WebviewExternalEndpoint {
+    export const pattern = 'THEIA_WEBVIEW_EXTERNAL_ENDPOINT';
+    export const defaultPattern = '{{uuid}}.webview.{{hostname}}';
+}
+
+export interface LoadWebviewResourceParams {
+    uri: string
+    eTag?: string
+}
+
+export interface LoadWebviewResourceResult {
+    buffer: number[]
+    eTag: string
+}
+
+export const WebviewResourceLoader = Symbol('WebviewResourceLoader');
+export interface WebviewResourceLoader {
+    /**
+     * Loads initial webview resource data.
+     * Returns `undefined` if a resource has not beed modified.
+     * Throws if a resource cannot be loaded.
+     */
+    load(params: LoadWebviewResourceParams): Promise<LoadWebviewResourceResult | undefined>
+}
+export const WebviewResourceLoaderPath = '/services/webview-resource-loader';
diff --git a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts
index 4d1b2d9047316..c364c7ae25c89 100644
--- a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts
+++ b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts
@@ -34,8 +34,17 @@ import { PluginPathsService, pluginPathsServicePath } from '../common/plugin-pat
 import { PluginPathsServiceImpl } from './paths/plugin-paths-service';
 import { PluginServerHandler } from './plugin-server-handler';
 import { PluginCliContribution } from './plugin-cli-contribution';
+import { WebviewResourceLoaderImpl } from './webview-resource-loader-impl';
+import { WebviewResourceLoaderPath } from '../common/webview-protocol';
 
 export function bindMainBackend(bind: interfaces.Bind): void {
+    bind(WebviewResourceLoaderImpl).toSelf().inSingletonScope();
+    bind(ConnectionHandler).toDynamicValue(ctx =>
+        new JsonRpcConnectionHandler(WebviewResourceLoaderPath, () =>
+            ctx.container.get(WebviewResourceLoaderImpl)
+        )
+    ).inSingletonScope();
+
     bind(PluginApiContribution).toSelf().inSingletonScope();
     bind(BackendApplicationContribution).toService(PluginApiContribution);
 
diff --git a/packages/plugin-ext/src/main/node/plugin-service.ts b/packages/plugin-ext/src/main/node/plugin-service.ts
index 7715448592973..1f2572f5672bb 100644
--- a/packages/plugin-ext/src/main/node/plugin-service.ts
+++ b/packages/plugin-ext/src/main/node/plugin-service.ts
@@ -13,23 +13,37 @@
  *
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
+
+import * as path from 'path';
+import connect = require('connect');
+import serveStatic = require('serve-static');
+const vhost = require('vhost');
 import * as express from 'express';
 import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
 import { injectable } from 'inversify';
-import { FileUri } from '@theia/core/lib/node';
+import { WebviewExternalEndpoint } from '../common/webview-protocol';
 
 const pluginPath = (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) + './theia/plugins/';
 
 @injectable()
 export class PluginApiContribution implements BackendApplicationContribution {
+
     configure(app: express.Application): void {
         app.get('/plugin/:path(*)', (req, res) => {
             const filePath: string = req.params.path;
             res.sendFile(pluginPath + filePath);
         });
 
-        app.get('/webview/:path(*)', (req, res) => {
-            res.sendFile(FileUri.fsPath('file:/' + req.params.path));
-        });
+        const webviewApp = connect();
+        webviewApp.use('/webview', serveStatic(path.join(__dirname, '../../../src/main/browser/webview/pre')));
+        const webviewExternalEndpoint = this.webviewExternalEndpoint();
+        console.log(`Configuring to accept webviews on '${webviewExternalEndpoint}' hostname.`);
+        app.use(vhost(new RegExp(webviewExternalEndpoint, 'i'), webviewApp));
+    }
+
+    protected webviewExternalEndpoint(): string {
+        return (process.env[WebviewExternalEndpoint.pattern] || WebviewExternalEndpoint.defaultPattern)
+            .replace('{{uuid}}', '.+')
+            .replace('{{hostname}}', '.+');
     }
 }
diff --git a/packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts b/packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts
new file mode 100644
index 0000000000000..16d769a327cde
--- /dev/null
+++ b/packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts
@@ -0,0 +1,43 @@
+/********************************************************************************
+ * Copyright (C) 2019 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as fs from 'fs-extra';
+import * as crypto from 'crypto';
+import { injectable } from 'inversify';
+import { WebviewResourceLoader, LoadWebviewResourceParams, LoadWebviewResourceResult } from '../common/webview-protocol';
+import { FileUri } from '@theia/core/lib/node/file-uri';
+
+@injectable()
+export class WebviewResourceLoaderImpl implements WebviewResourceLoader {
+
+    async load(params: LoadWebviewResourceParams): Promise<LoadWebviewResourceResult | undefined> {
+        const fsPath = FileUri.fsPath(params.uri);
+        const stat = await fs.stat(fsPath);
+        const eTag = this.compileETag(fsPath, stat);
+        if ('eTag' in params && params.eTag === eTag) {
+            return undefined;
+        }
+        const buffer = await fs.readFile(FileUri.fsPath(params.uri));
+        return { buffer: buffer.toJSON().data, eTag };
+    }
+
+    protected compileETag(fsPath: string, stat: fs.Stats): string {
+        return crypto.createHash('md5')
+            .update(fsPath + stat.mtime.getTime() + stat.size, 'utf8')
+            .digest('base64');
+    }
+
+}
diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts
index 2937388b309fd..05e05bee14978 100644
--- a/packages/plugin-ext/src/plugin/plugin-context.ts
+++ b/packages/plugin-ext/src/plugin/plugin-context.ts
@@ -130,7 +130,6 @@ import { MarkdownString } from './markdown-string';
 import { TreeViewsExtImpl } from './tree/tree-views';
 import { LanguagesContributionExtImpl } from './languages-contribution-ext';
 import { ConnectionExtImpl } from './connection-ext';
-import { WebviewsExtImpl } from './webviews';
 import { TasksExtImpl } from './tasks/tasks';
 import { DebugExtImpl } from './node/debug/debug';
 import { FileSystemExtImpl } from './file-system';
@@ -140,6 +139,7 @@ import { DecorationProvider, LineChange } from '@theia/plugin';
 import { DecorationsExtImpl } from './decorations';
 import { TextEditorExt } from './text-editor';
 import { ClipboardExt } from './clipboard-ext';
+import { WebviewsExtImpl } from './webviews';
 
 export function createAPIFactory(
     rpc: RPCProtocol,
@@ -150,7 +150,8 @@ export function createAPIFactory(
     editorsAndDocumentsExt: EditorsAndDocumentsExtImpl,
     workspaceExt: WorkspaceExtImpl,
     messageRegistryExt: MessageRegistryExt,
-    clipboard: ClipboardExt
+    clipboard: ClipboardExt,
+    webviewExt: WebviewsExtImpl
 ): PluginAPIFactory {
 
     const commandRegistry = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc));
@@ -165,7 +166,6 @@ export function createAPIFactory(
     const outputChannelRegistryExt = rpc.set(MAIN_RPC_CONTEXT.OUTPUT_CHANNEL_REGISTRY_EXT, new OutputChannelRegistryExtImpl(rpc));
     const languagesExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_EXT, new LanguagesExtImpl(rpc, documents, commandRegistry));
     const treeViewsExt = rpc.set(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT, new TreeViewsExtImpl(rpc, commandRegistry));
-    const webviewExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, new WebviewsExtImpl(rpc));
     const tasksExt = rpc.set(MAIN_RPC_CONTEXT.TASKS_EXT, new TasksExtImpl(rpc));
     const connectionExt = rpc.set(MAIN_RPC_CONTEXT.CONNECTION_EXT, new ConnectionExtImpl(rpc));
     const languagesContributionExt = rpc.set(MAIN_RPC_CONTEXT.LANGUAGES_CONTRIBUTION_EXT, new LanguagesContributionExtImpl(rpc, connectionExt));
@@ -338,11 +338,11 @@ export function createAPIFactory(
             createWebviewPanel(viewType: string,
                 title: string,
                 showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions,
-                options: theia.WebviewPanelOptions & theia.WebviewOptions): theia.WebviewPanel {
-                return webviewExt.createWebview(viewType, title, showOptions, options, Uri.file(plugin.pluginPath));
+                options: theia.WebviewPanelOptions & theia.WebviewOptions = {}): theia.WebviewPanel {
+                return webviewExt.createWebview(viewType, title, showOptions, options, plugin);
             },
             registerWebviewPanelSerializer(viewType: string, serializer: theia.WebviewPanelSerializer): theia.Disposable {
-                return webviewExt.registerWebviewPanelSerializer(viewType, serializer);
+                return webviewExt.registerWebviewPanelSerializer(viewType, serializer, plugin);
             },
             get state(): theia.WindowState {
                 return windowStateExt.getWindowState();
@@ -508,6 +508,9 @@ export function createAPIFactory(
             },
             openExternal(uri: theia.Uri): PromiseLike<boolean> {
                 return windowStateExt.openUri(uri);
+            },
+            asExternalUri(target: theia.Uri): PromiseLike<theia.Uri> {
+                return windowStateExt.asExternalUri(target);
             }
         });
 
diff --git a/packages/plugin-ext/src/plugin/plugin-icon-path.ts b/packages/plugin-ext/src/plugin/plugin-icon-path.ts
new file mode 100644
index 0000000000000..3c709ab31ce77
--- /dev/null
+++ b/packages/plugin-ext/src/plugin/plugin-icon-path.ts
@@ -0,0 +1,50 @@
+/********************************************************************************
+ * Copyright (C) 2019 Red Hat, Inc. and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as path from 'path';
+import Uri from 'vscode-uri';
+import { IconUrl, PluginPackage } from '../common/plugin-protocol';
+import { Plugin } from '../common/plugin-api-rpc';
+
+export type PluginIconPath = string | Uri | {
+    light: string | Uri,
+    dark: string | Uri
+};
+export namespace PluginIconPath {
+    export function toUrl(iconPath: PluginIconPath | undefined, plugin: Plugin): IconUrl | undefined {
+        if (!iconPath) {
+            return undefined;
+        }
+        if (typeof iconPath === 'object' && 'light' in iconPath) {
+            return {
+                light: asString(iconPath.light, plugin),
+                dark: asString(iconPath.dark, plugin)
+            };
+        }
+        return asString(iconPath, plugin);
+    }
+    export function asString(arg: string | Uri, plugin: Plugin): string {
+        arg = arg instanceof Uri && arg.scheme === 'file' ? arg.fsPath : arg;
+        if (typeof arg !== 'string') {
+            return arg.toString(true);
+        }
+        const { packagePath } = plugin.rawModel;
+        const absolutePath = path.isAbsolute(arg) ? arg : path.join(packagePath, arg);
+        const normalizedPath = path.normalize(absolutePath);
+        const relativePath = path.relative(packagePath, normalizedPath);
+        return PluginPackage.toPluginUrl(plugin.rawModel, relativePath);
+    }
+}
diff --git a/packages/plugin-ext/src/plugin/plugin-manager.ts b/packages/plugin-ext/src/plugin/plugin-manager.ts
index 4ccd8c4b6b0f5..4f794a2e725d1 100644
--- a/packages/plugin-ext/src/plugin/plugin-manager.ts
+++ b/packages/plugin-ext/src/plugin/plugin-manager.ts
@@ -39,6 +39,7 @@ import { RPCProtocol } from '../common/rpc-protocol';
 import { Emitter } from '@theia/core/lib/common/event';
 import * as os from 'os';
 import * as fs from 'fs-extra';
+import { WebviewsExtImpl } from './webviews';
 
 export interface PluginHost {
 
@@ -72,7 +73,8 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
         'onDebug', 'onDebugInitialConfigurations', 'onDebugResolve', 'onDebugAdapterProtocolTracker',
         'workspaceContains',
         'onView',
-        'onUri'
+        'onUri',
+        'onWebviewPanel'
     ]);
 
     private readonly registry = new Map<string, Plugin>();
@@ -94,6 +96,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
         private readonly envExt: EnvExtImpl,
         private readonly storageProxy: KeyValueStorageProxy,
         private readonly preferencesManager: PreferenceRegistryExtImpl,
+        private readonly webview: WebviewsExtImpl,
         private readonly rpc: RPCProtocol
     ) {
         this.messageRegistryProxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.MESSAGE_REGISTRY_MAIN);
@@ -149,6 +152,8 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
         if (params.extApi) {
             this.host.initExtApi(params.extApi);
         }
+
+        this.webview.init(params.webview);
     }
 
     async $start(params: PluginManagerStartParams): Promise<void> {
diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts
index 81a79ef52b60d..1ecc7fd96a728 100644
--- a/packages/plugin-ext/src/plugin/tree/tree-views.ts
+++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts
@@ -16,8 +16,6 @@
 
 // tslint:disable:no-any
 
-import * as path from 'path';
-import URI from 'vscode-uri';
 import {
     TreeDataProvider, TreeView, TreeViewExpansionEvent, TreeItem2, TreeItemLabel,
     TreeViewSelectionChangeEvent, TreeViewVisibilityChangeEvent
@@ -31,7 +29,7 @@ import { Plugin, PLUGIN_RPC_CONTEXT, TreeViewsExt, TreeViewsMain, TreeViewItem }
 import { RPCProtocol } from '../../common/rpc-protocol';
 import { CommandRegistryImpl, CommandsConverter } from '../command-registry';
 import { TreeViewSelection } from '../../common';
-import { PluginPackage } from '../../common/plugin-protocol';
+import { PluginIconPath } from '../plugin-icon-path';
 
 export class TreeViewsExtImpl implements TreeViewsExt {
 
@@ -279,31 +277,12 @@ class TreeViewExtImpl<T> implements Disposable {
                 let iconUrl;
                 let themeIconId;
                 const { iconPath } = treeItem;
-                if (iconPath) {
-                    const toUrl = (arg: string | URI) => {
-                        arg = arg instanceof URI && arg.scheme === 'file' ? arg.fsPath : arg;
-                        if (typeof arg !== 'string') {
-                            return arg.toString(true);
-                        }
-                        const { packagePath } = this.plugin.rawModel;
-                        const absolutePath = path.isAbsolute(arg) ? arg : path.join(packagePath, arg);
-                        const normalizedPath = path.normalize(absolutePath);
-                        const relativePath = path.relative(packagePath, normalizedPath);
-                        return PluginPackage.toPluginUrl(this.plugin.rawModel, relativePath);
-                    };
-                    if (typeof iconPath === 'string' && iconPath.indexOf('fa-') !== -1) {
-                        icon = iconPath;
-                    } else if (iconPath instanceof ThemeIcon) {
-                        themeIconId = iconPath.id;
-                    } else if (typeof iconPath === 'string' || iconPath instanceof URI) {
-                        iconUrl = toUrl(iconPath);
-                    } else {
-                        const { light, dark } = iconPath as { light: string | URI, dark: string | URI };
-                        iconUrl = {
-                            light: toUrl(light),
-                            dark: toUrl(dark)
-                        };
-                    }
+                if (typeof iconPath === 'string' && iconPath.indexOf('fa-') !== -1) {
+                    icon = iconPath;
+                } else if (iconPath instanceof ThemeIcon) {
+                    themeIconId = iconPath.id;
+                } else {
+                    iconUrl = PluginIconPath.toUrl(<PluginIconPath | undefined>iconPath, this.plugin);
                 }
 
                 const treeViewItem = {
diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts
index 7611998af941f..eeda7354eb984 100644
--- a/packages/plugin-ext/src/plugin/webviews.ts
+++ b/packages/plugin-ext/src/plugin/webviews.ts
@@ -14,25 +14,38 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
 
-import { WebviewsExt, WebviewPanelViewState, WebviewsMain, PLUGIN_RPC_CONTEXT, /* WebviewsMain, PLUGIN_RPC_CONTEXT  */ } from '../common/plugin-api-rpc';
+import { v4 } from 'uuid';
+import { WebviewsExt, WebviewPanelViewState, WebviewsMain, PLUGIN_RPC_CONTEXT, WebviewInitData, /* WebviewsMain, PLUGIN_RPC_CONTEXT  */ } from '../common/plugin-api-rpc';
 import * as theia from '@theia/plugin';
 import { RPCProtocol } from '../common/rpc-protocol';
-import URI from 'vscode-uri/lib/umd';
+import { Plugin } from '../common/plugin-api-rpc';
+import URI from 'vscode-uri';
 import { Emitter, Event } from '@theia/core/lib/common/event';
 import { fromViewColumn, toViewColumn, toWebviewPanelShowOptions } from './type-converters';
-import { IdGenerator } from '../common/id-generator';
 import { Disposable, WebviewPanelTargetArea } from './types-impl';
+import { WorkspaceExtImpl } from './workspace';
+import { PluginIconPath } from './plugin-icon-path';
 
 export class WebviewsExtImpl implements WebviewsExt {
     private readonly proxy: WebviewsMain;
-    private readonly idGenerator = new IdGenerator('v');
     private readonly webviewPanels = new Map<string, WebviewPanelImpl>();
-    private readonly serializers = new Map<string, theia.WebviewPanelSerializer>();
-
-    constructor(rpc: RPCProtocol) {
+    private readonly serializers = new Map<string, {
+        serializer: theia.WebviewPanelSerializer,
+        plugin: Plugin
+    }>();
+    private initData: WebviewInitData | undefined;
+
+    constructor(
+        rpc: RPCProtocol,
+        private readonly workspace: WorkspaceExtImpl,
+    ) {
         this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.WEBVIEWS_MAIN);
     }
 
+    init(initData: WebviewInitData): void {
+        this.initData = initData;
+    }
+
     // tslint:disable-next-line:no-any
     $onMessage(handle: string, message: any): void {
         const panel = this.getWebviewPanel(handle);
@@ -65,45 +78,55 @@ export class WebviewsExtImpl implements WebviewsExt {
         title: string,
         // tslint:disable-next-line:no-any
         state: any,
-        position: number,
+        viewState: WebviewPanelViewState,
         options: theia.WebviewOptions & theia.WebviewPanelOptions): PromiseLike<void> {
-        const serializer = this.serializers.get(viewType);
-        if (!serializer) {
+        if (!this.initData) {
+            return Promise.reject(new Error('Webviews are not initialized'));
+        }
+        const entry = this.serializers.get(viewType);
+        if (!entry) {
             return Promise.reject(new Error(`No serializer found for '${viewType}'`));
         }
+        const { serializer, plugin } = entry;
 
-        const webview = new WebviewImpl(viewId, this.proxy, options);
-        const revivedPanel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, toViewColumn(position)!, options, webview);
+        const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin);
+        const revivedPanel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, toViewColumn(viewState.position)!, options, webview);
+        revivedPanel.setActive(viewState.active);
+        revivedPanel.setVisible(viewState.visible);
         this.webviewPanels.set(viewId, revivedPanel);
         return serializer.deserializeWebviewPanel(revivedPanel, state);
     }
 
-    createWebview(viewType: string,
+    createWebview(
+        viewType: string,
         title: string,
         showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions,
-        options: (theia.WebviewPanelOptions & theia.WebviewOptions) | undefined,
-        extensionLocation: URI): theia.WebviewPanel {
-
+        options: theia.WebviewPanelOptions & theia.WebviewOptions,
+        plugin: Plugin
+    ): theia.WebviewPanel {
+        if (!this.initData) {
+            throw new Error('Webviews are not initialized');
+        }
         const webviewShowOptions = toWebviewPanelShowOptions(showOptions);
-        const viewId = this.idGenerator.nextId();
-        this.proxy.$createWebviewPanel(viewId, viewType, title, webviewShowOptions, options, extensionLocation);
+        const viewId = v4();
+        this.proxy.$createWebviewPanel(viewId, viewType, title, webviewShowOptions, WebviewImpl.toWebviewOptions(options, this.workspace, plugin));
 
-        const webview = new WebviewImpl(viewId, this.proxy, options);
+        const webview = new WebviewImpl(viewId, this.proxy, options, this.initData, this.workspace, plugin);
         const panel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, webviewShowOptions, options, webview);
         this.webviewPanels.set(viewId, panel);
         return panel;
-
     }
 
     registerWebviewPanelSerializer(
         viewType: string,
-        serializer: theia.WebviewPanelSerializer
+        serializer: theia.WebviewPanelSerializer,
+        plugin: Plugin
     ): theia.Disposable {
         if (this.serializers.has(viewType)) {
             throw new Error(`Serializer for '${viewType}' already registered`);
         }
 
-        this.serializers.set(viewType, serializer);
+        this.serializers.set(viewType, { serializer, plugin });
         this.proxy.$registerSerializer(viewType);
 
         return new Disposable(() => {
@@ -131,10 +154,15 @@ export class WebviewImpl implements theia.Webview {
     // tslint:disable-next-line:no-any
     public readonly onDidReceiveMessage: Event<any> = this.onMessageEmitter.event;
 
-    constructor(private readonly viewId: string,
+    constructor(
+        private readonly viewId: string,
         private readonly proxy: WebviewsMain,
-        options: theia.WebviewOptions | undefined) {
-        this._options = options!;
+        options: theia.WebviewOptions,
+        private readonly initData: WebviewInitData,
+        private readonly workspace: WorkspaceExtImpl,
+        readonly plugin: Plugin
+    ) {
+        this._options = options;
     }
 
     dispose(): void {
@@ -145,33 +173,30 @@ export class WebviewImpl implements theia.Webview {
         this.onMessageEmitter.dispose();
     }
 
-    // tslint:disable-next-line:no-any
-    postMessage(message: any): PromiseLike<boolean> {
+    asWebviewUri(resource: theia.Uri): theia.Uri {
+        const uri = this.initData.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 + resource.toString().replace(/^\S+?:/, ''))
+            .replace('{{uuid}}', this.viewId);
+        return URI.parse(uri);
+    }
+
+    get cspSource(): string {
+        return this.initData.webviewCspSource.replace('{{uuid}}', this.viewId);
+    }
+
+    get html(): string {
         this.checkIsDisposed();
-        // replace theia-resource: content in the given message
-        const decoded = JSON.stringify(message);
-        let newMessage = decoded.replace(new RegExp('theia-resource:/', 'g'), '/webview/');
-        if (this._options && this._options.localResourceRoots) {
-            newMessage = this.filterLocalRoots(newMessage, this._options.localResourceRoots);
-        }
-        return this.proxy.$postMessage(this.viewId, JSON.parse(newMessage));
+        return this._html;
     }
 
-    protected filterLocalRoots(content: string, localResourceRoots: ReadonlyArray<theia.Uri>): string {
-        const webViewsRegExp = /"(\/webview\/.*?)\"/g;
-        let m;
-        while ((m = webViewsRegExp.exec(content)) !== null) {
-            if (m.index === webViewsRegExp.lastIndex) {
-                webViewsRegExp.lastIndex++;
-            }
-            // take group 1 which is webview URL
-            const url = m[1];
-            const isIncluded = localResourceRoots.some((uri): boolean => url.substring('/webview'.length).startsWith(uri.fsPath));
-            if (!isIncluded) {
-                content = content.replace(url, url.replace('/webview', '/webview-disallowed-localroot'));
-            }
+    set html(value: string) {
+        this.checkIsDisposed();
+        if (this._html !== value) {
+            this._html = value;
+            this.proxy.$setHtml(this.viewId, value);
         }
-        return content;
     }
 
     get options(): theia.WebviewOptions {
@@ -181,26 +206,14 @@ export class WebviewImpl implements theia.Webview {
 
     set options(newOptions: theia.WebviewOptions) {
         this.checkIsDisposed();
-        this.proxy.$setOptions(this.viewId, newOptions);
+        this.proxy.$setOptions(this.viewId, WebviewImpl.toWebviewOptions(newOptions, this.workspace, this.plugin));
         this._options = newOptions;
     }
 
-    get html(): string {
-        this.checkIsDisposed();
-        return this._html;
-    }
-
-    set html(html: string) {
-        let newHtml = html.replace(new RegExp('theia-resource:/', 'g'), '/webview/');
-        if (this._options && this._options.localResourceRoots) {
-            newHtml = this.filterLocalRoots(newHtml, this._options.localResourceRoots);
-        }
-
+    // tslint:disable-next-line:no-any
+    postMessage(message: any): PromiseLike<boolean> {
         this.checkIsDisposed();
-        if (this._html !== newHtml) {
-            this._html = newHtml;
-            this.proxy.$setHtml(this.viewId, newHtml);
-        }
+        return this.proxy.$postMessage(this.viewId, message);
     }
 
     private checkIsDisposed(): void {
@@ -208,6 +221,16 @@ export class WebviewImpl implements theia.Webview {
             throw new Error('This Webview is disposed!');
         }
     }
+
+    static toWebviewOptions(options: theia.WebviewOptions, workspace: WorkspaceExtImpl, plugin: Plugin): theia.WebviewOptions {
+        return {
+            ...options,
+            localResourceRoots: options.localResourceRoots || [
+                ...(workspace.workspaceFolders || []).map(x => x.uri),
+                URI.file(plugin.pluginFolder)
+            ]
+        };
+    }
 }
 
 export class WebviewPanelImpl implements theia.WebviewPanel {
@@ -216,6 +239,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel {
     private _active = true;
     private _visible = true;
     private _showOptions: theia.WebviewPanelShowOptions;
+    private _iconPath: theia.Uri | { light: theia.Uri; dark: theia.Uri } | undefined;
 
     readonly onDisposeEmitter = new Emitter<void>();
     public readonly onDidDispose: Event<void> = this.onDisposeEmitter.event;
@@ -228,11 +252,10 @@ export class WebviewPanelImpl implements theia.WebviewPanel {
         private readonly _viewType: string,
         private _title: string,
         showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions,
-        private readonly _options: theia.WebviewPanelOptions | undefined,
+        private readonly _options: theia.WebviewPanelOptions,
         private readonly _webview: WebviewImpl
     ) {
         this._showOptions = typeof showOptions === 'object' ? showOptions : { viewColumn: showOptions as theia.ViewColumn };
-        this.setViewColumn(undefined);
     }
 
     dispose(): void {
@@ -268,19 +291,15 @@ export class WebviewPanelImpl implements theia.WebviewPanel {
         }
     }
 
-    set iconPath(iconPath: theia.Uri | { light: theia.Uri; dark: theia.Uri }) {
+    get iconPath(): theia.Uri | { light: theia.Uri; dark: theia.Uri } | undefined {
+        return this._iconPath;
+    }
+
+    set iconPath(iconPath: theia.Uri | { light: theia.Uri; dark: theia.Uri } | undefined) {
         this.checkIsDisposed();
-        if (URI.isUri(iconPath)) {
-            if ('http' === iconPath.scheme || 'https' === iconPath.scheme) {
-                this.proxy.$setIconPath(this.viewId, iconPath.toString());
-            } else {
-                this.proxy.$setIconPath(this.viewId, (<theia.Uri>iconPath).path);
-            }
-        } else {
-            this.proxy.$setIconPath(this.viewId, {
-                light: (<{ light: theia.Uri; dark: theia.Uri }>iconPath).light.path,
-                dark: (<{ light: theia.Uri; dark: theia.Uri }>iconPath).dark.path
-            });
+        if (this._iconPath !== iconPath) {
+            this._iconPath = iconPath;
+            this.proxy.$setIconPath(this.viewId, PluginIconPath.toUrl(iconPath, this._webview.plugin));
         }
     }
 
@@ -291,7 +310,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel {
 
     get options(): theia.WebviewPanelOptions {
         this.checkIsDisposed();
-        return this._options!;
+        return this._options;
     }
 
     get viewColumn(): theia.ViewColumn | undefined {
diff --git a/packages/plugin-ext/src/plugin/window-state.ts b/packages/plugin-ext/src/plugin/window-state.ts
index a75a34cb3bca5..eb5c5a065d031 100644
--- a/packages/plugin-ext/src/plugin/window-state.ts
+++ b/packages/plugin-ext/src/plugin/window-state.ts
@@ -19,6 +19,7 @@ import { WindowState } from '@theia/plugin';
 import { WindowStateExt, WindowMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
 import { Event, Emitter } from '@theia/core/lib/common/event';
 import { RPCProtocol } from '../common/rpc-protocol';
+import { Schemes } from '../common/uri-components';
 
 export class WindowStateExtImpl implements WindowStateExt {
 
@@ -52,4 +53,16 @@ export class WindowStateExtImpl implements WindowStateExt {
         return this.proxy.$openUri(uri);
     }
 
+    async asExternalUri(target: URI): Promise<URI> {
+        if (!target.scheme.trim().length) {
+            throw new Error('Invalid scheme - cannot be empty');
+        }
+        if (Schemes.HTTP !== target.scheme && Schemes.HTTPS !== target.scheme) {
+            throw new Error(`Invalid scheme '${target.scheme}'`);
+        }
+
+        const uri = await this.proxy.$asExternalUri(target);
+        return URI.revive(uri);
+    }
+
 }
diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts
index 562c9e4926f0d..271666d6210cd 100644
--- a/packages/plugin/src/theia.d.ts
+++ b/packages/plugin/src/theia.d.ts
@@ -2718,6 +2718,21 @@ declare module '@theia/plugin' {
         update(key: string, value: any): PromiseLike<void>;
     }
 
+    /**
+     * Defines a port mapping used for localhost inside the webview.
+     */
+    export interface WebviewPortMapping {
+        /**
+         * Localhost port to remap inside the webview.
+         */
+        readonly webviewPort: number;
+
+        /**
+         * Destination port. The `webviewPort` is resolved to this port.
+         */
+        readonly extensionHostPort: number;
+    }
+
     /**
      * Content settings for a webview.
      */
@@ -2744,6 +2759,21 @@ declare module '@theia/plugin' {
          * Pass in an empty array to disallow access to any local resources.
          */
         readonly localResourceRoots?: ReadonlyArray<Uri>;
+
+        /**
+         * Mappings of localhost ports used inside the webview.
+         *
+         * Port mapping allow webviews to transparently define how localhost ports are resolved. This can be used
+         * to allow using a static localhost port inside the webview that is resolved to random port that a service is
+         * running on.
+         *
+         * If a webview accesses localhost content, we recommend that you specify port mappings even if
+         * the `webviewPort` and `extensionHostPort` ports are the same.
+         *
+         * *Note* that port mappings only work for `http` or `https` urls. Websocket urls (e.g. `ws://localhost:3000`)
+         * cannot be mapped to another port.
+         */
+        readonly portMapping?: ReadonlyArray<WebviewPortMapping>;
     }
 
     /**
@@ -2775,6 +2805,30 @@ declare module '@theia/plugin' {
          * @param message Body of the message.
          */
         postMessage(message: any): PromiseLike<boolean>;
+
+        /**
+         * Convert a uri for the local file system to one that can be used inside webviews.
+         *
+         * Webviews cannot directly load resources from the workspace or local file system using `file:` uris. The
+         * `asWebviewUri` function takes a local `file:` uri and converts it into a uri that can be used inside of
+         * a webview to load the same resource:
+         *
+         * ```ts
+         * webview.html = `<img src="${webview.asWebviewUri(vscode.Uri.file('/Users/codey/workspace/cat.gif'))}">`
+         * ```
+         */
+        asWebviewUri(localResource: Uri): Uri;
+
+        /**
+         * Content security policy source for webview resources.
+         *
+         * This is the origin that should be used in a content security policy rule:
+         *
+         * ```
+         * img-src https: ${webview.cspSource} ...;
+         * ```
+         */
+        readonly cspSource: string;
     }
 
     /**
@@ -4824,6 +4878,28 @@ declare module '@theia/plugin' {
          */
         export function openExternal(target: Uri): PromiseLike<boolean>;
 
+        /**
+         * Resolves an *external* uri, such as a `http:` or `https:` link, from where the extension is running to a
+         * uri to the same resource on the client machine.
+         *
+         * This is a no-op if the extension is running on the client machine. Currently only supports
+         * `https:` and `http:` uris.
+         *
+         * If the extension is running remotely, this function automatically establishes a port forwarding tunnel
+         * from the local machine to `target` on the remote and returns a local uri to the tunnel. The lifetime of
+         * the port fowarding tunnel is managed by VS Code and the tunnel can be closed by the user.
+         *
+         * Extensions should not cache the result of `asExternalUri` as the resolved uri may become invalid due to
+         * a system or user action — for example, in remote cases, a user may close a port forwardng tunnel
+         * that was opened by `asExternalUri`.
+         *
+         * *Note* that uris passed through `openExternal` are automatically resolved and you should not call `asExternalUri`
+         * on them.
+         *
+         * @return A uri that can be used on the client machine.
+         */
+        export function asExternalUri(target: Uri): PromiseLike<Uri>;
+
     }
 
     /**
@@ -7533,9 +7609,9 @@ declare module '@theia/plugin' {
          */
         readonly name: string;
 
-		/**
-		 * The "resolved" [debug configuration](#DebugConfiguration) of this session.
-		 */
+        /**
+         * The "resolved" [debug configuration](#DebugConfiguration) of this session.
+         */
         readonly configuration: DebugConfiguration;
 
         /**
@@ -8424,10 +8500,10 @@ declare module '@theia/plugin' {
     }
 
     export interface TaskFilter {
-		/**
-		 * The task version as used in the tasks.json file.
-		 * The string support the package.json semver notation.
-		 */
+        /**
+         * The task version as used in the tasks.json file.
+         * The string support the package.json semver notation.
+         */
         version?: string;
 
         /**
@@ -8457,11 +8533,11 @@ declare module '@theia/plugin' {
         export function fetchTasks(filter?: TaskFilter): PromiseLike<Task[]>;
 
         /**
-		 * Executes a task that is managed by VS Code. The returned
-		 * task execution can be used to terminate the task.
-		 *
-		 * @param task the task to execute
-		 */
+         * Executes a task that is managed by VS Code. The returned
+         * task execution can be used to terminate the task.
+         *
+         * @param task the task to execute
+         */
         export function executeTask(task: Task): PromiseLike<TaskExecution>;
 
         /**
diff --git a/yarn.lock b/yarn.lock
index 2fd7fefc56f47..0fe1495f4dcce 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -767,7 +767,7 @@
   dependencies:
     "@phosphor/algorithm" "^1.2.0"
 
-"@phosphor/widgets@^1.5.0":
+"@phosphor/widgets@^1.9.3":
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/@phosphor/widgets/-/widgets-1.9.3.tgz#b8b7ad69fd7cc7af8e8c312ebead0e0965a4cefd"
   integrity sha512-61jsxloDrW/+WWQs8wOgsS5waQ/MSsXBuhONt0o6mtdeL93HVz7CYO5krOoot5owammfF6oX1z0sDaUYIYgcPA==
@@ -898,7 +898,7 @@
   resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.4.tgz#8936cffad3c96ec470a2dc26a38c3ba8b9b6f619"
   integrity sha512-7qvf9F9tMTzo0akeswHPGqgUx/gIaJqrOEET/FCD8CFRkSUHlygQiM5yB6OvjrtdxBVLSyw7COJubsFYs0683g==
 
-"@types/connect@*":
+"@types/connect@*", "@types/connect@^3.4.32":
   version "3.4.32"
   resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
   integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==
@@ -1060,6 +1060,11 @@
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
   integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
 
+"@types/mime@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
+  integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
+
 "@types/minimatch@*", "@types/minimatch@3.0.3":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -1175,7 +1180,7 @@
   resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
   integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
 
-"@types/serve-static@*":
+"@types/serve-static@*", "@types/serve-static@^1.13.3":
   version "1.13.3"
   resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
   integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==
@@ -3623,6 +3628,16 @@ conf@^2.0.0:
     pkg-up "^2.0.0"
     write-file-atomic "^2.3.0"
 
+connect@^3.7.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
+  integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
+  dependencies:
+    debug "2.6.9"
+    finalhandler "1.1.2"
+    parseurl "~1.3.3"
+    utils-merge "1.0.1"
+
 console-browserify@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
@@ -5402,7 +5417,7 @@ fill-range@^4.0.0:
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
-finalhandler@~1.1.2:
+finalhandler@1.1.2, finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
   integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
@@ -8287,6 +8302,11 @@ mime@^2.0.3:
   resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
   integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
 
+mime@^2.4.4:
+  version "2.4.4"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
+  integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
+
 mimic-fn@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
@@ -11000,7 +11020,7 @@ serialize-javascript@^1.4.0, serialize-javascript@^1.7.0:
   resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
   integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==
 
-serve-static@1.14.1:
+serve-static@1.14.1, serve-static@^1.14.1:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
   integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
@@ -12532,6 +12552,11 @@ verror@1.10.0:
     core-util-is "1.0.2"
     extsprintf "^1.2.0"
 
+vhost@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/vhost/-/vhost-3.0.2.tgz#2fb1decd4c466aa88b0f9341af33dc1aff2478d5"
+  integrity sha1-L7HezUxGaqiLD5NBrzPcGv8keNU=
+
 vinyl-file@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a"