Prototyping custom editors (#77789)

* Custom Editor exploration

For #77131

Adds a prototype of custom editors contributed by extensions. This change does the following:

- Introduces a new contribution point for the declarative parts of a custom editor
- Adds API for registering a webview editor provider. This lets VS Code decided when to create a webview editor
- Adds an `openWith` command that lets you select which editor to use to open a resource from the file explorer
- Adds a setting that lets you say that you always want to use a custom editor for a given file extension
- Hooks up auto opening of a custom editor when opening a file from quick open or explorer
- Adds a new extension that contributes a custom image preview for png and jpg files

Still needs a lot of UX work and testing. We are also going to explore a more generic "open handler" based approach for supporting custom editors

Revert

* Re-use existing custom editor if one is already open

* Don't re-create custom editor webview when clicking on already visible custom editor

* Move customEditorInput to own file

* First draft of serializing custom editor inputs

* Use glob patterns instead of simple file extensions for matching custom resoruces for custom editors

* Add descriptions

* Try opening standard editor while prompting for custom editor

* Make sure we hide image status on dispose

* Make sure we restore editor group too

* Use glob patterns for workbench.editor.custom

* Allow users to configure custom editors for additional file types

* Use filename glob instead of glob on full resource path

* Adding placeholder for prompt open with

* Add enableByDefault setting for editor contributions

* Enable custom editors by default and add `discretion` enum

Changes `enableByDefault` boolean to a `discretion` enum. This should give more flexibility if we want other options (such as forcing a given custom editor to always be used even if there are other default ones)

* Allow custom editors to specify both a scheme and filenamePattern they are active for

* Rework custom editor setting

* Don't allow custom editors to be enabled for all resources by a config mistake

* Replace built-in image editor with one from extension

* Adding reopen with command

* Improve comment

* Remove commented code

* Localize package.json and remove image

* Remove extra lib setting from tsconfig
This commit is contained in:
Matt Bierner 2019-09-10 17:56:57 -07:00 committed by GitHub
parent df802950e0
commit 011836a150
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1660 additions and 567 deletions

View file

@ -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"

View file

@ -0,0 +1,10 @@
test/**
src/**
tsconfig.json
out/test/**
out/**
extension.webpack.config.js
cgmanifest.json
yarn.lock
preview-src/**
webpack.config.js

View file

@ -0,0 +1,3 @@
# Image Preview
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
{
"displayName": "Image Preview",
"description": "Previews images.",
"webviewEditors.displayName": "Image Preview"
}

View file

@ -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<T extends vscode.Disposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed() {
return this._isDisposed;
}
}

View file

@ -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<void> {
// tslint:disable-next-line: no-unused-expression
new Preview(extensionRoot, resource, editor, sizeStatusBarEntry, zoomStatusBarEntry);
}
}));
}

View file

@ -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 */`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Image Preview</title>
<link rel="stylesheet" class="code-user-style" href="${escapeAttribute(this.extensionResource('/media/main.css'))}" type="text/css" media="screen">
<meta id="image-preview-settings" data-settings="${escapeAttribute(JSON.stringify(settings))}">
</head>
<body class="container image scale-to-fit">
<img src="${escapeAttribute(webviewEditor.webview.asWebviewUri(resource))}">
<script src="${escapeAttribute(this.extensionResource('/media/main.js'))}"></script>
</body>
</html>`;
}
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, '&quot;');
}

View file

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

View file

@ -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.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference types='@types/node'/>

View file

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

View file

@ -0,0 +1,10 @@
{
"extends": "../shared.tsconfig.json",
"compilerOptions": {
"outDir": "./out",
"experimentalDecorators": true
},
"include": [
"src/**/*"
]
}

View file

@ -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=

View file

