diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 570d97669c786..da50b1eb2d86a 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -170,6 +170,10 @@ "name": "vs/workbench/contrib/webview", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/customEditor", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/welcome", "project": "vscode-workbench" diff --git a/extensions/image-preview/.vscodeignore b/extensions/image-preview/.vscodeignore new file mode 100644 index 0000000000000..30d948fbc6610 --- /dev/null +++ b/extensions/image-preview/.vscodeignore @@ -0,0 +1,10 @@ +test/** +src/** +tsconfig.json +out/test/** +out/** +extension.webpack.config.js +cgmanifest.json +yarn.lock +preview-src/** +webpack.config.js diff --git a/extensions/image-preview/README.md b/extensions/image-preview/README.md new file mode 100644 index 0000000000000..e8664c77a90fe --- /dev/null +++ b/extensions/image-preview/README.md @@ -0,0 +1,3 @@ +# Image Preview + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. diff --git a/extensions/image-preview/extension.webpack.config.js b/extensions/image-preview/extension.webpack.config.js new file mode 100644 index 0000000000000..de88398eca0d3 --- /dev/null +++ b/extensions/image-preview/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + resolve: { + mainFields: ['module', 'main'] + }, + entry: { + extension: './src/extension.ts', + } +}); diff --git a/extensions/image-preview/media/main.css b/extensions/image-preview/media/main.css new file mode 100644 index 0000000000000..e2779bca0d1d9 --- /dev/null +++ b/extensions/image-preview/media/main.css @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +html, body { + height: 100%; + max-height: 100%; +} + + +body img { + max-width: none; + max-height: none; +} + +.container:focus { + outline: none !important; +} + +.container { + padding: 5px 0 0 10px; + box-sizing: border-box; + user-select: none; +} + +.container.image { + padding: 0; + display: flex; + box-sizing: border-box; +} + +.container.image img { + padding: 0; + background-position: 0 0, 8px 8px; + background-size: 16px 16px; +} + +.container.image img { + background-image: + linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)), + linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)); +} + +.vscode-dark.container.image img { + background-image: + linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)), + linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)); +} + +.container img.pixelated { + image-rendering: pixelated; +} + +.container img.scale-to-fit { + max-width: calc(100% - 20px); + max-height: calc(100% - 20px); + object-fit: contain; +} + +.container img { + margin: auto; +} + +.container.zoom-in { + cursor: zoom-in; +} + +.container.zoom-out { + cursor: zoom-out; +} + +.container .embedded-link, +.container .embedded-link:hover { + cursor: pointer; + text-decoration: underline; + margin-left: 5px; +} diff --git a/extensions/image-preview/media/main.js b/extensions/image-preview/media/main.js new file mode 100644 index 0000000000000..94ee971a7d747 --- /dev/null +++ b/extensions/image-preview/media/main.js @@ -0,0 +1,258 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +"use strict"; + +(function () { + /** + * @param {number} value + * @param {number} min + * @param {number} max + * @return {number} + */ + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + function getSettings() { + const element = document.getElementById('image-preview-settings'); + if (element) { + const data = element.getAttribute('data-settings'); + if (data) { + return JSON.parse(data); + } + } + + throw new Error(`Could not load settings`); + } + + /** + * Enable image-rendering: pixelated for images scaled by more than this. + */ + const PIXELATION_THRESHOLD = 3; + + const SCALE_PINCH_FACTOR = 0.075; + const MAX_SCALE = 20; + const MIN_SCALE = 0.1; + + const zoomLevels = [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1, + 1.5, + 2, + 3, + 5, + 7, + 10, + 15, + 20 + ]; + + const isMac = getSettings().isMac; + + const vscode = acquireVsCodeApi(); + + const initialState = vscode.getState() || { scale: 'fit', offsetX: 0, offsetY: 0 }; + + // State + let scale = initialState.scale; + let ctrlPressed = false; + let altPressed = false; + + // Elements + const container = /** @type {HTMLElement} */(document.querySelector('body')); + const image = document.querySelector('img'); + + function updateScale(newScale) { + if (!image || !image.parentElement) { + return; + } + + if (newScale === 'fit') { + scale = 'fit'; + image.classList.add('scale-to-fit'); + image.classList.remove('pixelated'); + image.style.minWidth = 'auto'; + image.style.width = 'auto'; + vscode.setState(undefined); + } else { + const oldWidth = image.width; + const oldHeight = image.height; + + scale = clamp(newScale, MIN_SCALE, MAX_SCALE); + if (scale >= PIXELATION_THRESHOLD) { + image.classList.add('pixelated'); + } else { + image.classList.remove('pixelated'); + } + + const { scrollTop, scrollLeft } = image.parentElement; + const dx = (scrollLeft + image.parentElement.clientWidth / 2) / image.parentElement.scrollWidth; + const dy = (scrollTop + image.parentElement.clientHeight / 2) / image.parentElement.scrollHeight; + + image.classList.remove('scale-to-fit'); + image.style.minWidth = `${(image.naturalWidth * scale)}px`; + image.style.width = `${(image.naturalWidth * scale)}px`; + + const newWidth = image.width; + const scaleFactor = (newWidth - oldWidth) / oldWidth; + + const newScrollLeft = ((oldWidth * scaleFactor * dx) + scrollLeft); + const newScrollTop = ((oldHeight * scaleFactor * dy) + scrollTop); + // scrollbar.setScrollPosition({ + // scrollLeft: newScrollLeft, + // scrollTop: newScrollTop, + // }); + + vscode.setState({ scale: scale, offsetX: newScrollLeft, offsetY: newScrollTop }); + } + + vscode.postMessage({ + type: 'zoom', + value: scale + }); + } + + function firstZoom() { + if (!image) { + return; + } + + scale = image.clientWidth / image.naturalWidth; + updateScale(scale); + } + + window.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => { + if (!image) { + return; + } + ctrlPressed = e.ctrlKey; + altPressed = e.altKey; + + if (isMac ? altPressed : ctrlPressed) { + container.classList.remove('zoom-in'); + container.classList.add('zoom-out'); + } + }); + + window.addEventListener('keyup', (/** @type {KeyboardEvent} */ e) => { + if (!image) { + return; + } + + ctrlPressed = e.ctrlKey; + altPressed = e.altKey; + + if (!(isMac ? altPressed : ctrlPressed)) { + container.classList.remove('zoom-out'); + container.classList.add('zoom-in'); + } + }); + + container.addEventListener('click', (/** @type {MouseEvent} */ e) => { + if (!image) { + return; + } + + if (e.button !== 0) { + return; + } + + // left click + if (scale === 'fit') { + firstZoom(); + } + + if (!(isMac ? altPressed : ctrlPressed)) { // zoom in + let i = 0; + for (; i < zoomLevels.length; ++i) { + if (zoomLevels[i] > scale) { + break; + } + } + updateScale(zoomLevels[i] || MAX_SCALE); + } else { + let i = zoomLevels.length - 1; + for (; i >= 0; --i) { + if (zoomLevels[i] < scale) { + break; + } + } + updateScale(zoomLevels[i] || MIN_SCALE); + } + }); + + container.addEventListener('wheel', (/** @type {WheelEvent} */ e) => { + if (!image) { + return; + } + + const isScrollWheelKeyPressed = isMac ? altPressed : ctrlPressed; + if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl + return; + } + + e.preventDefault(); + e.stopPropagation(); + + if (scale === 'fit') { + firstZoom(); + } + + let delta = e.deltaY > 0 ? 1 : -1; + updateScale(scale * (1 - delta * SCALE_PINCH_FACTOR)); + }); + + window.addEventListener('scroll', () => { + if (!image || !image.parentElement || scale === 'fit') { + return; + } + + const entry = vscode.getState(); + if (entry) { + vscode.setState({ scale: entry.scale, offsetX: window.scrollX, offsetY: window.scrollY }); + } + }); + + container.classList.add('image'); + container.classList.add('zoom-in'); + + image.classList.add('scale-to-fit'); + image.style.visibility = 'hidden'; + + image.addEventListener('load', () => { + if (!image) { + return; + } + + vscode.postMessage({ + type: 'size', + value: `${image.naturalWidth}x${image.naturalHeight}`, + }); + + image.style.visibility = 'visible'; + updateScale(scale); + + if (initialState.scale !== 'fit') { + window.scrollTo(initialState.offsetX, initialState.offsetY); + } + }); + + window.addEventListener('message', e => { + switch (e.data.type) { + case 'setScale': + updateScale(e.data.scale); + break; + } + }); +}()); diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json new file mode 100644 index 0000000000000..aad1416c7b281 --- /dev/null +++ b/extensions/image-preview/package.json @@ -0,0 +1,43 @@ +{ + "name": "image-preview", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "enableProposedApi": true, + "license": "MIT", + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "engines": { + "vscode": "^1.39.0" + }, + "main": "./out/extension", + "categories": [ + "Other" + ], + "activationEvents": [ + "onWebviewEditor:imagePreview.previewEditor" + ], + "contributes": { + "webviewEditors": [ + { + "viewType": "imagePreview.previewEditor", + "displayName": "%webviewEditors.displayName%", + "selector": [ + { + "filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,tga,tif,tiff,webp}" + } + ] + } + ] + }, + "scripts": { + "compile": "gulp compile-extension:image-preview", + "watch": "npm run build-preview && gulp watch-extension:image-preview", + "vscode:prepublish": "npm run build-ext", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:image-preview ./tsconfig.json" + }, + "dependencies": { + "vscode-extension-telemetry": "0.1.1", + "vscode-nls": "^4.0.0" + } +} diff --git a/extensions/image-preview/package.nls.json b/extensions/image-preview/package.nls.json new file mode 100644 index 0000000000000..78c753d1d546a --- /dev/null +++ b/extensions/image-preview/package.nls.json @@ -0,0 +1,5 @@ +{ + "displayName": "Image Preview", + "description": "Previews images.", + "webviewEditors.displayName": "Image Preview" +} diff --git a/extensions/image-preview/src/dispose.ts b/extensions/image-preview/src/dispose.ts new file mode 100644 index 0000000000000..548094c28e5f5 --- /dev/null +++ b/extensions/image-preview/src/dispose.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function disposeAll(disposables: vscode.Disposable[]) { + while (disposables.length) { + const item = disposables.pop(); + if (item) { + item.dispose(); + } + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: vscode.Disposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed() { + return this._isDisposed; + } +} \ No newline at end of file diff --git a/extensions/image-preview/src/extension.ts b/extensions/image-preview/src/extension.ts new file mode 100644 index 0000000000000..a47e4cb8bc760 --- /dev/null +++ b/extensions/image-preview/src/extension.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Preview } from './preview'; +import { SizeStatusBarEntry } from './sizeStatusBarEntry'; +import { ZoomStatusBarEntry } from './zoomStatusBarEntry'; + +export function activate(context: vscode.ExtensionContext) { + const extensionRoot = vscode.Uri.file(context.extensionPath); + + const sizeStatusBarEntry = new SizeStatusBarEntry(); + context.subscriptions.push(sizeStatusBarEntry); + + const zoomStatusBarEntry = new ZoomStatusBarEntry(); + context.subscriptions.push(zoomStatusBarEntry); + + context.subscriptions.push(vscode.window.registerWebviewEditorProvider( + Preview.viewType, + { + async resolveWebviewEditor(resource: vscode.Uri, editor: vscode.WebviewEditor): Promise { + // tslint:disable-next-line: no-unused-expression + new Preview(extensionRoot, resource, editor, sizeStatusBarEntry, zoomStatusBarEntry); + } + })); +} \ No newline at end of file diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts new file mode 100644 index 0000000000000..a30e635c65c91 --- /dev/null +++ b/extensions/image-preview/src/preview.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { SizeStatusBarEntry } from './sizeStatusBarEntry'; +import { ZoomStatusBarEntry } from './zoomStatusBarEntry'; +import { Disposable } from './dispose'; + +export class Preview extends Disposable { + + public static readonly viewType = 'imagePreview.previewEditor'; + + private _active = true; + + constructor( + private readonly extensionRoot: vscode.Uri, + resource: vscode.Uri, + private readonly webviewEditor: vscode.WebviewEditor, + private readonly sizeStatusBarEntry: SizeStatusBarEntry, + private readonly zoomStatusBarEntry: ZoomStatusBarEntry, + ) { + super(); + const resourceRoot = resource.with({ + path: resource.path.replace(/\/[^\/]+?\.\w+$/, '/'), + }); + + webviewEditor.webview.options = { + enableScripts: true, + localResourceRoots: [ + resourceRoot, + extensionRoot, + ] + }; + + webviewEditor.webview.html = this.getWebiewContents(webviewEditor, resource); + + this._register(webviewEditor.webview.onDidReceiveMessage(message => { + switch (message.type) { + case 'size': + { + this.sizeStatusBarEntry.update(message.value); + break; + } + case 'zoom': + { + this.zoomStatusBarEntry.update(message.value); + break; + } + } + })); + + this._register(zoomStatusBarEntry.onDidChangeScale(e => { + this.webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale }); + })); + + this._register(webviewEditor.onDidChangeViewState(() => { + this.update(); + })); + this._register(webviewEditor.onDidDispose(() => { + if (this._active) { + this.sizeStatusBarEntry.hide(); + this.zoomStatusBarEntry.hide(); + } + })); + this.update(); + } + + private update() { + this._active = this.webviewEditor.active; + if (this._active) { + this.sizeStatusBarEntry.show(); + this.zoomStatusBarEntry.show(); + } else { + this.sizeStatusBarEntry.hide(); + this.zoomStatusBarEntry.hide(); + } + } + + private getWebiewContents(webviewEditor: vscode.WebviewEditor, resource: vscode.Uri): string { + const settings = { + isMac: process.platform === 'darwin' + }; + + return /* html */` + + + + + + Image Preview + + + + + + + + +`; + } + + private extensionResource(path: string) { + return this.webviewEditor.webview.asWebviewUri(this.extensionRoot.with({ + path: this.extensionRoot.path + path + })); + } +} + +function escapeAttribute(value: string | vscode.Uri): string { + return value.toString().replace(/"/g, '"'); +} diff --git a/extensions/image-preview/src/sizeStatusBarEntry.ts b/extensions/image-preview/src/sizeStatusBarEntry.ts new file mode 100644 index 0000000000000..88f75f0cfd69e --- /dev/null +++ b/extensions/image-preview/src/sizeStatusBarEntry.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { Disposable } from './dispose'; + +export class SizeStatusBarEntry extends Disposable { + private readonly _entry: vscode.StatusBarItem; + + constructor() { + super(); + this._entry = this._register(vscode.window.createStatusBarItem({ + id: 'imagePreview.size', + name: 'Image Size', + alignment: vscode.StatusBarAlignment.Right, + priority: 101 /* to the left of editor status (100) */, + })); + } + + public show() { + this._entry.show(); + } + + public hide() { + this._entry.hide(); + } + + public update(text: string) { + this._entry.text = text; + } +} \ No newline at end of file diff --git a/extensions/image-preview/src/typings/ref.d.ts b/extensions/image-preview/src/typings/ref.d.ts new file mode 100644 index 0000000000000..954bab971e334 --- /dev/null +++ b/extensions/image-preview/src/typings/ref.d.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// diff --git a/extensions/image-preview/src/zoomStatusBarEntry.ts b/extensions/image-preview/src/zoomStatusBarEntry.ts new file mode 100644 index 0000000000000..11580b842996a --- /dev/null +++ b/extensions/image-preview/src/zoomStatusBarEntry.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { Disposable } from './dispose'; + +const localize = nls.loadMessageBundle(); + +const selectZoomLevelCommandId = '_imagePreview.selectZoomLevel'; + +type Scale = number | 'fit'; + +export class ZoomStatusBarEntry extends Disposable { + private readonly _entry: vscode.StatusBarItem; + + private readonly _onDidChangeScale = this._register(new vscode.EventEmitter<{ scale: Scale }>()); + public readonly onDidChangeScale = this._onDidChangeScale.event; + + constructor() { + super(); + this._entry = this._register(vscode.window.createStatusBarItem({ + id: 'imagePreview.zoom', + name: 'Image Zoom', + alignment: vscode.StatusBarAlignment.Right, + priority: 102 /* to the left of editor size entry (101) */, + })); + + this._register(vscode.commands.registerCommand(selectZoomLevelCommandId, async () => { + type MyPickItem = vscode.QuickPickItem & { scale: Scale }; + + const scales: Scale[] = [10, 5, 2, 1, 0.5, 0.2, 'fit']; + const options = scales.map((scale): MyPickItem => ({ + label: this.zoomLabel(scale), + scale + })); + + const pick = await vscode.window.showQuickPick(options, { + placeHolder: localize('zoomStatusBar.placeholder', "Select zoom level") + }); + if (pick) { + this._onDidChangeScale.fire({ scale: pick.scale }); + } + })); + + this._entry.command = selectZoomLevelCommandId; + } + + public show() { + this._entry.show(); + } + + public hide() { + this._entry.hide(); + } + + public update(scale: Scale) { + this._entry.text = this.zoomLabel(scale); + } + + private zoomLabel(scale: Scale): string { + return scale === 'fit' + ? localize('zoomStatusBar.wholeImageLabel', "Whole Image") + : `${Math.round(scale * 100)}%`; + } +} \ No newline at end of file diff --git a/extensions/image-preview/tsconfig.json b/extensions/image-preview/tsconfig.json new file mode 100644 index 0000000000000..d0797affbad80 --- /dev/null +++ b/extensions/image-preview/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../shared.tsconfig.json", + "compilerOptions": { + "outDir": "./out", + "experimentalDecorators": true + }, + "include": [ + "src/**/*" + ] +} diff --git a/extensions/image-preview/yarn.lock b/extensions/image-preview/yarn.lock new file mode 100644 index 0000000000000..e79cafaa0b201 --- /dev/null +++ b/extensions/image-preview/yarn.lock @@ -0,0 +1,46 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +applicationinsights@1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" + integrity sha512-KzOOGdphOS/lXWMFZe5440LUdFbrLpMvh2SaRxn7BmiI550KAoSb2gIhiq6kJZ9Ir3AxRRztjhzif+e5P5IXIg== + dependencies: + diagnostic-channel "0.2.0" + diagnostic-channel-publishers "0.2.1" + zone.js "0.7.6" + +diagnostic-channel-publishers@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" + integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM= + +diagnostic-channel@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17" + integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc= + dependencies: + semver "^5.3.0" + +semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== + +vscode-extension-telemetry@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b" + integrity sha512-TkKKG/B/J94DP5qf6xWB4YaqlhWDg6zbbqVx7Bz//stLQNnfE9XS1xm3f6fl24c5+bnEK0/wHgMgZYKIKxPeUA== + dependencies: + applicationinsights "1.0.8" + +vscode-nls@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" + integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== + +zone.js@0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" + integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk= diff --git a/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json b/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json index 02053ebab1365..14d84e6ee631b 100644 --- a/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/jsdoc.injection.tmLanguage.json @@ -1,3 +1,4 @@ + { "injectionSelector": "L:comment.block.documentation", "patterns": [ diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 2efdfb5dc1289..a58d88d1a8819 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -15,7 +15,7 @@ import { CodeAction, CodeActionContext, CodeActionProviderRegistry, CodeActionTr import { IModelService } from 'vs/editor/common/services/modelService'; import { CodeActionFilter, CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind } from './codeActionTrigger'; import { TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState'; -import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; export interface CodeActionSet extends IDisposable { readonly actions: readonly CodeAction[]; diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 65e7ec7713c8d..09d22bda22540 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -174,6 +174,11 @@ export interface IEditorOptions { * message as needed. By default, an error will be presented as notification if opening was not possible. */ readonly ignoreError?: boolean; + + /** + * Does not use editor overrides while opening the editor + */ + readonly ignoreOverrides?: boolean; } export interface ITextEditorSelection { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 1a53eed0c91d6..911dc43e83174 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -986,4 +986,29 @@ declare module 'vscode' { } //#endregion + + //#region Custom editors, mjbvz + + export interface WebviewEditor extends WebviewPanel { } + + export interface WebviewEditorProvider { + /** + * Fills out a `WebviewEditor` for a given resource. + * + * The provider should take ownership of passed in `editor`. + */ + resolveWebviewEditor( + resource: Uri, + editor: WebviewEditor + ): Thenable; + } + + namespace window { + export function registerWebviewEditorProvider( + viewType: string, + provider: WebviewEditorProvider, + ): Disposable; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 18e94e7f29781..56d9c7399f0fc 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -16,7 +16,6 @@ import { IProductService } from 'vs/platform/product/common/product'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions, WebviewPanelViewStateData } from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; -import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -78,6 +77,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews private readonly _proxy: ExtHostWebviewsShape; private readonly _webviewEditorInputs = new WebviewHandleStore(); private readonly _revivers = new Map(); + private readonly _editorProviders = new Map(); constructor( context: IExtHostContext, @@ -95,11 +95,11 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews this._register(_editorService.onDidActiveEditorChange(this.updateWebviewViewStates, this)); this._register(_editorService.onDidVisibleEditorsChange(this.updateWebviewViewStates, this)); - // This reviver's only job is to activate webview extensions + // This reviver's only job is to activate webview panel extensions // This should trigger the real reviver to be registered from the extension host side. - this._register(_webviewEditorService.registerReviver({ - canRevive: (webview: WebviewEditorInput) => { - if (!webview.webview.state) { + this._register(_webviewEditorService.registerResolver({ + canResolve: (webview: WebviewEditorInput) => { + if (!webview.webview.state && !webview.editorResource) { return false; } @@ -109,7 +109,7 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } return false; }, - reviveWebview: () => { throw new Error('not implemented'); } + resolveWebview: () => { throw new Error('not implemented'); } })); } @@ -160,13 +160,13 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } public $setHtml(handle: WebviewPanelHandle, value: string): void { - const webview = this.getWebview(handle); - webview.html = value; + const webview = this.getWebviewEditorInput(handle); + webview.webview.html = value; } public $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void { - const webview = this.getWebview(handle); - webview.contentOptions = reviveWebviewOptions(options as any /*todo@mat */); + const webview = this.getWebviewEditorInput(handle); + webview.webview.contentOptions = reviveWebviewOptions(options as any /*todo@mat */); } public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void { @@ -182,8 +182,8 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews } public async $postMessage(handle: WebviewPanelHandle, message: any): Promise { - const webview = this.getWebview(handle); - webview.sendMessage(message); + const webview = this.getWebviewEditorInput(handle); + webview.webview.sendMessage(message); return true; } @@ -192,11 +192,11 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews throw new Error(`Reviver for ${viewType} already registered`); } - this._revivers.set(viewType, this._webviewEditorService.registerReviver({ - canRevive: (webviewEditorInput) => { + this._revivers.set(viewType, this._webviewEditorService.registerResolver({ + canResolve: (webviewEditorInput) => { return !!webviewEditorInput.webview.state && webviewEditorInput.viewType === this.getInternalWebviewViewType(viewType); }, - reviveWebview: async (webviewEditorInput): Promise => { + resolveWebview: async (webviewEditorInput): Promise => { const viewType = this.fromInternalWebviewViewType(webviewEditorInput.viewType); if (!viewType) { webviewEditorInput.webview.html = MainThreadWebviews.getDeserializationFailedContents(webviewEditorInput.viewType); @@ -245,6 +245,48 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews this._revivers.delete(viewType); } + public $registerEditorProvider(viewType: string): void { + if (this._editorProviders.has(viewType)) { + throw new Error(`Provider for ${viewType} already registered`); + } + + this._editorProviders.set(viewType, this._webviewEditorService.registerResolver({ + canResolve: (webviewEditorInput) => { + return !!webviewEditorInput.editorResource && webviewEditorInput.viewType === viewType; + }, + resolveWebview: async (webview: WebviewEditorInput) => { + const handle = `resolved-${MainThreadWebviews.revivalPool++}`; + this._webviewEditorInputs.add(handle, webview); + this.hookupWebviewEventDelegate(handle, webview); + + try { + await this._proxy.$resolveWebviewEditor( + webview.editorResource, + handle, + viewType, + webview.getTitle(), + webview.webview.state, + editorGroupToViewColumn(this._editorGroupService, webview.group || 0), + webview.webview.options + ); + } catch (error) { + onUnexpectedError(error); + webview.webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType); + } + } + })); + } + + public $unregisterEditorProvider(viewType: string): void { + const provider = this._editorProviders.get(viewType); + if (!provider) { + throw new Error(`No provider for ${viewType} registered`); + } + + provider.dispose(); + this._editorProviders.delete(viewType); + } + private getInternalWebviewViewType(viewType: string): string { return `mainThreadWebview-${viewType}`; } @@ -334,10 +376,6 @@ export class MainThreadWebviews extends Disposable implements MainThreadWebviews return this._webviewEditorInputs.getInputForHandle(handle); } - private getWebview(handle: WebviewPanelHandle): Webview { - return this.getWebviewEditorInput(handle).webview; - } - private static getDeserializationFailedContents(viewType: string) { return ` diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 79317b0233f78..8ad00122b890a 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -523,6 +523,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); }, + registerWebviewEditorProvider: (viewType: string, provider: vscode.WebviewEditorProvider) => { + checkProposedApiEnabled(extension); + return extHostWebviews.registerWebviewEditorProvider(viewType, provider); + }, registerDecorationProvider(provider: vscode.DecorationProvider) { checkProposedApiEnabled(extension); return extHostDecorations.registerDecorationProvider(provider, extension.identifier); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d208aca0e8c12..901509be9852c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -548,6 +548,9 @@ export interface MainThreadWebviewsShape extends IDisposable { $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; + + $registerEditorProvider(viewType: string): void; + $unregisterEditorProvider(viewType: string): void; } export interface WebviewPanelViewStateData { @@ -564,6 +567,7 @@ export interface ExtHostWebviewsShape { $onDidChangeWebviewPanelViewStates(newState: WebviewPanelViewStateData): void; $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; + $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; } export interface MainThreadUrlsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index d4b554e00cc5d..53b0e57dc5403 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; @@ -241,6 +241,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { private readonly _proxy: MainThreadWebviewsShape; private readonly _webviewPanels = new Map(); private readonly _serializers = new Map(); + private readonly _editorProviders = new Map(); constructor( mainContext: IMainContext, @@ -288,6 +289,23 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { }); } + public registerWebviewEditorProvider( + viewType: string, + provider: vscode.WebviewEditorProvider + ): vscode.Disposable { + if (this._editorProviders.has(viewType)) { + throw new Error(`Editor provider for '${viewType}' already registered`); + } + + this._editorProviders.set(viewType, provider); + this._proxy.$registerEditorProvider(viewType); + + return new Disposable(() => { + this._editorProviders.delete(viewType); + this._proxy.$unregisterEditorProvider(viewType); + }); + } + public $onMessage( handle: WebviewPanelHandle, message: any @@ -371,6 +389,27 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewPanel | undefined { return this._webviewPanels.get(handle); } + + async $resolveWebviewEditor( + resource: UriComponents, + webviewHandle: WebviewPanelHandle, + viewType: string, + title: string, + state: any, + position: EditorViewColumn, + options: modes.IWebviewOptions & modes.IWebviewPanelOptions + ): Promise { + const provider = this._editorProviders.get(viewType); + if (!provider) { + return Promise.reject(new Error(`No provider found for '${viewType}'`)); + } + + const webview = new ExtHostWebview(webviewHandle, this._proxy, options, this.initData); + const revivedPanel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + this._webviewPanels.set(webviewHandle, revivedPanel); + return Promise.resolve(provider.resolveWebviewEditor(URI.revive(resource), revivedPanel)); + } + } function convertWebviewOptions( diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index e4add229a8353..a8b4de851425c 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -174,4 +174,4 @@ export const Extensions = { Editors: 'workbench.contributions.editors' }; -Registry.add(Extensions.Editors, new EditorRegistry()); \ No newline at end of file +Registry.add(Extensions.Editors, new EditorRegistry()); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index dda9116bc9a97..26cc06502caf1 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -809,7 +809,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onWillOpenEditor.fire(event); const prevented = event.isPrevented(); if (prevented) { - return prevented(); + return prevented().then(withUndefinedAsNull); } // Proceed with opening @@ -1520,7 +1520,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } class EditorOpeningEvent implements IEditorOpeningEvent { - private override: () => Promise; + private override: () => Promise; constructor( private _group: GroupIdentifier, @@ -1541,11 +1541,11 @@ class EditorOpeningEvent implements IEditorOpeningEvent { return this._options; } - prevent(callback: () => Promise): void { + prevent(callback: () => Promise): void { this.override = callback; } - isPrevented(): () => Promise { + isPrevented(): () => Promise { return this.override; } } diff --git a/src/vs/workbench/browser/parts/editor/media/resourceviewer.css b/src/vs/workbench/browser/parts/editor/media/resourceviewer.css index b6f7b107db7b9..705c27036ab96 100644 --- a/src/vs/workbench/browser/parts/editor/media/resourceviewer.css +++ b/src/vs/workbench/browser/parts/editor/media/resourceviewer.css @@ -12,52 +12,6 @@ box-sizing: border-box; } -.monaco-resource-viewer.image { - padding: 0; - display: flex; - box-sizing: border-box; -} - -.monaco-resource-viewer.image img { - padding: 0; - background-position: 0 0, 8px 8px; - background-size: 16px 16px; -} - -.vs .monaco-resource-viewer.image img { - background-image: - linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)), - linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)); -} - -.vs-dark .monaco-resource-viewer.image img { - background-image: - linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)), - linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)); -} - -.monaco-resource-viewer img.pixelated { - image-rendering: pixelated; -} - -.monaco-resource-viewer img.scale-to-fit { - max-width: calc(100% - 20px); - max-height: calc(100% - 20px); - object-fit: contain; -} - -.monaco-resource-viewer img { - margin: auto; -} - -.monaco-resource-viewer.zoom-in { - cursor: zoom-in; -} - -.monaco-resource-viewer.zoom-out { - cursor: zoom-out; -} - .monaco-resource-viewer .embedded-link, .monaco-resource-viewer .embedded-link:hover { cursor: pointer; diff --git a/src/vs/workbench/browser/parts/editor/resourceViewer.ts b/src/vs/workbench/browser/parts/editor/resourceViewer.ts index ed9f4f5bff15a..f5145606ebbe8 100644 --- a/src/vs/workbench/browser/parts/editor/resourceViewer.ts +++ b/src/vs/workbench/browser/parts/editor/resourceViewer.ts @@ -3,25 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/resourceviewer'; -import * as nls from 'vs/nls'; -import * as mimes from 'vs/base/common/mime'; -import { URI } from 'vs/base/common/uri'; import * as DOM from 'vs/base/browser/dom'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { LRUCache } from 'vs/base/common/map'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { clamp } from 'vs/base/common/numbers'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IDisposable, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Action } from 'vs/base/common/actions'; -import { memoize } from 'vs/base/common/decorators'; -import * as platform from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import 'vs/css!./media/resourceviewer'; +import * as nls from 'vs/nls'; import { IFileService } from 'vs/platform/files/common/files'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITheme, registerThemingParticipant, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { ICssStyleCollector, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IMAGE_PREVIEW_BORDER } from 'vs/workbench/common/theme'; export interface IResourceDescriptor { @@ -94,11 +85,6 @@ export class ResourceViewer { // Ensure CSS class container.className = 'monaco-resource-viewer'; - // Images - if (ResourceViewer.isImageResource(descriptor)) { - return ImageView.create(container, descriptor, fileService, scrollbar, delegate, instantiationService); - } - // Large Files if (descriptor.size > ResourceViewer.MAX_OPEN_INTERNAL_SIZE) { return FileTooLargeFileView.create(container, descriptor, scrollbar, delegate); @@ -110,82 +96,8 @@ export class ResourceViewer { } } - private static isImageResource(descriptor: IResourceDescriptor) { - const mime = getMime(descriptor); - - // Chrome does not support tiffs - return mime.indexOf('image/') >= 0 && mime !== 'image/tiff'; - } -} - -class ImageView { - private static readonly MAX_IMAGE_SIZE = BinarySize.MB * 10; // showing images inline is memory intense, so we have a limit - private static readonly BASE64_MARKER = 'base64,'; - - static create( - container: HTMLElement, - descriptor: IResourceDescriptor, - fileService: IFileService, - scrollbar: DomScrollableElement, - delegate: ResourceViewerDelegate, - instantiationService: IInstantiationService, - ): ResourceViewerContext { - if (ImageView.shouldShowImageInline(descriptor)) { - return InlineImageView.create(container, descriptor, fileService, scrollbar, delegate, instantiationService); - } - - return LargeImageView.create(container, descriptor, delegate); - } - - private static shouldShowImageInline(descriptor: IResourceDescriptor): boolean { - let skipInlineImage: boolean; - - // Data URI - if (descriptor.resource.scheme === Schemas.data) { - const base64MarkerIndex = descriptor.resource.path.indexOf(ImageView.BASE64_MARKER); - const hasData = base64MarkerIndex >= 0 && descriptor.resource.path.substring(base64MarkerIndex + ImageView.BASE64_MARKER.length).length > 0; - - skipInlineImage = !hasData || descriptor.size > ImageView.MAX_IMAGE_SIZE || descriptor.resource.path.length > ImageView.MAX_IMAGE_SIZE; - } - - // File URI - else { - skipInlineImage = typeof descriptor.size !== 'number' || descriptor.size > ImageView.MAX_IMAGE_SIZE; - } - - return !skipInlineImage; - } } -class LargeImageView { - static create( - container: HTMLElement, - descriptor: IResourceDescriptor, - delegate: ResourceViewerDelegate - ) { - const size = BinarySize.formatSize(descriptor.size); - delegate.metadataClb(size); - - DOM.clearNode(container); - - const disposables = new DisposableStore(); - - const label = document.createElement('p'); - label.textContent = nls.localize('largeImageError', "The image is not displayed in the editor because it is too large ({0}).", size); - container.appendChild(label); - - const openExternal = delegate.openExternalClb; - if (descriptor.resource.scheme === Schemas.file && openExternal) { - const link = DOM.append(label, DOM.$('a.embedded-link')); - link.setAttribute('role', 'button'); - link.textContent = nls.localize('resourceOpenExternalButton', "Open image using external program?"); - - disposables.add(DOM.addDisposableListener(link, DOM.EventType.CLICK, () => openExternal(descriptor.resource))); - } - - return disposables; - } -} class FileTooLargeFileView { static create( @@ -239,349 +151,3 @@ class FileSeemsBinaryFileView { return disposables; } } - -type Scale = number | 'fit'; - -export class ZoomStatusbarItem extends Disposable { - - private statusbarItem?: IStatusbarEntryAccessor; - - constructor( - private readonly onSelectScale: (scale: Scale) => void, - @IEditorService editorService: IEditorService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IStatusbarService private readonly statusbarService: IStatusbarService, - ) { - super(); - this._register(editorService.onDidActiveEditorChange(() => { - if (this.statusbarItem) { - this.statusbarItem.dispose(); - this.statusbarItem = undefined; - } - })); - } - - updateStatusbar(scale: Scale): void { - const entry: IStatusbarEntry = { - text: this.zoomLabel(scale) - }; - - if (!this.statusbarItem) { - this.statusbarItem = this.statusbarService.addEntry(entry, 'status.imageZoom', nls.localize('status.imageZoom', "Image Zoom"), StatusbarAlignment.RIGHT, 101 /* to the left of editor status (100) */); - - this._register(this.statusbarItem); - - const element = document.getElementById('status.imageZoom')!; - this._register(DOM.addDisposableListener(element, DOM.EventType.CLICK, (e: MouseEvent) => { - this.contextMenuService.showContextMenu({ - getAnchor: () => element, - getActions: () => this.zoomActions - }); - })); - } else { - this.statusbarItem.update(entry); - } - } - - @memoize - private get zoomActions(): Action[] { - const scales: Scale[] = [10, 5, 2, 1, 0.5, 0.2, 'fit']; - return scales.map(scale => - new Action(`zoom.${scale}`, this.zoomLabel(scale), undefined, undefined, () => { - this.updateStatusbar(scale); - if (this.onSelectScale) { - this.onSelectScale(scale); - } - - return Promise.resolve(undefined); - })); - } - - private zoomLabel(scale: Scale): string { - return scale === 'fit' - ? nls.localize('zoom.action.fit.label', 'Whole Image') - : `${Math.round(scale * 100)}%`; - } -} - -interface ImageState { - scale: Scale; - offsetX: number; - offsetY: number; -} - -class InlineImageView { - private static readonly SCALE_PINCH_FACTOR = 0.075; - private static readonly MAX_SCALE = 20; - private static readonly MIN_SCALE = 0.1; - - private static readonly zoomLevels: Scale[] = [ - 0.1, - 0.2, - 0.3, - 0.4, - 0.5, - 0.6, - 0.7, - 0.8, - 0.9, - 1, - 1.5, - 2, - 3, - 5, - 7, - 10, - 15, - 20 - ]; - - /** - * Enable image-rendering: pixelated for images scaled by more than this. - */ - private static readonly PIXELATION_THRESHOLD = 3; - - /** - * Store the scale and position of an image so it can be restored when changing editor tabs - */ - private static readonly imageStateCache = new LRUCache(100); - - static create( - container: HTMLElement, - descriptor: IResourceDescriptor, - fileService: IFileService, - scrollbar: DomScrollableElement, - delegate: ResourceViewerDelegate, - @IInstantiationService instantiationService: IInstantiationService, - ) { - const disposables = new DisposableStore(); - - const zoomStatusbarItem = disposables.add(instantiationService.createInstance(ZoomStatusbarItem, - newScale => updateScale(newScale))); - - const context: ResourceViewerContext = { - layout(dimension: DOM.Dimension) { }, - dispose: () => disposables.dispose() - }; - - const cacheKey = `${descriptor.resource.toString()}:${descriptor.etag}`; - - let ctrlPressed = false; - let altPressed = false; - - const initialState: ImageState = InlineImageView.imageStateCache.get(cacheKey) || { scale: 'fit', offsetX: 0, offsetY: 0 }; - let scale = initialState.scale; - let image: HTMLImageElement | null = null; - - function updateScale(newScale: Scale) { - if (!image || !image.parentElement) { - return; - } - - if (newScale === 'fit') { - scale = 'fit'; - DOM.addClass(image, 'scale-to-fit'); - DOM.removeClass(image, 'pixelated'); - image.style.minWidth = 'auto'; - image.style.width = 'auto'; - InlineImageView.imageStateCache.delete(cacheKey); - } else { - const oldWidth = image.width; - const oldHeight = image.height; - - scale = clamp(newScale, InlineImageView.MIN_SCALE, InlineImageView.MAX_SCALE); - if (scale >= InlineImageView.PIXELATION_THRESHOLD) { - DOM.addClass(image, 'pixelated'); - } else { - DOM.removeClass(image, 'pixelated'); - } - - const { scrollTop, scrollLeft } = image.parentElement; - const dx = (scrollLeft + image.parentElement.clientWidth / 2) / image.parentElement.scrollWidth; - const dy = (scrollTop + image.parentElement.clientHeight / 2) / image.parentElement.scrollHeight; - - DOM.removeClass(image, 'scale-to-fit'); - image.style.minWidth = `${(image.naturalWidth * scale)}px`; - image.style.width = `${(image.naturalWidth * scale)}px`; - - const newWidth = image.width; - const scaleFactor = (newWidth - oldWidth) / oldWidth; - - const newScrollLeft = ((oldWidth * scaleFactor * dx) + scrollLeft); - const newScrollTop = ((oldHeight * scaleFactor * dy) + scrollTop); - scrollbar.setScrollPosition({ - scrollLeft: newScrollLeft, - scrollTop: newScrollTop, - }); - - InlineImageView.imageStateCache.set(cacheKey, { scale: scale, offsetX: newScrollLeft, offsetY: newScrollTop }); - } - - zoomStatusbarItem.updateStatusbar(scale); - scrollbar.scanDomNode(); - } - - function firstZoom() { - if (!image) { - return; - } - - scale = image.clientWidth / image.naturalWidth; - updateScale(scale); - } - - disposables.add(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { - if (!image) { - return; - } - ctrlPressed = e.ctrlKey; - altPressed = e.altKey; - - if (platform.isMacintosh ? altPressed : ctrlPressed) { - DOM.removeClass(container, 'zoom-in'); - DOM.addClass(container, 'zoom-out'); - } - })); - - disposables.add(DOM.addDisposableListener(window, DOM.EventType.KEY_UP, (e: KeyboardEvent) => { - if (!image) { - return; - } - - ctrlPressed = e.ctrlKey; - altPressed = e.altKey; - - if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { - DOM.removeClass(container, 'zoom-out'); - DOM.addClass(container, 'zoom-in'); - } - })); - - disposables.add(DOM.addDisposableListener(container, DOM.EventType.CLICK, (e: MouseEvent) => { - if (!image) { - return; - } - - if (e.button !== 0) { - return; - } - - // left click - if (scale === 'fit') { - firstZoom(); - } - - if (!(platform.isMacintosh ? altPressed : ctrlPressed)) { // zoom in - let i = 0; - for (; i < InlineImageView.zoomLevels.length; ++i) { - if (InlineImageView.zoomLevels[i] > scale) { - break; - } - } - updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MAX_SCALE); - } else { - let i = InlineImageView.zoomLevels.length - 1; - for (; i >= 0; --i) { - if (InlineImageView.zoomLevels[i] < scale) { - break; - } - } - updateScale(InlineImageView.zoomLevels[i] || InlineImageView.MIN_SCALE); - } - })); - - disposables.add(DOM.addDisposableListener(container, DOM.EventType.WHEEL, (e: WheelEvent) => { - if (!image) { - return; - } - - const isScrollWheelKeyPressed = platform.isMacintosh ? altPressed : ctrlPressed; - if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl - return; - } - - e.preventDefault(); - e.stopPropagation(); - - if (scale === 'fit') { - firstZoom(); - } - - let delta = e.deltaY > 0 ? 1 : -1; - - updateScale(scale as number * (1 - delta * InlineImageView.SCALE_PINCH_FACTOR)); - })); - - disposables.add(DOM.addDisposableListener(container, DOM.EventType.SCROLL, () => { - if (!image || !image.parentElement || scale === 'fit') { - return; - } - - const entry = InlineImageView.imageStateCache.get(cacheKey); - if (entry) { - const { scrollTop, scrollLeft } = image.parentElement; - InlineImageView.imageStateCache.set(cacheKey, { scale: entry.scale, offsetX: scrollLeft, offsetY: scrollTop }); - } - })); - - DOM.clearNode(container); - DOM.addClasses(container, 'image', 'zoom-in'); - - image = DOM.append(container, DOM.$('img.scale-to-fit')); - image.style.visibility = 'hidden'; - - disposables.add(DOM.addDisposableListener(image, DOM.EventType.LOAD, e => { - if (!image) { - return; - } - if (typeof descriptor.size === 'number') { - delegate.metadataClb(nls.localize('imgMeta', '{0}x{1} {2}', image.naturalWidth, image.naturalHeight, BinarySize.formatSize(descriptor.size))); - } else { - delegate.metadataClb(nls.localize('imgMetaNoSize', '{0}x{1}', image.naturalWidth, image.naturalHeight)); - } - - scrollbar.scanDomNode(); - image.style.visibility = 'visible'; - updateScale(scale); - if (initialState.scale !== 'fit') { - scrollbar.setScrollPosition({ - scrollLeft: initialState.offsetX, - scrollTop: initialState.offsetY, - }); - } - })); - - InlineImageView.imageSrc(descriptor, fileService).then(src => { - const img = container.querySelector('img'); - if (img) { - if (typeof src === 'string') { - img.src = src; - } else { - const url = URL.createObjectURL(src); - disposables.add(toDisposable(() => URL.revokeObjectURL(url))); - img.src = url; - } - } - }); - - return context; - } - - private static async imageSrc(descriptor: IResourceDescriptor, fileService: IFileService): Promise { - if (descriptor.resource.scheme === Schemas.data) { - return descriptor.resource.toString(true /* skip encoding */); - } - - const { value } = await fileService.readFile(descriptor.resource); - return new Blob([value.buffer], { type: getMime(descriptor) }); - } -} - -function getMime(descriptor: IResourceDescriptor) { - let mime: string | undefined = descriptor.mime; - if (!mime && descriptor.resource.scheme !== Schemas.data) { - mime = mimes.getMediaMime(descriptor.resource.path); - } - - return mime || mimes.MIME_BINARY; -} diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 0a4df5dc9b391..97884ee612311 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -773,6 +773,11 @@ export class EditorOptions implements IEditorOptions { */ ignoreError: boolean | undefined; + /** + * Does not use editor overrides while opening the editor. + */ + ignoreOverrides: boolean | undefined; + /** * Overwrites option values from the provided bag. */ @@ -813,6 +818,10 @@ export class EditorOptions implements IEditorOptions { this.index = options.index; } + if (typeof options.ignoreOverrides === 'boolean') { + this.ignoreOverrides = options.ignoreOverrides; + } + return this; } } diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts new file mode 100644 index 0000000000000..1e6552cec99cc --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import * as nls from 'vs/nls'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IListService } from 'vs/platform/list/browser/listService'; +import { ResourceContextKey } from 'vs/workbench/common/resources'; +import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; +import { WebviewPanelResourceScheme } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +const viewCategory = nls.localize('viewCategory', "View"); + +// #region Open With + +const OPEN_WITH_COMMAND_ID = 'openWith'; +const OPEN_WITH_TITLE = { value: nls.localize('openWith.title', 'Open With'), original: 'Open With' }; + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: OPEN_WITH_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: EditorContextKeys.focus.toNegated(), + handler: async (accessor: ServicesAccessor, resource: URI | object) => { + const editorService = accessor.get(IEditorService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService); + const targetResource = resources[0]; + if (!targetResource) { + return; + } + return accessor.get(ICustomEditorService).promptOpenWith(targetResource, undefined, undefined); + } +}); + +MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { + group: 'navigation', + order: 20, + command: { + id: OPEN_WITH_COMMAND_ID, + title: OPEN_WITH_TITLE, + }, + when: ResourceContextKey.Scheme.isEqualTo(Schemas.file) +}); + +// #endregion + +// #region Reopen With + +const REOPEN_WITH_COMMAND_ID = 'reOpenWith'; +const REOPEN_WITH_TITLE = { value: nls.localize('reopenWith.title', 'Reopen With'), original: 'Reopen With' }; + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: REOPEN_WITH_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + handler: async (accessor: ServicesAccessor, resource: URI | undefined) => { + const customEditorService = accessor.get(ICustomEditorService); + const editorService = accessor.get(IEditorService); + if (!resource) { + if (editorService.activeEditor) { + resource = editorService.activeEditor.getResource(); + } + } + + if (!resource) { + return; + } + + if (resource.scheme === WebviewPanelResourceScheme) { + resource = URI.parse(decodeURIComponent(resource.query)); + } + + // Make sure the context menu has been dismissed before we prompt. + // Otherwise with webviews, we will sometimes close the prompt instantly when the webview is + // refocused by the workbench + setTimeout(() => { + customEditorService.promptOpenWith(resource!, undefined, undefined); + }, 10); + } +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { + order: 40, + command: { + id: REOPEN_WITH_COMMAND_ID, + title: REOPEN_WITH_TITLE, + } +}); + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: REOPEN_WITH_COMMAND_ID, + title: REOPEN_WITH_TITLE, + category: viewCategory, + } +}); + +// #endregion diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts new file mode 100644 index 0000000000000..fedd607b6b7ec --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { memoize } from 'vs/base/common/decorators'; +import { UnownedDisposable } from 'vs/base/common/lifecycle'; +import { basename } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IEditorInput, Verbosity } from 'vs/workbench/common/editor'; +import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; +import { IWebviewEditorService } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export class CustomFileEditorInput extends WebviewEditorInput { + private name?: string; + private _hasResolved = false; + + constructor( + resource: URI, + viewType: string, + id: string, + webview: UnownedDisposable, + @ILabelService + private readonly labelService: ILabelService, + @IWebviewEditorService + private readonly _webviewEditorService: IWebviewEditorService, + @IExtensionService + private readonly _extensionService: IExtensionService + ) { + super(id, viewType, '', undefined, webview, resource); + } + + getName(): string { + if (!this.name) { + this.name = basename(this.labelService.getUriLabel(this.editorResource)); + } + return this.name; + } + + matches(other: IEditorInput): boolean { + return this === other || (other instanceof CustomFileEditorInput + && this.viewType === other.viewType + && this.editorResource.toString() === other.editorResource.toString()); + } + + @memoize + private get shortTitle(): string { + return this.getName(); + } + + @memoize + private get mediumTitle(): string { + return this.labelService.getUriLabel(this.editorResource, { relative: true }); + } + + @memoize + private get longTitle(): string { + return this.labelService.getUriLabel(this.editorResource); + } + + getTitle(verbosity: Verbosity): string { + switch (verbosity) { + case Verbosity.SHORT: + return this.shortTitle; + default: + case Verbosity.MEDIUM: + return this.mediumTitle; + case Verbosity.LONG: + return this.longTitle; + } + } + + public async resolve(): Promise { + if (!this._hasResolved) { + this._hasResolved = true; + this._extensionService.activateByEvent(`onWebviewEditor:${this.viewType}`); + await this._webviewEditorService.resolveWebview(this); + } + return super.resolve(); + } +} diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts new file mode 100644 index 0000000000000..2fac7d6265901 --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { UnownedDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; +import { IWebviewEditorService } from 'vs/workbench/contrib/webview/browser/webviewEditorService'; + +export class CustomEditoInputFactory extends WebviewEditorInputFactory { + + public static readonly ID = CustomFileEditorInput.typeId; + + public constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWebviewEditorService private readonly webviewService: IWebviewEditorService, + ) { + super(webviewService); + } + + public serialize(input: CustomFileEditorInput): string | undefined { + const data = { + ...this.toJson(input), + editorResource: input.editorResource.toJSON() + }; + + try { + return JSON.stringify(data); + } catch { + return undefined; + } + } + + public deserialize( + _instantiationService: IInstantiationService, + serializedEditorInput: string + ): CustomFileEditorInput { + const data = this.fromJson(serializedEditorInput); + const webviewInput = this.webviewService.reviveWebview(generateUuid(), data.viewType, data.title, data.iconPath, data.state, data.options, data.extensionLocation ? { + location: data.extensionLocation, + id: data.extensionId + } : undefined, data.group); + + const customInput = this._instantiationService.createInstance(CustomFileEditorInput, URI.from((data as any).editorResource), data.viewType, generateUuid(), new UnownedDisposable(webviewInput.webview)); + if (typeof data.group === 'number') { + customInput.updateGroup(data.group); + } + return customInput; + } +} diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts new file mode 100644 index 0000000000000..b1bbbe4af0771 --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { find } from 'vs/base/common/arrays'; +import * as glob from 'vs/base/common/glob'; +import { UnownedDisposable } from 'vs/base/common/lifecycle'; +import { basename } from 'vs/base/common/resources'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IEditor, IEditorInput } from 'vs/workbench/common/editor'; +import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; +import { CustomEditorDiscretion, CustomEditorInfo, CustomEditorSelector, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; +import { CustomFileEditorInput } from './customEditorInput'; + +export class CustomEditorService implements ICustomEditorService { + _serviceBrand: any; + + private readonly customEditors: Array = []; + + constructor( + @IEditorService private readonly editorService: IEditorService, + @IWebviewService private readonly webviewService: IWebviewService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + ) { + webviewEditorsExtensionPoint.setHandler(extensions => { + for (const extension of extensions) { + for (const webviewEditorContribution of extension.value) { + this.customEditors.push({ + id: webviewEditorContribution.viewType, + displayName: webviewEditorContribution.displayName, + selector: webviewEditorContribution.selector || [], + discretion: webviewEditorContribution.discretion || CustomEditorDiscretion.default, + }); + } + } + }); + } + + public getCustomEditorsForResource(resource: URI): readonly CustomEditorInfo[] { + return this.customEditors.filter(customEditor => + customEditor.selector.some(selector => matches(selector, resource))); + } + + public async promptOpenWith( + resource: URI, + options?: ITextEditorOptions, + group?: IEditorGroup, + ): Promise { + const preferredEditors = await this.getCustomEditorsForResource(resource); + const defaultEditorId = 'default'; + const pick = await this.quickInputService.pick([ + { + label: nls.localize('promptOpenWith.defaultEditor', "Default built-in editor"), + id: defaultEditorId, + }, + ...preferredEditors.map((editorDescriptor): IQuickPickItem => ({ + label: editorDescriptor.displayName, + id: editorDescriptor.id, + })) + ], { + placeHolder: nls.localize('promptOpenWith.placeHolder', "Select editor to use for '{0}'...", basename(resource)), + }); + + if (!pick) { + return; + } + + if (pick.id === defaultEditorId) { + const fileInput = this.instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); + return this.editorService.openEditor(fileInput, { ...options, ignoreOverrides: true }, group); + } else { + return this.openWith(resource, pick.id!, options, group); + } + } + + public openWith( + resource: URI, + viewType: string, + options?: ITextEditorOptions, + group?: IEditorGroup, + ): Promise { + if (!this.customEditors.some(x => x.id === viewType)) { + return this.promptOpenWith(resource, options, group); + } + + const id = generateUuid(); + const webview = this.webviewService.createWebviewEditorOverlay(id, {}, {}); + const input = this.instantiationService.createInstance(CustomFileEditorInput, resource, viewType, id, new UnownedDisposable(webview)); + if (group) { + input.updateGroup(group!.id); + } + return this.editorService.openEditor(input, options, group); + } +} + +export const customEditorsAssociationsKey = 'workbench.experimental.editorAssociations'; + +export type CustomEditorsAssociations = readonly (CustomEditorSelector & { readonly viewType: string })[]; + +export class CustomEditorContribution implements IWorkbenchContribution { + constructor( + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ICustomEditorService private readonly customEditorService: ICustomEditorService, + ) { + this.editorService.overrideOpenEditor((editor, options, group) => this.onEditorOpening(editor, options, group)); + } + + private getConfiguredCustomEditor(resource: URI): string | undefined { + const config = this.configurationService.getValue(customEditorsAssociationsKey) || []; + const match = find(config, association => matches(association, resource)); + return match ? match.viewType : undefined; + } + + private onEditorOpening( + editor: IEditorInput, + options: ITextEditorOptions | undefined, + group: IEditorGroup + ): IOpenEditorOverride | undefined { + if (editor instanceof CustomFileEditorInput) { + return; + } + + const resource = editor.getResource(); + if (!resource) { + return; + } + + const userConfiguredViewType = this.getConfiguredCustomEditor(resource); + const customEditors = this.customEditorService.getCustomEditorsForResource(resource); + + if (!userConfiguredViewType) { + if (!customEditors.length) { + return; + } + + const defaultEditors = customEditors.filter(editor => editor.discretion === CustomEditorDiscretion.default); + if (defaultEditors.length === 1) { + return { + override: this.customEditorService.openWith(resource, defaultEditors[0].id, options, group), + }; + } + } + + for (const input of group.editors) { + if (input instanceof CustomFileEditorInput && input.editorResource.toString() === resource.toString()) { + return { + override: group.openEditor(input, options).then(withNullAsUndefined) + }; + } + } + + if (userConfiguredViewType) { + return { + override: this.customEditorService.openWith(resource, userConfiguredViewType, options, group), + }; + } + + // Open default editor but prompt user to see if they wish to use a custom one instead + return { + override: (async () => { + const standardEditor = await this.editorService.openEditor(editor, { ...options, ignoreOverrides: true }, group); + const selectedEditor = await this.customEditorService.promptOpenWith(resource, options, group); + if (selectedEditor && selectedEditor.input) { + await group.replaceEditors([{ + editor, + replacement: selectedEditor.input + }]); + return selectedEditor; + } + + return standardEditor; + })() + }; + } +} + +function matches(selector: CustomEditorSelector, resource: URI): boolean { + if (!selector.filenamePattern && !selector.scheme) { + return false; + } + if (selector.filenamePattern) { + if (!glob.match(selector.filenamePattern.toLowerCase(), basename(resource).toLowerCase())) { + return false; + } + } + if (selector.scheme) { + if (resource.scheme !== selector.scheme) { + return false; + } + } + return true; +} diff --git a/src/vs/workbench/contrib/customEditor/browser/extensionPoint.ts b/src/vs/workbench/contrib/customEditor/browser/extensionPoint.ts new file mode 100644 index 0000000000000..a17205467bb4a --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/browser/extensionPoint.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import * as nls from 'vs/nls'; +import { CustomEditorDiscretion, CustomEditorSelector } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { languagesExtPoint } from 'vs/workbench/services/mode/common/workbenchModeService'; + +namespace WebviewEditorContribution { + export const viewType = 'viewType'; + export const displayName = 'displayName'; + export const selector = 'selector'; + export const discretion = 'discretion'; +} + +interface IWebviewEditorsExtensionPoint { + readonly [WebviewEditorContribution.viewType]: string; + readonly [WebviewEditorContribution.displayName]: string; + readonly [WebviewEditorContribution.selector]?: readonly CustomEditorSelector[]; + readonly [WebviewEditorContribution.discretion]?: CustomEditorDiscretion; +} + +const webviewEditorsContribution: IJSONSchema = { + description: nls.localize('contributes.webviewEditors', 'Contributes webview editors.'), + type: 'array', + defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }], + items: { + type: 'object', + required: [ + WebviewEditorContribution.viewType, + WebviewEditorContribution.displayName, + WebviewEditorContribution.selector, + ], + properties: { + [WebviewEditorContribution.viewType]: { + type: 'string', + description: nls.localize('contributes.viewType', 'Unique identifier of the custom editor.'), + }, + [WebviewEditorContribution.displayName]: { + type: 'string', + description: nls.localize('contributes.displayName', 'Name of the custom editor displayed to users.'), + }, + [WebviewEditorContribution.selector]: { + type: 'array', + description: nls.localize('contributes.selector', 'Set of globs that the custom editor is enabled for.'), + items: { + type: 'object', + properties: { + filenamePattern: { + type: 'string', + description: nls.localize('contributes.selector.filenamePattern', 'Glob that the custom editor is enabled for.'), + }, + scheme: { + type: 'string', + description: nls.localize('contributes.selector.scheme', 'File scheme that the custom editor is enabled for.'), + } + } + } + }, + [WebviewEditorContribution.discretion]: { + type: 'string', + description: nls.localize('contributes.discretion', 'Controls when the custom editor is used. May be overridden by users.'), + enum: [ + CustomEditorDiscretion.default, + CustomEditorDiscretion.option + ], + enumDescriptions: [ + nls.localize('contributes.discretion.default', 'Editor is automatically used for a resource if no other default custom editors are registered for it.'), + nls.localize('contributes.discretion.option', 'Editor is not automatically used but can be selected by a user.'), + ], + default: 'default' + } + } + } +}; + +export const webviewEditorsExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'webviewEditors', + deps: [languagesExtPoint], + jsonSchema: webviewEditorsContribution +}); diff --git a/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts new file mode 100644 index 0000000000000..8fff4953c7886 --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/browser/webviewEditor.contribution.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { CustomEditoInputFactory } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; +import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor'; +import './commands'; +import { CustomFileEditorInput } from './customEditorInput'; +import { CustomEditorContribution, customEditorsAssociationsKey, CustomEditorService } from './customEditors'; + +registerSingleton(ICustomEditorService, CustomEditorService); + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(CustomEditorContribution, LifecyclePhase.Starting); + +Registry.as(EditorExtensions.Editors).registerEditor( + new EditorDescriptor( + WebviewEditor, + WebviewEditor.ID, + 'Webview Editor', + ), [ + new SyncDescriptor(CustomFileEditorInput) +]); + +Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( + CustomEditoInputFactory.ID, + CustomEditoInputFactory); + +Registry.as(ConfigurationExtensions.Configuration) + .registerConfiguration({ + 'id': 'workbench', + 'order': 7, + 'title': nls.localize('workbenchConfigurationTitle', "Workbench"), + 'type': 'object', + 'properties': { + [customEditorsAssociationsKey]: { + type: 'array', + markdownDescription: nls.localize('editor.editorAssociations', "Configure which editor to use for a resource."), + items: { + type: 'object', + properties: { + 'viewType': { + type: 'string', + description: nls.localize('editor.editorAssociations.viewType', "Editor view type."), + }, + 'scheme': { + type: 'string', + description: nls.localize('editor.editorAssociations.scheme', "Uri scheme the editor should be used for."), + }, + 'filenamePattern': { + type: 'string', + description: nls.localize('editor.editorAssociations.filenamePattern', "Glob pattern the the editor should be used for."), + } + } + } + } + } + }); diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts new file mode 100644 index 0000000000000..6cb044c58e544 --- /dev/null +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IEditor } from 'vs/workbench/common/editor'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; + + +export const ICustomEditorService = createDecorator('customEditorService'); + +export interface ICustomEditorService { + _serviceBrand: any; + + getCustomEditorsForResource(resource: URI): readonly CustomEditorInfo[]; + + openWith(resource: URI, customEditorViewType: string, options?: ITextEditorOptions, group?: IEditorGroup): Promise; + promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise; +} + +export const enum CustomEditorDiscretion { + default = 'default', + option = 'option', +} + +export interface CustomEditorSelector { + readonly scheme?: string; + readonly filenamePattern?: string; +} + +export interface CustomEditorInfo { + readonly id: string; + readonly displayName: string; + readonly discretion: CustomEditorDiscretion; + readonly selector: readonly CustomEditorSelector[]; +} diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index 791f21b8457fc..fc30557231e0b 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -13,7 +13,7 @@ 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 { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, EditorInput } from 'vs/workbench/common/editor'; import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic export class WebviewEditor extends BaseEditor { - public static readonly ID = 'WebviewEditor'; + public static ID = 'WebviewEditor'; private readonly _scopedContextKeyService = this._register(new MutableDisposable()); private _findWidgetVisible: IContextKey; @@ -136,7 +136,11 @@ export class WebviewEditor extends BaseEditor { super.clearInput(); } - public async setInput(input: WebviewEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + public async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + if (input.matches(this.input)) { + return; + } + if (this.input && this.input instanceof WebviewEditorInput) { this.input.webview.release(this); } @@ -147,11 +151,13 @@ export class WebviewEditor extends BaseEditor { return; } - if (this.group) { - input.updateGroup(this.group.id); - } + if (input instanceof WebviewEditorInput) { + if (this.group) { + input.updateGroup(this.group.id); + } - this.claimWebview(input); + this.claimWebview(input); + } } private claimWebview(input: WebviewEditorInput): void { diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts index 316c112a8bd1c..5cfed10b78969 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInput.ts @@ -7,10 +7,12 @@ import { memoize } from 'vs/base/common/decorators'; import { URI } from 'vs/base/common/uri'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { EditorInput, EditorModel, GroupIdentifier, IEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, EditorModel, GroupIdentifier, IEditorInput, Verbosity } from 'vs/workbench/common/editor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { UnownedDisposable as Unowned } from 'vs/base/common/lifecycle'; +export const WebviewPanelResourceScheme = 'webview-panel'; + class WebviewIconsManager { private readonly _icons = new Map(); @@ -70,6 +72,7 @@ export class WebviewEditorInput extends EditorInput { readonly id: ExtensionIdentifier; }, webview: Unowned, + public readonly editorResource: URI, ) { super(); @@ -85,8 +88,9 @@ export class WebviewEditorInput extends EditorInput { public getResource(): URI { return URI.from({ - scheme: 'webview-panel', - path: `webview-panel/webview-${this.id}` + scheme: WebviewPanelResourceScheme, + path: `webview-panel/webview-${this.id}`, + query: this.editorResource ? encodeURIComponent(this.editorResource.toString(true)) : '' }); } @@ -94,7 +98,7 @@ export class WebviewEditorInput extends EditorInput { return this._name; } - public getTitle() { + public getTitle(_verbosity?: Verbosity) { return this.getName(); } @@ -154,8 +158,9 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput { }, private readonly reviver: (input: WebviewEditorInput) => Promise, webview: Unowned, + public readonly editorResource: URI, ) { - super(id, viewType, name, extension, webview); + super(id, viewType, name, extension, webview, editorResource); } public async resolve(): Promise { diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts index a8dbabc2c8726..22dd4fce15848 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorInputFactory.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI, UriComponents } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; 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'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { generateUuid } from 'vs/base/common/uuid'; interface SerializedIconPath { light: string | UriComponents; @@ -35,24 +35,12 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { @IWebviewEditorService private readonly _webviewService: IWebviewEditorService ) { } - public serialize( - input: WebviewEditorInput - ): string | undefined { + public serialize(input: WebviewEditorInput): string | undefined { if (!this._webviewService.shouldPersist(input)) { return undefined; } - const data: SerializedWebview = { - viewType: input.viewType, - title: input.getName(), - options: { ...input.webview.options, ...input.webview.contentOptions }, - extensionLocation: input.extension ? input.extension.location : undefined, - extensionId: input.extension && input.extension.id ? input.extension.id.value : undefined, - state: input.webview.state, - iconPath: input.iconPath ? { light: input.iconPath.light, dark: input.iconPath.dark, } : undefined, - group: input.group - }; - + const data = this.toJson(input); try { return JSON.stringify(data); } catch { @@ -64,17 +52,36 @@ export class WebviewEditorInputFactory implements IEditorInputFactory { _instantiationService: IInstantiationService, serializedEditorInput: string ): WebviewEditorInput { - const data: SerializedWebview = JSON.parse(serializedEditorInput); - const extensionLocation = reviveUri(data.extensionLocation); - const extensionId = data.extensionId ? new ExtensionIdentifier(data.extensionId) : undefined; - const iconPath = reviveIconPath(data.iconPath); - const state = reviveState(data.state); - - return this._webviewService.reviveWebview(generateUuid(), data.viewType, data.title, iconPath, state, data.options, extensionLocation ? { - location: extensionLocation, - id: extensionId + const data = this.fromJson(serializedEditorInput); + return this._webviewService.reviveWebview(generateUuid(), data.viewType, data.title, data.iconPath, data.state, data.options, data.extensionLocation ? { + location: data.extensionLocation, + id: data.extensionId } : undefined, data.group); } + + protected fromJson(serializedEditorInput: string) { + const data: SerializedWebview = JSON.parse(serializedEditorInput); + return { + ...data, + extensionLocation: reviveUri(data.extensionLocation), + extensionId: data.extensionId ? new ExtensionIdentifier(data.extensionId) : undefined, + iconPath: reviveIconPath(data.iconPath), + state: reviveState(data.state), + }; + } + + protected toJson(input: WebviewEditorInput): SerializedWebview { + return { + viewType: input.viewType, + title: input.getName(), + options: { ...input.webview.options, ...input.webview.contentOptions }, + extensionLocation: input.extension ? input.extension.location : undefined, + extensionId: input.extension && input.extension.id ? input.extension.id.value : undefined, + state: input.webview.state, + iconPath: input.iconPath ? { light: input.iconPath.light, dark: input.iconPath.dark, } : undefined, + group: input.group + }; + } } function reviveIconPath(data: SerializedIconPath | undefined) { diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts index 2918330cf01bc..59a0126963de2 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditorService.ts @@ -60,22 +60,26 @@ export interface IWebviewEditorService { preserveFocus: boolean ): void; - registerReviver( - reviver: WebviewReviver + registerResolver( + reviver: WebviewResolve ): IDisposable; shouldPersist( input: WebviewEditorInput ): boolean; + + resolveWebview( + webview: WebviewEditorInput, + ): Promise; } -export interface WebviewReviver { - canRevive( - webview: WebviewEditorInput +export interface WebviewResolve { + canResolve( + webview: WebviewEditorInput, ): boolean; - reviveWebview( - webview: WebviewEditorInput + resolveWebview( + webview: WebviewEditorInput, ): Promise; } @@ -95,11 +99,11 @@ export function areWebviewInputOptionsEqual(a: WebviewInputOptions, b: WebviewIn && (a.portMapping === b.portMapping || (Array.isArray(a.portMapping) && Array.isArray(b.portMapping) && equals(a.portMapping, b.portMapping, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort))); } -function canRevive(reviver: WebviewReviver, webview: WebviewEditorInput): boolean { +function canRevive(reviver: WebviewResolve, webview: WebviewEditorInput): boolean { if (webview.isDisposed()) { return false; } - return reviver.canRevive(webview); + return reviver.canResolve(webview); } class RevivalPool { @@ -109,12 +113,12 @@ class RevivalPool { this._awaitingRevival.push({ input, resolve }); } - public reviveFor(reviver: WebviewReviver) { + public reviveFor(reviver: WebviewResolve) { 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); + reviver.resolveWebview(input).then(resolve); } } } @@ -122,7 +126,7 @@ class RevivalPool { export class WebviewEditorService implements IWebviewEditorService { _serviceBrand: undefined; - private readonly _revivers = new Set(); + private readonly _revivers = new Set(); private readonly _revivalPool = new RevivalPool(); constructor( @@ -146,7 +150,7 @@ export class WebviewEditorService implements IWebviewEditorService { ): WebviewEditorInput { const webview = this.createWebiew(id, extension, options); - const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, id, viewType, title, extension, new UnownedDisposable(webview)); + const webviewInput = this._instantiationService.createInstance(WebviewEditorInput, id, viewType, title, extension, new UnownedDisposable(webview), undefined); this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus, @@ -204,7 +208,7 @@ export class WebviewEditorService implements IWebviewEditorService { const promise = new Promise(r => { resolve = r; }); this._revivalPool.add(webview, resolve!); return promise; - }, new UnownedDisposable(webview)); + }, new UnownedDisposable(webview), null!/*TODO*/); webviewInput.iconPath = iconPath; @@ -214,8 +218,8 @@ export class WebviewEditorService implements IWebviewEditorService { return webviewInput; } - public registerReviver( - reviver: WebviewReviver + public registerResolver( + reviver: WebviewResolve ): IDisposable { this._revivers.add(reviver); this._revivalPool.reviveFor(reviver); @@ -247,13 +251,22 @@ export class WebviewEditorService implements IWebviewEditorService { ): Promise { for (const reviver of values(this._revivers)) { if (canRevive(reviver, webview)) { - await reviver.reviveWebview(webview); + await reviver.resolveWebview(webview); return true; } } return false; } + public async resolveWebview( + webview: WebviewEditorInput, + ): Promise { + const didRevive = await this.tryRevive(webview); + if (!didRevive) { + this._revivalPool.add(webview, () => { }); + } + } + private createWebiew(id: string, extension: { location: URI; id: ExtensionIdentifier; } | undefined, options: WebviewInputOptions) { return this._webviewService.createWebviewEditorOverlay(id, { extension: extension, diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index f36b9f96a4f68..a056de4bf9d98 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -146,6 +146,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { } private onGroupWillOpenEditor(group: IEditorGroup, event: IEditorOpeningEvent): void { + if (event.options && event.options.ignoreOverrides) { + return; + } + for (const handler of this.openEditorHandlers) { const result = handler(event.editor, event.options, group); if (result && result.override) { diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts index 5b5fa096f9d1e..8e9d984ffe5c6 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -300,4 +300,4 @@ suite('Workbench base editor', () => { MyEditor: MyEditor, MyOtherEditor: MyOtherEditor }; -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 2c625970c9604..6b3876cffa10b 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -171,6 +171,7 @@ import 'vs/workbench/contrib/url/common/url.contribution'; // Webview import 'vs/workbench/contrib/webview/browser/webview.contribution'; +import 'vs/workbench/contrib/customEditor/browser/webviewEditor.contribution'; // Extensions Management import 'vs/workbench/contrib/extensions/browser/extensions.contribution';