mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Vscode merge (#4582)
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
This commit is contained in:
455
src/vs/workbench/contrib/webview/electron-browser/webview-pre.js
Normal file
455
src/vs/workbench/contrib/webview/electron-browser/webview-pre.js
Normal file
@@ -0,0 +1,455 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 () {
|
||||
'use strict';
|
||||
|
||||
// @ts-ignore
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
const registerVscodeResourceScheme = (function () {
|
||||
let hasRegistered = false;
|
||||
return () => {
|
||||
if (hasRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasRegistered = true;
|
||||
|
||||
// @ts-ignore
|
||||
require('electron').webFrame.registerURLSchemeAsPrivileged('vscode-resource', {
|
||||
secure: true,
|
||||
bypassCSP: false,
|
||||
allowServiceWorkers: false,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true
|
||||
});
|
||||
};
|
||||
}());
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
// state
|
||||
let firstLoad = true;
|
||||
let loadTimeout;
|
||||
let pendingMessages = [];
|
||||
let isInDevelopmentMode = false;
|
||||
|
||||
const initData = {
|
||||
initialScrollProgress: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLDocument} document
|
||||
* @param {HTMLElement} body
|
||||
*/
|
||||
const applyStyles = (document, body) => {
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast');
|
||||
body.classList.add(initData.activeTheme);
|
||||
|
||||
if (initData.styles) {
|
||||
for (const variable of Object.keys(initData.styles)) {
|
||||
document.documentElement.style.setProperty(`--${variable}`, initData.styles[variable]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getActiveFrame = () => {
|
||||
return /** @type {HTMLIFrameElement} */ (document.getElementById('active-frame'));
|
||||
};
|
||||
|
||||
const getPendingFrame = () => {
|
||||
return /** @type {HTMLIFrameElement} */ (document.getElementById('pending-frame'));
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
ipcRenderer.sendToHost('did-click-link', node.href);
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} e
|
||||
*/
|
||||
const handleInnerKeydown = (e) => {
|
||||
ipcRenderer.sendToHost('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
|
||||
});
|
||||
};
|
||||
|
||||
const onMessage = (message) => {
|
||||
ipcRenderer.sendToHost(message.data.command, message.data.data);
|
||||
};
|
||||
|
||||
let isHandlingScroll = false;
|
||||
const handleInnerScroll = (event) => {
|
||||
if (isHandlingScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = event.currentTarget.scrollY / event.target.body.clientHeight;
|
||||
if (isNaN(progress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isHandlingScroll = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
try {
|
||||
ipcRenderer.sendToHost('did-scroll', progress);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
isHandlingScroll = false;
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ipcRenderer.on('baseUrl', (_event, value) => {
|
||||
initData.baseUrl = value;
|
||||
});
|
||||
|
||||
ipcRenderer.on('styles', (_event, variables, activeTheme) => {
|
||||
initData.styles = variables;
|
||||
initData.activeTheme = activeTheme;
|
||||
|
||||
const target = getActiveFrame();
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyStyles(target.contentDocument, target.contentDocument.body);
|
||||
});
|
||||
|
||||
// propagate focus
|
||||
ipcRenderer.on('focus', () => {
|
||||
const target = getActiveFrame();
|
||||
if (target) {
|
||||
target.contentWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// update iframe-contents
|
||||
ipcRenderer.on('content', (_event, data) => {
|
||||
const options = data.options;
|
||||
|
||||
registerVscodeResourceScheme();
|
||||
|
||||
const text = data.contents;
|
||||
const newDocument = new DOMParser().parseFromString(text, 'text/html');
|
||||
|
||||
newDocument.querySelectorAll('a').forEach(a => {
|
||||
if (!a.title) {
|
||||
a.title = a.getAttribute('href');
|
||||
}
|
||||
});
|
||||
|
||||
// set base-url if applicable
|
||||
if (initData.baseUrl && newDocument.head.getElementsByTagName('base').length === 0) {
|
||||
const baseElement = newDocument.createElement('base');
|
||||
baseElement.href = initData.baseUrl;
|
||||
newDocument.head.appendChild(baseElement);
|
||||
}
|
||||
|
||||
// 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;
|
||||
`;
|
||||
|
||||
newDocument.head.prepend(defaultScript);
|
||||
}
|
||||
|
||||
// apply default styles
|
||||
const defaultStyles = newDocument.createElement('style');
|
||||
defaultStyles.id = '_defaultStyles';
|
||||
defaultStyles.innerHTML = defaultCssRules;
|
||||
newDocument.head.prepend(defaultStyles);
|
||||
|
||||
applyStyles(newDocument, newDocument.body);
|
||||
|
||||
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.onbeforeunload = () => {
|
||||
if (isInDevelopmentMode) { // Allow reloads while developing a webview
|
||||
ipcRenderer.sendToHost('do-reload');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block navigation when not in development mode
|
||||
console.log('prevented webview navigation');
|
||||
return false;
|
||||
};
|
||||
|
||||
let onLoad = (contentDocument, contentWindow) => {
|
||||
if (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 === 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();
|
||||
|
||||
ipcRenderer.sendToHost('did-set-content');
|
||||
});
|
||||
|
||||
// Forward message to the embedded iframe
|
||||
ipcRenderer.on('message', (_event, data) => {
|
||||
const pending = getPendingFrame();
|
||||
if (!pending) {
|
||||
const target = getActiveFrame();
|
||||
if (target) {
|
||||
target.contentWindow.postMessage(data, '*');
|
||||
return;
|
||||
}
|
||||
}
|
||||
pendingMessages.push(data);
|
||||
});
|
||||
|
||||
ipcRenderer.on('initial-scroll-position', (_event, progress) => {
|
||||
initData.initialScrollProgress = progress;
|
||||
});
|
||||
|
||||
ipcRenderer.on('devtools-opened', () => {
|
||||
isInDevelopmentMode = true;
|
||||
});
|
||||
|
||||
trackFocus({
|
||||
onFocus: () => { ipcRenderer.sendToHost('did-focus'); },
|
||||
onBlur: () => { ipcRenderer.sendToHost('did-blur'); }
|
||||
});
|
||||
|
||||
// Forward messages from the embedded iframe
|
||||
window.onmessage = onMessage;
|
||||
|
||||
// signal ready
|
||||
ipcRenderer.sendToHost('webview-ready', process.pid);
|
||||
});
|
||||
|
||||
const defaultCssRules = `
|
||||
body {
|
||||
background-color: var(--vscode-editor-background);
|
||||
color: var(--vscode-editor-foreground);
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-weight: var(--vscode-editor-font-weight);
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
a:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--vscode-textPreformat-foreground);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background: var(--vscode-textBlockQuote-background);
|
||||
border-color: var(--vscode-textBlockQuote-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--vscode-scrollbarSlider-background);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}`;
|
||||
}());
|
||||
@@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
|
||||
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/electron-browser/webviewEditorInputFactory';
|
||||
import { HideWebViewEditorFindCommand, OpenWebviewDeveloperToolsAction, ReloadWebviewAction, ShowWebViewEditorFindWidgetCommand, SelectAllWebviewEditorCommand, CopyWebviewEditorCommand, PasteWebviewEditorCommand, CutWebviewEditorCommand, UndoWebviewEditorCommand, RedoWebviewEditorCommand } from './webviewCommands';
|
||||
import { WebviewEditor, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from './webviewEditor';
|
||||
import { WebviewEditorInput } from './webviewEditorInput';
|
||||
import { IWebviewEditorService, WebviewEditorService } from './webviewEditorService';
|
||||
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
|
||||
(Registry.as<IEditorRegistry>(EditorExtensions.Editors)).registerEditor(new EditorDescriptor(
|
||||
WebviewEditor,
|
||||
WebviewEditor.ID,
|
||||
localize('webview.editor.label', "webview editor")),
|
||||
[new SyncDescriptor(WebviewEditorInput)]);
|
||||
|
||||
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
|
||||
WebviewEditorInputFactory.ID,
|
||||
WebviewEditorInputFactory);
|
||||
|
||||
registerSingleton(IWebviewEditorService, WebviewEditorService, true);
|
||||
|
||||
|
||||
const webviewDeveloperCategory = localize('developer', "Developer");
|
||||
|
||||
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
|
||||
export 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({
|
||||
id: ShowWebViewEditorFindWidgetCommand.ID,
|
||||
precondition: contextKeyExpr,
|
||||
kbOpts: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_F,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
showNextFindWidgetCommand.register();
|
||||
|
||||
(new HideWebViewEditorFindCommand({
|
||||
id: HideWebViewEditorFindCommand.ID,
|
||||
precondition: ContextKeyExpr.and(
|
||||
contextKeyExpr,
|
||||
KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE),
|
||||
kbOpts: {
|
||||
primary: KeyCode.Escape,
|
||||
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',
|
||||
webviewDeveloperCategory);
|
||||
@@ -0,0 +1,155 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Command } from 'vs/editor/browser/editorExtensions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { WebviewEditor } from 'vs/workbench/contrib/webview/electron-browser/webviewEditor';
|
||||
|
||||
export class ShowWebViewEditorFindWidgetCommand extends Command {
|
||||
public static readonly ID = 'editor.action.webvieweditor.showFind';
|
||||
|
||||
public runCommand(accessor: ServicesAccessor, args: any): void {
|
||||
const webViewEditor = getActiveWebviewEditor(accessor);
|
||||
if (webViewEditor) {
|
||||
webViewEditor.showFind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class HideWebViewEditorFindCommand extends Command {
|
||||
public static readonly ID = 'editor.action.webvieweditor.hideFind';
|
||||
|
||||
public runCommand(accessor: ServicesAccessor, args: any): void {
|
||||
const webViewEditor = getActiveWebviewEditor(accessor);
|
||||
if (webViewEditor) {
|
||||
webViewEditor.hideFind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
public constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private readonly editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public run(): Promise<any> {
|
||||
for (const webview of this.getVisibleWebviews()) {
|
||||
webview.reload();
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
private getVisibleWebviews() {
|
||||
return this.editorService.visibleControls
|
||||
.filter(control => control && (control as WebviewEditor).isWebviewEditor)
|
||||
.map(control => control as WebviewEditor);
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveWebviewEditor(accessor: ServicesAccessor): WebviewEditor | null {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const activeControl = editorService.activeControl as WebviewEditor;
|
||||
return activeControl.isWebviewEditor ? activeControl : null;
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { URI } from 'vs/base/common/uri';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
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/electron-browser/webviewEditorInput';
|
||||
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { WebviewElement } from './webviewElement';
|
||||
|
||||
/** A context key that is set when the find widget in a webview is visible. */
|
||||
export const KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE = new RawContextKey<boolean>('webviewFindWidgetVisible', false);
|
||||
|
||||
|
||||
export class WebviewEditor extends BaseEditor {
|
||||
|
||||
protected _webview: WebviewElement | undefined;
|
||||
protected findWidgetVisible: IContextKey<boolean>;
|
||||
|
||||
public static readonly ID = 'WebviewEditor';
|
||||
|
||||
private _editorFrame: HTMLElement;
|
||||
private _content?: HTMLElement;
|
||||
private _webviewContent: HTMLElement | undefined;
|
||||
|
||||
private _webviewFocusTrackerDisposables: IDisposable[] = [];
|
||||
private _onFocusWindowHandler?: IDisposable;
|
||||
|
||||
private readonly _onDidFocusWebview = this._register(new Emitter<void>());
|
||||
public get onDidFocus(): Event<any> { return this._onDidFocusWebview.event; }
|
||||
|
||||
private pendingMessages: any[] = [];
|
||||
|
||||
constructor(
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IContextKeyService private _contextKeyService: IContextKeyService,
|
||||
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(WebviewEditor.ID, telemetryService, themeService, storageService);
|
||||
if (_contextKeyService) {
|
||||
this.findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(_contextKeyService);
|
||||
}
|
||||
}
|
||||
|
||||
protected createEditor(parent: HTMLElement): void {
|
||||
this._editorFrame = parent;
|
||||
this._content = document.createElement('div');
|
||||
parent.appendChild(this._content);
|
||||
}
|
||||
|
||||
private doUpdateContainer() {
|
||||
const webviewContainer = this.input && (this.input as WebviewEditorInput).container;
|
||||
if (webviewContainer && webviewContainer.parentElement) {
|
||||
const frameRect = this._editorFrame.getBoundingClientRect();
|
||||
const containerRect = webviewContainer.parentElement.getBoundingClientRect();
|
||||
|
||||
webviewContainer.style.position = 'absolute';
|
||||
webviewContainer.style.top = `${frameRect.top - containerRect.top}px`;
|
||||
webviewContainer.style.left = `${frameRect.left - containerRect.left}px`;
|
||||
webviewContainer.style.width = `${frameRect.width}px`;
|
||||
webviewContainer.style.height = `${frameRect.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.pendingMessages = [];
|
||||
|
||||
// Let the editor input dispose of the webview.
|
||||
this._webview = undefined;
|
||||
this._webviewContent = undefined;
|
||||
|
||||
if (this._content && this._content.parentElement) {
|
||||
this._content.parentElement.removeChild(this._content);
|
||||
this._content = undefined;
|
||||
}
|
||||
|
||||
this._webviewFocusTrackerDisposables = dispose(this._webviewFocusTrackerDisposables);
|
||||
|
||||
if (this._onFocusWindowHandler) {
|
||||
this._onFocusWindowHandler.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public sendMessage(data: any): void {
|
||||
if (this._webview) {
|
||||
this._webview.sendMessage(data);
|
||||
} else {
|
||||
this.pendingMessages.push(data);
|
||||
}
|
||||
}
|
||||
public showFind() {
|
||||
if (this._webview) {
|
||||
this._webview.showFind();
|
||||
this.findWidgetVisible.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
public hideFind() {
|
||||
this.findWidgetVisible.reset();
|
||||
if (this._webview) {
|
||||
this._webview.hideFind();
|
||||
}
|
||||
}
|
||||
|
||||
public get isWebviewEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.withWebviewElement(webview => webview.reload());
|
||||
}
|
||||
|
||||
public layout(_dimension: DOM.Dimension): void {
|
||||
this.withWebviewElement(webview => {
|
||||
this.doUpdateContainer();
|
||||
webview.layout();
|
||||
});
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
super.focus();
|
||||
if (!this._onFocusWindowHandler) {
|
||||
|
||||
// Make sure we restore focus when switching back to a VS Code window
|
||||
this._onFocusWindowHandler = this._windowService.onDidChangeFocus(focused => {
|
||||
if (focused && this._editorService.activeControl === this) {
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
this.withWebviewElement(webview => webview.focus());
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
this.withWebviewElement(webview => webview.selectAll());
|
||||
}
|
||||
|
||||
public copy(): void {
|
||||
this.withWebviewElement(webview => webview.copy());
|
||||
}
|
||||
|
||||
public paste(): void {
|
||||
this.withWebviewElement(webview => webview.paste());
|
||||
}
|
||||
|
||||
public cut(): void {
|
||||
this.withWebviewElement(webview => webview.cut());
|
||||
}
|
||||
|
||||
public undo(): void {
|
||||
this.withWebviewElement(webview => webview.undo());
|
||||
}
|
||||
|
||||
public redo(): void {
|
||||
this.withWebviewElement(webview => webview.redo());
|
||||
}
|
||||
|
||||
private withWebviewElement(f: (element: WebviewElement) => void): void {
|
||||
if (this._webview) {
|
||||
f(this._webview);
|
||||
}
|
||||
}
|
||||
|
||||
protected setEditorVisible(visible: boolean, group: IEditorGroup): void {
|
||||
if (this.input && this.input instanceof WebviewEditorInput) {
|
||||
if (visible) {
|
||||
this.input.claimWebview(this);
|
||||
} else {
|
||||
this.input.releaseWebview(this);
|
||||
}
|
||||
|
||||
this.updateWebview(this.input as WebviewEditorInput);
|
||||
}
|
||||
|
||||
if (this._webviewContent) {
|
||||
if (visible) {
|
||||
this._webviewContent.style.visibility = 'visible';
|
||||
this.doUpdateContainer();
|
||||
} else {
|
||||
this._webviewContent.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
super.setEditorVisible(visible, group);
|
||||
}
|
||||
|
||||
public clearInput() {
|
||||
if (this.input && this.input instanceof WebviewEditorInput) {
|
||||
this.input.releaseWebview(this);
|
||||
}
|
||||
|
||||
this._webview = undefined;
|
||||
this._webviewContent = undefined;
|
||||
this.pendingMessages = [];
|
||||
|
||||
super.clearInput();
|
||||
}
|
||||
|
||||
setInput(input: WebviewEditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
|
||||
if (this.input) {
|
||||
(this.input as WebviewEditorInput).releaseWebview(this);
|
||||
this._webview = undefined;
|
||||
this._webviewContent = undefined;
|
||||
}
|
||||
this.pendingMessages = [];
|
||||
return super.setInput(input, options, token)
|
||||
.then(() => input.resolve())
|
||||
.then(() => {
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
if (this.group) {
|
||||
input.updateGroup(this.group.id);
|
||||
}
|
||||
this.updateWebview(input);
|
||||
});
|
||||
}
|
||||
|
||||
private updateWebview(input: WebviewEditorInput) {
|
||||
const webview = this.getWebview(input);
|
||||
input.claimWebview(this);
|
||||
webview.update(input.html, {
|
||||
allowScripts: input.options.enableScripts,
|
||||
localResourceRoots: input.options.localResourceRoots || this.getDefaultLocalResourceRoots(),
|
||||
}, !!input.options.retainContextWhenHidden);
|
||||
|
||||
if (this._webviewContent) {
|
||||
this._webviewContent.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
this.doUpdateContainer();
|
||||
}
|
||||
|
||||
private getDefaultLocalResourceRoots(): URI[] {
|
||||
const rootPaths = this._contextService.getWorkspace().folders.map(x => x.uri);
|
||||
const extensionLocation = (this.input as WebviewEditorInput).extensionLocation;
|
||||
if (extensionLocation) {
|
||||
rootPaths.push(extensionLocation);
|
||||
}
|
||||
return rootPaths;
|
||||
}
|
||||
|
||||
private getWebview(input: WebviewEditorInput): WebviewElement {
|
||||
if (this._webview) {
|
||||
return this._webview;
|
||||
}
|
||||
|
||||
this._webviewContent = input.container;
|
||||
|
||||
if (input.webview) {
|
||||
this._webview = input.webview;
|
||||
} 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._webview = this._instantiationService.createInstance(WebviewElement,
|
||||
this._layoutService.getContainer(Parts.EDITOR_PART),
|
||||
{
|
||||
allowSvgs: true,
|
||||
extensionLocation: input.extensionLocation,
|
||||
enableFindWidget: input.options.enableFindWidget
|
||||
},
|
||||
{});
|
||||
this._webview.mountTo(this._webviewContent);
|
||||
input.webview = this._webview;
|
||||
|
||||
if (input.options.tryRestoreScrollPosition) {
|
||||
this._webview.initialScrollProgress = input.scrollYPercentage;
|
||||
}
|
||||
|
||||
this._webview.state = input.webviewState;
|
||||
|
||||
this._content!.setAttribute('aria-flowto', this._webviewContent.id);
|
||||
|
||||
this.doUpdateContainer();
|
||||
}
|
||||
|
||||
for (const message of this.pendingMessages) {
|
||||
this._webview.sendMessage(message);
|
||||
}
|
||||
this.pendingMessages = [];
|
||||
|
||||
this.trackFocus();
|
||||
|
||||
return this._webview;
|
||||
}
|
||||
|
||||
private trackFocus() {
|
||||
this._webviewFocusTrackerDisposables = dispose(this._webviewFocusTrackerDisposables);
|
||||
|
||||
// Track focus in webview content
|
||||
const webviewContentFocusTracker = DOM.trackFocus(this._webviewContent!);
|
||||
this._webviewFocusTrackerDisposables.push(webviewContentFocusTracker);
|
||||
this._webviewFocusTrackerDisposables.push(webviewContentFocusTracker.onDidFocus(() => this._onDidFocusWebview.fire()));
|
||||
|
||||
// Track focus in webview element
|
||||
this._webviewFocusTrackerDisposables.push(this._webview!.onDidFocus(() => this._onDidFocusWebview.fire()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEditorModel } from 'vs/platform/editor/common/editor';
|
||||
import { EditorInput, EditorModel, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import * as vscode from 'vscode';
|
||||
import { WebviewEvents, WebviewInputOptions } from './webviewEditorService';
|
||||
import { WebviewElement } from './webviewElement';
|
||||
|
||||
export class WebviewEditorInput extends EditorInput {
|
||||
private static handlePool = 0;
|
||||
|
||||
private static _styleElement?: HTMLStyleElement;
|
||||
|
||||
private static _icons = new Map<number, { light: URI, dark: URI }>();
|
||||
|
||||
private static updateStyleElement(
|
||||
id: number,
|
||||
iconPath: { light: URI, dark: URI } | undefined
|
||||
) {
|
||||
if (!this._styleElement) {
|
||||
this._styleElement = dom.createStyleSheet();
|
||||
this._styleElement.className = 'webview-icons';
|
||||
}
|
||||
|
||||
if (!iconPath) {
|
||||
this._icons.delete(id);
|
||||
} else {
|
||||
this._icons.set(id, iconPath);
|
||||
}
|
||||
|
||||
const cssRules: string[] = [];
|
||||
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()}); }`);
|
||||
} else {
|
||||
cssRules.push(`.vs ${webviewSelector} { content: ""; background-image: url(${value.light.toString()}); }`);
|
||||
cssRules.push(`.vs-dark ${webviewSelector} { content: ""; background-image: url(${value.dark.toString()}); }`);
|
||||
}
|
||||
});
|
||||
this._styleElement.innerHTML = cssRules.join('\n');
|
||||
}
|
||||
|
||||
public static readonly typeId = 'workbench.editors.webviewInput';
|
||||
|
||||
private _name: string;
|
||||
private _iconPath?: { light: URI, dark: URI };
|
||||
private _options: WebviewInputOptions;
|
||||
private _html: string = '';
|
||||
private _currentWebviewHtml: string = '';
|
||||
public _events: WebviewEvents | undefined;
|
||||
private _container?: HTMLElement;
|
||||
private _webview: WebviewElement | undefined;
|
||||
private _webviewOwner: any;
|
||||
private _webviewDisposables: IDisposable[] = [];
|
||||
private _group?: GroupIdentifier;
|
||||
private _scrollYPercentage: number = 0;
|
||||
private _state: any;
|
||||
|
||||
public readonly extensionLocation: URI | undefined;
|
||||
private readonly _id: number;
|
||||
|
||||
constructor(
|
||||
public readonly viewType: string,
|
||||
id: number | undefined,
|
||||
name: string,
|
||||
options: WebviewInputOptions,
|
||||
state: any,
|
||||
events: WebviewEvents,
|
||||
extensionLocation: URI | undefined,
|
||||
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
|
||||
) {
|
||||
super();
|
||||
|
||||
if (typeof id === 'number') {
|
||||
this._id = id;
|
||||
WebviewEditorInput.handlePool = Math.max(id, WebviewEditorInput.handlePool) + 1;
|
||||
} else {
|
||||
this._id = WebviewEditorInput.handlePool++;
|
||||
}
|
||||
|
||||
this._name = name;
|
||||
this._options = options;
|
||||
this._events = events;
|
||||
this._state = state;
|
||||
this.extensionLocation = extensionLocation;
|
||||
}
|
||||
|
||||
public getTypeId(): string {
|
||||
return WebviewEditorInput.typeId;
|
||||
}
|
||||
|
||||
public getId(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeIcon = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeIcon = this._onDidChangeIcon.event;
|
||||
|
||||
public dispose() {
|
||||
this.disposeWebview();
|
||||
|
||||
if (this._container) {
|
||||
this._container.remove();
|
||||
this._container = undefined;
|
||||
}
|
||||
|
||||
if (this._events && this._events.onDispose) {
|
||||
this._events.onDispose();
|
||||
}
|
||||
this._events = undefined;
|
||||
|
||||
this._webview = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public getResource(): URI {
|
||||
return URI.from({
|
||||
scheme: 'webview-panel',
|
||||
path: `webview-panel/webview-${this._id}`
|
||||
});
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
public getTitle() {
|
||||
return this.getName();
|
||||
}
|
||||
|
||||
public getDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public setName(value: string): void {
|
||||
this._name = value;
|
||||
this._onDidChangeLabel.fire();
|
||||
}
|
||||
|
||||
public get iconPath() {
|
||||
return this._iconPath;
|
||||
}
|
||||
|
||||
public set iconPath(value: { light: URI, dark: URI } | undefined) {
|
||||
this._iconPath = value;
|
||||
WebviewEditorInput.updateStyleElement(this._id, value);
|
||||
}
|
||||
|
||||
public matches(other: IEditorInput): boolean {
|
||||
return other === this || (other instanceof WebviewEditorInput && other._id === this._id);
|
||||
}
|
||||
|
||||
public get group(): GroupIdentifier | undefined {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
public get html(): string {
|
||||
return this._html;
|
||||
}
|
||||
|
||||
public set html(value: string) {
|
||||
if (value === this._currentWebviewHtml) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._html = value;
|
||||
|
||||
if (this._webview) {
|
||||
this._webview.contents = value;
|
||||
this._currentWebviewHtml = value;
|
||||
}
|
||||
}
|
||||
|
||||
public get state(): any {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public set state(value: any) {
|
||||
this._state = value;
|
||||
}
|
||||
|
||||
public get webviewState() {
|
||||
return this._state.state;
|
||||
}
|
||||
|
||||
public get options(): WebviewInputOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
public setOptions(value: vscode.WebviewOptions) {
|
||||
this._options = {
|
||||
...this._options,
|
||||
...value
|
||||
};
|
||||
|
||||
if (this._webview) {
|
||||
this._webview.options = {
|
||||
allowScripts: this._options.enableScripts,
|
||||
localResourceRoots: this._options.localResourceRoots
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public resolve(): Promise<IEditorModel> {
|
||||
return Promise.resolve(new EditorModel());
|
||||
}
|
||||
|
||||
public supportsSplitEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public get container(): HTMLElement {
|
||||
if (!this._container) {
|
||||
this._container = document.createElement('div');
|
||||
this._container.id = `webview-${this._id}`;
|
||||
const part = this._layoutService.getContainer(Parts.EDITOR_PART);
|
||||
part.appendChild(this._container);
|
||||
}
|
||||
return this._container;
|
||||
}
|
||||
|
||||
public get webview(): WebviewElement | undefined {
|
||||
return this._webview;
|
||||
}
|
||||
|
||||
public set webview(value: WebviewElement | undefined) {
|
||||
this._webviewDisposables = dispose(this._webviewDisposables);
|
||||
|
||||
this._webview = value;
|
||||
if (!this._webview) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._webview.onDidClickLink(link => {
|
||||
if (this._events && this._events.onDidClickLink) {
|
||||
this._events.onDidClickLink(link, this._options);
|
||||
}
|
||||
}, null, this._webviewDisposables);
|
||||
|
||||
this._webview.onMessage(message => {
|
||||
if (this._events && this._events.onMessage) {
|
||||
this._events.onMessage(message);
|
||||
}
|
||||
}, null, this._webviewDisposables);
|
||||
|
||||
this._webview.onDidScroll(message => {
|
||||
this._scrollYPercentage = message.scrollYPercentage;
|
||||
}, null, this._webviewDisposables);
|
||||
|
||||
this._webview.onDidUpdateState(newState => {
|
||||
this._state.state = newState;
|
||||
}, null, this._webviewDisposables);
|
||||
}
|
||||
|
||||
public get scrollYPercentage() {
|
||||
return this._scrollYPercentage;
|
||||
}
|
||||
|
||||
public claimWebview(owner: any) {
|
||||
this._webviewOwner = owner;
|
||||
}
|
||||
|
||||
public releaseWebview(owner: any) {
|
||||
if (this._webviewOwner === owner) {
|
||||
this._webviewOwner = undefined;
|
||||
if (this._options.retainContextWhenHidden && this._container) {
|
||||
this._container.style.visibility = 'hidden';
|
||||
} else {
|
||||
this.disposeWebview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public disposeWebview() {
|
||||
// The input owns the webview and its parent
|
||||
if (this._webview) {
|
||||
this._webview.dispose();
|
||||
this._webview = undefined;
|
||||
}
|
||||
|
||||
this._webviewDisposables = dispose(this._webviewDisposables);
|
||||
|
||||
this._webviewOwner = undefined;
|
||||
|
||||
if (this._container) {
|
||||
this._container.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
this._currentWebviewHtml = '';
|
||||
}
|
||||
|
||||
public updateGroup(group: GroupIdentifier): void {
|
||||
this._group = group;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class RevivedWebviewEditorInput extends WebviewEditorInput {
|
||||
private _revived: boolean = false;
|
||||
|
||||
constructor(
|
||||
viewType: string,
|
||||
id: number | undefined,
|
||||
name: string,
|
||||
options: WebviewInputOptions,
|
||||
state: any,
|
||||
events: WebviewEvents,
|
||||
extensionLocation: URI | undefined,
|
||||
public readonly reviver: (input: WebviewEditorInput) => Promise<void>,
|
||||
@IWorkbenchLayoutService partService: IWorkbenchLayoutService,
|
||||
) {
|
||||
super(viewType, id, name, options, state, events, extensionLocation, partService);
|
||||
}
|
||||
|
||||
public async resolve(): Promise<IEditorModel> {
|
||||
if (!this._revived) {
|
||||
this._revived = true;
|
||||
await this.reviver(this);
|
||||
}
|
||||
return super.resolve();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IEditorInputFactory } from 'vs/workbench/common/editor';
|
||||
import { WebviewEditorInput } from './webviewEditorInput';
|
||||
import { IWebviewEditorService, WebviewInputOptions } from './webviewEditorService';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
|
||||
interface SerializedIconPath {
|
||||
light: string | UriComponents;
|
||||
dark: string | UriComponents;
|
||||
}
|
||||
|
||||
interface SerializedWebview {
|
||||
readonly viewType: string;
|
||||
readonly id: number;
|
||||
readonly title: string;
|
||||
readonly options: WebviewInputOptions;
|
||||
readonly extensionLocation: string | UriComponents | undefined;
|
||||
readonly state: any;
|
||||
readonly iconPath: SerializedIconPath | undefined;
|
||||
readonly group?: number;
|
||||
}
|
||||
|
||||
export class WebviewEditorInputFactory implements IEditorInputFactory {
|
||||
|
||||
public static readonly ID = WebviewEditorInput.typeId;
|
||||
|
||||
public constructor(
|
||||
@IWebviewEditorService private readonly _webviewService: IWebviewEditorService
|
||||
) { }
|
||||
|
||||
public serialize(
|
||||
input: WebviewEditorInput
|
||||
): string | null {
|
||||
if (!this._webviewService.shouldPersist(input)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: SerializedWebview = {
|
||||
viewType: input.viewType,
|
||||
id: input.getId(),
|
||||
title: input.getName(),
|
||||
options: input.options,
|
||||
extensionLocation: input.extensionLocation,
|
||||
state: input.state,
|
||||
iconPath: input.iconPath ? { light: input.iconPath.light, dark: input.iconPath.dark, } : undefined,
|
||||
group: input.group
|
||||
};
|
||||
|
||||
try {
|
||||
return JSON.stringify(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public deserialize(
|
||||
_instantiationService: IInstantiationService,
|
||||
serializedEditorInput: string
|
||||
): WebviewEditorInput {
|
||||
const data: SerializedWebview = JSON.parse(serializedEditorInput);
|
||||
const extensionLocation = reviveUri(data.extensionLocation);
|
||||
const iconPath = reviveIconPath(data.iconPath);
|
||||
return this._webviewService.reviveWebview(data.viewType, data.id, data.title, iconPath, data.state, data.options, extensionLocation, data.group);
|
||||
}
|
||||
}
|
||||
function reviveIconPath(data: SerializedIconPath | undefined) {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const light = reviveUri(data.light);
|
||||
const dark = reviveUri(data.dark);
|
||||
return light && dark ? { light, dark } : undefined;
|
||||
}
|
||||
|
||||
function reviveUri(data: string | UriComponents | undefined): URI | undefined {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
return URI.parse(data);
|
||||
}
|
||||
return URI.from(data);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { GroupIdentifier } from 'vs/workbench/common/editor';
|
||||
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService';
|
||||
import * as vscode from 'vscode';
|
||||
import { RevivedWebviewEditorInput, WebviewEditorInput } from './webviewEditorInput';
|
||||
|
||||
export const IWebviewEditorService = createDecorator<IWebviewEditorService>('webviewEditorService');
|
||||
|
||||
export interface ICreateWebViewShowOptions {
|
||||
group: IEditorGroup | GroupIdentifier | ACTIVE_GROUP_TYPE | SIDE_GROUP_TYPE;
|
||||
preserveFocus: boolean;
|
||||
}
|
||||
|
||||
export interface IWebviewEditorService {
|
||||
_serviceBrand: any;
|
||||
|
||||
createWebview(
|
||||
viewType: string,
|
||||
title: string,
|
||||
showOptions: ICreateWebViewShowOptions,
|
||||
options: WebviewInputOptions,
|
||||
extensionLocation: URI | undefined,
|
||||
events: WebviewEvents
|
||||
): WebviewEditorInput;
|
||||
|
||||
reviveWebview(
|
||||
viewType: string,
|
||||
id: number,
|
||||
title: string,
|
||||
iconPath: { light: URI, dark: URI } | undefined,
|
||||
state: any,
|
||||
options: WebviewInputOptions,
|
||||
extensionLocation: URI | undefined,
|
||||
group: number | undefined
|
||||
): WebviewEditorInput;
|
||||
|
||||
revealWebview(
|
||||
webview: WebviewEditorInput,
|
||||
group: IEditorGroup,
|
||||
preserveFocus: boolean
|
||||
): void;
|
||||
|
||||
registerReviver(
|
||||
reviver: WebviewReviver
|
||||
): IDisposable;
|
||||
|
||||
shouldPersist(
|
||||
input: WebviewEditorInput
|
||||
): boolean;
|
||||
}
|
||||
|
||||
export interface WebviewReviver {
|
||||
canRevive(
|
||||
webview: WebviewEditorInput
|
||||
): boolean;
|
||||
|
||||
reviveWebview(
|
||||
webview: WebviewEditorInput
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface WebviewEvents {
|
||||
onMessage?(message: any): void;
|
||||
onDispose?(): void;
|
||||
onDidClickLink?(link: URI, options: vscode.WebviewOptions): void;
|
||||
}
|
||||
|
||||
export interface WebviewInputOptions extends vscode.WebviewOptions, vscode.WebviewPanelOptions {
|
||||
tryRestoreScrollPosition?: boolean;
|
||||
}
|
||||
|
||||
export function areWebviewInputOptionsEqual(a: WebviewInputOptions, b: WebviewInputOptions): boolean {
|
||||
return a.enableCommandUris === b.enableCommandUris
|
||||
&& a.enableFindWidget === b.enableFindWidget
|
||||
&& a.enableScripts === b.enableScripts
|
||||
&& 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())));
|
||||
}
|
||||
|
||||
function canRevive(reviver: WebviewReviver, webview: WebviewEditorInput): boolean {
|
||||
if (webview.isDisposed()) {
|
||||
return false;
|
||||
}
|
||||
return reviver.canRevive(webview);
|
||||
}
|
||||
|
||||
class RevivalPool {
|
||||
private _awaitingRevival: Array<{ input: WebviewEditorInput, resolve: () => void }> = [];
|
||||
|
||||
public add(input: WebviewEditorInput, resolve: () => void) {
|
||||
this._awaitingRevival.push({ input, resolve });
|
||||
}
|
||||
|
||||
public reviveFor(reviver: WebviewReviver) {
|
||||
const toRevive = this._awaitingRevival.filter(({ input }) => canRevive(reviver, input));
|
||||
this._awaitingRevival = this._awaitingRevival.filter(({ input }) => !canRevive(reviver, input));
|
||||
|
||||
for (const { input, resolve } of toRevive) {
|
||||
reviver.reviveWebview(input).then(resolve);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WebviewEditorService implements IWebviewEditorService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _revivers = new Set<WebviewReviver>();
|
||||
private readonly _revivalPool = new RevivalPool();
|
||||
|
||||
constructor(
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
|
||||
) { }
|
||||
|
||||
public createWebview(
|
||||
viewType: string,
|
||||
title: string,
|
||||
showOptions: ICreateWebViewShowOptions,
|
||||
options: vscode.WebviewOptions,
|
||||
extensionLocation: URI | undefined,
|
||||
events: WebviewEvents
|
||||
): WebviewEditorInput {
|
||||
const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, viewType, undefined, title, options, {}, events, extensionLocation, undefined);
|
||||
this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus }, showOptions.group);
|
||||
return webviewInput;
|
||||
}
|
||||
|
||||
public revealWebview(
|
||||
webview: WebviewEditorInput,
|
||||
group: IEditorGroup,
|
||||
preserveFocus: boolean
|
||||
): void {
|
||||
if (webview.group === group.id) {
|
||||
this._editorService.openEditor(webview, { preserveFocus }, webview.group);
|
||||
} else {
|
||||
const groupView = this._editorGroupService.getGroup(webview.group!);
|
||||
if (groupView) {
|
||||
groupView.moveEditor(webview, group, { preserveFocus });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public reviveWebview(
|
||||
viewType: string,
|
||||
id: number,
|
||||
title: string,
|
||||
iconPath: { light: URI, dark: URI } | undefined,
|
||||
state: any,
|
||||
options: WebviewInputOptions,
|
||||
extensionLocation: URI,
|
||||
group: number | undefined,
|
||||
): WebviewEditorInput {
|
||||
const webviewInput = this._instantiationService.createInstance(RevivedWebviewEditorInput, viewType, id, title, options, state, {}, extensionLocation, async (webview: WebviewEditorInput): Promise<void> => {
|
||||
const didRevive = await this.tryRevive(webview);
|
||||
if (didRevive) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// A reviver may not be registered yet. Put into pool and resolve promise when we can revive
|
||||
let resolve: () => void;
|
||||
const promise = new Promise<void>(r => { resolve = r; });
|
||||
this._revivalPool.add(webview, resolve!);
|
||||
return promise;
|
||||
});
|
||||
webviewInput.iconPath = iconPath;
|
||||
if (typeof group === 'number') {
|
||||
webviewInput.updateGroup(group);
|
||||
}
|
||||
return webviewInput;
|
||||
}
|
||||
|
||||
public registerReviver(
|
||||
reviver: WebviewReviver
|
||||
): IDisposable {
|
||||
this._revivers.add(reviver);
|
||||
this._revivalPool.reviveFor(reviver);
|
||||
|
||||
return toDisposable(() => {
|
||||
this._revivers.delete(reviver);
|
||||
});
|
||||
}
|
||||
|
||||
public shouldPersist(
|
||||
webview: WebviewEditorInput
|
||||
): boolean {
|
||||
// Has no state, don't persist
|
||||
if (!webview.state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (values(this._revivers).some(reviver => canRevive(reviver, webview))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Revived webviews may not have an actively registered reviver but we still want to presist them
|
||||
// since a reviver should exist when it is actually needed.
|
||||
return !(webview instanceof RevivedWebviewEditorInput);
|
||||
}
|
||||
|
||||
private async tryRevive(
|
||||
webview: WebviewEditorInput
|
||||
): Promise<boolean> {
|
||||
for (const reviver of values(this._revivers)) {
|
||||
if (canRevive(reviver, webview)) {
|
||||
await reviver.reviveWebview(webview);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { addClass, addDisposableListener } from 'vs/base/browser/dom';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
|
||||
import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService';
|
||||
import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols';
|
||||
import { areWebviewInputOptionsEqual } from './webviewEditorService';
|
||||
import { WebviewFindWidget } from './webviewFindWidget';
|
||||
import { endsWith } from 'vs/base/common/strings';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
|
||||
export interface WebviewOptions {
|
||||
readonly allowSvgs?: boolean;
|
||||
readonly extensionLocation?: URI;
|
||||
readonly enableFindWidget?: boolean;
|
||||
}
|
||||
|
||||
export interface WebviewContentOptions {
|
||||
readonly allowScripts?: boolean;
|
||||
readonly svgWhiteList?: string[];
|
||||
readonly localResourceRoots?: ReadonlyArray<URI>;
|
||||
}
|
||||
|
||||
interface IKeydownEvent {
|
||||
key: string;
|
||||
keyCode: number;
|
||||
code: string;
|
||||
shiftKey: boolean;
|
||||
altKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
repeat: boolean;
|
||||
}
|
||||
|
||||
class WebviewProtocolProvider extends Disposable {
|
||||
constructor(
|
||||
webview: Electron.WebviewTag,
|
||||
private readonly _extensionLocation: URI | undefined,
|
||||
private readonly _getLocalResourceRoots: () => ReadonlyArray<URI>,
|
||||
private readonly _environmentService: IEnvironmentService,
|
||||
private readonly _fileService: IFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
let loaded = false;
|
||||
this._register(addDisposableListener(webview, 'did-start-loading', () => {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
loaded = true;
|
||||
|
||||
const contents = webview.getWebContents();
|
||||
if (contents) {
|
||||
this.registerFileProtocols(contents);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private registerFileProtocols(contents: Electron.WebContents) {
|
||||
if (contents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appRootUri = URI.file(this._environmentService.appRoot);
|
||||
|
||||
registerFileProtocol(contents, WebviewProtocol.CoreResource, this._fileService, null, () => [
|
||||
appRootUri
|
||||
]);
|
||||
|
||||
registerFileProtocol(contents, WebviewProtocol.VsCodeResource, this._fileService, this._extensionLocation, () =>
|
||||
this._getLocalResourceRoots()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SvgBlocker extends Disposable {
|
||||
|
||||
private readonly _onDidBlockSvg = this._register(new Emitter<void>());
|
||||
public readonly onDidBlockSvg = this._onDidBlockSvg.event;
|
||||
|
||||
constructor(
|
||||
webview: Electron.WebviewTag,
|
||||
private readonly _options: WebviewContentOptions,
|
||||
) {
|
||||
super();
|
||||
|
||||
let loaded = false;
|
||||
this._register(addDisposableListener(webview, 'did-start-loading', () => {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
loaded = true;
|
||||
|
||||
const contents = webview.getWebContents();
|
||||
if (!contents) {
|
||||
return;
|
||||
}
|
||||
|
||||
contents.session.webRequest.onBeforeRequest((details, callback) => {
|
||||
if (details.url.indexOf('.svg') > 0) {
|
||||
const uri = URI.parse(details.url);
|
||||
if (uri && !uri.scheme.match(/file/i) && endsWith(uri.path, '.svg') && !this.isAllowedSvg(uri)) {
|
||||
this._onDidBlockSvg.fire();
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
}
|
||||
return callback({});
|
||||
});
|
||||
|
||||
contents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
const contentType: string[] = details.responseHeaders['content-type'] || details.responseHeaders['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)) {
|
||||
this._onDidBlockSvg.fire();
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
}
|
||||
return callback({ cancel: false, responseHeaders: details.responseHeaders });
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private isAllowedSvg(uri: URI): boolean {
|
||||
if (this._options.svgWhiteList) {
|
||||
return this._options.svgWhiteList.indexOf(uri.authority.toLowerCase()) >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class WebviewKeyboardHandler extends Disposable {
|
||||
|
||||
private _ignoreMenuShortcut = false;
|
||||
|
||||
constructor(
|
||||
private readonly _webview: Electron.WebviewTag
|
||||
) {
|
||||
super();
|
||||
|
||||
if (this.shouldToggleMenuShortcutsEnablement) {
|
||||
this._register(addDisposableListener(this._webview, 'did-start-loading', () => {
|
||||
const contents = this.getWebContents();
|
||||
if (contents) {
|
||||
contents.on('before-input-event', (_event, input) => {
|
||||
if (input.type === 'keyDown' && document.activeElement === this._webview) {
|
||||
this._ignoreMenuShortcut = input.control || input.meta;
|
||||
this.setIgnoreMenuShortcuts(this._ignoreMenuShortcut);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this._register(addDisposableListener(this._webview, 'ipc-message', (event) => {
|
||||
switch (event.channel) {
|
||||
case 'did-keydown':
|
||||
// Electron: workaround for https://github.com/electron/electron/issues/14258
|
||||
// We have to detect keyboard events in the <webview> and dispatch them to our
|
||||
// keybinding service because these events do not bubble to the parent window anymore.
|
||||
this.handleKeydown(event.args[0]);
|
||||
return;
|
||||
|
||||
case 'did-focus':
|
||||
this.setIgnoreMenuShortcuts(this._ignoreMenuShortcut);
|
||||
break;
|
||||
|
||||
case 'did-blur':
|
||||
this.setIgnoreMenuShortcuts(false);
|
||||
return;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private get shouldToggleMenuShortcutsEnablement() {
|
||||
return isMacintosh;
|
||||
}
|
||||
|
||||
private setIgnoreMenuShortcuts(value: boolean) {
|
||||
if (!this.shouldToggleMenuShortcutsEnablement) {
|
||||
return;
|
||||
}
|
||||
const contents = this.getWebContents();
|
||||
if (contents) {
|
||||
contents.setIgnoreMenuShortcuts(value);
|
||||
}
|
||||
}
|
||||
|
||||
private getWebContents(): Electron.WebContents | undefined {
|
||||
const contents = this._webview.getWebContents();
|
||||
if (contents && !contents.isDestroyed()) {
|
||||
return contents;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private handleKeydown(event: IKeydownEvent): void {
|
||||
// Create a fake KeyboardEvent from the data provided
|
||||
const emulatedKeyboardEvent = new KeyboardEvent('keydown', event);
|
||||
// Force override the target
|
||||
Object.defineProperty(emulatedKeyboardEvent, 'target', {
|
||||
get: () => this._webview
|
||||
});
|
||||
// And re-dispatch
|
||||
window.dispatchEvent(emulatedKeyboardEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class WebviewElement extends Disposable {
|
||||
private _webview: Electron.WebviewTag;
|
||||
private _ready: Promise<void>;
|
||||
|
||||
private _webviewFindWidget: WebviewFindWidget;
|
||||
private _findStarted: boolean = false;
|
||||
private _contents: string = '';
|
||||
private _state: string | undefined = undefined;
|
||||
private _focused = false;
|
||||
|
||||
private readonly _onDidFocus = this._register(new Emitter<void>());
|
||||
public get onDidFocus(): Event<void> { return this._onDidFocus.event; }
|
||||
|
||||
constructor(
|
||||
private readonly _styleElement: Element,
|
||||
private readonly _options: WebviewOptions,
|
||||
private _contentOptions: WebviewContentOptions,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
this._webview = document.createElement('webview');
|
||||
this._webview.setAttribute('partition', `webview${Date.now()}`);
|
||||
|
||||
this._webview.setAttribute('webpreferences', 'contextIsolation=yes');
|
||||
|
||||
this._webview.style.flex = '0 1';
|
||||
this._webview.style.width = '0';
|
||||
this._webview.style.height = '0';
|
||||
this._webview.style.outline = '0';
|
||||
|
||||
this._webview.preload = require.toUrl('./webview-pre.js');
|
||||
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') {
|
||||
// console.info('[PID Webview] ' event.args[0]);
|
||||
addClass(this._webview, 'ready'); // can be found by debug command
|
||||
|
||||
subscription.dispose();
|
||||
resolve();
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
this._register(
|
||||
new WebviewProtocolProvider(
|
||||
this._webview,
|
||||
this._options.extensionLocation,
|
||||
() => (this._contentOptions.localResourceRoots || []),
|
||||
environmentService,
|
||||
fileService));
|
||||
|
||||
if (!this._options.allowSvgs) {
|
||||
const svgBlocker = this._register(new SvgBlocker(this._webview, this._contentOptions));
|
||||
svgBlocker.onDidBlockSvg(() => this.onDidBlockSvg());
|
||||
}
|
||||
|
||||
this._register(new WebviewKeyboardHandler(this._webview));
|
||||
|
||||
this._register(addDisposableListener(this._webview, 'console-message', function (e: { level: number; message: string; line: number; sourceId: string; }) {
|
||||
console.log(`[Embedded Page] ${e.message}`);
|
||||
}));
|
||||
this._register(addDisposableListener(this._webview, 'dom-ready', () => {
|
||||
this.layout();
|
||||
|
||||
// Workaround for https://github.com/electron/electron/issues/14474
|
||||
if (this._focused || document.activeElement === this._webview) {
|
||||
this._webview.blur();
|
||||
this._webview.focus();
|
||||
}
|
||||
}));
|
||||
this._register(addDisposableListener(this._webview, 'crashed', () => {
|
||||
console.error('embedded page crashed');
|
||||
}));
|
||||
this._register(addDisposableListener(this._webview, 'ipc-message', (event) => {
|
||||
switch (event.channel) {
|
||||
case 'onmessage':
|
||||
if (event.args && event.args.length) {
|
||||
this._onMessage.fire(event.args[0]);
|
||||
}
|
||||
return;
|
||||
|
||||
case 'did-click-link':
|
||||
let [uri] = event.args;
|
||||
this._onDidClickLink.fire(URI.parse(uri));
|
||||
return;
|
||||
|
||||
case 'did-set-content':
|
||||
this._webview.style.flex = '';
|
||||
this._webview.style.width = '100%';
|
||||
this._webview.style.height = '100%';
|
||||
this.layout();
|
||||
return;
|
||||
|
||||
case 'did-scroll':
|
||||
if (event.args && typeof event.args[0] === 'number') {
|
||||
this._onDidScroll.fire({ scrollYPercentage: event.args[0] });
|
||||
}
|
||||
return;
|
||||
|
||||
case 'do-reload':
|
||||
this.reload();
|
||||
return;
|
||||
|
||||
case 'do-update-state':
|
||||
this._state = event.args[0];
|
||||
this._onDidUpdateState.fire(this._state);
|
||||
return;
|
||||
|
||||
case 'did-focus':
|
||||
this.handleFocusChange(true);
|
||||
return;
|
||||
|
||||
case 'did-blur':
|
||||
this.handleFocusChange(false);
|
||||
return;
|
||||
}
|
||||
}));
|
||||
this._register(addDisposableListener(this._webview, 'devtools-opened', () => {
|
||||
this._send('devtools-opened');
|
||||
}));
|
||||
|
||||
if (_options.enableFindWidget) {
|
||||
this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this));
|
||||
}
|
||||
|
||||
this.style(themeService.getTheme());
|
||||
themeService.onThemeChange(this.style, this, this._toDispose);
|
||||
}
|
||||
|
||||
public mountTo(parent: HTMLElement) {
|
||||
if (this._webviewFindWidget) {
|
||||
parent.appendChild(this._webviewFindWidget.getDomNode()!);
|
||||
}
|
||||
parent.appendChild(this._webview);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._webview) {
|
||||
if (this._webview.parentElement) {
|
||||
this._webview.parentElement.removeChild(this._webview);
|
||||
}
|
||||
}
|
||||
|
||||
this._webview = undefined!;
|
||||
this._webviewFindWidget = undefined!;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
private _send(channel: string, ...args: any[]): void {
|
||||
this._ready
|
||||
.then(() => this._webview.send(channel, ...args))
|
||||
.catch(err => console.error(err));
|
||||
}
|
||||
|
||||
public set initialScrollProgress(value: number) {
|
||||
this._send('initial-scroll-position', value);
|
||||
}
|
||||
|
||||
public set state(value: string | undefined) {
|
||||
this._state = value;
|
||||
}
|
||||
|
||||
public set options(value: WebviewContentOptions) {
|
||||
if (this._contentOptions && areWebviewInputOptionsEqual(value, this._contentOptions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._contentOptions = value;
|
||||
this._send('content', {
|
||||
contents: this._contents,
|
||||
options: this._contentOptions,
|
||||
state: this._state
|
||||
});
|
||||
}
|
||||
|
||||
public set contents(value: string) {
|
||||
this._contents = value;
|
||||
this._send('content', {
|
||||
contents: value,
|
||||
options: this._contentOptions,
|
||||
state: this._state
|
||||
});
|
||||
}
|
||||
|
||||
public update(value: string, options: WebviewContentOptions, retainContextWhenHidden: boolean) {
|
||||
if (retainContextWhenHidden && value === this._contents && this._contentOptions && areWebviewInputOptionsEqual(options, this._contentOptions)) {
|
||||
return;
|
||||
}
|
||||
this._contents = value;
|
||||
this._contentOptions = options;
|
||||
this._send('content', {
|
||||
contents: this._contents,
|
||||
options: this._contentOptions,
|
||||
state: this._state
|
||||
});
|
||||
}
|
||||
|
||||
public set baseUrl(value: string) {
|
||||
this._send('baseUrl', value);
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this._webview.focus();
|
||||
this._send('focus');
|
||||
|
||||
// Handle focus change programmatically (do not rely on event from <webview>)
|
||||
this.handleFocusChange(true);
|
||||
}
|
||||
|
||||
private handleFocusChange(isFocused: boolean): void {
|
||||
this._focused = isFocused;
|
||||
if (isFocused) {
|
||||
this._onDidFocus.fire();
|
||||
}
|
||||
}
|
||||
|
||||
public sendMessage(data: any): void {
|
||||
this._send('message', data);
|
||||
}
|
||||
|
||||
private onDidBlockSvg() {
|
||||
this.sendMessage({
|
||||
name: 'vscode-did-block-svg'
|
||||
});
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
public style(theme: ITheme): void {
|
||||
const { fontFamily, fontWeight, fontSize } = window.getComputedStyle(this._styleElement); // TODO@theme avoid styleElement
|
||||
|
||||
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;
|
||||
}, {});
|
||||
|
||||
|
||||
const styles = {
|
||||
'vscode-editor-font-family': fontFamily,
|
||||
'vscode-editor-font-weight': fontWeight,
|
||||
'vscode-editor-font-size': fontSize,
|
||||
...exportedColors
|
||||
};
|
||||
|
||||
const activeTheme = ApiThemeClassName.fromTheme(theme);
|
||||
this._send('styles', styles, activeTheme);
|
||||
|
||||
if (this._webviewFindWidget) {
|
||||
this._webviewFindWidget.updateTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
public layout(): void {
|
||||
const contents = this._webview.getWebContents();
|
||||
if (!contents || contents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
const window = (contents as any).getOwnerBrowserWindow();
|
||||
if (!window || !window.webContents || window.webContents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
window.webContents.getZoomFactor(factor => {
|
||||
if (contents.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
contents.setZoomFactor(factor);
|
||||
});
|
||||
}
|
||||
|
||||
public startFind(value: string, options?: Electron.FindInPageOptions) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure options is defined without modifying the original
|
||||
options = options || {};
|
||||
|
||||
// FindNext must be false for a first request
|
||||
const findOptions: Electron.FindInPageOptions = {
|
||||
forward: options.forward,
|
||||
findNext: false,
|
||||
matchCase: options.matchCase,
|
||||
medialCapitalAsWordStart: options.medialCapitalAsWordStart
|
||||
};
|
||||
|
||||
this._findStarted = true;
|
||||
this._webview.findInPage(value, findOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Webviews expose a stateful find API.
|
||||
* Successive calls to find will move forward or backward through onFindResults
|
||||
* depending on the supplied options.
|
||||
*
|
||||
* @param value The string to search for. Empty strings are ignored.
|
||||
*/
|
||||
public find(value: string, options?: Electron.FindInPageOptions): void {
|
||||
// Searching with an empty value will throw an exception
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._findStarted) {
|
||||
this.startFind(value, options);
|
||||
return;
|
||||
}
|
||||
|
||||
this._webview.findInPage(value, options);
|
||||
}
|
||||
|
||||
public stopFind(keepSelection?: boolean): void {
|
||||
this._findStarted = false;
|
||||
this._webview.stopFindInPage(keepSelection ? 'keepSelection' : 'clearSelection');
|
||||
}
|
||||
|
||||
public showFind() {
|
||||
if (this._webviewFindWidget) {
|
||||
this._webviewFindWidget.reveal();
|
||||
}
|
||||
}
|
||||
|
||||
public hideFind() {
|
||||
if (this._webviewFindWidget) {
|
||||
this._webviewFindWidget.hide();
|
||||
}
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.contents = this._contents;
|
||||
}
|
||||
|
||||
public selectAll() {
|
||||
this._webview.selectAll();
|
||||
}
|
||||
|
||||
public copy() {
|
||||
this._webview.copy();
|
||||
}
|
||||
|
||||
public paste() {
|
||||
this._webview.paste();
|
||||
}
|
||||
|
||||
public cut() {
|
||||
this._webview.cut();
|
||||
}
|
||||
|
||||
public undo() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { SimpleFindWidget } from 'vs/editor/contrib/find/simpleFindWidget';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { WebviewElement } from './webviewElement';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
export class WebviewFindWidget extends SimpleFindWidget {
|
||||
|
||||
constructor(
|
||||
private _webview: WebviewElement | undefined,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(contextViewService, contextKeyService);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._webview = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public find(previous: boolean) {
|
||||
if (!this._webview) {
|
||||
return;
|
||||
}
|
||||
const val = this.inputValue;
|
||||
if (val) {
|
||||
this._webview.find(val, { findNext: true, forward: !previous });
|
||||
}
|
||||
}
|
||||
|
||||
public hide() {
|
||||
super.hide();
|
||||
if (this._webview) {
|
||||
this._webview.stopFind(true);
|
||||
this._webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public onInputChanged() {
|
||||
if (!this._webview) {
|
||||
return;
|
||||
}
|
||||
const val = this.inputValue;
|
||||
if (val) {
|
||||
this._webview.startFind(val);
|
||||
} else {
|
||||
this._webview.stopFind(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected onFocusTrackerFocus() { }
|
||||
|
||||
protected onFocusTrackerBlur() { }
|
||||
|
||||
protected onFindInputFocusTrackerFocus() { }
|
||||
|
||||
protected onFindInputFocusTrackerBlur() { }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { extname, sep } from 'vs/base/common/path';
|
||||
import { getMediaMime, MIME_UNKNOWN } from 'vs/base/common/mime';
|
||||
import { startsWith } 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';
|
||||
|
||||
export const enum WebviewProtocol {
|
||||
CoreResource = 'vscode-core-resource',
|
||||
VsCodeResource = 'vscode-resource'
|
||||
}
|
||||
|
||||
function resolveContent(fileService: IFileService, resource: URI, mime: string, callback: any): void {
|
||||
fileService.resolveContent(resource, { encoding: 'binary' }).then(contents => {
|
||||
callback({
|
||||
data: Buffer.from(contents.value, contents.encoding),
|
||||
mimeType: mime
|
||||
});
|
||||
}, (err) => {
|
||||
console.log(err);
|
||||
callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ });
|
||||
});
|
||||
}
|
||||
|
||||
export function registerFileProtocol(
|
||||
contents: Electron.WebContents,
|
||||
protocol: WebviewProtocol,
|
||||
fileService: IFileService,
|
||||
extensionLocation: URI | null | undefined,
|
||||
getRoots: () => ReadonlyArray<URI>
|
||||
) {
|
||||
contents.session.protocol.registerBufferProtocol(protocol, (request, callback: any) => {
|
||||
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
|
||||
})
|
||||
});
|
||||
resolveContent(fileService, redirectedUri, getMimeType(requestUri), callback);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestPath = URI.parse(request.url).path;
|
||||
const normalizedPath = URI.file(requestPath);
|
||||
for (const root of getRoots()) {
|
||||
if (startsWith(normalizedPath.fsPath, root.fsPath + sep)) {
|
||||
resolveContent(fileService, normalizedPath, getMimeType(normalizedPath), callback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
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 */ });
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
console.error('Failed to register protocol ' + protocol);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user