Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

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

View File

@ -0,0 +1,16 @@
# 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`
- `*.webp`

View File

@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* 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 withBrowserDefaults = require('../shared.webpack.config').browser;
module.exports = withBrowserDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts'
},
});

View File

@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
resolve: {
mainFields: ['module', 'main']
},
entry: {
extension: './src/extension.ts',
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#C5C5C5;}
</style>
<g>
<path class="st0" d="M2,14.1l-1.1-1L0.9,3l1-1.1l12.1,0l1.1,1l0,10.1L14,14.1H2z M13.9,12.9V3.1H2.1v9.8H13.9z M8.9,12.1V7.9h4.2
v4.2H8.9z M11.9,10.9V9.1h-1.8v1.8H11.9z M2.9,12.1v-1.2h4.2v1.2H2.9z M2.9,9.1V7.9h4.2v1.2H2.9z M2.9,7.1V3.9h10.2v3.2H2.9z
M11.9,5.9V5.1H4.1v0.8H11.9z"/>
<path d="M14,2l1,1v10l-1,1H2l-1-1V3l1-1H14 M2,13h12V3H2V13 M13,4v3H3V4H13 M4,6h8V5H4V6 M13,8v4H9V8H13 M10,11h2V9h-2V11 M7,8v1H3
V8H7 M7,11v1H3v-1H7 M14.1,1.8L14.1,1.8H2H1.9L1.9,1.9l-1,1L0.8,2.9V3v10v0.1l0.1,0.1l1,1l0.1,0.1H2h12h0.1l0.1-0.1l1-1l0.1-0.1V13
V3V2.9l-0.1-0.1L14.1,1.8L14.1,1.8L14.1,1.8z M2.2,3.2h11.6v9.6H2.2V3.2L2.2,3.2z M13.2,3.8H13H3H2.8V4v3v0.2H3h10h0.2V7V4V3.8
L13.2,3.8z M4.2,5.2h7.6v0.6H4.2V5.2L4.2,5.2z M13.2,7.8H13H9H8.8V8v4v0.2H9h4h0.2V12V8V7.8L13.2,7.8z M10.2,9.2h1.6v1.6h-1.6V9.2
L10.2,9.2z M7.2,7.8H7H3H2.8V8v1v0.2H3h4h0.2V9V8V7.8L7.2,7.8z M7.2,10.8H7H3H2.8V11v1v0.2H3h4h0.2V12v-1V10.8L7.2,10.8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g style="fill:grey;">
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g style="fill:white;">
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g>
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
html, body {
width: 100%;
height: 100%;
text-align: center;
}
body img {
max-width: none;
max-height: none;
}
.container:focus {
outline: none !important;
}
.container {
padding: 5px 0 0 10px;
box-sizing: border-box;
-webkit-user-select: none;
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;
border: 1px solid var(--vscode-imagePreview-border);
}
.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.ready.zoom-in {
cursor: zoom-in;
}
.container.ready.zoom-out {
cursor: zoom-out;
}
.container .embedded-link,
.container .embedded-link:hover {
cursor: pointer;
text-decoration: underline;
margin-left: 5px;
}
.container.loading,
.container.error {
display: flex;
justify-content: center;
align-items: center;
}
.loading-indicator {
width: 30px;
height: 30px;
background-image: url('./loading.svg');
background-size: cover;
}
.loading-indicator,
.image-load-error {
display: none;
}
.loading .loading-indicator,
.error .image-load-error {
display: block;
}
.image-load-error {
margin: 1em;
}
.vscode-dark .loading-indicator {
background-image: url('./loading-dark.svg');
}
.vscode-high-contrast .loading-indicator {
background-image: url('./loading-hc.svg');
}

View File

@ -0,0 +1,340 @@
/*---------------------------------------------------------------------------------------------
* 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 settings = getSettings();
const isMac = settings.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;
let hasLoadedImage = false;
let consumeClick = true;
let isActive = false;
// Elements
const container = document.body;
const image = document.createElement('img');
function updateScale(newScale) {
if (!image || !hasLoadedImage || !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 {
scale = clamp(newScale, MIN_SCALE, MAX_SCALE);
if (scale >= PIXELATION_THRESHOLD) {
image.classList.add('pixelated');
} else {
image.classList.remove('pixelated');
}
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 newScrollX = container.scrollWidth * dx - container.clientWidth / 2;
const newScrollY = container.scrollHeight * dy - container.clientHeight / 2;
window.scrollTo(newScrollX, newScrollY);
vscode.setState({ scale: scale, offsetX: newScrollX, offsetY: newScrollY });
}
vscode.postMessage({
type: 'zoom',
value: scale
});
}
function setActive(value) {
isActive = value;
if (value) {
if (isMac ? altPressed : ctrlPressed) {
container.classList.remove('zoom-in');
container.classList.add('zoom-out');
} else {
container.classList.remove('zoom-out');
container.classList.add('zoom-in');
}
} else {
ctrlPressed = false;
altPressed = false;
container.classList.remove('zoom-out');
container.classList.remove('zoom-in');
}
}
function firstZoom() {
if (!image || !hasLoadedImage) {
return;
}
scale = image.clientWidth / image.naturalWidth;
updateScale(scale);
}
function zoomIn() {
if (scale === 'fit') {
firstZoom();
}
let i = 0;
for (; i < zoomLevels.length; ++i) {
if (zoomLevels[i] > scale) {
break;
}
}
updateScale(zoomLevels[i] || MAX_SCALE);
}
function zoomOut() {
if (scale === 'fit') {
firstZoom();
}
let i = zoomLevels.length - 1;
for (; i >= 0; --i) {
if (zoomLevels[i] < scale) {
break;
}
}
updateScale(zoomLevels[i] || MIN_SCALE);
}
window.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => {
if (!image || !hasLoadedImage) {
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 || !hasLoadedImage) {
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
if (!(isMac ? altPressed : ctrlPressed)) {
container.classList.remove('zoom-out');
container.classList.add('zoom-in');
}
});
container.addEventListener('mousedown', (/** @type {MouseEvent} */ e) => {
if (!image || !hasLoadedImage) {
return;
}
if (e.button !== 0) {
return;
}
ctrlPressed = e.ctrlKey;
altPressed = e.altKey;
consumeClick = !isActive;
});
container.addEventListener('click', (/** @type {MouseEvent} */ e) => {
if (!image || !hasLoadedImage) {
return;
}
if (e.button !== 0) {
return;
}
if (consumeClick) {
consumeClick = false;
return;
}
// left click
if (scale === 'fit') {
firstZoom();
}
if (!(isMac ? altPressed : ctrlPressed)) { // zoom in
zoomIn();
} else {
zoomOut();
}
});
container.addEventListener('wheel', (/** @type {WheelEvent} */ e) => {
// Prevent pinch to zoom
if (e.ctrlKey) {
e.preventDefault();
}
if (!image || !hasLoadedImage) {
return;
}
const isScrollWheelKeyPressed = isMac ? altPressed : ctrlPressed;
if (!isScrollWheelKeyPressed && !e.ctrlKey) { // pinching is reported as scroll wheel + ctrl
return;
}
if (scale === 'fit') {
firstZoom();
}
let delta = e.deltaY > 0 ? 1 : -1;
updateScale(scale * (1 - delta * SCALE_PINCH_FACTOR));
}, { passive: false });
window.addEventListener('scroll', e => {
if (!image || !hasLoadedImage || !image.parentElement || scale === 'fit') {
return;
}
const entry = vscode.getState();
if (entry) {
vscode.setState({ scale: entry.scale, offsetX: window.scrollX, offsetY: window.scrollY });
}
}, { passive: true });
container.classList.add('image');
image.classList.add('scale-to-fit');
image.addEventListener('load', () => {
if (hasLoadedImage) {
return;
}
hasLoadedImage = true;
vscode.postMessage({
type: 'size',
value: `${image.naturalWidth}x${image.naturalHeight}`,
});
document.body.classList.remove('loading');
document.body.classList.add('ready');
document.body.append(image);
updateScale(scale);
if (initialState.scale !== 'fit') {
window.scrollTo(initialState.offsetX, initialState.offsetY);
}
});
image.addEventListener('error', e => {
if (hasLoadedImage) {
return;
}
hasLoadedImage = true;
document.body.classList.add('error');
document.body.classList.remove('loading');
});
image.src = settings.src;
document.querySelector('.open-file-link').addEventListener('click', () => {
vscode.postMessage({
type: 'reopen-as-text',
});
});
window.addEventListener('message', e => {
switch (e.data.type) {
case 'setScale':
updateScale(e.data.scale);
break;
case 'setActive':
setActive(e.data.value);
break;
case 'zoomIn':
zoomIn();
break;
case 'zoomOut':
zoomOut();
break;
}
});
}());

