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:
Anthony Dresser
2019-03-19 17:44:35 -07:00
committed by GitHub
parent 833d197412
commit 87765e8673
1879 changed files with 54505 additions and 38058 deletions

View 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);
}`;
}());

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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()));
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

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

View File

@@ -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;
}