Merge from vscode c58aaab8a1cc22a7139b761166a0d4f37d41e998 (#7880)

* Merge from vscode c58aaab8a1cc22a7139b761166a0d4f37d41e998

* fix pipelines

* fix strict-null-checks

* add missing files
This commit is contained in:
Anthony Dresser
2019-10-21 22:12:22 -07:00
committed by GitHub
parent 7c9be74970
commit 1e22f47304
913 changed files with 18898 additions and 16536 deletions

View File

@@ -1,3 +1,17 @@
# Image Preview
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
## Features
This extension provides VS Code's built-in image preview functionality.
Supported image formats:
- `*.jpg`, `*.jpe`, `*.jpeg`
- `*.png`
- `*.bmp`
- `*.gif`
- `*.ico`
- `*.tga`
- `*.webp`

View File

@@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/
html, body {
width: 100%;
height: 100%;
max-height: 100%;
text-align: center;
}
body img {
@@ -77,22 +78,38 @@ body img {
margin-left: 5px;
}
.loading {
position: fixed;
.container.loading,
.container.error {
display: flex;
justify-content: center;
align-items: center;
}
.loading-indicator {
width: 30px;
height: 30px;
left: 50%;
top: 50%;
margin-top: -15px;
margin-left: -15px;
background-image: url('./loading.svg');
background-size: cover;
}
.vscode-dark .loading {
.loading-indicator,
.image-load-error-message {
display: none;
}
.loading .loading-indicator,
.error .image-load-error-message {
display: block;
}
.image-load-error-message {
margin: 1em;
}
.vscode-dark .loading-indicator {
background-image: url('./loading-dark.svg');
}
.vscode-high-contrast .loading {
.vscode-high-contrast .loading-indicator {
background-image: url('./loading-hc.svg');
}

View File

@@ -70,9 +70,10 @@
let ctrlPressed = false;
let altPressed = false;
let hasLoadedImage = false;
let consumeClick = false;
// Elements
const container = /** @type {HTMLElement} */(document.querySelector('body'));
const container = document.body;
const image = document.createElement('img');
function updateScale(newScale) {
@@ -88,9 +89,6 @@
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');
@@ -98,25 +96,19 @@
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;
const dx = (window.scrollX + container.clientWidth / 2) / container.scrollWidth;
const dy = (window.scrollY + container.clientHeight / 2) / container.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 newScrollX = container.scrollWidth * dx - container.clientWidth / 2;
const newScrollY = container.scrollHeight * dy - container.clientHeight / 2;
const newScrollLeft = ((oldWidth * scaleFactor * dx) + scrollLeft);
const newScrollTop = ((oldHeight * scaleFactor * dy) + scrollTop);
// scrollbar.setScrollPosition({
// scrollLeft: newScrollLeft,
// scrollTop: newScrollTop,
// });
window.scrollTo(newScrollX, newScrollY);
vscode.setState({ scale: scale, offsetX: newScrollLeft, offsetY: newScrollTop });
vscode.setState({ scale: scale, offsetX: newScrollX, offsetY: newScrollY });
}
vscode.postMessage({
@@ -125,6 +117,18 @@
});
}
function changeActive(value) {
if (value) {
container.classList.add('zoom-in');
consumeClick = true;
} else {
ctrlPressed = false;
altPressed = false;
container.classList.remove('zoom-out');
container.classList.remove('zoom-in');
}
}
function firstZoom() {
if (!image || !hasLoadedImage) {
return;
@@ -161,6 +165,18 @@
}
});
container.addEventListener('mousedown', (/** @type {MouseEvent} */ e) => {
if (!image || !hasLoadedImage) {
return;
}
if (e.button !== 0) {
return;
}
consumeClick = false;
});
container.addEventListener('click', (/** @type {MouseEvent} */ e) => {
if (!image || !hasLoadedImage) {
return;
@@ -170,6 +186,18 @@
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
if (isMac ? altPressed : ctrlPressed) {
container.classList.remove('zoom-in');
container.classList.add('zoom-out');
}
if (consumeClick) {
consumeClick = false;
return;
}
// left click
if (scale === 'fit') {
firstZoom();
@@ -227,24 +255,19 @@
});
container.classList.add('image');
container.classList.add('zoom-in');
image.classList.add('scale-to-fit');
image.addEventListener('load', () => {
document.querySelector('.loading').remove();
hasLoadedImage = true;
if (!image) {
return;
}
vscode.postMessage({
type: 'size',
value: `${image.naturalWidth}x${image.naturalHeight}`,
});
container.classList.add('ready');
document.body.classList.remove('loading');
document.body.classList.add('ready');
document.body.append(image);
updateScale(scale);
@@ -254,6 +277,12 @@
}
});
image.addEventListener('error', () => {
hasLoadedImage = true;
document.body.classList.add('error');
document.body.classList.remove('loading');
});
image.src = decodeURI(settings.src);
window.addEventListener('message', e => {
@@ -261,6 +290,9 @@
case 'setScale':
updateScale(e.data.scale);
break;
case 'setActive':
changeActive(e.data.value);
break;
}
});
}());

View File

@@ -24,9 +24,10 @@
{
"viewType": "imagePreview.previewEditor",
"displayName": "%webviewEditors.displayName%",
"priority": "builtin",
"selector": [
{
"filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,tga,tif,tiff,webp}",
"filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,tga,webp}",
"mime": "image/*"
}
]

View File

@@ -1,5 +1,5 @@
{
"displayName": "Image Preview",
"description": "Previews images.",
"description": "Provides VS Code's built-in image preview",
"webviewEditors.displayName": "Image Preview"
}

View File

@@ -4,19 +4,32 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { SizeStatusBarEntry } from './sizeStatusBarEntry';
import { ZoomStatusBarEntry } from './zoomStatusBarEntry';
import * as nls from 'vscode-nls';
import { Disposable } from './dispose';
import { SizeStatusBarEntry } from './sizeStatusBarEntry';
import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry';
const localize = nls.loadMessageBundle();
const enum PreviewState {
Disposed,
Visible,
Active,
}
export class Preview extends Disposable {
public static readonly viewType = 'imagePreview.previewEditor';
private _active = true;
private readonly id: string = `${Date.now()}-${Math.random().toString()}`;
private _previewState = PreviewState.Visible;
private _imageSize: string | undefined;
private _imageZoom: Scale | undefined;
constructor(
private readonly extensionRoot: vscode.Uri,
resource: vscode.Uri,
private readonly resource: vscode.Uri,
private readonly webviewEditor: vscode.WebviewEditor,
private readonly sizeStatusBarEntry: SizeStatusBarEntry,
private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
@@ -34,56 +47,91 @@ export class Preview extends Disposable {
]
};
webviewEditor.webview.html = this.getWebiewContents(webviewEditor, resource);
this._register(webviewEditor.webview.onDidReceiveMessage(message => {
switch (message.type) {
case 'size':
{
this.sizeStatusBarEntry.update(message.value);
this._imageSize = message.value;
this.update();
break;
}
case 'zoom':
{
this.zoomStatusBarEntry.update(message.value);
this._imageZoom = message.value;
this.update();
break;
}
}
}));
this._register(zoomStatusBarEntry.onDidChangeScale(e => {
this.webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale });
if (this._previewState === PreviewState.Active) {
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();
if (this._previewState === PreviewState.Active) {
this.sizeStatusBarEntry.hide(this.id);
this.zoomStatusBarEntry.hide(this.id);
}
this._previewState = PreviewState.Disposed;
}));
const watcher = this._register(vscode.workspace.createFileSystemWatcher(resource.fsPath));
this._register(watcher.onDidChange(e => {
if (e.toString() === this.resource.toString()) {
this.render();
}
}));
this._register(watcher.onDidDelete(e => {
if (e.toString() === this.resource.toString()) {
this.webviewEditor.dispose();
}
}));
this.render();
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 render() {
if (this._previewState !== PreviewState.Disposed) {
this.webviewEditor.webview.html = this.getWebiewContents();
}
}
private getWebiewContents(webviewEditor: vscode.WebviewEditor, resource: vscode.Uri): string {
private update() {
if (this._previewState === PreviewState.Disposed) {
return;
}
if (this.webviewEditor.active) {
this._previewState = PreviewState.Active;
this.sizeStatusBarEntry.show(this.id, this._imageSize || '');
this.zoomStatusBarEntry.show(this.id, this._imageZoom || 'fit');
} else {
if (this._previewState === PreviewState.Active) {
this.sizeStatusBarEntry.hide(this.id);
this.zoomStatusBarEntry.hide(this.id);
}
this._previewState = PreviewState.Visible;
}
this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active });
}
private getWebiewContents(): string {
const version = Date.now().toString();
const settings = {
isMac: process.platform === 'darwin',
src: this.getResourcePath(webviewEditor, resource)
src: this.getResourcePath(this.webviewEditor, this.resource, version),
};
const nonce = Date.now().toString();
return /* html */`<!DOCTYPE html>
<html lang="en">
<head>
@@ -91,23 +139,33 @@ export class Preview extends Disposable {
<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">
<link rel="stylesheet" href="${escapeAttribute(this.extensionResource('/media/main.css'))}" type="text/css" media="screen" nonce="${nonce}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' data: ${this.webviewEditor.webview.cspSource}; script-src 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}';">
<meta id="image-preview-settings" data-settings="${escapeAttribute(JSON.stringify(settings))}">
</head>
<body class="container image scale-to-fit">
<div class='loading'></div>
<script src="${escapeAttribute(this.extensionResource('/media/main.js'))}"></script>
<body class="container image scale-to-fit loading">
<div class="loading-indicator"></div>
<div class="image-load-error-message">${localize('preview.imageLoadError', "An error occurred while loading the image")}</div>
<script src="${escapeAttribute(this.extensionResource('/media/main.js'))}" nonce="${nonce}"></script>
</body>
</html>`;
}
private getResourcePath(webviewEditor: vscode.WebviewEditor, resource: vscode.Uri) {
if (resource.scheme === 'data') {
return encodeURI(resource.toString(true));
}
private getResourcePath(webviewEditor: vscode.WebviewEditor, resource: vscode.Uri, version: string) {
switch (resource.scheme) {
case 'data':
return encodeURI(resource.toString(true));
return encodeURI(webviewEditor.webview.asWebviewUri(resource).toString(true));
case 'git':
// Show blank image
return encodeURI('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg==');
default:
return encodeURI(webviewEditor.webview.asWebviewUri(resource).toString(true) + `?version=${version}`);
}
}
private extensionResource(path: string) {

View File

@@ -5,29 +5,35 @@
import * as vscode from 'vscode';
import { Disposable } from './dispose';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class SizeStatusBarEntry extends Disposable {
private readonly _entry: vscode.StatusBarItem;
private _showingOwner: string | undefined;
constructor() {
super();
this._entry = this._register(vscode.window.createStatusBarItem({
id: 'imagePreview.size',
name: 'Image Size',
name: localize('sizeStatusBar.name', "Image Size"),
alignment: vscode.StatusBarAlignment.Right,
priority: 101 /* to the left of editor status (100) */,
}));
}
public show() {
public show(owner: string, text: string) {
this._showingOwner = owner;
this._entry.text = text;
this._entry.show();
}
public hide() {
this._entry.hide();
}
public update(text: string) {
this._entry.text = text;
public hide(owner: string) {
if (owner === this._showingOwner) {
this._entry.hide();
this._showingOwner = undefined;
}
}
}

View File

@@ -11,7 +11,7 @@ const localize = nls.loadMessageBundle();
const selectZoomLevelCommandId = '_imagePreview.selectZoomLevel';
type Scale = number | 'fit';
export type Scale = number | 'fit';
export class ZoomStatusBarEntry extends Disposable {
private readonly _entry: vscode.StatusBarItem;
@@ -19,11 +19,13 @@ export class ZoomStatusBarEntry extends Disposable {
private readonly _onDidChangeScale = this._register(new vscode.EventEmitter<{ scale: Scale }>());
public readonly onDidChangeScale = this._onDidChangeScale.event;
private _showOwner: string | undefined;
constructor() {
super();
this._entry = this._register(vscode.window.createStatusBarItem({
id: 'imagePreview.zoom',
name: 'Image Zoom',
name: localize('zoomStatusBar.name', "Image Zoom"),
alignment: vscode.StatusBarAlignment.Right,
priority: 102 /* to the left of editor size entry (101) */,
}));
@@ -48,16 +50,17 @@ export class ZoomStatusBarEntry extends Disposable {
this._entry.command = selectZoomLevelCommandId;
}
public show() {
public show(owner: string, scale: Scale) {
this._showOwner = owner;
this._entry.text = this.zoomLabel(scale);
this._entry.show();
}
public hide() {
this._entry.hide();
}
public update(scale: Scale) {
this._entry.text = this.zoomLabel(scale);
public hide(owner: string) {
if (owner === this._showOwner) {
this._entry.hide();
this._showOwner = undefined;
}
}
private zoomLabel(scale: Scale): string {