mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)
* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 * disable strict null check
This commit is contained in:
99
src/vs/workbench/contrib/webview/browser/pre/host.js
Normal file
99
src/vs/workbench/contrib/webview/browser/pre/host.js
Normal 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
|
||||
});
|
||||
}());
|
||||
19
src/vs/workbench/contrib/webview/browser/pre/index.html
Normal file
19
src/vs/workbench/contrib/webview/browser/pre/index.html
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}());
|
||||
|
||||
276
src/vs/workbench/contrib/webview/browser/pre/service-worker.js
Normal file
276
src/vs/workbench/contrib/webview/browser/pre/service-worker.js
Normal 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));
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
323
src/vs/workbench/contrib/webview/browser/webviewElement.ts
Normal file
323
src/vs/workbench/contrib/webview/browser/webviewElement.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export class WebviewFindWidget extends SimpleFindWidget {
|
||||
} else {
|
||||
this._delegate.stopFind(false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected onFocusTrackerFocus() { }
|
||||
|
||||
27
src/vs/workbench/contrib/webview/browser/webviewService.ts
Normal file
27
src/vs/workbench/contrib/webview/browser/webviewService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
26
src/vs/workbench/contrib/webview/common/mimeTypes.ts
Normal file
26
src/vs/workbench/contrib/webview/common/mimeTypes.ts
Normal 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;
|
||||
}
|
||||
87
src/vs/workbench/contrib/webview/common/portMapping.ts
Normal file
87
src/vs/workbench/contrib/webview/common/portMapping.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
80
src/vs/workbench/contrib/webview/common/resourceLoader.ts
Normal file
80
src/vs/workbench/contrib/webview/common/resourceLoader.ts
Normal 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);
|
||||
}
|
||||
63
src/vs/workbench/contrib/webview/common/themeing.ts
Normal file
63
src/vs/workbench/contrib/webview/common/themeing.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}());
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user