View File

@ -0,0 +1,81 @@
{
"name": "image-preview",
"displayName": "%displayName%",
"description": "%description%",
"extensionKind": [
"ui",
"workspace",
"web"
],
"version": "1.0.0",
"publisher": "vscode",
"icon": "icon.png",
"enableProposedApi": true,
"license": "MIT",
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
"engines": {
"vscode": "^1.39.0"
},
"main": "./out/extension",
"browser": "./dist/browser/extension.js",
"categories": [
"Other"
],
"activationEvents": [
"onCustomEditor:imagePreview.previewEditor",
"onCommand:imagePreview.zoomIn",
"onCommand:imagePreview.zoomOut"
],
"contributes": {
"customEditors": [
{
"viewType": "imagePreview.previewEditor",
"displayName": "%customEditors.displayName%",
"priority": "builtin",
"selector": [
{
"filenamePattern": "*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp}"
}
]
}
],
"commands": [
{
"command": "imagePreview.zoomIn",
"title": "%command.zoomIn%",
"category": "Image Preview"
},
{
"command": "imagePreview.zoomOut",
"title": "%command.zoomOut%",
"category": "Image Preview"
}
],
"menus": {
"commandPalette": [
{
"command": "imagePreview.zoomIn",
"when": "imagePreviewFocus",
"group": "1_imagePreview"
},
{
"command": "imagePreview.zoomOut",
"when": "imagePreviewFocus",
"group": "1_imagePreview"
}
]
}
},
"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",
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
},
"dependencies": {
"vscode-extension-telemetry": "0.1.1",
"vscode-nls": "^4.0.0"
}
}

