Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)

* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973

* disable strict null check
This commit is contained in:
Anthony Dresser
2019-07-15 22:35:46 -07:00
committed by GitHub
parent f720ec642f
commit 0b7e7ddbf9
2406 changed files with 59140 additions and 35464 deletions

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @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.log('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.log('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
});
}());

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en" style="width: 100%; height: 100%;">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'self'; frame-src 'self'; style-src 'unsafe-inline'; worker-src 'self';" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>

View File

@@ -3,41 +3,54 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
'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
* @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
*/
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'));
};
(function () {
'use strict';
const getPendingFrame = () => {
return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame'));
};
/**
* 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 defaultCssRules = `
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);
@@ -93,151 +106,156 @@ const defaultCssRules = `
background-color: var(--vscode-scrollbarSlider-activeBackground);
}`;
/**
* @typedef {{ postMessage: (channel: string, data?: any) => void, onMessage: (channel: string, handler: any) => void }} HostCommunications
*/
/**
* @param {HostCommunications} host
*/
module.exports = function createWebviewManager(host) {
// state
let firstLoad = true;
let loadTimeout;
let pendingMessages = [];
let isInDevelopmentMode = false;
const initData = {
initialScrollProgress: undefined
};
/**
* @param {HTMLDocument?} document
* @param {HTMLElement?} body
* @param {*} [state]
* @return {string}
*/
const applyStyles = (document, body) => {
if (!document) {
return;
}
function getVsCodeApiScript(state) {
return `
const acquireVsCodeApi = (function() {
const originalPostMessage = window.parent.postMessage.bind(window.parent);
const targetOrigin = '*';
let acquired = false;
if (body) {
body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast');
body.classList.add(initData.activeTheme);
}
let state = ${state ? `JSON.parse(${JSON.stringify(state)})` : undefined};
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();
return () => {
if (acquired) {
throw new Error('An instance of the VS Code API has already been acquired');
}
} else {
host.postMessage('did-click-link', node.href.baseVal || node.href);
}
event.preventDefault();
break;
}
node = node.parentNode;
}
};
acquired = true;
return Object.freeze({
postMessage: function(msg) {
return originalPostMessage({ command: 'onmessage', data: msg }, targetOrigin);
},
setState: function(newState) {
state = newState;
originalPostMessage({ command: 'do-update-state', data: JSON.stringify(newState) }, targetOrigin);
return newState;
},
getState: function() {
return state;
}
});
};
})();
delete window.parent;
delete window.top;
delete window.frameElement;
`;
}
/**
* @param {KeyboardEvent} e
* @param {WebviewHost} host
*/
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
});
};
function createWebviewManager(host) {
// state
let firstLoad = true;
let loadTimeout;
let pendingMessages = [];
const onMessage = (message) => {
host.postMessage(message.data.command, message.data.data);
};
const initData = {
initialScrollProgress: undefined
};
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;
});
};
document.addEventListener('DOMContentLoaded', () => {
if (!document.body) {
return;
}
host.onMessage('styles', (_event, variables, activeTheme) => {
initData.styles = variables;
initData.activeTheme = activeTheme;
const target = getActiveFrame();
if (!target) {
/**
* @param {HTMLDocument?} document
* @param {HTMLElement?} body
*/
const applyStyles = (document, body) => {
if (!document) {
return;
}
if (target.contentDocument) {
applyStyles(target.contentDocument, target.contentDocument.body);
if (body) {
body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast');
body.classList.add(initData.activeTheme);
}
});
// propagate focus
host.onMessage('focus', () => {
const target = getActiveFrame();
if (target) {
target.contentWindow.focus();
if (initData.styles) {
for (const variable of Object.keys(initData.styles)) {
document.documentElement.style.setProperty(`--${variable}`, initData.styles[variable]);
}
}
});
};
// update iframe-contents
host.onMessage('content', (_event, data) => {
/**
* @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 {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');
@@ -250,38 +268,7 @@ module.exports = function createWebviewManager(host) {
// apply default script
if (options.allowScripts) {
const defaultScript = newDocument.createElement('script');
defaultScript.textContent = `
const acquireVsCodeApi = (function() {
const originalPostMessage = window.parent.postMessage.bind(window.parent);
let acquired = false;
let state = ${data.state ? `JSON.parse(${JSON.stringify(data.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 }, '*');
},
setState: function(newState) {
state = newState;
originalPostMessage({ command: 'do-update-state', data: JSON.stringify(newState) }, '*');
return newState;
},
getState: function() {
return state;
}
});
};
})();
delete window.parent;
delete window.top;
delete window.frameElement;
`;
defaultScript.textContent = getVsCodeApiScript(data.state);
newDocument.head.prepend(defaultScript);
}
@@ -293,154 +280,219 @@ module.exports = function createWebviewManager(host) {
applyStyles(newDocument, newDocument.body);
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');
newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden';
document.body.appendChild(newFrame);
// write new content onto iframe
newFrame.contentDocument.open('text/html', 'replace');
newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown);
newFrame.contentWindow.addEventListener('DOMContentLoaded', e => {
const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined;
if (contentDocument) {
applyStyles(contentDocument, contentDocument.body);
}
});
newFrame.contentWindow.onbeforeunload = () => {
if (isInDevelopmentMode) { // Allow reloads while developing a webview
host.postMessage('do-reload');
return false;
}
// Block navigation when not in development mode
console.log('prevented webview navigation');
return false;
};
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';
newFrame.contentWindow.focus();
contentWindow.addEventListener('scroll', handleInnerScroll);
pendingMessages.forEach((data) => {
contentWindow.postMessage(data, '*');
});
pendingMessages = [];
}
};
clearTimeout(loadTimeout);
loadTimeout = undefined;
loadTimeout = setTimeout(() => {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(newFrame.contentDocument, newFrame.contentWindow);
}, 200);
newFrame.contentWindow.addEventListener('load', function (e) {
if (loadTimeout) {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(e.target, this);
}
});
// Bubble out link clicks
newFrame.contentWindow.addEventListener('click', handleInnerClick);
// 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
newFrame.contentDocument.write('<!DOCTYPE html>');
newFrame.contentDocument.write(newDocument.documentElement.innerHTML);
newFrame.contentDocument.close();
return '<!DOCTYPE html>\n' + newDocument.documentElement.outerHTML;
}
host.postMessage('did-set-content', undefined);
});
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;
// 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, '*');
if (!target) {
return;
}
}
pendingMessages.push(data);
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('keydown', handleInnerKeydown);
newFrame.contentWindow.addEventListener('DOMContentLoaded', e => {
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);
}
});
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) {
clearTimeout(loadTimeout);
loadTimeout = undefined;
loadTimeout = setTimeout(() => {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(newFrame.contentDocument, newFrame.contentWindow);
}, 200);
newFrame.contentWindow.addEventListener('load', function (e) {
if (loadTimeout) {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(e.target, this);
}
});
// Bubble out link clicks
newFrame.contentWindow.addEventListener('click', handleInnerClick);
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', {});
});
}
host.onMessage('initial-scroll-position', (_event, progress) => {
initData.initialScrollProgress = progress;
});
host.onMessage('devtools-opened', () => {
isInDevelopmentMode = true;
});
trackFocus({
onFocus: () => host.postMessage('did-focus'),
onBlur: () => host.postMessage('did-blur')
});
// Forward messages from the embedded iframe
window.onmessage = onMessage;
// signal ready
host.postMessage('webview-ready', process.pid);
});
};
if (typeof module !== 'undefined') {
module.exports = createWebviewManager;
} else {
window.createWebviewManager = createWebviewManager;
}
}());

View File

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

View File

@@ -4,11 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { isMacintosh } from 'vs/base/common/platform';
import { localize } from 'vs/nls';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
@@ -17,8 +15,8 @@ import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } fro
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory';
import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/common/webview';
import { CopyWebviewEditorCommand, CutWebviewEditorCommand, HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, PasteWebviewEditorCommand, RedoWebviewEditorCommand, ReloadWebviewAction, SelectAllWebviewEditorCommand, ShowWebViewEditorFindWidgetCommand, UndoWebviewEditorCommand } from '../browser/webviewCommands';
import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/common/webview';
import { HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand } from '../browser/webviewCommands';
import { WebviewEditor } from '../browser/webviewEditor';
import { WebviewEditorInput } from '../browser/webviewEditorInput';
import { IWebviewEditorService, WebviewEditorService } from '../browser/webviewEditorService';
@@ -35,12 +33,9 @@ Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactor
registerSingleton(IWebviewEditorService, WebviewEditorService, true);
const webviewDeveloperCategory = localize('developer', "Developer");
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
export function registerWebViewCommands(editorId: string): void {
function registerWebViewCommands(editorId: string): void {
const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */);
const showNextFindWidgetCommand = new ShowWebViewEditorFindWidgetCommand({
@@ -63,76 +58,11 @@ export function registerWebViewCommands(editorId: string): void {
weight: KeybindingWeight.EditorContrib
}
})).register();
(new SelectAllWebviewEditorCommand({
id: SelectAllWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
weight: KeybindingWeight.EditorContrib
}
})).register();
// These commands are only needed on MacOS where we have to disable the menu bar commands
if (isMacintosh) {
(new CopyWebviewEditorCommand({
id: CopyWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new PasteWebviewEditorCommand({
id: PasteWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new CutWebviewEditorCommand({
id: CutWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_X,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new UndoWebviewEditorCommand({
id: UndoWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_Z,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new RedoWebviewEditorCommand({
id: RedoWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_Y,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z],
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z },
weight: KeybindingWeight.EditorContrib
}
})).register();
}
}
registerWebViewCommands(WebviewEditor.ID);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(OpenWebviewDeveloperToolsAction, OpenWebviewDeveloperToolsAction.ID, OpenWebviewDeveloperToolsAction.LABEL),
'Webview Tools',
webviewDeveloperCategory);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(ReloadWebviewAction, ReloadWebviewAction.ID, ReloadWebviewAction.LABEL),
'Reload Webview',
'Reload Webviews',
webviewDeveloperCategory);

View File

@@ -32,96 +32,6 @@ export class HideWebViewEditorFindCommand extends Command {
}
}
export class SelectAllWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.selectAll';
public runCommand(accessor: ServicesAccessor, args: any): void {
const webViewEditor = getActiveWebviewEditor(accessor);
if (webViewEditor) {
webViewEditor.selectAll();
}
}
}
export class CopyWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.copy';
public runCommand(accessor: ServicesAccessor, args: any): void {
const webViewEditor = getActiveWebviewEditor(accessor);
if (webViewEditor) {
webViewEditor.copy();
}
}
}
export class PasteWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.paste';
public runCommand(accessor: ServicesAccessor, args: any): void {
const webViewEditor = getActiveWebviewEditor(accessor);
if (webViewEditor) {
webViewEditor.paste();
}
}
}
export class CutWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.cut';
public runCommand(accessor: ServicesAccessor, args: any): void {
const webViewEditor = getActiveWebviewEditor(accessor);
if (webViewEditor) {
webViewEditor.cut();
}
}
}
export class UndoWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.undo';
public runCommand(accessor: ServicesAccessor, args: any): void {
const webViewEditor = getActiveWebviewEditor(accessor);
if (webViewEditor) {
webViewEditor.undo();
}
}
}
export class RedoWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.redo';
public runCommand(accessor: ServicesAccessor, args: any): void {
const webViewEditor = getActiveWebviewEditor(accessor);
if (webViewEditor) {
webViewEditor.redo();
}
}
}
export class OpenWebviewDeveloperToolsAction extends Action {
static readonly ID = 'workbench.action.webview.openDeveloperTools';
static readonly LABEL = nls.localize('openToolsLabel', "Open Webview Developer Tools");
public constructor(
id: string,
label: string
) {
super(id, label);
}
public run(): Promise<any> {
const elements = document.querySelectorAll('webview.ready');
for (let i = 0; i < elements.length; i++) {
try {
(elements.item(i) as Electron.WebviewTag).openDevTools();
} catch (e) {
console.error(e);
}
}
return Promise.resolve(true);
}
}
export class ReloadWebviewAction extends Action {
static readonly ID = 'workbench.action.webview.reloadWebviewAction';
static readonly LABEL = nls.localize('refreshWebviewLabel', "Reload Webviews");

View File

@@ -6,7 +6,7 @@
import * as DOM from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService } from 'vs/platform/storage/common/storage';
@@ -17,29 +17,29 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorOptions } from 'vs/workbench/common/editor';
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
import { IWebviewService, Webview, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/common/webview';
import { IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview } from 'vs/workbench/contrib/webview/common/webview';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
export class WebviewEditor extends BaseEditor {
protected _webview: Webview | undefined;
protected findWidgetVisible: IContextKey<boolean>;
public static readonly ID = 'WebviewEditor';
private _webview: Webview | undefined;
private _findWidgetVisible: IContextKey<boolean>;
private _editorFrame: HTMLElement;
private _content?: HTMLElement;
private _webviewContent: HTMLElement | undefined;
private _webviewFocusTrackerDisposables: IDisposable[] = [];
private _onFocusWindowHandler?: IDisposable;
private readonly _webviewFocusTrackerDisposables = this._register(new DisposableStore());
private readonly _onFocusWindowHandler = this._register(new MutableDisposable());
private readonly _onDidFocusWebview = this._register(new Emitter<void>());
public get onDidFocus(): Event<any> { return this._onDidFocusWebview.event; }
private pendingMessages: any[] = [];
private _pendingMessages: any[] = [];
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@@ -53,7 +53,7 @@ export class WebviewEditor extends BaseEditor {
) {
super(WebviewEditor.ID, telemetryService, themeService, storageService);
if (_contextKeyService) {
this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(_contextKeyService);
this._findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(_contextKeyService);
}
}
@@ -78,7 +78,7 @@ export class WebviewEditor extends BaseEditor {
}
public dispose(): void {
this.pendingMessages = [];
this._pendingMessages = [];
// Let the editor input dispose of the webview.
this._webview = undefined;
@@ -89,12 +89,6 @@ export class WebviewEditor extends BaseEditor {
this._content = undefined;
}
this._webviewFocusTrackerDisposables = dispose(this._webviewFocusTrackerDisposables);
if (this._onFocusWindowHandler) {
this._onFocusWindowHandler.dispose();
}
super.dispose();
}
@@ -102,18 +96,18 @@ export class WebviewEditor extends BaseEditor {
if (this._webview) {
this._webview.sendMessage(data);
} else {
this.pendingMessages.push(data);
this._pendingMessages.push(data);
}
}
public showFind() {
if (this._webview) {
this._webview.showFind();
this.findWidgetVisible.set(true);
this._findWidgetVisible.set(true);
}
}
public hideFind() {
this.findWidgetVisible.reset();
this._findWidgetVisible.reset();
if (this._webview) {
this._webview.hideFind();
}
@@ -136,10 +130,10 @@ export class WebviewEditor extends BaseEditor {
public focus(): void {
super.focus();
if (!this._onFocusWindowHandler) {
if (!this._onFocusWindowHandler.value) {
// Make sure we restore focus when switching back to a VS Code window
this._onFocusWindowHandler = this._windowService.onDidChangeFocus(focused => {
this._onFocusWindowHandler.value = this._windowService.onDidChangeFocus(focused => {
if (focused && this._editorService.activeControl === this) {
this.focus();
}
@@ -148,31 +142,7 @@ export class WebviewEditor extends BaseEditor {
this.withWebview(webview => webview.focus());
}
public selectAll(): void {
this.withWebview(webview => webview.selectAll());
}
public copy(): void {
this.withWebview(webview => webview.copy());
}
public paste(): void {
this.withWebview(webview => webview.paste());
}
public cut(): void {
this.withWebview(webview => webview.cut());
}
public undo(): void {
this.withWebview(webview => webview.undo());
}
public redo(): void {
this.withWebview(webview => webview.redo());
}
private withWebview(f: (element: Webview) => void): void {
public withWebview(f: (element: Webview) => void): void {
if (this._webview) {
f(this._webview);
}
@@ -208,7 +178,7 @@ export class WebviewEditor extends BaseEditor {
this._webview = undefined;
this._webviewContent = undefined;
this.pendingMessages = [];
this._pendingMessages = [];
super.clearInput();
}
@@ -219,7 +189,7 @@ export class WebviewEditor extends BaseEditor {
this._webview = undefined;
this._webviewContent = undefined;
}
this.pendingMessages = [];
this._pendingMessages = [];
return super.setInput(input, options, token)
.then(() => input.resolve())
.then(() => {
@@ -270,10 +240,10 @@ export class WebviewEditor extends BaseEditor {
} else {
if (input.options.enableFindWidget) {
this._contextKeyService = this._register(this._contextKeyService.createScoped(this._webviewContent));
this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService);
this._findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(this._contextKeyService);
}
this._webview = this._webviewService.createWebview(
this._webview = this._webviewService.createWebview(input.id,
{
allowSvgs: true,
extension: input.extension,
@@ -286,17 +256,17 @@ export class WebviewEditor extends BaseEditor {
this._webview.initialScrollProgress = input.scrollYPercentage;
}
this._webview.state = input.webviewState;
this._webview.state = input.state ? input.state.state : undefined;
this._content!.setAttribute('aria-flowto', this._webviewContent.id);
this.doUpdateContainer();
}
for (const message of this.pendingMessages) {
for (const message of this._pendingMessages) {
this._webview.sendMessage(message);
}
this.pendingMessages = [];
this._pendingMessages = [];
this.trackFocus();
@@ -304,14 +274,14 @@ export class WebviewEditor extends BaseEditor {
}
private trackFocus() {
this._webviewFocusTrackerDisposables = dispose(this._webviewFocusTrackerDisposables);
this._webviewFocusTrackerDisposables.clear();
// Track focus in webview content
const webviewContentFocusTracker = DOM.trackFocus(this._webviewContent!);
this._webviewFocusTrackerDisposables.push(webviewContentFocusTracker);
this._webviewFocusTrackerDisposables.push(webviewContentFocusTracker.onDidFocus(() => this._onDidFocusWebview.fire()));
this._webviewFocusTrackerDisposables.add(webviewContentFocusTracker);
this._webviewFocusTrackerDisposables.add(webviewContentFocusTracker.onDidFocus(() => this._onDidFocusWebview.fire()));
// Track focus in webview element
this._webviewFocusTrackerDisposables.push(this._webview!.onDidFocus(() => this._onDidFocusWebview.fire()));
this._webviewFocusTrackerDisposables.add(this._webview!.onDidFocus(() => this._onDidFocusWebview.fire()));
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { Emitter } from 'vs/base/common/event';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { IEditorModel } from 'vs/platform/editor/common/editor';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
@@ -13,15 +13,14 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro
import { WebviewEvents, WebviewInputOptions } from './webviewEditorService';
import { Webview, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview';
export class WebviewEditorInput extends EditorInput {
private static handlePool = 0;
export class WebviewEditorInput<State = any> extends EditorInput {
private static _styleElement?: HTMLStyleElement;
private static _icons = new Map<number, { light: URI, dark: URI }>();
private static _icons = new Map<string, { light: URI, dark: URI }>();
private static updateStyleElement(
id: number,
id: string,
iconPath: { light: URI, dark: URI } | undefined
) {
if (!this._styleElement) {
@@ -39,10 +38,10 @@ export class WebviewEditorInput extends EditorInput {
this._icons.forEach((value, key) => {
const webviewSelector = `.show-file-icons .webview-${key}-name-file-icon::before`;
if (URI.isUri(value)) {
cssRules.push(`${webviewSelector} { content: ""; background-image: url(${value.toString()}); }`);
cssRules.push(`${webviewSelector} { content: ""; background-image: url(${dom.asDomUri(value).toString()}); }`);
} else {
cssRules.push(`.vs ${webviewSelector} { content: ""; background-image: url(${value.light.toString()}); }`);
cssRules.push(`.vs-dark ${webviewSelector} { content: ""; background-image: url(${value.dark.toString()}); }`);
cssRules.push(`.vs ${webviewSelector} { content: ""; background-image: url(${dom.asDomUri(value.light).toString()}); }`);
cssRules.push(`.vs-dark ${webviewSelector} { content: ""; background-image: url(${dom.asDomUri(value.dark).toString()}); }`);
}
});
this._styleElement.innerHTML = cssRules.join('\n');
@@ -59,22 +58,22 @@ export class WebviewEditorInput extends EditorInput {
private _container?: HTMLElement;
private _webview?: Webview;
private _webviewOwner: any;
private _webviewDisposables: IDisposable[] = [];
private readonly _webviewDisposables = this._register(new DisposableStore());
private _group?: GroupIdentifier;
private _scrollYPercentage: number = 0;
private _state: any;
private _state: State;
public readonly extension?: {
readonly location: URI;
readonly id: ExtensionIdentifier;
};
private readonly _id: number;
constructor(
public readonly id: string,
public readonly viewType: string,
name: string,
options: WebviewInputOptions,
state: any,
state: State,
events: WebviewEvents,
extension: undefined | {
readonly location: URI;
@@ -84,8 +83,6 @@ export class WebviewEditorInput extends EditorInput {
) {
super();
this._id = WebviewEditorInput.handlePool++;
this._name = name;
this._options = options;
this._events = events;
@@ -120,7 +117,7 @@ export class WebviewEditorInput extends EditorInput {
public getResource(): URI {
return URI.from({
scheme: 'webview-panel',
path: `webview-panel/webview-${this._id}`
path: `webview-panel/webview-${this.id}`
});
}
@@ -133,7 +130,7 @@ export class WebviewEditorInput extends EditorInput {
}
public getDescription() {
return null;
return undefined;
}
public setName(value: string): void {
@@ -147,7 +144,7 @@ export class WebviewEditorInput extends EditorInput {
public set iconPath(value: { light: URI, dark: URI } | undefined) {
this._iconPath = value;
WebviewEditorInput.updateStyleElement(this._id, value);
WebviewEditorInput.updateStyleElement(this.id, value);
}
public matches(other: IEditorInput): boolean {
@@ -175,18 +172,14 @@ export class WebviewEditorInput extends EditorInput {
}
}
public get state(): any {
public get state(): State {
return this._state;
}
public set state(value: any) {
public set state(value: State) {
this._state = value;
}
public get webviewState() {
return this._state.state;
}
public get options(): WebviewInputOptions {
return this._options;
}
@@ -217,7 +210,7 @@ export class WebviewEditorInput extends EditorInput {
public get container(): HTMLElement {
if (!this._container) {
this._container = document.createElement('div');
this._container.id = `webview-${this._id}`;
this._container.id = `webview-${this.id}`;
const part = this._layoutService.getContainer(Parts.EDITOR_PART);
part.appendChild(this._container);
}
@@ -229,7 +222,7 @@ export class WebviewEditorInput extends EditorInput {
}
public set webview(value: Webview | undefined) {
this._webviewDisposables = dispose(this._webviewDisposables);
this._webviewDisposables.clear();
this._webview = value;
if (!this._webview) {
@@ -253,7 +246,9 @@ export class WebviewEditorInput extends EditorInput {
}, null, this._webviewDisposables);
this._webview.onDidUpdateState(newState => {
this._state.state = newState;
if (this._events && this._events.onDidUpdateWebviewState) {
this._events.onDidUpdateWebviewState(newState);
}
}, null, this._webviewDisposables);
}
@@ -262,7 +257,6 @@ export class WebviewEditorInput extends EditorInput {
}
public claimWebview(owner: any) {
this._webviewOwner = owner;
}
@@ -284,8 +278,7 @@ export class WebviewEditorInput extends EditorInput {
this._webview = undefined;
}
this._webviewDisposables = dispose(this._webviewDisposables);
this._webviewDisposables.clear();
this._webviewOwner = undefined;
if (this._container) {
@@ -305,6 +298,7 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput {
private _revived: boolean = false;
constructor(
id: string,
viewType: string,
name: string,
options: WebviewInputOptions,
@@ -317,7 +311,7 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput {
private readonly reviver: (input: WebviewEditorInput) => Promise<void>,
@IWorkbenchLayoutService partService: IWorkbenchLayoutService,
) {
super(viewType, name, options, state, events, extension, partService);
super(id, viewType, name, options, state, events, extension, partService);
}
public async resolve(): Promise<IEditorModel> {
@@ -327,4 +321,4 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput {
}
return super.resolve();
}
}
}

View File

@@ -9,6 +9,7 @@ import { WebviewEditorInput } from './webviewEditorInput';
import { IWebviewEditorService, WebviewInputOptions } from './webviewEditorService';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { generateUuid } from 'vs/base/common/uuid';
interface SerializedIconPath {
light: string | UriComponents;
@@ -67,7 +68,7 @@ export class WebviewEditorInputFactory implements IEditorInputFactory {
const extensionLocation = reviveUri(data.extensionLocation);
const extensionId = data.extensionId ? new ExtensionIdentifier(data.extensionId) : undefined;
const iconPath = reviveIconPath(data.iconPath);
return this._webviewService.reviveWebview(data.viewType, data.title, iconPath, data.state, data.options, extensionLocation ? {
return this._webviewService.reviveWebview(generateUuid(), data.viewType, data.title, iconPath, data.state, data.options, extensionLocation ? {
location: extensionLocation,
id: extensionId
} : undefined, data.group);

View File

@@ -26,6 +26,7 @@ export interface IWebviewEditorService {
_serviceBrand: any;
createWebview(
id: string,
viewType: string,
title: string,
showOptions: ICreateWebViewShowOptions,
@@ -38,6 +39,7 @@ export interface IWebviewEditorService {
): WebviewEditorInput;
reviveWebview(
id: string,
viewType: string,
title: string,
iconPath: { light: URI, dark: URI } | undefined,
@@ -79,6 +81,7 @@ export interface WebviewEvents {
onMessage?(message: any): void;
onDispose?(): void;
onDidClickLink?(link: URI, options: IWebviewOptions): void;
onDidUpdateWebviewState?(newState: any): void;
}
export interface WebviewInputOptions extends IWebviewOptions, IWebviewPanelOptions {
@@ -92,7 +95,7 @@ export function areWebviewInputOptionsEqual(a: WebviewInputOptions, b: WebviewIn
&& a.retainContextWhenHidden === b.retainContextWhenHidden
&& a.tryRestoreScrollPosition === b.tryRestoreScrollPosition
&& (a.localResourceRoots === b.localResourceRoots || (Array.isArray(a.localResourceRoots) && Array.isArray(b.localResourceRoots) && equals(a.localResourceRoots, b.localResourceRoots, (a, b) => a.toString() === b.toString())))
&& (a.portMapping === b.portMapping || (Array.isArray(a.portMapping) && Array.isArray(b.portMapping) && equals(a.portMapping, b.portMapping, (a, b) => a.from === b.from && a.to === b.to)));
&& (a.portMapping === b.portMapping || (Array.isArray(a.portMapping) && Array.isArray(b.portMapping) && equals(a.portMapping, b.portMapping, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort)));
}
function canRevive(reviver: WebviewReviver, webview: WebviewEditorInput): boolean {
@@ -132,6 +135,7 @@ export class WebviewEditorService implements IWebviewEditorService {
) { }
public createWebview(
id: string,
viewType: string,
title: string,
showOptions: ICreateWebViewShowOptions,
@@ -142,7 +146,7 @@ export class WebviewEditorService implements IWebviewEditorService {
},
events: WebviewEvents
): WebviewEditorInput {
const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, title, options, {}, events, extension);
const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, id, viewType, title, options, {}, events, extension);
this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus }, showOptions.group);
return webviewInput;
}
@@ -163,6 +167,7 @@ export class WebviewEditorService implements IWebviewEditorService {
}
public reviveWebview(
id: string,
viewType: string,
title: string,
iconPath: { light: URI, dark: URI } | undefined,
@@ -174,7 +179,7 @@ export class WebviewEditorService implements IWebviewEditorService {
},
group: number | undefined,
): WebviewEditorInput {
const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise<void> => {
const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, id, viewType, title, options, state, {}, extension, async (webview: WebviewEditorInput): Promise<void> => {
const didRevive = await this.tryRevive(webview);
if (didRevive) {
return Promise.resolve(undefined);

View File

@@ -0,0 +1,323 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { Disposable } from 'vs/base/common/lifecycle';
import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browser/webviewEditorService';
import { addDisposableListener, addClass } from 'vs/base/browser/dom';
import { getWebviewThemeData } from 'vs/workbench/contrib/webview/common/themeing';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { loadLocalResource } from 'vs/workbench/contrib/webview/common/resourceLoader';
import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
interface WebviewContent {
readonly html: string;
readonly options: WebviewContentOptions;
readonly state: string | undefined;
}
export class IFrameWebview extends Disposable implements Webview {
private element?: HTMLIFrameElement;
private readonly _ready: Promise<void>;
private content: WebviewContent;
private _focused = false;
private readonly _portMappingManager: WebviewPortMappingManager;
constructor(
private readonly id: string,
private _options: WebviewOptions,
contentOptions: WebviewContentOptions,
@IThemeService themeService: IThemeService,
@ITunnelService tunnelService: ITunnelService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super();
if (typeof environmentService.webviewEndpoint !== 'string') {
throw new Error('To use iframe based webviews, you must configure `environmentService.webviewEndpoint`');
}
this._portMappingManager = this._register(new WebviewPortMappingManager(
this._options.extension ? this._options.extension.location : undefined,
() => this.content.options.portMappings || [],
tunnelService
));
this.content = {
html: '',
options: contentOptions,
state: undefined
};
this.element = document.createElement('iframe');
this.element.sandbox.add('allow-scripts', 'allow-same-origin');
this.element.setAttribute('src', `${this.endpoint}/index.html?id=${this.id}`);
this.element.style.border = 'none';
this.element.style.width = '100%';
this.element.style.height = '100%';
this._register(addDisposableListener(window, 'message', e => {
if (!e || !e.data || e.data.target !== this.id) {
return;
}
switch (e.data.channel) {
case 'onmessage':
if (e.data.data) {
this._onMessage.fire(e.data.data);
}
return;
case 'did-click-link':
const uri = e.data.data;
this._onDidClickLink.fire(URI.parse(uri));
return;
case 'did-scroll':
// if (e.args && typeof e.args[0] === 'number') {
// this._onDidScroll.fire({ scrollYPercentage: e.args[0] });
// }
return;
case 'do-reload':
this.reload();
return;
case 'do-update-state':
const state = e.data.data;
this.state = state;
this._onDidUpdateState.fire(state);
return;
case 'did-focus':
this.handleFocusChange(true);
return;
case 'did-blur':
this.handleFocusChange(false);
return;
case 'load-resource':
{
const requestPath = e.data.data.path;
const uri = URI.file(decodeURIComponent(requestPath));
this.loadResource(requestPath, uri);
return;
}
case 'load-localhost':
{
this.localLocalhost(e.data.data.origin);
return;
}
}
}));
this._ready = new Promise(resolve => {
const subscription = this._register(addDisposableListener(window, 'message', (e) => {
if (e.data && e.data.target === this.id && e.data.channel === 'webview-ready') {
if (this.element) {
addClass(this.element, 'ready');
}
subscription.dispose();
resolve();
}
}));
});
this.style(themeService.getTheme());
this._register(themeService.onThemeChange(this.style, this));
}
private get endpoint(): string {
const endpoint = this.environmentService.webviewEndpoint!.replace('{{uuid}}', this.id);
if (endpoint[endpoint.length - 1] === '/') {
return endpoint.slice(0, endpoint.length - 1);
}
return endpoint;
}
public mountTo(parent: HTMLElement) {
if (this.element) {
parent.appendChild(this.element);
}
}
public set options(options: WebviewContentOptions) {
if (areWebviewInputOptionsEqual(options, this.content.options)) {
return;
}
this.content = {
html: this.content.html,
options: options,
state: this.content.state,
};
this.doUpdateContent();
}
public set html(value: string) {
this.content = {
html: this.preprocessHtml(value),
options: this.content.options,
state: this.content.state,
};
this.doUpdateContent();
}
private preprocessHtml(value: string): string {
return value.replace(/(["'])vscode-resource:([^\s'"]+?)(["'])/gi, (_, startQuote, path, endQuote) =>
`${startQuote}${this.endpoint}/vscode-resource${path}${endQuote}`);
}
public update(html: string, options: WebviewContentOptions, retainContextWhenHidden: boolean) {
if (retainContextWhenHidden && html === this.content.html && areWebviewInputOptionsEqual(options, this.content.options)) {
return;
}
this.content = {
html: this.preprocessHtml(html),
options: options,
state: this.content.state,
};
this.doUpdateContent();
}
private doUpdateContent() {
this._send('content', {
contents: this.content.html,
options: this.content.options,
state: this.content.state
});
}
private handleFocusChange(isFocused: boolean): void {
this._focused = isFocused;
if (this._focused) {
this._onDidFocus.fire();
}
}
initialScrollProgress: number;
private readonly _onDidFocus = this._register(new Emitter<void>());
public readonly onDidFocus = this._onDidFocus.event;
private readonly _onDidClickLink = this._register(new Emitter<URI>());
public readonly onDidClickLink = this._onDidClickLink.event;
private readonly _onDidScroll = this._register(new Emitter<{ scrollYPercentage: number }>());
public readonly onDidScroll = this._onDidScroll.event;
private readonly _onDidUpdateState = this._register(new Emitter<string | undefined>());
public readonly onDidUpdateState = this._onDidUpdateState.event;
private readonly _onMessage = this._register(new Emitter<any>());
public readonly onMessage = this._onMessage.event;
sendMessage(data: any): void {
this._send('message', data);
}
layout(): void {
// noop
}
focus(): void {
if (this.element) {
this.element.focus();
}
}
dispose(): void {
if (this.element) {
if (this.element.parentElement) {
this.element.parentElement.removeChild(this.element);
}
}
this.element = undefined!;
super.dispose();
}
reload(): void {
this.doUpdateContent();
}
showFind(): void {
throw new Error('Method not implemented.');
}
hideFind(): void {
throw new Error('Method not implemented.');
}
public set state(state: string | undefined) {
this.content = {
html: this.content.html,
options: this.content.options,
state,
};
}
private _send(channel: string, data: any): void {
this._ready
.then(() => {
if (!this.element) {
return;
}
this.element.contentWindow!.postMessage({
channel: channel,
args: data
}, '*');
})
.catch(err => console.error(err));
}
private style(theme: ITheme): void {
const { styles, activeTheme } = getWebviewThemeData(theme, this._configurationService);
this._send('styles', { styles, activeTheme });
}
private async loadResource(requestPath: string, uri: URI) {
try {
const result = await loadLocalResource(uri, this.fileService, this._options.extension ? this._options.extension.location : undefined,
() => (this.content.options.localResourceRoots || []));
if (result.type === 'success') {
return this._send('did-load-resource', {
status: 200,
path: requestPath,
mime: result.mimeType,
data: result.data.buffer
});
}
} catch {
// noop
}
return this._send('did-load-resource', {
status: 404,
path: uri.path
});
}
private async localLocalhost(origin: string) {
const redirect = await this._portMappingManager.getRedirect(origin);
return this._send('did-load-localhost', {
origin,
location: redirect
});
}
}

View File

@@ -44,6 +44,7 @@ export class WebviewFindWidget extends SimpleFindWidget {
} else {
this._delegate.stopFind(false);
}
return false;
}
protected onFocusTrackerFocus() { }

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IFrameWebview as WebviewElement } from 'vs/workbench/contrib/webview/browser/webviewElement';
import { IWebviewService, WebviewOptions, WebviewContentOptions, Webview } from 'vs/workbench/contrib/webview/common/webview';
export class WebviewService implements IWebviewService {
_serviceBrand: any;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) { }
createWebview(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions
): Webview {
return this._instantiationService.createInstance(WebviewElement,
id,
options,
contentOptions);
}
}

View File

@@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getMediaMime, MIME_UNKNOWN } from 'vs/base/common/mime';
import { extname } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
const webviewMimeTypes = new Map([
['.svg', 'image/svg+xml'],
['.txt', 'text/plain'],
['.css', 'text/css'],
['.js', 'application/javascript'],
['.json', 'application/json'],
['.html', 'text/html'],
['.htm', 'text/html'],
['.xhtml', 'application/xhtml+xml'],
['.oft', 'font/otf'],
['.xml', 'application/xml'],
]);
export function getWebviewContentMimeType(normalizedPath: URI): string {
const ext = extname(normalizedPath.fsPath).toLowerCase();
return webviewMimeTypes.get(ext) || getMediaMime(normalizedPath.fsPath) || MIME_UNKNOWN;
}

View File

@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined {
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return undefined;
}
const localhostMatch = /^(localhost|127\.0\.0\.1):(\d+)$/.exec(uri.authority);
if (!localhostMatch) {
return undefined;
}
return {
address: localhostMatch[1],
port: +localhostMatch[2],
};
}
export class WebviewPortMappingManager extends Disposable {
private readonly _tunnels = new Map<number, Promise<RemoteTunnel>>();
constructor(
private readonly extensionLocation: URI | undefined,
private readonly mappings: () => ReadonlyArray<modes.IWebviewPortMapping>,
private readonly tunnelService: ITunnelService
) {
super();
}
public async getRedirect(url: string): Promise<string | undefined> {
const uri = URI.parse(url);
const requestLocalHostInfo = extractLocalHostUriMetaDataForPortMapping(uri);
if (!requestLocalHostInfo) {
return undefined;
}
for (const mapping of this.mappings()) {
if (mapping.webviewPort === requestLocalHostInfo.port) {
if (this.extensionLocation && this.extensionLocation.scheme === REMOTE_HOST_SCHEME) {
const tunnel = await this.getOrCreateTunnel(mapping.extensionHostPort);
if (tunnel) {
return uri.with({
authority: `127.0.0.1:${tunnel.tunnelLocalPort}`,
}).toString();
}
}
if (mapping.webviewPort !== mapping.extensionHostPort) {
return uri.with({
authority: `${requestLocalHostInfo.address}:${mapping.extensionHostPort}`
}).toString();
}
}
}
return undefined;
}
dispose() {
super.dispose();
for (const tunnel of this._tunnels.values()) {
tunnel.then(tunnel => tunnel.dispose());
}
this._tunnels.clear();
}
private getOrCreateTunnel(remotePort: number): Promise<RemoteTunnel> | undefined {
const existing = this._tunnels.get(remotePort);
if (existing) {
return existing;
}
const tunnel = this.tunnelService.openTunnel(remotePort);
if (tunnel) {
this._tunnels.set(remotePort, tunnel);
}
return tunnel;
}
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from 'vs/base/common/buffer';
import { sep } from 'vs/base/common/path';
import { startsWith, endsWith } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { getWebviewContentMimeType } from 'vs/workbench/contrib/webview/common/mimeTypes';
class Success {
readonly type = 'success';
constructor(
public readonly data: VSBuffer,
public readonly mimeType: string
) { }
}
const Failed = new class { readonly type = 'failed'; };
const AccessDenied = new class { readonly type = 'access-denied'; };
type LocalResourceResponse = Success | typeof Failed | typeof AccessDenied;
async function resolveContent(
fileService: IFileService,
resource: URI,
mime: string
): Promise<LocalResourceResponse> {
try {
const contents = await fileService.readFile(resource);
return new Success(contents.value, mime);
} catch (err) {
console.log(err);
return Failed;
}
}
export async function loadLocalResource(
requestUri: URI,
fileService: IFileService,
extensionLocation: URI | undefined,
getRoots: () => ReadonlyArray<URI>
): Promise<LocalResourceResponse> {
const normalizedPath = requestUri.with({
scheme: 'file',
fragment: '',
query: '',
});
for (const root of getRoots()) {
if (!containsResource(root, normalizedPath)) {
continue;
}
if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) {
const redirectedUri = URI.from({
scheme: REMOTE_HOST_SCHEME,
authority: extensionLocation.authority,
path: '/vscode-resource',
query: JSON.stringify({
requestResourcePath: requestUri.path
})
});
return resolveContent(fileService, redirectedUri, getWebviewContentMimeType(requestUri));
} else {
return resolveContent(fileService, normalizedPath, getWebviewContentMimeType(normalizedPath));
}
}
return AccessDenied;
}
function containsResource(root: URI, resource: URI): boolean {
const rootPath = root.fsPath + (endsWith(root.fsPath, sep) ? '' : sep);
return startsWith(resource.fsPath, rootPath);
}

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
import { ITheme, LIGHT, DARK } from 'vs/platform/theme/common/themeService';
interface WebviewThemeData {
readonly activeTheme: string;
readonly styles: { readonly [key: string]: string | number };
}
export function getWebviewThemeData(
theme: ITheme,
configurationService: IConfigurationService
): WebviewThemeData {
const configuration = configurationService.getValue<IEditorOptions>('editor');
const editorFontFamily = configuration.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
const editorFontWeight = configuration.fontWeight || EDITOR_FONT_DEFAULTS.fontWeight;
const editorFontSize = configuration.fontSize || EDITOR_FONT_DEFAULTS.fontSize;
const exportedColors = colorRegistry.getColorRegistry().getColors().reduce((colors, entry) => {
const color = theme.getColor(entry.id);
if (color) {
colors['vscode-' + entry.id.replace('.', '-')] = color.toString();
}
return colors;
}, {} as { [key: string]: string });
const styles = {
'vscode-font-family': '-apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", ans-serif',
'vscode-font-weight': 'normal',
'vscode-font-size': '13px',
'vscode-editor-font-family': editorFontFamily,
'vscode-editor-font-weight': editorFontWeight,
'vscode-editor-font-size': editorFontSize,
...exportedColors
};
const activeTheme = ApiThemeClassName.fromTheme(theme);
return { styles, activeTheme };
}
enum ApiThemeClassName {
light = 'vscode-light',
dark = 'vscode-dark',
highContrast = 'vscode-high-contrast'
}
namespace ApiThemeClassName {
export function fromTheme(theme: ITheme): ApiThemeClassName {
if (theme.type === LIGHT) {
return ApiThemeClassName.light;
} else if (theme.type === DARK) {
return ApiThemeClassName.dark;
} else {
return ApiThemeClassName.highContrast;
}
}
}

View File

@@ -4,11 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import * as modes from 'vs/editor/common/modes';
import * as nls from 'vs/nls';
/**
* Set when the find widget in a webview is visible.
@@ -24,11 +26,14 @@ export interface IWebviewService {
_serviceBrand: any;
createWebview(
id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions,
): Webview;
}
export const WebviewResourceScheme = 'vscode-resource';
export interface WebviewOptions {
readonly allowSvgs?: boolean;
readonly extension?: {
@@ -45,7 +50,7 @@ export interface WebviewContentOptions {
readonly portMappings?: ReadonlyArray<modes.IWebviewPortMapping>;
}
export interface Webview {
export interface Webview extends IDisposable {
html: string;
options: WebviewContentOptions;
@@ -68,17 +73,10 @@ export interface Webview {
layout(): void;
mountTo(parent: HTMLElement): void;
focus(): void;
dispose(): void;
reload(): void;
selectAll(): void;
copy(): void;
paste(): void;
cut(): void;
undo(): void;
redo(): void;
showFind(): void;
hideFind(): void;
}
export const webviewDeveloperCategory = nls.localize('developer', "Developer");

View File

@@ -28,16 +28,64 @@
// @ts-ignore
const ipcRenderer = require('electron').ipcRenderer;
require('../../browser/pre/main')({
let isInDevelopmentMode = false;
/**
* @type {import('../../browser/pre/main').WebviewHost}
*/
const host = {
postMessage: (channel, data) => {
ipcRenderer.sendToHost(channel, data);
},
onMessage: (channel, handler) => {
ipcRenderer.on(channel, handler);
},
focusIframeOnCreate: true,
onIframeLoaded: (newFrame) => {
newFrame.contentWindow.onbeforeunload = () => {
if (isInDevelopmentMode) { // Allow reloads while developing a webview
host.postMessage('do-reload');
return false;
}
// Block navigation when not in development mode
console.log('prevented webview navigation');
return false;
};
// Electron 4 eats mouseup events from inside webviews
// https://github.com/microsoft/vscode/issues/75090
// Try to fix this by rebroadcasting mouse moves and mouseups so that we can
// emulate these on the main window
let isMouseDown = false;
newFrame.contentWindow.addEventListener('mousedown', () => {
isMouseDown = true;
});
const tryDispatchSyntheticMouseEvent = (e) => {
if (!isMouseDown) {
host.postMessage('synthetic-mouse-event', { type: e.type, screenX: e.screenX, screenY: e.screenY, clientX: e.clientX, clientY: e.clientY });
}
};
newFrame.contentWindow.addEventListener('mouseup', e => {
tryDispatchSyntheticMouseEvent(e);
isMouseDown = false;
});
newFrame.contentWindow.addEventListener('mousemove', tryDispatchSyntheticMouseEvent);
}
};
host.onMessage('devtools-opened', () => {
isInDevelopmentMode = true;
});
document.addEventListener('DOMContentLoaded', () => {
registerVscodeResourceScheme();
// Forward messages from the embedded iframe
window.onmessage = (message) => {
ipcRenderer.sendToHost(message.data.command, message.data.data);
};
});
require('../../browser/pre/main')(host);
}());

View File

@@ -3,8 +3,90 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { isMacintosh } from 'vs/base/common/platform';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWebviewService } from 'vs/workbench/contrib/webview/common/webview';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor';
import { IWebviewService, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/common/webview';
import * as webviewCommands from 'vs/workbench/contrib/webview/electron-browser/webviewCommands';
import { WebviewService } from 'vs/workbench/contrib/webview/electron-browser/webviewService';
registerSingleton(IWebviewService, WebviewService, true);
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(
new SyncActionDescriptor(webviewCommands.OpenWebviewDeveloperToolsAction, webviewCommands.OpenWebviewDeveloperToolsAction.ID, webviewCommands.OpenWebviewDeveloperToolsAction.LABEL),
webviewCommands.OpenWebviewDeveloperToolsAction.ALIAS,
webviewDeveloperCategory);
function registerWebViewCommands(editorId: string): void {
const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', editorId), ContextKeyExpr.not('editorFocus') /* https://github.com/Microsoft/vscode/issues/58668 */);
(new webviewCommands.SelectAllWebviewEditorCommand({
id: webviewCommands.SelectAllWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_A,
weight: KeybindingWeight.EditorContrib
}
})).register();
// These commands are only needed on MacOS where we have to disable the menu bar commands
if (isMacintosh) {
(new webviewCommands.CopyWebviewEditorCommand({
id: webviewCommands.CopyWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new webviewCommands.PasteWebviewEditorCommand({
id: webviewCommands.PasteWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new webviewCommands.CutWebviewEditorCommand({
id: webviewCommands.CutWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_X,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new webviewCommands.UndoWebviewEditorCommand({
id: webviewCommands.UndoWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_Z,
weight: KeybindingWeight.EditorContrib
}
})).register();
(new webviewCommands.RedoWebviewEditorCommand({
id: webviewCommands.RedoWebviewEditorCommand.ID,
precondition: ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)),
kbOpts: {
primary: KeyMod.CtrlCmd | KeyCode.KEY_Y,
secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z],
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z },
weight: KeybindingWeight.EditorContrib
}
})).register();
}
}
registerWebViewCommands(WebviewEditor.ID);

View File

@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import * as nls from 'vs/nls';
import { Command, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
export class OpenWebviewDeveloperToolsAction extends Action {
static readonly ID = 'workbench.action.webview.openDeveloperTools';
static readonly ALIAS = 'Open Webview Developer Tools';
static readonly LABEL = nls.localize('openToolsLabel', "Open Webview Developer Tools");
public constructor(id: string, label: string) {
super(id, label);
}
public run(): Promise<any> {
const elements = document.querySelectorAll('webview.ready');
for (let i = 0; i < elements.length; i++) {
try {
(elements.item(i) as Electron.WebviewTag).openDevTools();
} catch (e) {
console.error(e);
}
}
return Promise.resolve(true);
}
}
export class SelectAllWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.selectAll';
public runCommand(accessor: ServicesAccessor, args: any): void {
withActiveWebviewBasedWebview(accessor, webview => webview.selectAll());
}
}
export class CopyWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.copy';
public runCommand(accessor: ServicesAccessor, _args: any): void {
withActiveWebviewBasedWebview(accessor, webview => webview.copy());
}
}
export class PasteWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.paste';
public runCommand(accessor: ServicesAccessor, _args: any): void {
withActiveWebviewBasedWebview(accessor, webview => webview.paste());
}
}
export class CutWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.cut';
public runCommand(accessor: ServicesAccessor, _args: any): void {
withActiveWebviewBasedWebview(accessor, webview => webview.cut());
}
}
export class UndoWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.undo';
public runCommand(accessor: ServicesAccessor, args: any): void {
withActiveWebviewBasedWebview(accessor, webview => webview.undo());
}
}
export class RedoWebviewEditorCommand extends Command {
public static readonly ID = 'editor.action.webvieweditor.redo';
public runCommand(accessor: ServicesAccessor, args: any): void {
withActiveWebviewBasedWebview(accessor, webview => webview.redo());
}
}
function getActiveWebviewEditor(accessor: ServicesAccessor): WebviewEditor | undefined {
const editorService = accessor.get(IEditorService);
const activeControl = editorService.activeControl as WebviewEditor;
return activeControl.isWebviewEditor ? activeControl : undefined;
}
function withActiveWebviewBasedWebview(accessor: ServicesAccessor, f: (webview: WebviewElement) => void): void {
const webViewEditor = getActiveWebviewEditor(accessor);
if (webViewEditor) {
webViewEditor.withWebview(webview => {
if (webview instanceof WebviewElement) {
f(webview);
}
});
}
}

View File

@@ -11,20 +11,17 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { isMacintosh } from 'vs/base/common/platform';
import { endsWith } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import * as modes from 'vs/editor/common/modes';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService';
import { Webview, WebviewContentOptions, WebviewOptions } from 'vs/workbench/contrib/webview/common/webview';
import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { WebviewPortMappingManager } from 'vs/workbench/contrib/webview/common/portMapping';
import { getWebviewThemeData } from 'vs/workbench/contrib/webview/common/themeing';
import { Webview, WebviewContentOptions, WebviewOptions, WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/webview';
import { registerFileProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols';
import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService';
import { WebviewFindWidget } from '../browser/webviewFindWidget';
@@ -117,7 +114,6 @@ class WebviewProtocolProvider extends Disposable {
webview: Electron.WebviewTag,
private readonly _extensionLocation: URI | undefined,
private readonly _getLocalResourceRoots: () => ReadonlyArray<URI>,
private readonly _environmentService: IEnvironmentService,
private readonly _fileService: IFileService,
) {
super();
@@ -135,13 +131,7 @@ class WebviewProtocolProvider extends Disposable {
return;
}
const appRootUri = URI.file(this._environmentService.appRoot);
registerFileProtocol(contents, WebviewProtocol.CoreResource, this._fileService, undefined, () => [
appRootUri
]);
registerFileProtocol(contents, WebviewProtocol.VsCodeResource, this._fileService, this._extensionLocation, () =>
registerFileProtocol(contents, WebviewResourceScheme, this._fileService, this._extensionLocation, () =>
this._getLocalResourceRoots()
);
}
@@ -149,88 +139,22 @@ class WebviewProtocolProvider extends Disposable {
class WebviewPortMappingProvider extends Disposable {
private readonly _tunnels = new Map<number, Promise<RemoteTunnel>>();
private readonly _manager: WebviewPortMappingManager;
constructor(
session: WebviewSession,
extensionLocation: URI | undefined,
mappings: () => ReadonlyArray<modes.IWebviewPortMapping>,
private readonly tunnelService: ITunnelService,
extensionId: ExtensionIdentifier | undefined,
@ITelemetryService telemetryService: ITelemetryService
tunnelService: ITunnelService,
) {
super();
this._manager = this._register(new WebviewPortMappingManager(extensionLocation, mappings, tunnelService));
let hasLogged = false;
session.onBeforeRequest(async (details) => {
const uri = URI.parse(details.url);
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return undefined;
}
const localhostMatch = /^localhost:(\d+)$/.exec(uri.authority);
if (localhostMatch) {
if (!hasLogged && extensionId) {
hasLogged = true;
/* __GDPR__
"webview.accessLocalhost" : {
"extension" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetryService.publicLog('webview.accessLocalhost', { extension: extensionId.value });
}
const port = +localhostMatch[1];
for (const mapping of mappings()) {
if (mapping.webviewPort === port) {
if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) {
const tunnel = await this.getOrCreateTunnel(mapping.extensionHostPort);
if (tunnel) {
return {
redirectURL: details.url.replace(
new RegExp(`^${uri.scheme}://localhost:${mapping.webviewPort}/`),
`${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`)
};
}
}
if (mapping.webviewPort !== mapping.extensionHostPort) {
return {
redirectURL: details.url.replace(
new RegExp(`^${uri.scheme}://localhost:${mapping.webviewPort}/`),
`${uri.scheme}://localhost:${mapping.extensionHostPort}/`)
};
}
}
}
}
return undefined;
session.onBeforeRequest(async details => {
const redirect = await this._manager.getRedirect(details.url);
return redirect ? { redirectURL: redirect } : undefined;
});
}
dispose() {
super.dispose();
for (const tunnel of this._tunnels.values()) {
tunnel.then(tunnel => tunnel.dispose());
}
this._tunnels.clear();
}
private getOrCreateTunnel(remotePort: number): Promise<RemoteTunnel> | undefined {
const existing = this._tunnels.get(remotePort);
if (existing) {
return existing;
}
const tunnel = this.tunnelService.openTunnel(remotePort);
if (tunnel) {
this._tunnels.set(remotePort, tunnel);
}
return tunnel;
}
}
class SvgBlocker extends Disposable {
@@ -257,7 +181,8 @@ class SvgBlocker extends Disposable {
});
session.onHeadersReceived((details) => {
const contentType: string[] = details.responseHeaders['content-type'] || details.responseHeaders['Content-Type'];
const headers: any = details.responseHeaders;
const contentType: string[] = headers['content-type'] || headers['Content-Type'];
if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) {
const uri = URI.parse(details.url);
if (uri && !this.isAllowedSvg(uri)) {
@@ -361,27 +286,25 @@ interface WebviewContent {
}
export class WebviewElement extends Disposable implements Webview {
private _webview: Electron.WebviewTag;
private _webview: Electron.WebviewTag | undefined;
private _ready: Promise<void>;
private _webviewFindWidget: WebviewFindWidget;
private _webviewFindWidget: WebviewFindWidget | undefined;
private _findStarted: boolean = false;
private content: WebviewContent;
private _focused = false;
private readonly _onDidFocus = this._register(new Emitter<void>());
public get onDidFocus(): Event<void> { return this._onDidFocus.event; }
public readonly onDidFocus: Event<void> = this._onDidFocus.event;
constructor(
private readonly _options: WebviewOptions,
contentOptions: WebviewContentOptions,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@ITunnelService tunnelService: ITunnelService,
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super();
@@ -404,8 +327,8 @@ export class WebviewElement extends Disposable implements Webview {
this._webview.src = 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E';
this._ready = new Promise(resolve => {
const subscription = this._register(addDisposableListener(this._webview, 'ipc-message', (event) => {
if (event.channel === 'webview-ready') {
const subscription = this._register(addDisposableListener(this._webview!, 'ipc-message', (event) => {
if (this._webview && event.channel === 'webview-ready') {
// console.info('[PID Webview] ' event.args[0]);
addClass(this._webview, 'ready'); // can be found by debug command
@@ -421,7 +344,6 @@ export class WebviewElement extends Disposable implements Webview {
this._webview,
this._options.extension ? this._options.extension.location : undefined,
() => (this.content.options.localResourceRoots || []),
environmentService,
fileService));
this._register(new WebviewPortMappingProvider(
@@ -429,8 +351,6 @@ export class WebviewElement extends Disposable implements Webview {
_options.extension ? _options.extension.location : undefined,
() => (this.content.options.portMappings || []),
tunnelService,
_options.extension ? _options.extension.id : undefined,
telemetryService
));
if (!this._options.allowSvgs) {
@@ -447,7 +367,7 @@ export class WebviewElement extends Disposable implements Webview {
this.layout();
// Workaround for https://github.com/electron/electron/issues/14474
if (this._focused || document.activeElement === this._webview) {
if (this._webview && (this._focused || document.activeElement === this._webview)) {
this._webview.blur();
this._webview.focus();
}
@@ -456,6 +376,10 @@ export class WebviewElement extends Disposable implements Webview {
console.error('embedded page crashed');
}));
this._register(addDisposableListener(this._webview, 'ipc-message', (event) => {
if (!this._webview) {
return;
}
switch (event.channel) {
case 'onmessage':
if (event.args && event.args.length) {
@@ -468,6 +392,18 @@ export class WebviewElement extends Disposable implements Webview {
this._onDidClickLink.fire(URI.parse(uri));
return;
case 'synthetic-mouse-event':
{
const rawEvent = event.args[0];
const bounds = this._webview.getBoundingClientRect();
window.dispatchEvent(new MouseEvent(rawEvent.type, {
...rawEvent,
clientX: rawEvent.clientX + bounds.left,
clientY: rawEvent.clientY + bounds.top,
}));
return;
}
case 'did-set-content':
this._webview.style.flex = '';
this._webview.style.width = '100%';
@@ -509,10 +445,14 @@ export class WebviewElement extends Disposable implements Webview {
}
this.style(themeService.getTheme());
themeService.onThemeChange(this.style, this, this._toDispose);
this._register(themeService.onThemeChange(this.style, this));
}
public mountTo(parent: HTMLElement) {
if (!this._webview) {
return;
}
if (this._webviewFindWidget) {
parent.appendChild(this._webviewFindWidget.getDomNode()!);
}
@@ -524,10 +464,13 @@ export class WebviewElement extends Disposable implements Webview {
if (this._webview.parentElement) {
this._webview.parentElement.removeChild(this._webview);
}
this._webview = undefined;
}
this._webview = undefined!;
this._webviewFindWidget = undefined!;
if (this._webviewFindWidget) {
this._webviewFindWidget.dispose();
this._webviewFindWidget = undefined;
}
super.dispose();
}
@@ -543,9 +486,13 @@ export class WebviewElement extends Disposable implements Webview {
private readonly _onMessage = this._register(new Emitter<any>());
public readonly onMessage = this._onMessage.event;
private _send(channel: string, ...args: any[]): void {
private _send(channel: string, data?: any): void {
this._ready
.then(() => this._webview.send(channel, ...args))
.then(() => {
if (this._webview) {
this._webview.send(channel, data);
}
})
.catch(err => console.error(err));
}
@@ -604,6 +551,9 @@ export class WebviewElement extends Disposable implements Webview {
}
public focus(): void {
if (!this._webview) {
return;
}
this._webview.focus();
this._send('focus');
@@ -629,31 +579,8 @@ export class WebviewElement extends Disposable implements Webview {
}
private style(theme: ITheme): void {
const configuration = this._configurationService.getValue<IEditorOptions>('editor');
const editorFontFamily = configuration.fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
const editorFontWeight = configuration.fontWeight || EDITOR_FONT_DEFAULTS.fontWeight;
const editorFontSize = configuration.fontSize || EDITOR_FONT_DEFAULTS.fontSize;
const exportedColors = colorRegistry.getColorRegistry().getColors().reduce((colors, entry) => {
const color = theme.getColor(entry.id);
if (color) {
colors['vscode-' + entry.id.replace('.', '-')] = color.toString();
}
return colors;
}, {} as { [key: string]: string });
const styles = {
'vscode-font-family': '-apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif',
'vscode-font-weight': 'normal',
'vscode-font-size': '13px',
'vscode-editor-font-family': editorFontFamily,
'vscode-editor-font-weight': editorFontWeight,
'vscode-editor-font-size': editorFontSize,
...exportedColors
};
const activeTheme = ApiThemeClassName.fromTheme(theme);
this._send('styles', styles, activeTheme);
const { styles, activeTheme } = getWebviewThemeData(theme, this._configurationService);
this._send('styles', { styles, activeTheme });
if (this._webviewFindWidget) {
this._webviewFindWidget.updateTheme(theme);
@@ -661,6 +588,9 @@ export class WebviewElement extends Disposable implements Webview {
}
public layout(): void {
if (!this._webview) {
return;
}
const contents = this._webview.getWebContents();
if (!contents || contents.isDestroyed()) {
return;
@@ -679,7 +609,7 @@ export class WebviewElement extends Disposable implements Webview {
}
public startFind(value: string, options?: Electron.FindInPageOptions) {
if (!value) {
if (!value || !this._webview) {
return;
}
@@ -706,6 +636,10 @@ export class WebviewElement extends Disposable implements Webview {
* @param value The string to search for. Empty strings are ignored.
*/
public find(value: string, previous: boolean): void {
if (!this._webview) {
return;
}
// Searching with an empty value will throw an exception
if (!value) {
return;
@@ -721,6 +655,9 @@ export class WebviewElement extends Disposable implements Webview {
}
public stopFind(keepSelection?: boolean): void {
if (!this._webview) {
return;
}
this._findStarted = false;
this._webview.stopFindInPage(keepSelection ? 'keepSelection' : 'clearSelection');
}
@@ -742,45 +679,38 @@ export class WebviewElement extends Disposable implements Webview {
}
public selectAll() {
this._webview.selectAll();
if (this._webview) {
this._webview.selectAll();
}
}
public copy() {
this._webview.copy();
if (this._webview) {
this._webview.copy();
}
}
public paste() {
this._webview.paste();
if (this._webview) {
this._webview.paste();
}
}
public cut() {
this._webview.cut();
if (this._webview) {
this._webview.cut();
}
}
public undo() {
this._webview.undo();
if (this._webview) {
this._webview.undo();
}
}
public redo() {
this._webview.redo();
}
}
enum ApiThemeClassName {
light = 'vscode-light',
dark = 'vscode-dark',
highContrast = 'vscode-high-contrast'
}
namespace ApiThemeClassName {
export function fromTheme(theme: ITheme): ApiThemeClassName {
if (theme.type === LIGHT) {
return ApiThemeClassName.light;
} else if (theme.type === DARK) {
return ApiThemeClassName.dark;
} else {
return ApiThemeClassName.highContrast;
if (this._webview) {
this._webview.redo();
}
}
}

View File

@@ -2,67 +2,36 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getMediaMime, MIME_UNKNOWN } from 'vs/base/common/mime';
import { extname, sep } from 'vs/base/common/path';
import { startsWith } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { IFileService } from 'vs/platform/files/common/files';
import * as electron from 'electron';
type BufferProtocolCallback = (buffer?: Buffer | electron.MimeTypedBuffer | { error: number }) => void;
export const enum WebviewProtocol {
CoreResource = 'vscode-core-resource',
VsCodeResource = 'vscode-resource',
}
function resolveContent(fileService: IFileService, resource: URI, mime: string, callback: BufferProtocolCallback): void {
fileService.readFile(resource).then(contents => {
callback({
data: Buffer.from(contents.value.buffer),
mimeType: mime
});
}, (err) => {
console.log(err);
callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
});
}
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { loadLocalResource } from 'vs/workbench/contrib/webview/common/resourceLoader';
export function registerFileProtocol(
contents: electron.WebContents,
protocol: WebviewProtocol,
protocol: string,
fileService: IFileService,
extensionLocation: URI | undefined,
getRoots: () => ReadonlyArray<URI>
) {
contents.session.protocol.registerBufferProtocol(protocol, (request, callback: any) => {
const requestPath = URI.parse(request.url).path;
const normalizedPath = URI.file(requestPath);
for (const root of getRoots()) {
if (!startsWith(normalizedPath.fsPath, root.fsPath + sep)) {
continue;
}
if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) {
const requestUri = URI.parse(request.url);
const redirectedUri = URI.from({
scheme: REMOTE_HOST_SCHEME,
authority: extensionLocation.authority,
path: '/vscode-resource',
query: JSON.stringify({
requestResourcePath: requestUri.path
})
contents.session.protocol.registerBufferProtocol(protocol, async (request, callback: any) => {
try {
const result = await loadLocalResource(URI.parse(request.url), fileService, extensionLocation, getRoots);
if (result.type === 'success') {
return callback({
data: Buffer.from(result.data.buffer),
mimeType: result.mimeType
});
resolveContent(fileService, redirectedUri, getMimeType(requestUri), callback);
return;
} else {
resolveContent(fileService, normalizedPath, getMimeType(normalizedPath), callback);
return;
}
if (result.type === 'access-denied') {
console.error('Webview: Cannot load resource outside of protocol root');
return callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
}
} catch {
// noop
}
console.error('Webview: Cannot load resource outside of protocol root');
callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
return callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
}, (error) => {
if (error) {
console.error(`Failed to register '${protocol}' protocol`);
@@ -70,20 +39,3 @@ export function registerFileProtocol(
});
}
const webviewMimeTypes = {
'.svg': 'image/svg+xml',
'.txt': 'text/plain',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.html': 'text/html',
'.htm': 'text/html',
'.xhtml': 'application/xhtml+xml',
'.oft': 'font/otf',
'.xml': 'application/xml',
};
function getMimeType(normalizedPath: URI): string {
const ext = extname(normalizedPath.fsPath).toLowerCase();
return webviewMimeTypes[ext] || getMediaMime(normalizedPath.fsPath) || MIME_UNKNOWN;
}

View File

@@ -15,13 +15,12 @@ export class WebviewService implements IWebviewService {
) { }
createWebview(
_id: string,
options: WebviewOptions,
contentOptions: WebviewContentOptions
): Webview {
const element = this._instantiationService.createInstance(WebviewElement,
return this._instantiationService.createInstance(WebviewElement,
options,
contentOptions);
return element;
}
}