@ -1,3 +1,4 @@
{
"injectionSelector": "L:comment.block.documentation",
"patterns": [

View file

@ -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[];

View file

@ -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 {

View file

@ -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<void>;
}
namespace window {
export function registerWebviewEditorProvider(
viewType: string,
provider: WebviewEditorProvider,
): Disposable;
}
//#endregion
}

View file

@ -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<string, IDisposable>();
private readonly _editorProviders = new Map<string, IDisposable>();
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<boolean> {
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<void> => {
resolveWebview: async (webviewEditorInput): Promise<void> => {
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 `<!DOCTYPE html>
<html>

View file

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

View file

@ -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<void>;
$deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise<void>;
$resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise<void>;
}
export interface MainThreadUrlsShape extends IDisposable {

View file

@ -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<WebviewPanelHandle, ExtHostWebviewPanel>();
private readonly _serializers = new Map<string, vscode.WebviewPanelSerializer>();
private readonly _editorProviders = new Map<string, vscode.WebviewEditorProvider>();
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<void> {
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(

View file

@ -174,4 +174,4 @@ export const Extensions = {
Editors: 'workbench.contributions.editors'
};
Registry.add(Extensions.Editors, new EditorRegistry());
Registry.add(Extensions.Editors, new EditorRegistry());

View file

@ -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<IEditor>;
private override: () => Promise<IEditor | undefined>;
constructor(
private _group: GroupIdentifier,
@ -1541,11 +1541,11 @@ class EditorOpeningEvent implements IEditorOpeningEvent {
return this._options;
}
prevent(callback: () => Promise<IEditor>): void {
prevent(callback: () => Promise<IEditor | undefined>): void {
this.override = callback;
}
isPrevented(): () => Promise<IEditor> {
isPrevented(): () => Promise<IEditor | undefined> {
return this.override;
}
}

View file

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

View file

@ -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<string, ImageState>(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.$<HTMLImageElement>('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<string | Blob> {
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;
}

View file

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

View file

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

View file

@ -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<WebviewEditorOverlay>,
@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<IEditorModel> {
if (!this._hasResolved) {
this._hasResolved = true;
this._extensionService.activateByEvent(`onWebviewEditor:${this.viewType}`);
await this._webviewEditorService.resolveWebview(this);
}
return super.resolve();
}
}

View file

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

View file

@ -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<CustomEditorInfo> = [];
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<IEditor | undefined> {
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<IEditor | undefined> {
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<CustomEditorsAssociations>(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;
}

View file

@ -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<IWebviewEditorsExtensionPoint[]>({
extensionPoint: 'webviewEditors',
deps: [languagesExtPoint],
jsonSchema: webviewEditorsContribution
});

View file

@ -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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench)
.registerWorkbenchContribution(CustomEditorContribution, LifecyclePhase.Starting);
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
new EditorDescriptor(
WebviewEditor,
WebviewEditor.ID,
'Webview Editor',
), [
new SyncDescriptor(CustomFileEditorInput)
]);
Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(
CustomEditoInputFactory.ID,
CustomEditoInputFactory);
Registry.as<IConfigurationRegistry>(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."),
}
}
}
}
}
});

View file

@ -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<ICustomEditorService>('customEditorService');
export interface ICustomEditorService {
_serviceBrand: any;
getCustomEditorsForResource(resource: URI): readonly CustomEditorInfo[];
openWith(resource: URI, customEditorViewType: string, options?: ITextEditorOptions, group?: IEditorGroup): Promise<IEditor | undefined>;
promptOpenWith(resource: URI, options?: ITextEditorOptions, group?: IEditorGroup): Promise<IEditor | undefined>;
}
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[];
}

View file

@ -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<IContextKeyService>());
private _findWidgetVisible: IContextKey<boolean>;
@ -136,7 +136,11 @@ export class WebviewEditor extends BaseEditor {
super.clearInput();
}
public async setInput(input: WebviewEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
public async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
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 {

View file

@ -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<string, { light: URI, dark: URI }>();
@ -70,6 +72,7 @@ export class WebviewEditorInput extends EditorInput {
readonly id: ExtensionIdentifier;
},
webview: Unowned<WebviewEditorOverlay>,
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<void>,
webview: Unowned<WebviewEditorOverlay>,
public readonly editorResource: URI,
) {
super(id, viewType, name, extension, webview);
super(id, viewType, name, extension, webview, editorResource);
}
public async resolve(): Promise<IEditorModel> {

View file

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

View file

@ -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<void>;
}
export interface WebviewReviver {
canRevive(
webview: WebviewEditorInput
export interface WebviewResolve {
canResolve(
webview: WebviewEditorInput,
): boolean;
reviveWebview(
webview: WebviewEditorInput
resolveWebview(
webview: WebviewEditorInput,
): Promise<void>;
}
@ -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<WebviewReviver>();
private readonly _revivers = new Set<WebviewResolve>();
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<void>(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<boolean> {
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<void> {
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,

View file

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

View file

@ -300,4 +300,4 @@ suite('Workbench base editor', () => {
MyEditor: MyEditor,
MyOtherEditor: MyOtherEditor
};
});
});

View file

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