View File

@ -0,0 +1,7 @@
{
"displayName": "Image Preview",
"description": "Provides VS Code's built-in image preview",
"customEditors.displayName": "Image Preview",
"command.zoomIn": "Zoom in",
"command.zoomOut": "Zoom out"
}

View File

@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* 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 { PreviewStatusBarEntry } from './ownedStatusBarEntry';
const localize = nls.loadMessageBundle();
class BinarySize {
static readonly KB = 1024;
static readonly MB = BinarySize.KB * BinarySize.KB;
static readonly GB = BinarySize.MB * BinarySize.KB;
static readonly TB = BinarySize.GB * BinarySize.KB;
static formatSize(size: number): string {
if (size < BinarySize.KB) {
return localize('sizeB', "{0}B", size);
}
if (size < BinarySize.MB) {
return localize('sizeKB', "{0}KB", (size / BinarySize.KB).toFixed(2));
}
if (size < BinarySize.GB) {
return localize('sizeMB', "{0}MB", (size / BinarySize.MB).toFixed(2));
}
if (size < BinarySize.TB) {
return localize('sizeGB', "{0}GB", (size / BinarySize.GB).toFixed(2));
}
return localize('sizeTB', "{0}TB", (size / BinarySize.TB).toFixed(2));
}
}
export class BinarySizeStatusBarEntry extends PreviewStatusBarEntry {
constructor() {
super({
id: 'imagePreview.binarySize',
name: localize('sizeStatusBar.name', "Image Binary Size"),
alignment: vscode.StatusBarAlignment.Right,
priority: 100,
});
}
public show(owner: string, size: number | undefined) {
if (typeof size === 'number') {
super.showItem(owner, BinarySize.formatSize(size));
} else {
this.hide(owner);
}
}
}

View File

@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export function disposeAll(disposables: vscode.Disposable[]) {
while (disposables.length) {
const item = disposables.pop();
if (item) {
item.dispose();
}
}
}
export abstract class Disposable {
private _isDisposed = false;
protected _disposables: vscode.Disposable[] = [];
public dispose(): any {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
disposeAll(this._disposables);
}
protected _register<T extends vscode.Disposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed() {
return this._isDisposed;
}
}

View File

@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* 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 { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry';
import { PreviewManager } from './preview';
import { SizeStatusBarEntry } from './sizeStatusBarEntry';
import { ZoomStatusBarEntry } from './zoomStatusBarEntry';
export function activate(context: vscode.ExtensionContext) {
const sizeStatusBarEntry = new SizeStatusBarEntry();
context.subscriptions.push(sizeStatusBarEntry);
const binarySizeStatusBarEntry = new BinarySizeStatusBarEntry();
context.subscriptions.push(binarySizeStatusBarEntry);
const zoomStatusBarEntry = new ZoomStatusBarEntry();
context.subscriptions.push(zoomStatusBarEntry);
const previewManager = new PreviewManager(context.extensionUri, sizeStatusBarEntry, binarySizeStatusBarEntry, zoomStatusBarEntry);
context.subscriptions.push(vscode.window.registerCustomEditorProvider(PreviewManager.viewType, previewManager, {
supportsMultipleEditorsPerDocument: true,
}));
context.subscriptions.push(vscode.commands.registerCommand('imagePreview.zoomIn', () => {
previewManager.activePreview?.zoomIn();
}));
context.subscriptions.push(vscode.commands.registerCommand('imagePreview.zoomOut', () => {
previewManager.activePreview?.zoomOut();
}));
}

View File

@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* 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 abstract class PreviewStatusBarEntry extends Disposable {
private _showOwner: string | undefined;
protected readonly entry: vscode.StatusBarItem;
constructor(options: vscode.window.StatusBarItemOptions) {
super();
this.entry = this._register(vscode.window.createStatusBarItem(options));
}
protected showItem(owner: string, text: string) {
this._showOwner = owner;
this.entry.text = text;
this.entry.show();
}
public hide(owner: string) {
if (owner === this._showOwner) {
this.entry.hide();
this._showOwner = undefined;
}
}
}

View File

@ -0,0 +1,266 @@
/*---------------------------------------------------------------------------------------------
* 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';
import { SizeStatusBarEntry } from './sizeStatusBarEntry';
import { Scale, ZoomStatusBarEntry } from './zoomStatusBarEntry';
import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry';
const localize = nls.loadMessageBundle();
export class PreviewManager implements vscode.CustomReadonlyEditorProvider {
public static readonly viewType = 'imagePreview.previewEditor';
private readonly _previews = new Set<Preview>();
private _activePreview: Preview | undefined;
constructor(
private readonly extensionRoot: vscode.Uri,
private readonly sizeStatusBarEntry: SizeStatusBarEntry,
private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry,
private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
) { }
public async openCustomDocument(uri: vscode.Uri) {
return { uri, dispose: () => { } };
}
public async resolveCustomEditor(
document: vscode.CustomDocument,
webviewEditor: vscode.WebviewPanel,
): Promise<void> {
const preview = new Preview(this.extensionRoot, document.uri, webviewEditor, this.sizeStatusBarEntry, this.binarySizeStatusBarEntry, this.zoomStatusBarEntry);
this._previews.add(preview);
this.setActivePreview(preview);
webviewEditor.onDidDispose(() => { this._previews.delete(preview); });
webviewEditor.onDidChangeViewState(() => {
if (webviewEditor.active) {
this.setActivePreview(preview);
} else if (this._activePreview === preview && !webviewEditor.active) {
this.setActivePreview(undefined);
}
});
}
public get activePreview() { return this._activePreview; }
private setActivePreview(value: Preview | undefined): void {
this._activePreview = value;
this.setPreviewActiveContext(!!value);
}
private setPreviewActiveContext(value: boolean) {
vscode.commands.executeCommand('setContext', 'imagePreviewFocus', value);
}
}
const enum PreviewState {
Disposed,
Visible,
Active,
}
class Preview extends Disposable {
private readonly id: string = `${Date.now()}-${Math.random().toString()}`;
private _previewState = PreviewState.Visible;
private _imageSize: string | undefined;
private _imageBinarySize: number | undefined;
private _imageZoom: Scale | undefined;
private readonly emptyPngDataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg==';
constructor(
private readonly extensionRoot: vscode.Uri,
private readonly resource: vscode.Uri,
private readonly webviewEditor: vscode.WebviewPanel,
private readonly sizeStatusBarEntry: SizeStatusBarEntry,
private readonly binarySizeStatusBarEntry: BinarySizeStatusBarEntry,
private readonly zoomStatusBarEntry: ZoomStatusBarEntry,
) {
super();
const resourceRoot = resource.with({
path: resource.path.replace(/\/[^\/]+?\.\w+$/, '/'),
});
webviewEditor.webview.options = {
enableScripts: true,
localResourceRoots: [
resourceRoot,
extensionRoot,
]
};
this._register(webviewEditor.webview.onDidReceiveMessage(message => {
switch (message.type) {
case 'size':
{
this._imageSize = message.value;
this.update();
break;
}
case 'zoom':
{
this._imageZoom = message.value;
this.update();
break;
}
case 'reopen-as-text':
{
vscode.commands.executeCommand('vscode.openWith', resource, 'default', webviewEditor.viewColumn);
break;
}
}
}));
this._register(zoomStatusBarEntry.onDidChangeScale(e => {
if (this._previewState === PreviewState.Active) {
this.webviewEditor.webview.postMessage({ type: 'setScale', scale: e.scale });
}
}));
this._register(webviewEditor.onDidChangeViewState(() => {
this.update();
this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active });
}));
this._register(webviewEditor.onDidDispose(() => {
if (this._previewState === PreviewState.Active) {
this.sizeStatusBarEntry.hide(this.id);
this.binarySizeStatusBarEntry.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();
}
}));
vscode.workspace.fs.stat(resource).then(({ size }) => {
this._imageBinarySize = size;
this.update();
});
this.render();
this.update();
this.webviewEditor.webview.postMessage({ type: 'setActive', value: this.webviewEditor.active });
}
public zoomIn() {
if (this._previewState === PreviewState.Active) {
this.webviewEditor.webview.postMessage({ type: 'zoomIn' });
}
}
public zoomOut() {
if (this._previewState === PreviewState.Active) {
this.webviewEditor.webview.postMessage({ type: 'zoomOut' });
}
}
private async render() {
if (this._previewState !== PreviewState.Disposed) {
this.webviewEditor.webview.html = await this.getWebviewContents();
}
}
private update() {
if (this._previewState === PreviewState.Disposed) {
return;
}
if (this.webviewEditor.active) {
this._previewState = PreviewState.Active;
this.sizeStatusBarEntry.show(this.id, this._imageSize || '');
this.binarySizeStatusBarEntry.show(this.id, this._imageBinarySize);
this.zoomStatusBarEntry.show(this.id, this._imageZoom || 'fit');
} else {
if (this._previewState === PreviewState.Active) {
this.sizeStatusBarEntry.hide(this.id);
this.binarySizeStatusBarEntry.hide(this.id);
this.zoomStatusBarEntry.hide(this.id);
}
this._previewState = PreviewState.Visible;
}
}
private async getWebviewContents(): Promise<string> {
const version = Date.now().toString();
const settings = {
isMac: process.platform === 'darwin',
src: await this.getResourcePath(this.webviewEditor, this.resource, version),
};
const nonce = Date.now().toString();
return /* html */`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Disable pinch zooming -->
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>Image Preview</title>
<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 loading">
<div class="loading-indicator"></div>
<div class="image-load-error">
<p>${localize('preview.imageLoadError', "An error occurred while loading the image.")}</p>
<a href="#" class="open-file-link">${localize('preview.imageLoadErrorLink', "Open file using VS Code's standard text/binary editor?")}</a>
</div>
<script src="${escapeAttribute(this.extensionResource('/media/main.js'))}" nonce="${nonce}"></script>
</body>
</html>`;
}
private async getResourcePath(webviewEditor: vscode.WebviewPanel, resource: vscode.Uri, version: string): Promise<string> {
if (resource.scheme === 'git') {
const stat = await vscode.workspace.fs.stat(resource);
if (stat.size === 0) {
return this.emptyPngDataUri;
}
}
// Avoid adding cache busting if there is already a query string
if (resource.query) {
return webviewEditor.webview.asWebviewUri(resource).toString();
}
return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString();
}
private extensionResource(path: string) {
return this.webviewEditor.webview.asWebviewUri(this.extensionRoot.with({
path: this.extensionRoot.path + path
}));
}
}
function escapeAttribute(value: string | vscode.Uri): string {
return value.toString().replace(/"/g, '&quot;');
}

View File

@ -0,0 +1,26 @@
/*---------------------------------------------------------------------------------------------
* 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 { PreviewStatusBarEntry } from './ownedStatusBarEntry';
const localize = nls.loadMessageBundle();
export class SizeStatusBarEntry extends PreviewStatusBarEntry {
constructor() {
super({
id: 'imagePreview.size',
name: localize('sizeStatusBar.name', "Image Size"),
alignment: vscode.StatusBarAlignment.Right,
priority: 101 /* to the left of editor status (100) */,
});
}
public show(owner: string, text: string) {
this.showItem(owner, text);
}
}

View File

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference types='@types/node'/>

View File

@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* 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 { PreviewStatusBarEntry as OwnedStatusBarEntry } from './ownedStatusBarEntry';
const localize = nls.loadMessageBundle();
const selectZoomLevelCommandId = '_imagePreview.selectZoomLevel';
export type Scale = number | 'fit';
export class ZoomStatusBarEntry extends OwnedStatusBarEntry {
private readonly _onDidChangeScale = this._register(new vscode.EventEmitter<{ scale: Scale }>());
public readonly onDidChangeScale = this._onDidChangeScale.event;
constructor() {
super({
id: 'imagePreview.zoom',
name: localize('zoomStatusBar.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(owner: string, scale: Scale) {
this.showItem(owner, this.zoomLabel(scale));
}
private zoomLabel(scale: Scale): string {
return scale === 'fit'
? localize('zoomStatusBar.wholeImageLabel', "Whole Image")
: `${Math.round(scale * 100)}%`;
}
}

View File

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

View File

@ -0,0 +1,46 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
applicationinsights@1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5"
integrity sha512-KzOOGdphOS/lXWMFZe5440LUdFbrLpMvh2SaRxn7BmiI550KAoSb2gIhiq6kJZ9Ir3AxRRztjhzif+e5P5IXIg==
dependencies:
diagnostic-channel "0.2.0"
diagnostic-channel-publishers "0.2.1"
zone.js "0.7.6"
diagnostic-channel-publishers@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM=
diagnostic-channel@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
dependencies:
semver "^5.3.0"
semver@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
vscode-extension-telemetry@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b"
integrity sha512-TkKKG/B/J94DP5qf6xWB4YaqlhWDg6zbbqVx7Bz//stLQNnfE9XS1xm3f6fl24c5+bnEK0/wHgMgZYKIKxPeUA==
dependencies:
applicationinsights "1.0.8"
vscode-nls@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002"
integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw==
zone.js@0.7.6:
version "0.7.6"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"
integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=