cloudstudio
Version:
Run VS Code on a remote server.
1,193 lines (1,026 loc) • 35.4 kB
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 'sha256-JpX/ganPoxpavjxWCz9DUZgwVZ59o2lwSYTQrziPsdU=' 'self' https://*.cloudstudio.net https://*.cloud.tencent.com;
frame-src 'self' https://*.cloudstudio.net https://*.cloud.tencent.com; style-src 'unsafe-inline' https://*.cloudstudio.net https://*.cloud.tencent.com;">
<!-- Disable pinch zooming -->
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
</head>
<body style="margin: 0; overflow: hidden; width: 100%; height: 100%" role="document">
<script async type="module">
// @ts-check
/// <reference lib="dom" />
const isSafari = (
navigator.vendor && navigator.vendor.indexOf('Apple') > -1 &&
navigator.userAgent &&
navigator.userAgent.indexOf('CriOS') === -1 &&
navigator.userAgent.indexOf('FxiOS') === -1
);
const isFirefox = (
navigator.userAgent &&
navigator.userAgent.indexOf('Firefox') >= 0
);
const searchParams = new URL(location.toString()).searchParams;
const ID = searchParams.get('id');
const webviewOrigin = searchParams.get('origin');
const onElectron = searchParams.get('platform') === 'electron';
const expectedWorkerVersion = parseInt(searchParams.get('swVersion'));
/**
* 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 = 250;
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 | undefined} */ (document.getElementById('active-frame'));
};
const getPendingFrame = () => {
return /** @type {HTMLIFrameElement | undefined} */ (document.getElementById('pending-frame'));
};
/**
* @template T
* @param {T | undefined | null} obj
* @return {T}
*/
function assertIsDefined(obj) {
if (typeof obj === 'undefined' || obj === null) {
throw new Error('Found unexpected null');
}
return obj;
}
const vscodePostMessageFuncName = '__vscode_post_message__';
const defaultStyles = document.createElement('style');
defaultStyles.id = '_defaultStyles';
defaultStyles.textContent = `
html {
scrollbar-color: var(--vscode-scrollbarSlider-background) var(--vscode-editor-background);
}
body {
background-color: transparent;
color: var(--vscode-editor-foreground);
font-family: var(--vscode-font-family);
font-weight: var(--vscode-font-weight);
font-size: var(--vscode-font-size);
margin: 0;
padding: 0 20px;
}
img {
max-width: 100%;
max-height: 100%;
}
a, a code {
color: var(--vscode-textLink-foreground);
}
a:hover {
color: var(--vscode-textLink-activeForeground);
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
code {
color: var(--vscode-textPreformat-foreground);
}
blockquote {
background: var(--vscode-textBlockQuote-background);
border-color: var(--vscode-textBlockQuote-border);
}
kbd {
color: var(--vscode-editor-foreground);
border-radius: 3px;
vertical-align: middle;
padding: 1px 3px;
background-color: hsla(0,0%,50%,.17);
border: 1px solid rgba(71,71,71,.4);
border-bottom-color: rgba(88,88,88,.4);
box-shadow: inset 0 -1px 0 rgba(88,88,88,.4);
}
.vscode-light kbd {
background-color: hsla(0,0%,87%,.5);
border: 1px solid hsla(0,0%,80%,.7);
border-bottom-color: hsla(0,0%,73%,.7);
box-shadow: inset 0 -1px 0 hsla(0,0%,73%,.7);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-corner {
background-color: var(--vscode-editor-background);
}
::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground);
}
::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground);
}
::highlight(find-highlight) {
background-color: var(--vscode-editor-findMatchHighlightBackground);
}
::highlight(current-find-highlight) {
background-color: var(--vscode-editor-findMatchBackground);
}`;
/**
* @param {boolean} allowMultipleAPIAcquire
* @param {*} [state]
* @return {string}
*/
function getVsCodeApiScript(allowMultipleAPIAcquire, state) {
const encodedState = state ? encodeURIComponent(state) : undefined;
return /* js */`
globalThis.acquireVsCodeApi = (function() {
const originalPostMessage = window.parent['${vscodePostMessageFuncName}'].bind(window.parent);
const doPostMessage = (channel, data, transfer) => {
originalPostMessage(channel, data, transfer);
};
let acquired = false;
let state = ${state ? `JSON.parse(decodeURIComponent("${encodedState}"))` : undefined};
return () => {
if (acquired && !${allowMultipleAPIAcquire}) {
throw new Error('An instance of the VS Code API has already been acquired');
}
acquired = true;
return Object.freeze({
postMessage: function(message, transfer) {
doPostMessage('onmessage', { message, transfer }, transfer);
},
setState: function(newState) {
state = newState;
doPostMessage('do-update-state', JSON.stringify(newState));
return newState;
},
getState: function() {
return state;
}
});
};
})();
delete window.parent;
delete window.top;
delete window.frameElement;
`;
}
/** @type {Promise<void>} */
const workerReady = new Promise((resolve, reject) => {
if (!areServiceWorkersEnabled()) {
return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.'));
}
const swPath = `service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`;
navigator.serviceWorker.register(swPath)
.then(() => navigator.serviceWorker.ready)
.then(async registration => {
/**
* @param {MessageEvent} event
*/
const versionHandler = async (event) => {
if (event.data.channel !== 'version') {
return;
}
navigator.serviceWorker.removeEventListener('message', versionHandler);
if (event.data.version === expectedWorkerVersion) {
return resolve();
} else {
console.log(`Found unexpected service worker version. Found: ${event.data.version}. Expected: ${expectedWorkerVersion}`);
console.log(`Attempting to reload service worker`);
// If we have the wrong version, try once (and only once) to unregister and re-register
// Note that `.update` doesn't seem to work desktop electron at the moment so we use
// `unregister` and `register` here.
return registration.unregister()
.then(() => navigator.serviceWorker.register(swPath))
.then(() => navigator.serviceWorker.ready)
.finally(() => { resolve(); });
}
};
navigator.serviceWorker.addEventListener('message', versionHandler);
const postVersionMessage = (/** @type {ServiceWorker} */ controller) => {
controller.postMessage({ channel: 'version' });
};
// At this point, either the service worker is ready and
// became our controller, or we need to wait for it.
// Note that navigator.serviceWorker.controller could be a
// controller from a previously loaded service worker.
const currentController = navigator.serviceWorker.controller;
if (currentController?.scriptURL.endsWith(swPath)) {
// service worker already loaded & ready to receive messages
postVersionMessage(currentController);
} else {
// either there's no controlling service worker, or it's an old one:
// wait for it to change before posting the message
const onControllerChange = () => {
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
postVersionMessage(navigator.serviceWorker.controller);
};
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
}
}).catch(error => {
reject(new Error(`Could not register service workers: ${error}.`));
});
});
const hostMessaging = new class HostMessaging {
constructor() {
this.channel = new MessageChannel();
/** @type {Map<string, Array<(event: MessageEvent, data: any) => void>>} */
this.handlers = new Map();
this.channel.port1.onmessage = (e) => {
const channel = e.data.channel;
const handlers = this.handlers.get(channel);
if (handlers) {
for (const handler of handlers) {
handler(e, e.data.args);
}
} else {
console.log('no handler for ', e);
}
};
}
/**
* @param {string} channel
* @param {any} data
* @param {any} [transfer]
*/
postMessage(channel, data, transfer) {
this.channel.port1.postMessage({ channel, data }, transfer);
}
/**
* @param {string} channel
* @param {(event: MessageEvent, data: any) => void} handler
*/
onMessage(channel, handler) {
let handlers = this.handlers.get(channel);
if (!handlers) {
handlers = [];
this.handlers.set(channel, handlers);
}
handlers.push(handler);
}
async signalReady() {
const start = (/** @type {string} */ parentOrigin) => {
window.parent.postMessage({ target: ID, channel: 'webview-ready', data: {} }, parentOrigin, [this.channel.port2]);
};
const parentOrigin = searchParams.get('parentOrigin');
const hostname = location.hostname;
if (parentOrigin) {
return start(parentOrigin);
}
if (!crypto.subtle) {
// cannot validate, not running in a secure context
throw new Error(`Cannot validate in current context!`);
}
// Here the `parentOriginHash()` function from `src/vs/workbench/common/webview.ts` is inlined
// compute a sha-256 composed of `parentOrigin` and `salt` converted to base 32
let parentOriginHash;
try {
const strData = JSON.stringify({ parentOrigin, salt: webviewOrigin });
const encoder = new TextEncoder();
const arrData = encoder.encode(strData);
const hash = await crypto.subtle.digest('sha-256', arrData);
const hashArray = Array.from(new Uint8Array(hash));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32
parentOriginHash = BigInt(`0x${hashHex}`).toString(32).padStart(52, '0');
} catch (err) {
throw err instanceof Error ? err : new Error(String(err));
}
if (hostname === parentOriginHash || hostname.startsWith(parentOriginHash + '.')) {
// validation succeeded!
return start(parentOrigin);
}
throw new Error(`Expected '${parentOriginHash}' as hostname or subdomain!`);
}
}();
const unloadMonitor = new class {
constructor() {
this.confirmBeforeClose = 'keyboardOnly';
this.isModifierKeyDown = false;
hostMessaging.onMessage('set-confirm-before-close', (_e, /** @type {string} */ data) => {
this.confirmBeforeClose = data;
});
hostMessaging.onMessage('content', (_e, /** @type {any} */ data) => {
this.confirmBeforeClose = data.confirmBeforeClose;
});
window.addEventListener('beforeunload', (event) => {
if (onElectron) {
return;
}
switch (this.confirmBeforeClose) {
case 'always': {
event.preventDefault();
event.returnValue = '';
return '';
}
case 'never': {
break;
}
case 'keyboardOnly':
default: {
if (this.isModifierKeyDown) {
event.preventDefault();
event.returnValue = '';
return '';
}
break;
}
}
});
}
onIframeLoaded(/** @type {HTMLIFrameElement} */ frame) {
frame.contentWindow.addEventListener('keydown', e => {
this.isModifierKeyDown = e.metaKey || e.ctrlKey || e.altKey;
});
frame.contentWindow.addEventListener('keyup', () => {
this.isModifierKeyDown = false;
});
}
};
// state
let firstLoad = true;
/** @type {any} */
let loadTimeout;
let styleVersion = 0;
/** @type {Array<{ readonly message: any, transfer?: ArrayBuffer[] }>} */
let pendingMessages = [];
const initData = {
/** @type {number | undefined} */
initialScrollProgress: undefined,
/** @type {{ [key: string]: string } | undefined} */
styles: undefined,
/** @type {string | undefined} */
activeTheme: undefined,
/** @type {string | undefined} */
themeName: undefined,
/** @type {boolean} */
screenReader: false,
/** @type {boolean} */
reduceMotion: false,
};
hostMessaging.onMessage('did-load-resource', (_event, data) => {
navigator.serviceWorker.ready.then(registration => {
assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []);
});
});
hostMessaging.onMessage('did-load-localhost', (_event, data) => {
navigator.serviceWorker.ready.then(registration => {
assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data });
});
});
navigator.serviceWorker.addEventListener('message', event => {
switch (event.data.channel) {
case 'load-resource':
case 'load-localhost':
hostMessaging.postMessage(event.data.channel, event.data);
return;
}
});
/**
* @param {HTMLDocument?} document
* @param {HTMLElement?} body
*/
const applyStyles = (document, body) => {
if (!document) {
return;
}
if (body) {
body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-high-contrast-light', 'vscode-reduce-motion', 'vscode-using-screen-reader');
if (initData.activeTheme) {
body.classList.add(initData.activeTheme);
if (initData.activeTheme === 'vscode-high-contrast-light') {
// backwards compatibility
body.classList.add('vscode-high-contrast');
}
}
if (initData.reduceMotion) {
body.classList.add('vscode-reduce-motion');
}
if (initData.screenReader) {
body.classList.add('vscode-using-screen-reader');
}
body.dataset.vscodeThemeKind = initData.activeTheme;
body.dataset.vscodeThemeName = initData.themeName || '';
}
if (initData.styles) {
const documentStyle = document.documentElement.style;
// Remove stale properties
for (let i = documentStyle.length - 1; i >= 0; i--) {
const property = documentStyle[i];
// Don't remove properties that the webview might have added separately
if (property && property.startsWith('--vscode-')) {
documentStyle.removeProperty(property);
}
}
// Re-add new properties
for (const variable of Object.keys(initData.styles)) {
documentStyle.setProperty(`--${variable}`, initData.styles[variable]);
}
}
};
/**
* @param {MouseEvent} event
*/
const handleInnerClick = (event) => {
if (!event?.view?.document) {
return;
}
const baseElement = event.view.document.querySelector('base');
for (const pathElement of event.composedPath()) {
/** @type {any} */
const node = pathElement;
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 === baseElement.href + node.hash))) {
const fragment = node.hash.slice(1);
const scrollTarget = event.view.document.getElementById(fragment) ?? event.view.document.getElementById(decodeURIComponent(fragment));
scrollTarget?.scrollIntoView();
} else {
hostMessaging.postMessage('did-click-link', node.href.baseVal || node.href);
}
event.preventDefault();
return;
}
}
};
/**
* @param {MouseEvent} event
*/
const handleAuxClick = (event) => {
// Prevent middle clicks opening a broken link in the browser
if (!event?.view?.document) {
return;
}
if (event.button === 1) {
for (const pathElement of event.composedPath()) {
/** @type {any} */
const node = pathElement;
if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) {
event.preventDefault();
return;
}
}
}
};
/**
* @param {KeyboardEvent} e
*/
const handleInnerKeydown = (e) => {
// If the keypress would trigger a browser event, such as copy or paste,
// make sure we block the browser from dispatching it. Instead VS Code
// handles these events and will dispatch a copy/paste back to the webview
// if needed
if (isUndoRedo(e) || isPrint(e) || isFindEvent(e)) {
e.preventDefault();
} else if (isCopyPasteOrCut(e)) {
if (onElectron) {
e.preventDefault();
} else {
return; // let the browser handle this
}
}
hostMessaging.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
});
};
/**
* @param {KeyboardEvent} e
*/
const handleInnerUp = (e) => {
hostMessaging.postMessage('did-keyup', {
key: e.key,
keyCode: e.keyCode,
code: e.code,
shiftKey: e.shiftKey,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
repeat: e.repeat
});
};
/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isCopyPasteOrCut(e) {
const hasMeta = e.ctrlKey || e.metaKey;
const shiftInsert = e.shiftKey && e.key.toLowerCase() === 'insert';
return (hasMeta && ['c', 'v', 'x'].includes(e.key.toLowerCase())) || shiftInsert;
}
/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isUndoRedo(e) {
const hasMeta = e.ctrlKey || e.metaKey;
return hasMeta && ['z', 'y'].includes(e.key.toLowerCase());
}
/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isPrint(e) {
const hasMeta = e.ctrlKey || e.metaKey;
return hasMeta && e.key.toLowerCase() === 'p';
}
/**
* @param {KeyboardEvent} e
* @return {boolean}
*/
function isFindEvent(e) {
const hasMeta = e.ctrlKey || e.metaKey;
return hasMeta && e.key.toLowerCase() === 'f';
}
let isHandlingScroll = false;
/**
* @param {WheelEvent} event
*/
const handleWheel = (event) => {
if (isHandlingScroll) {
return;
}
hostMessaging.postMessage('did-scroll-wheel', {
deltaMode: event.deltaMode,
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaZ: event.deltaZ,
detail: event.detail,
type: event.type
});
};
/**
* @param {Event} event
*/
const handleInnerScroll = (event) => {
if (isHandlingScroll) {
return;
}
const target = /** @type {HTMLDocument | null} */ (event.target);
const currentTarget = /** @type {Window | null} */ (event.currentTarget);
if (!currentTarget || !target?.body) {
return;
}
const progress = currentTarget.scrollY / target.body.clientHeight;
if (isNaN(progress)) {
return;
}
isHandlingScroll = true;
window.requestAnimationFrame(() => {
try {
hostMessaging.postMessage('did-scroll', progress);
} catch (e) {
// noop
}
isHandlingScroll = false;
});
};
function handleInnerDragStartEvent(/** @type {DragEvent} */ e) {
if (e.defaultPrevented) {
// Extension code has already handled this event
return;
}
if (!e.dataTransfer || e.shiftKey) {
return;
}
// Only handle drags from outside editor for now
if (e.dataTransfer.items.length && Array.prototype.every.call(e.dataTransfer.items, item => item.kind === 'file')) {
hostMessaging.postMessage('drag-start');
}
}
/**
* @param {() => void} callback
*/
function onDomReady(callback) {
if (document.readyState === 'interactive' || document.readyState === 'complete') {
callback();
} else {
document.addEventListener('DOMContentLoaded', callback);
}
}
function areServiceWorkersEnabled() {
try {
return !!navigator.serviceWorker;
} catch (e) {
return false;
}
}
/**
* @typedef {{
* contents: string;
* options: {
* readonly allowScripts: boolean;
* readonly allowForms: boolean;
* readonly allowMultipleAPIAcquire: boolean;
* }
* state: any;
* cspSource: string;
* }} ContentUpdateData
*/
/**
* @param {ContentUpdateData} data
* @return {string}
*/
function toContentHtml(data) {
const options = data.options;
const text = data.contents;
const newDocument = new DOMParser().parseFromString(text, 'text/html');
newDocument.querySelectorAll('a').forEach(a => {
if (!a.title) {
const href = a.getAttribute('href');
if (typeof href === 'string') {
a.title = href;
}
}
});
// Set default aria role
if (!newDocument.body.hasAttribute('role')) {
newDocument.body.setAttribute('role', 'document');
}
// Inject default script
if (options.allowScripts) {
const defaultScript = newDocument.createElement('script');
defaultScript.id = '_vscodeApiScript';
defaultScript.textContent = getVsCodeApiScript(options.allowMultipleAPIAcquire, data.state);
newDocument.head.prepend(defaultScript);
}
// Inject default styles
newDocument.head.prepend(defaultStyles.cloneNode(true));
applyStyles(newDocument, newDocument.body);
// Strip out unsupported http-equiv tags
for (const metaElement of Array.from(newDocument.querySelectorAll('meta'))) {
const httpEquiv = metaElement.getAttribute('http-equiv');
if (httpEquiv && !/^(content-security-policy|default-style|content-type)$/i.test(httpEquiv)) {
console.warn(`Removing unsupported meta http-equiv: ${httpEquiv}`);
metaElement.remove();
}
}
// Check for CSP
const csp = newDocument.querySelector('meta[http-equiv="Content-Security-Policy"]');
if (!csp) {
hostMessaging.postMessage('no-csp-found');
} else {
try {
// Attempt to rewrite CSPs that hardcode old-style resource endpoint
const cspContent = csp.getAttribute('content');
if (cspContent) {
const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, data.cspSource);
csp.setAttribute('content', newCsp);
}
} catch (e) {
console.error(`Could not rewrite csp: ${e}`);
}
}
// set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off
// and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden
return '<!DOCTYPE html>\n' + newDocument.documentElement.outerHTML;
}
onDomReady(() => {
if (!document.body) {
return;
}
hostMessaging.onMessage('styles', (_event, data) => {
++styleVersion;
initData.styles = data.styles;
initData.activeTheme = data.activeTheme;
initData.themeName = data.themeName;
initData.reduceMotion = data.reduceMotion;
initData.screenReader = data.screenReader;
const target = getActiveFrame();
if (!target) {
return;
}
if (target.contentDocument) {
applyStyles(target.contentDocument, target.contentDocument.body);
}
});
// propagate focus
hostMessaging.onMessage('focus', () => {
const activeFrame = getActiveFrame();
if (!activeFrame || !activeFrame.contentWindow) {
// Focus the top level webview instead
window.focus();
return;
}
if (document.activeElement === activeFrame) {
// We are already focused on the iframe (or one of its children) so no need
// to refocus.
return;
}
activeFrame.contentWindow.focus();
});
// update iframe-contents
let updateId = 0;
hostMessaging.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => {
const currentUpdateId = ++updateId;
try {
await workerReady;
} catch (e) {
console.error(`Webview fatal error: ${e}`);
hostMessaging.postMessage('fatal-error', { message: e + '' });
return;
}
if (currentUpdateId !== updateId) {
return;
}
const options = data.options;
const newDocument = toContentHtml(data);
const initialStyleVersion = styleVersion;
const frame = getActiveFrame();
const wasFirstLoad = firstLoad;
// keep current scrollY around and use later
/** @type {(body: HTMLElement, window: Window) => void} */
let setInitialScrollPosition;
if (firstLoad) {
firstLoad = false;
setInitialScrollPosition = (body, window) => {
if (typeof initData.initialScrollProgress === 'number' && !isNaN(initData.initialScrollProgress)) {
if (window.scrollY === 0) {
window.scroll(0, body.clientHeight * initData.initialScrollProgress);
}
}
};
} else {
const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? assertIsDefined(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');
const sandboxRules = new Set(['allow-same-origin', 'allow-pointer-lock']);
if (options.allowScripts) {
sandboxRules.add('allow-scripts');
sandboxRules.add('allow-downloads');
}
if (options.allowForms) {
sandboxRules.add('allow-forms');
}
newFrame.setAttribute('sandbox', Array.from(sandboxRules).join(' '));
const allowRules = ['cross-origin-isolated;']
if(!isFirefox && options.allowScripts) {
allowRules.push('clipboard-read;','clipboard-write;')
}
newFrame.setAttribute('allow', allowRules.join(' '));
// 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.
const fakeUrlParams = new URLSearchParams({id: ID});
if(globalThis.crossOriginIsolated) {
fakeUrlParams.set('vscode-coi', '3') /*COOP+COEP*/
}
newFrame.src = `./fake.html?${fakeUrlParams.toString()}`;
newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden';
document.body.appendChild(newFrame);
/**
* @param {Document} contentDocument
*/
function onFrameLoaded(contentDocument) {
// Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325
setTimeout(() => {
contentDocument.open();
contentDocument.write(newDocument);
contentDocument.close();
hookupOnLoadHandlers(newFrame);
if (initialStyleVersion !== styleVersion) {
applyStyles(contentDocument, contentDocument.body);
}
}, 0);
}
if (!options.allowScripts && isSafari) {
// On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired: https://bugs.webkit.org/show_bug.cgi?id=33604
// Use polling instead.
const interval = setInterval(() => {
// If the frame is no longer mounted, loading has stopped
if (!newFrame.parentElement) {
clearInterval(interval);
return;
}
const contentDocument = assertIsDefined(newFrame.contentDocument);
if (contentDocument.location.pathname.endsWith('/fake.html') && contentDocument.readyState !== 'loading') {
clearInterval(interval);
onFrameLoaded(contentDocument);
}
}, 10);
} else {
assertIsDefined(newFrame.contentWindow).addEventListener('DOMContentLoaded', e => {
const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined;
onFrameLoaded(assertIsDefined(contentDocument));
});
}
/**
* @param {Document} contentDocument
* @param {Window} contentWindow
*/
const onLoad = (contentDocument, contentWindow) => {
if (contentDocument && contentDocument.body) {
// Workaround for https://github.com/microsoft/vscode/issues/12865
// check new scrollY and reset if necessary
setInitialScrollPosition(contentDocument.body, contentWindow);
}
const newFrame = getPendingFrame();
if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) {
const wasFocused = document.hasFocus();
const oldActiveFrame = getActiveFrame();
if (oldActiveFrame) {
document.body.removeChild(oldActiveFrame);
}
// Styles may have changed since we created the element. Make sure we re-style
if (initialStyleVersion !== styleVersion) {
applyStyles(newFrame.contentDocument, newFrame.contentDocument.body);
}
newFrame.setAttribute('id', 'active-frame');
newFrame.style.visibility = 'visible';
contentWindow.addEventListener('scroll', handleInnerScroll);
contentWindow.addEventListener('wheel', handleWheel);
if (wasFocused) {
contentWindow.focus();
}
pendingMessages.forEach((message) => {
contentWindow.postMessage(message.message, window.origin, message.transfer);
});
pendingMessages = [];
}
};
/**
* @param {HTMLIFrameElement} newFrame
*/
function hookupOnLoadHandlers(newFrame) {
clearTimeout(loadTimeout);
loadTimeout = undefined;
loadTimeout = setTimeout(() => {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(assertIsDefined(newFrame.contentDocument), assertIsDefined(newFrame.contentWindow));
}, 200);
const contentWindow = assertIsDefined(newFrame.contentWindow);
contentWindow.addEventListener('load', function (e) {
const contentDocument = /** @type {Document} */ (e.target);
if (loadTimeout) {
clearTimeout(loadTimeout);
loadTimeout = undefined;
onLoad(contentDocument, this);
}
});
// Bubble out various events
contentWindow.addEventListener('click', handleInnerClick);
contentWindow.addEventListener('auxclick', handleAuxClick);
contentWindow.addEventListener('keydown', handleInnerKeydown);
contentWindow.addEventListener('keyup', handleInnerUp);
contentWindow.addEventListener('contextmenu', e => {
if (e.defaultPrevented) {
// Extension code has already handled this event
return;
}
e.preventDefault();
let context = {};
/** @type HTMLElement */
let el = e.target;
while (true) {
if (!el) {
break;
}
// Search self/ancestors for the closest context data attribute
el = el.closest('[data-vscode-context]');
if (!el) {
break;
}
try {
context = { ...JSON.parse(el.dataset.vscodeContext), ...context };
} catch (e) {
console.error(`Error parsing 'data-vscode-context' as json`, el, e)
}
el = el.parentElement;
}
hostMessaging.postMessage('did-context-menu', {
clientX: e.clientX,
clientY: e.clientY,
context: context
});
});
contentWindow.addEventListener('dragenter', handleInnerDragStartEvent);
contentWindow.addEventListener('dragover', handleInnerDragStartEvent);
unloadMonitor.onIframeLoaded(newFrame);
}
});
// Forward message to the embedded iframe
hostMessaging.onMessage('message', (_event, /** @type {{message: any, transfer?: ArrayBuffer[] }} */ data) => {
const pending = getPendingFrame();
if (!pending) {
const target = getActiveFrame();
if (target) {
assertIsDefined(target.contentWindow).postMessage(data.message, window.origin, data.transfer);
return;
}
}
pendingMessages.push(data);
});
hostMessaging.onMessage('initial-scroll-position', (_event, progress) => {
initData.initialScrollProgress = progress;
});
hostMessaging.onMessage('execCommand', (_event, data) => {
const target = getActiveFrame();
if (!target) {
return;
}
assertIsDefined(target.contentDocument).execCommand(data);
});
/** @type {string | undefined} */
let lastFindValue = undefined;
hostMessaging.onMessage('find', (_event, data) => {
const target = getActiveFrame();
if (!target) {
return;
}
if (!data.previous && lastFindValue !== data.value) {
// Reset selection so we start search at the head of the last search
const selection = target.contentWindow.getSelection();
selection.collapse(selection.anchorNode);
}
lastFindValue = data.value;
const didFind = (/** @type {any} */ (target.contentWindow)).find(
data.value,
/* caseSensitive*/ false,
/* backwards*/ data.previous,
/* wrapAround*/ true,
/* wholeWord */ false,
/* searchInFrames*/ false,
false);
hostMessaging.postMessage('did-find', didFind);
});
hostMessaging.onMessage('find-stop', (_event, data) => {
const target = getActiveFrame();
if (!target) {
return;
}
lastFindValue = undefined;
if (!data.clearSelection) {
const selection = target.contentWindow.getSelection();
for (let i = 0; i < selection.rangeCount; i++) {
selection.removeRange(selection.getRangeAt(i));
}
}
});
trackFocus({
onFocus: () => hostMessaging.postMessage('did-focus'),
onBlur: () => hostMessaging.postMessage('did-blur')
});
(/** @type {any} */ (window))[vscodePostMessageFuncName] = (/** @type {string} */ command, /** @type {any} */ data) => {
switch (command) {
case 'onmessage':
case 'do-update-state':
hostMessaging.postMessage(command, data);
break;
}
};
// Also forward events before the contents of the webview have loaded
window.addEventListener('keydown', handleInnerKeydown);
window.addEventListener('dragenter', handleInnerDragStartEvent);
window.addEventListener('dragover', handleInnerDragStartEvent);
hostMessaging.signalReady();
});
</script>
</body>
</html>