mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
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:
parent
df802950e0
commit
011836a150
|
@ -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"
|
||||
|
|
10
extensions/image-preview/.vscodeignore
Normal file
10
extensions/image-preview/.vscodeignore
Normal 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
|
3
extensions/image-preview/README.md
Normal file
3
extensions/image-preview/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Image Preview
|
||||
|
||||
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
|
20
extensions/image-preview/extension.webpack.config.js
Normal file
20
extensions/image-preview/extension.webpack.config.js
Normal 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',
|
||||
}
|
||||
});
|
78
extensions/image-preview/media/main.css
Normal file
78
extensions/image-preview/media/main.css
Normal 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;
|
||||
}
|
258
extensions/image-preview/media/main.js
Normal file
258
extensions/image-preview/media/main.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}());
|
43
extensions/image-preview/package.json
Normal file
43
extensions/image-preview/package.json
Normal 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"
|
||||
}
|
||||
}
|
5
extensions/image-preview/package.nls.json
Normal file
5
extensions/image-preview/package.nls.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"displayName": "Image Preview",
|
||||
"description": "Previews images.",
|
||||
"webviewEditors.displayName": "Image Preview"
|
||||
}
|
42
extensions/image-preview/src/dispose.ts
Normal file
42
extensions/image-preview/src/dispose.ts
Normal 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;
|
||||
}
|
||||
}
|
28
extensions/image-preview/src/extension.ts
Normal file
28
extensions/image-preview/src/extension.ts
Normal 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);
|
||||
}
|
||||
}));
|
||||
}
|
113
extensions/image-preview/src/preview.ts
Normal file
113
extensions/image-preview/src/preview.ts
Normal 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, '"');
|
||||
}
|
33
extensions/image-preview/src/sizeStatusBarEntry.ts
Normal file
33
extensions/image-preview/src/sizeStatusBarEntry.ts
Normal 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;
|
||||
}
|
||||
}
|
8
extensions/image-preview/src/typings/ref.d.ts
vendored
Normal file
8
extensions/image-preview/src/typings/ref.d.ts
vendored
Normal 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'/>
|
68
extensions/image-preview/src/zoomStatusBarEntry.ts
Normal file
68
extensions/image-preview/src/zoomStatusBarEntry.ts
Normal 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)}%`;
|
||||
}
|
||||
}
|
10
extensions/image-preview/tsconfig.json
Normal file
10
extensions/image-preview/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../shared.tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
46
extensions/image-preview/yarn.lock
Normal file
46
extensions/image-preview/yarn.lock
Normal 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=
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
{
|
||||
"injectionSelector": "L:comment.block.documentation",
|
||||
"patterns": [
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 {
|
||||
|
|
25
src/vs/vscode.proposed.d.ts
vendored
25
src/vs/vscode.proposed.d.ts
vendored
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -174,4 +174,4 @@ export const Extensions = {
|
|||
Editors: 'workbench.contributions.editors'
|
||||
};
|
||||
|
||||
Registry.add(Extensions.Editors, new EditorRegistry());
|
||||
Registry.add(Extensions.Editors, new EditorRegistry());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
105
src/vs/workbench/contrib/customEditor/browser/commands.ts
Normal file
105
src/vs/workbench/contrib/customEditor/browser/commands.ts
Normal 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
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
207
src/vs/workbench/contrib/customEditor/browser/customEditors.ts
Normal file
207
src/vs/workbench/contrib/customEditor/browser/customEditors.ts
Normal 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;
|
||||
}
|
|
@ -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
|
||||
});
|
|
@ -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."),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
39
src/vs/workbench/contrib/customEditor/common/customEditor.ts
Normal file
39
src/vs/workbench/contrib/customEditor/common/customEditor.ts
Normal 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[];
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -300,4 +300,4 @@ suite('Workbench base editor', () => {
|
|||
MyEditor: MyEditor,
|
||||
MyOtherEditor: MyOtherEditor
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue